SPB Git forge

spb/ka-android

Public
4commits 1branches 0releases
1.6 MBsize
maindefault branch
1 mo agolast push
Kotlin 100%

v3.0.0 — logos officiels des 13 marques + sites web intégrés (WebView)

- Logos officiels ka-ui (12 univers + hub groupe-ka) en drawable-nodpi,
  helper logoResFor()/Universe.logoRes, composant KALogo : vrais logos
  dans la grille Explorer, l'en-tête d'univers, les collections de
  l'accueil et les univers favoris du profil.
- Nouveau SiteScreen : navigateur intégré (WebView, JS + domStorage),
  barre aux couleurs de la marque (logo, retour/avancer, recharger,
  partager, ouvrir dans le navigateur), progression accent, back
  matériel qui remonte d'abord l'historique web, schemes tel:/mailto:
  délégués au système. Route site/{universeId} ; « ouvrir le site »
  des univers + portail groupe-ka.com (accueil & profil) passent
  désormais par le navigateur intégré. Les liens « voir à la source »
  (sites tiers) restent externes.
- versionCode 3, versionName 3.0.0 ; tests LogoTests (24/24 verts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 21, 2026) parent cd1b864

22 changed files +326 −30

modified app/build.gradle.kts +2 −2
@@ -14,8 +14,8 @@ android {
14 14 applicationId = "com.groupeka.ka"
15 15 minSdk = 26
16 16 targetSdk = 34
17 versionCode = 2
18 versionName = "2.0.0"
17 + versionCode = 3
18 + versionName = "3.0.0"
19 19 }
20 20
21 21 signingConfigs {
modified app/src/main/java/com/groupeka/ka/MainActivity.kt +9 −2
@@ -63,6 +63,7 @@ import com.groupeka.ka.ui.OnboardingScreen
63 63 import com.groupeka.ka.ui.ProfileScreen
64 64 import com.groupeka.ka.ui.SavedSearchesScreen
65 65 import com.groupeka.ka.ui.SearchScreen
66 +import com.groupeka.ka.ui.SiteScreen
66 67 import com.groupeka.ka.ui.UniverseHomeScreen
67 68 import com.groupeka.ka.ui.UniversesScreen
68 69 import com.groupeka.ka.ui.theme.KAAppTheme
@@ -184,6 +185,11 @@ fun RootView() {
184 185 val id = entry.arguments?.getString("id")
185 186 Ecosystem.universe(id ?: "")?.let { UniverseHomeScreen(it, nav) }
186 187 }
188 + // Site web intégré : les 12 univers + le hub groupe-ka.com
189 + composable("site/{universeId}") { entry ->
190 + val id = entry.arguments?.getString("universeId") ?: "groupe-ka"
191 + SiteScreen(id, nav)
192 + }
187 193 composable("item/{data}") { entry ->
188 194 val item = entry.arguments?.getString("data")?.let { decodeNavArg<KAItem>(it) }
189 195 if (item != null) ItemDetailScreen(item, nav)
@@ -196,8 +202,9 @@ fun RootView() {
196 202 }
197 203
198 204 // Bulle KA Agent — flottante au-dessus de la tab bar, partout sauf
199 // dans l'agent et sur la carte (contrôles de zone au même endroit)
200 if (route != "agent" && route != "map") {
205 + // dans l'agent, sur la carte (contrôles de zone au même endroit)
206 + // et dans le navigateur intégré (toute la surface au site)
207 + if (route != "agent" && route != "map" && route?.startsWith("site/") != true) {
201 208 Box(
202 209 Modifier
203 210 .align(Alignment.BottomEnd)
modified app/src/main/java/com/groupeka/ka/core/Ecosystem.kt +24 −0
@@ -5,6 +5,7 @@
5 5 // une entrée ici, rien d'autre. Miroir exact de KA/Core/Ecosystem.swift (iOS).
6 6 package com.groupeka.ka.core
7 7
8 +import com.groupeka.ka.R
8 9 import kotlinx.serialization.Serializable
9 10 import kotlinx.serialization.json.JsonObject
10 11 import java.text.NumberFormat
@@ -57,6 +58,29 @@ data class Universe(
57 58 val map: ((JsonObject) -> KAItem?)?,
58 59 ) {
59 60 val baseURL: String get() = "https://$domain"
61 +
62 + /** Logo officiel de la marque (PNG ka-ui, coins déjà arrondis). */
63 + val logoRes: Int get() = logoResFor(id)
64 +}
65 +
66 +// MARK: - Logos officiels (drawable-nodpi, source : ka-ui/assets/<id>/logo-512.png)
67 +
68 +/** Ressource drawable du logo officiel d'une marque : les 12 univers + le hub
69 + * `groupe-ka`. Tout id inconnu retombe sur le logo du hub. */
70 +fun logoResFor(id: String): Int = when (id) {
71 + "lou-ka" -> R.drawable.logo_lou_ka
72 + "immo-ka" -> R.drawable.logo_immo_ka
73 + "vrai-prix" -> R.drawable.logo_vrai_prix
74 + "auto-ka" -> R.drawable.logo_auto_ka
75 + "fabri-ka" -> R.drawable.logo_fabri_ka
76 + "food-ka" -> R.drawable.logo_food_ka
77 + "resto-ka" -> R.drawable.logo_resto_ka
78 + "sorti-ka" -> R.drawable.logo_sorti_ka
79 + "crea-ka" -> R.drawable.logo_crea_ka
80 + "job-ka" -> R.drawable.logo_job_ka
81 + "trouve-ka" -> R.drawable.logo_trouve_ka
82 + "api-ka" -> R.drawable.logo_api_ka
83 + else -> R.drawable.logo_groupe_ka // groupe-ka (hub) et repli
60 84 }
61 85
62 86 object Ecosystem {
modified app/src/main/java/com/groupeka/ka/ui/HomeScreen.kt +31 −2
@@ -38,6 +38,7 @@ import androidx.compose.ui.unit.sp
38 38 import androidx.compose.foundation.shape.RoundedCornerShape
39 39 import androidx.compose.foundation.layout.size
40 40 import androidx.compose.material.icons.Icons
41 +import androidx.compose.material.icons.filled.ChevronRight
41 42 import androidx.compose.material.icons.filled.NotificationsActive
42 43 import androidx.compose.material3.Icon
43 44 import androidx.compose.runtime.collectAsState
@@ -54,9 +55,9 @@ import com.groupeka.ka.core.Universe
54 55 import com.groupeka.ka.core.UniverseService
55 56 import com.groupeka.ka.core.frFormatted
56 57 import com.groupeka.ka.encodeNavArg
57 import com.groupeka.ka.ui.theme.universeIcon
58 58 import com.groupeka.ka.ui.theme.GroupeKAMark
59 59 import com.groupeka.ka.ui.theme.KAImage
60 +import com.groupeka.ka.ui.theme.KALogo
60 61 import com.groupeka.ka.ui.theme.KATheme
61 62 import com.groupeka.ka.ui.theme.KAWordmark
62 63 import com.groupeka.ka.ui.theme.LocalKADark
@@ -272,7 +273,7 @@ fun HomeScreen(nav: NavHostController) {
272 273 item(key = "edit-${c.id}") {
273 274 Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
274 275 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
275 Icon(universeIcon(c.symbol), null, tint = uC.accent, modifier = Modifier.size(20.dp))
276 + KALogo(uC.id, size = 24.dp)
276 277 Column {
277 278 Text(c.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = KATheme.ink(dark))
278 279 Text(c.subtitle, style = MaterialTheme.typography.labelSmall, color = KATheme.ink2(dark))
@@ -339,6 +340,34 @@ fun HomeScreen(nav: NavHostController) {
339 340 }
340 341 }
341 342 }
343 + item {
344 + // — visiter le portail groupe-ka.com sans quitter l'app
345 + Row(
346 + Modifier
347 + .fillMaxWidth()
348 + .kaCard(KATheme.green)
349 + .clickable { nav.navigate("site/groupe-ka") }
350 + .padding(14.dp),
351 + verticalAlignment = Alignment.CenterVertically,
352 + ) {
353 + KALogo("groupe-ka", size = 40.dp)
354 + Spacer(Modifier.width(12.dp))
355 + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
356 + Text(
357 + "Visiter groupe-ka.com",
358 + style = MaterialTheme.typography.titleSmall,
359 + fontWeight = FontWeight.Bold,
360 + color = KATheme.ink(dark),
361 + )
362 + Text(
363 + "Le portail de l'écosystème — sans quitter l'app.",
364 + style = MaterialTheme.typography.labelMedium,
365 + color = KATheme.ink2(dark),
366 + )
367 + }
368 + Icon(Icons.Filled.ChevronRight, null, tint = KATheme.green, modifier = Modifier.size(18.dp))
369 + }
370 + }
342 371 item {
343 372 Text(
344 373 Ecosystem.disclaimer,
modified app/src/main/java/com/groupeka/ka/ui/ProfileScreen.kt +4 −5
@@ -60,12 +60,11 @@ import com.groupeka.ka.core.Ecosystem
60 60 import com.groupeka.ka.core.FavoritesStore
61 61 import com.groupeka.ka.core.Prefs
62 62 import com.groupeka.ka.core.RecentsStore
63 +import com.groupeka.ka.ui.theme.KALogo
63 64 import com.groupeka.ka.ui.theme.KATheme
64 65 import com.groupeka.ka.ui.theme.LocalKADark
65 import com.groupeka.ka.ui.theme.accent
66 66 import com.groupeka.ka.ui.theme.colorFromHex
67 67 import com.groupeka.ka.ui.theme.kaCard
68 import com.groupeka.ka.ui.theme.universeIcon
69 68
70 69 @Composable
71 70 fun ProfileScreen(nav: NavHostController) {
@@ -189,7 +188,7 @@ fun ProfileScreen(nav: NavHostController) {
189 188 .padding(vertical = 10.dp, horizontal = 14.dp),
190 189 verticalAlignment = Alignment.CenterVertically,
191 190 ) {
192 Icon(universeIcon(u.symbol), null, tint = u.accent, modifier = Modifier.size(22.dp))
191 + KALogo(u.id, size = 24.dp)
193 192 Spacer(Modifier.width(12.dp))
194 193 Text(u.name, color = KATheme.ink(dark))
195 194 Spacer(Modifier.weight(1f))
@@ -250,7 +249,7 @@ fun ProfileScreen(nav: NavHostController) {
250 249 }
251 250 HorizontalDivider(color = KATheme.ink(dark).copy(alpha = 0.08f))
252 251 }
253 ProfileLinkRow(Icons.Filled.Language, "groupe-ka.com — le portail") { openUrl(context, Ecosystem.hubURL) }
252 + ProfileLinkRow(Icons.Filled.Language, "Visiter groupe-ka.com — le portail") { nav.navigate("site/groupe-ka") }
254 253 HorizontalDivider(color = KATheme.ink(dark).copy(alpha = 0.08f))
255 254 ProfileLinkRow(Icons.Filled.MonitorHeart, "État des services en direct") { openUrl(context, Ecosystem.statusURL) }
256 255 }
@@ -285,7 +284,7 @@ fun ProfileScreen(nav: NavHostController) {
285 284 item {
286 285 Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
287 286 Text("À propos", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = KATheme.ink(dark))
288 Text("Version 1.0.0 · Écosystème : ${Ecosystem.all.size} univers", style = MaterialTheme.typography.labelMedium, color = KATheme.ink2(dark))
287 + Text("Version 3.0.0 · Écosystème : ${Ecosystem.all.size} univers", style = MaterialTheme.typography.labelMedium, color = KATheme.ink2(dark))
289 288 Text("KA — tout l'écosystème Groupe-KA dans votre poche. Fait au Québec. 💚", style = MaterialTheme.typography.labelMedium, color = KATheme.ink2(dark))
290 289 }
291 290 }
added app/src/main/java/com/groupeka/ka/ui/SiteScreen.kt +199 −0
@@ -0,0 +1,199 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// SiteScreen.kt — le navigateur intégré de l'écosystème : chaque univers (et
3 +// le hub groupe-ka.com) s'ouvre DANS l'app, dans une WebView aux couleurs de
4 +// la marque (logo officiel + accent), avec historique web (back matériel
5 +// inclus), rechargement, partage et repli vers le navigateur système.
6 +package com.groupeka.ka.ui
7 +
8 +import android.annotation.SuppressLint
9 +import android.content.Intent
10 +import android.graphics.Bitmap
11 +import android.webkit.WebChromeClient
12 +import android.webkit.WebResourceRequest
13 +import android.webkit.WebView
14 +import android.webkit.WebViewClient
15 +import androidx.activity.compose.BackHandler
16 +import androidx.compose.foundation.layout.Column
17 +import androidx.compose.foundation.layout.Row
18 +import androidx.compose.foundation.layout.Spacer
19 +import androidx.compose.foundation.layout.fillMaxSize
20 +import androidx.compose.foundation.layout.fillMaxWidth
21 +import androidx.compose.foundation.layout.height
22 +import androidx.compose.foundation.layout.padding
23 +import androidx.compose.foundation.layout.size
24 +import androidx.compose.foundation.layout.width
25 +import androidx.compose.material.icons.Icons
26 +import androidx.compose.material.icons.automirrored.filled.ArrowBack
27 +import androidx.compose.material.icons.automirrored.filled.ArrowForward
28 +import androidx.compose.material.icons.filled.Close
29 +import androidx.compose.material.icons.filled.OpenInBrowser
30 +import androidx.compose.material.icons.filled.Refresh
31 +import androidx.compose.material.icons.filled.Share
32 +import androidx.compose.material3.Icon
33 +import androidx.compose.material3.IconButton
34 +import androidx.compose.material3.LinearProgressIndicator
35 +import androidx.compose.material3.Surface
36 +import androidx.compose.material3.Text
37 +import androidx.compose.runtime.Composable
38 +import androidx.compose.runtime.DisposableEffect
39 +import androidx.compose.runtime.getValue
40 +import androidx.compose.runtime.mutableIntStateOf
41 +import androidx.compose.runtime.mutableStateOf
42 +import androidx.compose.runtime.remember
43 +import androidx.compose.runtime.setValue
44 +import androidx.compose.ui.Alignment
45 +import androidx.compose.ui.Modifier
46 +import androidx.compose.ui.graphics.Color
47 +import androidx.compose.ui.platform.LocalContext
48 +import androidx.compose.ui.text.font.FontWeight
49 +import androidx.compose.ui.text.style.TextOverflow
50 +import androidx.compose.ui.unit.dp
51 +import androidx.compose.ui.unit.sp
52 +import androidx.compose.ui.viewinterop.AndroidView
53 +import androidx.navigation.NavHostController
54 +import com.groupeka.ka.core.Ecosystem
55 +import com.groupeka.ka.ui.theme.KALogo
56 +import com.groupeka.ka.ui.theme.KATheme
57 +import com.groupeka.ka.ui.theme.accent
58 +
59 +// MARK: - Identité du site intégré (12 univers + le hub groupe-ka)
60 +
61 +private data class SiteBrand(val id: String, val name: String, val accent: Color, val url: String)
62 +
63 +private fun brandFor(universeId: String): SiteBrand {
64 + val u = Ecosystem.universe(universeId)
65 + return if (u != null) SiteBrand(u.id, u.name, u.accent, u.baseURL)
66 + else SiteBrand("groupe-ka", "Groupe KA", KATheme.green, Ecosystem.hubURL)
67 +}
68 +
69 +// MARK: - L'écran
70 +
71 +@SuppressLint("SetJavaScriptEnabled")
72 +@Composable
73 +fun SiteScreen(universeId: String, nav: NavHostController) {
74 + val brand = remember(universeId) { brandFor(universeId) }
75 + val context = LocalContext.current
76 +
77 + var webView by remember { mutableStateOf<WebView?>(null) }
78 + var progress by remember { mutableIntStateOf(0) }
79 + var canGoBack by remember { mutableStateOf(false) }
80 + var canGoForward by remember { mutableStateOf(false) }
81 + var currentUrl by remember { mutableStateOf(brand.url) }
82 +
83 + // Le back matériel remonte d'abord l'historique web, puis quitte l'écran.
84 + BackHandler(enabled = canGoBack) { webView?.goBack() }
85 +
86 + Column(Modifier.fillMaxSize()) {
87 + // — barre supérieure aux couleurs de la marque
88 + Surface(color = brand.accent, shadowElevation = 3.dp) {
89 + Column {
90 + Row(
91 + Modifier
92 + .fillMaxWidth()
93 + .padding(horizontal = 6.dp, vertical = 8.dp),
94 + verticalAlignment = Alignment.CenterVertically,
95 + ) {
96 + IconButton(onClick = { nav.popBackStack() }, modifier = Modifier.size(36.dp)) {
97 + Icon(Icons.Filled.Close, "Fermer", tint = Color.White)
98 + }
99 + Spacer(Modifier.width(4.dp))
100 + KALogo(brand.id, size = 28.dp)
101 + Spacer(Modifier.width(8.dp))
102 + Column(Modifier.weight(1f)) {
103 + Text(
104 + brand.name,
105 + fontWeight = FontWeight.Bold,
106 + fontSize = 14.sp,
107 + color = Color.White,
108 + maxLines = 1, overflow = TextOverflow.Ellipsis,
109 + )
110 + Text(
111 + currentUrl.removePrefix("https://").removePrefix("http://"),
112 + fontSize = 10.sp,
113 + color = Color.White.copy(alpha = 0.8f),
114 + maxLines = 1, overflow = TextOverflow.Ellipsis,
115 + )
116 + }
117 + IconButton(onClick = { webView?.goBack() }, enabled = canGoBack, modifier = Modifier.size(36.dp)) {
118 + Icon(
119 + Icons.AutoMirrored.Filled.ArrowBack, "Page précédente",
120 + tint = if (canGoBack) Color.White else Color.White.copy(alpha = 0.35f),
121 + )
122 + }
123 + IconButton(onClick = { webView?.goForward() }, enabled = canGoForward, modifier = Modifier.size(36.dp)) {
124 + Icon(
125 + Icons.AutoMirrored.Filled.ArrowForward, "Page suivante",
126 + tint = if (canGoForward) Color.White else Color.White.copy(alpha = 0.35f),
127 + )
128 + }
129 + IconButton(onClick = { webView?.reload() }, modifier = Modifier.size(36.dp)) {
130 + Icon(Icons.Filled.Refresh, "Recharger", tint = Color.White)
131 + }
132 + IconButton(onClick = { shareUrl(context, currentUrl, brand.name) }, modifier = Modifier.size(36.dp)) {
133 + Icon(Icons.Filled.Share, "Partager la page", tint = Color.White)
134 + }
135 + IconButton(onClick = { openUrl(context, currentUrl) }, modifier = Modifier.size(36.dp)) {
136 + Icon(Icons.Filled.OpenInBrowser, "Ouvrir dans le navigateur", tint = Color.White)
137 + }
138 + }
139 + // — progression de chargement (accent = blanc sur fond de marque)
140 + if (progress in 1..99) {
141 + LinearProgressIndicator(
142 + progress = { progress / 100f },
143 + color = Color.White,
144 + trackColor = Color.White.copy(alpha = 0.25f),
145 + modifier = Modifier.fillMaxWidth().height(3.dp),
146 + )
147 + }
148 + }
149 + }
150 +
151 + // — la WebView elle-même
152 + AndroidView(
153 + modifier = Modifier.weight(1f).fillMaxWidth(),
154 + factory = { ctx ->
155 + WebView(ctx).apply {
156 + settings.javaScriptEnabled = true
157 + settings.domStorageEnabled = true
158 + settings.loadWithOverviewMode = true
159 + settings.useWideViewPort = true
160 + webViewClient = object : WebViewClient() {
161 + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
162 + val uri = request?.url ?: return false
163 + // http(s) reste dans la WebView ; tel:, mailto:, geo:… → système
164 + return if (uri.scheme == "http" || uri.scheme == "https") {
165 + false
166 + } else {
167 + runCatching { ctx.startActivity(Intent(Intent.ACTION_VIEW, uri)) }
168 + true
169 + }
170 + }
171 +
172 + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
173 + url?.let { currentUrl = it }
174 + canGoBack = view?.canGoBack() == true
175 + canGoForward = view?.canGoForward() == true
176 + }
177 +
178 + override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {
179 + url?.let { currentUrl = it }
180 + canGoBack = view?.canGoBack() == true
181 + canGoForward = view?.canGoForward() == true
182 + }
183 + }
184 + webChromeClient = object : WebChromeClient() {
185 + override fun onProgressChanged(view: WebView?, newProgress: Int) {
186 + progress = newProgress
187 + }
188 + }
189 + loadUrl(brand.url)
190 + webView = this
191 + }
192 + },
193 + )
194 + }
195 +
196 + DisposableEffect(Unit) {
197 + onDispose { webView?.destroy() }
198 + }
199 +}
modified app/src/main/java/com/groupeka/ka/ui/UniversesScreen.kt +11 −19
@@ -59,7 +59,6 @@ import androidx.compose.runtime.setValue
59 59 import androidx.compose.ui.Alignment
60 60 import androidx.compose.ui.Modifier
61 61 import androidx.compose.ui.graphics.Color
62 import androidx.compose.ui.platform.LocalContext
63 62 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
64 63 import androidx.compose.ui.text.font.FontFamily
65 64 import androidx.compose.ui.text.font.FontWeight
@@ -79,14 +78,13 @@ import com.groupeka.ka.core.money0
79 78 import com.groupeka.ka.encodeNavArg
80 79 import com.groupeka.ka.ui.theme.KAEmptyState
81 80 import com.groupeka.ka.ui.theme.KAItemRow
81 +import com.groupeka.ka.ui.theme.KALogo
82 82 import com.groupeka.ka.ui.theme.KASkeletonRow
83 83 import com.groupeka.ka.ui.theme.KATheme
84 84 import com.groupeka.ka.ui.theme.KAWordmark
85 85 import com.groupeka.ka.ui.theme.LocalKADark
86 86 import com.groupeka.ka.ui.theme.accent
87 87 import com.groupeka.ka.ui.theme.kaCard
88 import com.groupeka.ka.ui.theme.universeIcon
89 import com.groupeka.ka.ui.openUrl
90 88 import kotlinx.coroutines.async
91 89 import kotlinx.coroutines.awaitAll
92 90 import kotlinx.coroutines.coroutineScope
@@ -134,14 +132,8 @@ private fun UniverseCard(universe: Universe, total: Int?, modifier: Modifier = M
134 132 verticalArrangement = Arrangement.spacedBy(10.dp),
135 133 ) {
136 134 Row(verticalAlignment = Alignment.CenterVertically) {
137 Box(
138 Modifier
139 .size(40.dp)
140 .background(universe.accent.copy(alpha = 0.14f), RoundedCornerShape(11.dp)),
141 contentAlignment = Alignment.Center,
142 ) {
143 Icon(universeIcon(universe.symbol), null, tint = universe.accent)
144 }
135 + // le VRAI logo de la marque (coins arrondis inclus dans le PNG)
136 + KALogo(universe.id, size = 40.dp)
145 137 Spacer(Modifier.weight(1f))
146 138 Icon(Icons.Filled.ChevronRight, null, tint = KATheme.ink2(dark), modifier = Modifier.size(16.dp))
147 139 }
@@ -173,21 +165,20 @@ fun UniverseHomeScreen(universe: Universe, nav: NavHostController) {
173 165 when {
174 166 universe.id == "vrai-prix" -> VraiPrixScreen(universe, nav)
175 167 universe.id == "api-ka" -> ApiKaPlaygroundScreen(universe)
176 universe.listPath == null -> WebUniverse(universe)
168 + universe.listPath == null -> WebUniverse(universe, nav)
177 169 else -> UniverseList(universe, nav)
178 170 }
179 171 }
180 172
181 173 @Composable
182 private fun WebUniverse(universe: Universe) {
183 val context = LocalContext.current
174 +private fun WebUniverse(universe: Universe, nav: NavHostController) {
184 175 val dark = LocalKADark.current
185 176 Column(
186 177 Modifier.fillMaxSize().padding(30.dp),
187 178 horizontalAlignment = Alignment.CenterHorizontally,
188 179 verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.CenterVertically),
189 180 ) {
190 Icon(universeIcon(universe.symbol), null, tint = universe.accent, modifier = Modifier.size(52.dp))
181 + KALogo(universe.id, size = 72.dp)
191 182 KAWordmark(universe, size = 30f)
192 183 Text(
193 184 universe.tagline,
@@ -198,7 +189,7 @@ private fun WebUniverse(universe: Universe) {
198 189 Row(
199 190 Modifier
200 191 .background(universe.accent, CircleShape)
201 .clickable { openUrl(context, universe.baseURL) }
192 + .clickable { nav.navigate("site/${universe.id}") }
202 193 .padding(horizontal = 22.dp, vertical = 13.dp),
203 194 verticalAlignment = Alignment.CenterVertically,
204 195 ) {
@@ -213,7 +204,6 @@ private fun WebUniverse(universe: Universe) {
213 204 @Composable
214 205 private fun UniverseList(universe: Universe, nav: NavHostController) {
215 206 val dark = LocalKADark.current
216 val context = LocalContext.current
217 207 val scope = rememberCoroutineScope()
218 208 val keyboard = LocalSoftwareKeyboardController.current
219 209 var items by remember { mutableStateOf<List<KAItem>>(emptyList()) }
@@ -259,11 +249,13 @@ private fun UniverseList(universe: Universe, nav: NavHostController) {
259 249 modifier = Modifier.fillMaxSize(),
260 250 ) {
261 251 item {
262 // barre titre + lien vers le site
252 + // barre titre : logo officiel + wordmark + site intégré
263 253 Row(verticalAlignment = Alignment.CenterVertically) {
254 + KALogo(universe.id, size = 34.dp)
255 + Spacer(Modifier.width(10.dp))
264 256 KAWordmark(universe, size = 22f)
265 257 Spacer(Modifier.weight(1f))
266 IconButton(onClick = { openUrl(context, universe.baseURL) }) {
258 + IconButton(onClick = { nav.navigate("site/${universe.id}") }) {
267 259 Icon(Icons.Filled.OpenInNew, "Ouvrir le site ${universe.name}", tint = universe.accent)
268 260 }
269 261 }
modified app/src/main/java/com/groupeka/ka/ui/theme/Theme.kt +14 −0
@@ -9,6 +9,7 @@ import androidx.compose.animation.core.animateFloat
9 9 import androidx.compose.animation.core.infiniteRepeatable
10 10 import androidx.compose.animation.core.rememberInfiniteTransition
11 11 import androidx.compose.animation.core.tween
12 +import androidx.compose.foundation.Image
12 13 import androidx.compose.foundation.background
13 14 import androidx.compose.foundation.border
14 15 import androidx.compose.foundation.layout.Arrangement
@@ -62,6 +63,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
62 63 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
63 64 import androidx.compose.ui.layout.ContentScale
64 65 import androidx.compose.ui.platform.LocalHapticFeedback
66 +import androidx.compose.ui.res.painterResource
65 67 import androidx.compose.ui.text.font.FontFamily
66 68 import androidx.compose.ui.text.font.FontWeight
67 69 import androidx.compose.ui.text.style.TextOverflow
@@ -76,6 +78,7 @@ import com.groupeka.ka.core.FavoritesStore
76 78 import com.groupeka.ka.core.KAItem
77 79 import com.groupeka.ka.core.Prefs
78 80 import com.groupeka.ka.core.Universe
81 +import com.groupeka.ka.core.logoResFor
79 82
80 83 // MARK: - Palette
81 84
@@ -233,6 +236,17 @@ fun KAChip(text: String, accent: Color? = null) {
233 236 )
234 237 }
235 238
239 +// MARK: - Logo officiel d'une marque (PNG ka-ui, coins déjà arrondis dans l'image)
240 +
241 +@Composable
242 +fun KALogo(id: String, size: Dp = 40.dp, modifier: Modifier = Modifier) {
243 + Image(
244 + painter = painterResource(logoResFor(id)),
245 + contentDescription = null,
246 + modifier = modifier.size(size),
247 + )
248 +}
249 +
236 250 // MARK: - Icônes d'univers (clé symbol → Material)
237 251
238 252 fun universeIcon(symbol: String): ImageVector = when (symbol) {
added app/src/main/res/drawable-nodpi/logo_api_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_auto_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_crea_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_fabri_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_food_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_groupe_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_immo_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_job_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_lou_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_resto_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_sorti_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_trouve_ka.png +0 −0

Binary file not shown.

added app/src/main/res/drawable-nodpi/logo_vrai_prix.png +0 −0

Binary file not shown.

added app/src/test/java/com/groupeka/ka/LogoTests.kt +32 −0
@@ -0,0 +1,32 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// LogoTests.kt — tests v3 : chaque marque (les 12 univers + le hub groupe-ka)
3 +// a son logo officiel, tous distincts, et tout id inconnu retombe sur le hub.
4 +package com.groupeka.ka
5 +
6 +import com.groupeka.ka.core.Ecosystem
7 +import com.groupeka.ka.core.logoResFor
8 +import org.junit.Assert.assertEquals
9 +import org.junit.Assert.assertTrue
10 +import org.junit.Test
11 +
12 +class LogoTests {
13 +
14 + @Test
15 + fun chaqueUniversASonLogoOfficielDistinct() {
16 + assertEquals(12, Ecosystem.all.size)
17 + val res = Ecosystem.all.map { logoResFor(it.id) }
18 + // un logo par univers, tous distincts et valides
19 + assertEquals(res.size, res.toSet().size)
20 + res.forEach { assertTrue(it != 0) }
21 + // le hub a le sien, différent de ceux des univers
22 + assertTrue(logoResFor("groupe-ka") !in res)
23 + }
24 +
25 + @Test
26 + fun logoHelperEtProprieteConcordentEtInconnuRetombeSurLeHub() {
27 + // la propriété du modèle passe par le même mapping
28 + Ecosystem.all.forEach { u -> assertEquals(logoResFor(u.id), u.logoRes) }
29 + // id inconnu → logo du hub (repli sûr)
30 + assertEquals(logoResFor("groupe-ka"), logoResFor("univers-inconnu"))
31 + }
32 +}
33