SPB Git

spb/ka-android

Public

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 (Aug 21, 2026) parent cd1b864 Browse files

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
Menu

Theme