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 applicationId = "com.groupeka.ka" 14 applicationId = "com.groupeka.ka"
15 minSdk = 26 15 minSdk = 26
16 targetSdk = 34 16 targetSdk = 34
17− versionCode = 2 17+ versionCode = 3
18− versionName = "2.0.0" 18+ versionName = "3.0.0"
19 } 19 }
20 20
21 signingConfigs { 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 import com.groupeka.ka.ui.ProfileScreen 63 import com.groupeka.ka.ui.ProfileScreen
64 import com.groupeka.ka.ui.SavedSearchesScreen 64 import com.groupeka.ka.ui.SavedSearchesScreen
65 import com.groupeka.ka.ui.SearchScreen 65 import com.groupeka.ka.ui.SearchScreen
66+import com.groupeka.ka.ui.SiteScreen
66 import com.groupeka.ka.ui.UniverseHomeScreen 67 import com.groupeka.ka.ui.UniverseHomeScreen
67 import com.groupeka.ka.ui.UniversesScreen 68 import com.groupeka.ka.ui.UniversesScreen
68 import com.groupeka.ka.ui.theme.KAAppTheme 69 import com.groupeka.ka.ui.theme.KAAppTheme
@@ -184,6 +185,11 @@ fun RootView() {
184 val id = entry.arguments?.getString("id") 185 val id = entry.arguments?.getString("id")
185 Ecosystem.universe(id ?: "")?.let { UniverseHomeScreen(it, nav) } 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 composable("item/{data}") { entry -> 193 composable("item/{data}") { entry ->
188 val item = entry.arguments?.getString("data")?.let { decodeNavArg<KAItem>(it) } 194 val item = entry.arguments?.getString("data")?.let { decodeNavArg<KAItem>(it) }
189 if (item != null) ItemDetailScreen(item, nav) 195 if (item != null) ItemDetailScreen(item, nav)
@@ -196,8 +202,9 @@ fun RootView() {
196 } 202 }
197 203
198 // Bulle KA Agent — flottante au-dessus de la tab bar, partout sauf 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) 205+ // dans l'agent, sur la carte (contrôles de zone au même endroit)
200− if (route != "agent" && route != "map") { 206+ // et dans le navigateur intégré (toute la surface au site)
207+ if (route != "agent" && route != "map" && route?.startsWith("site/") != true) {
201 Box( 208 Box(
202 Modifier 209 Modifier
203 .align(Alignment.BottomEnd) 210 .align(Alignment.BottomEnd)
modified app/src/main/java/com/groupeka/ka/core/Ecosystem.kt +24 −0
@@ -5,6 +5,7 @@
5 // une entrée ici, rien d'autre. Miroir exact de KA/Core/Ecosystem.swift (iOS). 5 // une entrée ici, rien d'autre. Miroir exact de KA/Core/Ecosystem.swift (iOS).
6 package com.groupeka.ka.core 6 package com.groupeka.ka.core
7 7
8+import com.groupeka.ka.R
8 import kotlinx.serialization.Serializable 9 import kotlinx.serialization.Serializable
9 import kotlinx.serialization.json.JsonObject 10 import kotlinx.serialization.json.JsonObject
10 import java.text.NumberFormat 11 import java.text.NumberFormat
@@ -57,6 +58,29 @@ data class Universe(
57 val map: ((JsonObject) -> KAItem?)?, 58 val map: ((JsonObject) -> KAItem?)?,
58 ) { 59 ) {
59 val baseURL: String get() = "https://$domain" 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 object Ecosystem { 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 import androidx.compose.foundation.shape.RoundedCornerShape 38 import androidx.compose.foundation.shape.RoundedCornerShape
39 import androidx.compose.foundation.layout.size 39 import androidx.compose.foundation.layout.size
40 import androidx.compose.material.icons.Icons 40 import androidx.compose.material.icons.Icons
41+import androidx.compose.material.icons.filled.ChevronRight
41 import androidx.compose.material.icons.filled.NotificationsActive 42 import androidx.compose.material.icons.filled.NotificationsActive
42 import androidx.compose.material3.Icon 43 import androidx.compose.material3.Icon
43 import androidx.compose.runtime.collectAsState 44 import androidx.compose.runtime.collectAsState
@@ -54,9 +55,9 @@ import com.groupeka.ka.core.Universe
54 import com.groupeka.ka.core.UniverseService 55 import com.groupeka.ka.core.UniverseService
55 import com.groupeka.ka.core.frFormatted 56 import com.groupeka.ka.core.frFormatted
56 import com.groupeka.ka.encodeNavArg 57 import com.groupeka.ka.encodeNavArg
57−import com.groupeka.ka.ui.theme.universeIcon
58 import com.groupeka.ka.ui.theme.GroupeKAMark 58 import com.groupeka.ka.ui.theme.GroupeKAMark
59 import com.groupeka.ka.ui.theme.KAImage 59 import com.groupeka.ka.ui.theme.KAImage
60+import com.groupeka.ka.ui.theme.KALogo
60 import com.groupeka.ka.ui.theme.KATheme 61 import com.groupeka.ka.ui.theme.KATheme
61 import com.groupeka.ka.ui.theme.KAWordmark 62 import com.groupeka.ka.ui.theme.KAWordmark
62 import com.groupeka.ka.ui.theme.LocalKADark 63 import com.groupeka.ka.ui.theme.LocalKADark
@@ -272,7 +273,7 @@ fun HomeScreen(nav: NavHostController) {
272 item(key = "edit-${c.id}") { 273 item(key = "edit-${c.id}") {
273 Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { 274 Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
274 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { 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 Column { 277 Column {
277 Text(c.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = KATheme.ink(dark)) 278 Text(c.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = KATheme.ink(dark))
278 Text(c.subtitle, style = MaterialTheme.typography.labelSmall, color = KATheme.ink2(dark)) 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 item { 371 item {
343 Text( 372 Text(
344 Ecosystem.disclaimer, 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 import com.groupeka.ka.core.FavoritesStore 60 import com.groupeka.ka.core.FavoritesStore
61 import com.groupeka.ka.core.Prefs 61 import com.groupeka.ka.core.Prefs
62 import com.groupeka.ka.core.RecentsStore 62 import com.groupeka.ka.core.RecentsStore
63+import com.groupeka.ka.ui.theme.KALogo
63 import com.groupeka.ka.ui.theme.KATheme 64 import com.groupeka.ka.ui.theme.KATheme
64 import com.groupeka.ka.ui.theme.LocalKADark 65 import com.groupeka.ka.ui.theme.LocalKADark
65−import com.groupeka.ka.ui.theme.accent
66 import com.groupeka.ka.ui.theme.colorFromHex 66 import com.groupeka.ka.ui.theme.colorFromHex
67 import com.groupeka.ka.ui.theme.kaCard 67 import com.groupeka.ka.ui.theme.kaCard
68−import com.groupeka.ka.ui.theme.universeIcon
69 68
70 @Composable 69 @Composable
71 fun ProfileScreen(nav: NavHostController) { 70 fun ProfileScreen(nav: NavHostController) {
@@ -189,7 +188,7 @@ fun ProfileScreen(nav: NavHostController) {
189 .padding(vertical = 10.dp, horizontal = 14.dp), 188 .padding(vertical = 10.dp, horizontal = 14.dp),
190 verticalAlignment = Alignment.CenterVertically, 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 Spacer(Modifier.width(12.dp)) 192 Spacer(Modifier.width(12.dp))
194 Text(u.name, color = KATheme.ink(dark)) 193 Text(u.name, color = KATheme.ink(dark))
195 Spacer(Modifier.weight(1f)) 194 Spacer(Modifier.weight(1f))
@@ -250,7 +249,7 @@ fun ProfileScreen(nav: NavHostController) {
250 } 249 }
251 HorizontalDivider(color = KATheme.ink(dark).copy(alpha = 0.08f)) 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 HorizontalDivider(color = KATheme.ink(dark).copy(alpha = 0.08f)) 253 HorizontalDivider(color = KATheme.ink(dark).copy(alpha = 0.08f))
255 ProfileLinkRow(Icons.Filled.MonitorHeart, "État des services en direct") { openUrl(context, Ecosystem.statusURL) } 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 item { 284 item {
286 Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { 285 Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
287 Text("À propos", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = KATheme.ink(dark)) 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 Text("KA — tout l'écosystème Groupe-KA dans votre poche. Fait au Québec. 💚", style = MaterialTheme.typography.labelMedium, color = KATheme.ink2(dark)) 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 import androidx.compose.ui.Alignment 59 import androidx.compose.ui.Alignment
60 import androidx.compose.ui.Modifier 60 import androidx.compose.ui.Modifier
61 import androidx.compose.ui.graphics.Color 61 import androidx.compose.ui.graphics.Color
62−import androidx.compose.ui.platform.LocalContext
63 import androidx.compose.ui.platform.LocalSoftwareKeyboardController 62 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
64 import androidx.compose.ui.text.font.FontFamily 63 import androidx.compose.ui.text.font.FontFamily
65 import androidx.compose.ui.text.font.FontWeight 64 import androidx.compose.ui.text.font.FontWeight
@@ -79,14 +78,13 @@ import com.groupeka.ka.core.money0
79 import com.groupeka.ka.encodeNavArg 78 import com.groupeka.ka.encodeNavArg
80 import com.groupeka.ka.ui.theme.KAEmptyState 79 import com.groupeka.ka.ui.theme.KAEmptyState
81 import com.groupeka.ka.ui.theme.KAItemRow 80 import com.groupeka.ka.ui.theme.KAItemRow
81+import com.groupeka.ka.ui.theme.KALogo
82 import com.groupeka.ka.ui.theme.KASkeletonRow 82 import com.groupeka.ka.ui.theme.KASkeletonRow
83 import com.groupeka.ka.ui.theme.KATheme 83 import com.groupeka.ka.ui.theme.KATheme
84 import com.groupeka.ka.ui.theme.KAWordmark 84 import com.groupeka.ka.ui.theme.KAWordmark
85 import com.groupeka.ka.ui.theme.LocalKADark 85 import com.groupeka.ka.ui.theme.LocalKADark
86 import com.groupeka.ka.ui.theme.accent 86 import com.groupeka.ka.ui.theme.accent
87 import com.groupeka.ka.ui.theme.kaCard 87 import com.groupeka.ka.ui.theme.kaCard
88−import com.groupeka.ka.ui.theme.universeIcon
89−import com.groupeka.ka.ui.openUrl
90 import kotlinx.coroutines.async 88 import kotlinx.coroutines.async
91 import kotlinx.coroutines.awaitAll 89 import kotlinx.coroutines.awaitAll
92 import kotlinx.coroutines.coroutineScope 90 import kotlinx.coroutines.coroutineScope
@@ -134,14 +132,8 @@ private fun UniverseCard(universe: Universe, total: Int?, modifier: Modifier = M
134 verticalArrangement = Arrangement.spacedBy(10.dp), 132 verticalArrangement = Arrangement.spacedBy(10.dp),
135 ) { 133 ) {
136 Row(verticalAlignment = Alignment.CenterVertically) { 134 Row(verticalAlignment = Alignment.CenterVertically) {
137− Box( 135+ // le VRAI logo de la marque (coins arrondis inclus dans le PNG)
138− Modifier 136+ KALogo(universe.id, size = 40.dp)
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− }
145 Spacer(Modifier.weight(1f)) 137 Spacer(Modifier.weight(1f))
146 Icon(Icons.Filled.ChevronRight, null, tint = KATheme.ink2(dark), modifier = Modifier.size(16.dp)) 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 when { 165 when {
174 universe.id == "vrai-prix" -> VraiPrixScreen(universe, nav) 166 universe.id == "vrai-prix" -> VraiPrixScreen(universe, nav)
175 universe.id == "api-ka" -> ApiKaPlaygroundScreen(universe) 167 universe.id == "api-ka" -> ApiKaPlaygroundScreen(universe)
176− universe.listPath == null -> WebUniverse(universe) 168+ universe.listPath == null -> WebUniverse(universe, nav)
177 else -> UniverseList(universe, nav) 169 else -> UniverseList(universe, nav)
178 } 170 }
179 } 171 }
180 172
181 @Composable 173 @Composable
182−private fun WebUniverse(universe: Universe) { 174+private fun WebUniverse(universe: Universe, nav: NavHostController) {
183− val context = LocalContext.current
184 val dark = LocalKADark.current 175 val dark = LocalKADark.current
185 Column( 176 Column(
186 Modifier.fillMaxSize().padding(30.dp), 177 Modifier.fillMaxSize().padding(30.dp),
187 horizontalAlignment = Alignment.CenterHorizontally, 178 horizontalAlignment = Alignment.CenterHorizontally,
188 verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.CenterVertically), 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 KAWordmark(universe, size = 30f) 182 KAWordmark(universe, size = 30f)
192 Text( 183 Text(
193 universe.tagline, 184 universe.tagline,
@@ -198,7 +189,7 @@ private fun WebUniverse(universe: Universe) {
198 Row( 189 Row(
199 Modifier 190 Modifier
200 .background(universe.accent, CircleShape) 191 .background(universe.accent, CircleShape)
201− .clickable { openUrl(context, universe.baseURL) } 192+ .clickable { nav.navigate("site/${universe.id}") }
202 .padding(horizontal = 22.dp, vertical = 13.dp), 193 .padding(horizontal = 22.dp, vertical = 13.dp),
203 verticalAlignment = Alignment.CenterVertically, 194 verticalAlignment = Alignment.CenterVertically,
204 ) { 195 ) {
@@ -213,7 +204,6 @@ private fun WebUniverse(universe: Universe) {
213 @Composable 204 @Composable
214 private fun UniverseList(universe: Universe, nav: NavHostController) { 205 private fun UniverseList(universe: Universe, nav: NavHostController) {
215 val dark = LocalKADark.current 206 val dark = LocalKADark.current
216− val context = LocalContext.current
217 val scope = rememberCoroutineScope() 207 val scope = rememberCoroutineScope()
218 val keyboard = LocalSoftwareKeyboardController.current 208 val keyboard = LocalSoftwareKeyboardController.current
219 var items by remember { mutableStateOf<List<KAItem>>(emptyList()) } 209 var items by remember { mutableStateOf<List<KAItem>>(emptyList()) }
@@ -259,11 +249,13 @@ private fun UniverseList(universe: Universe, nav: NavHostController) {
259 modifier = Modifier.fillMaxSize(), 249 modifier = Modifier.fillMaxSize(),
260 ) { 250 ) {
261 item { 251 item {
262− // barre titre + lien vers le site 252+ // barre titre : logo officiel + wordmark + site intégré
263 Row(verticalAlignment = Alignment.CenterVertically) { 253 Row(verticalAlignment = Alignment.CenterVertically) {
254+ KALogo(universe.id, size = 34.dp)
255+ Spacer(Modifier.width(10.dp))
264 KAWordmark(universe, size = 22f) 256 KAWordmark(universe, size = 22f)
265 Spacer(Modifier.weight(1f)) 257 Spacer(Modifier.weight(1f))
266− IconButton(onClick = { openUrl(context, universe.baseURL) }) { 258+ IconButton(onClick = { nav.navigate("site/${universe.id}") }) {
267 Icon(Icons.Filled.OpenInNew, "Ouvrir le site ${universe.name}", tint = universe.accent) 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 import androidx.compose.animation.core.infiniteRepeatable 9 import androidx.compose.animation.core.infiniteRepeatable
10 import androidx.compose.animation.core.rememberInfiniteTransition 10 import androidx.compose.animation.core.rememberInfiniteTransition
11 import androidx.compose.animation.core.tween 11 import androidx.compose.animation.core.tween
12+import androidx.compose.foundation.Image
12 import androidx.compose.foundation.background 13 import androidx.compose.foundation.background
13 import androidx.compose.foundation.border 14 import androidx.compose.foundation.border
14 import androidx.compose.foundation.layout.Arrangement 15 import androidx.compose.foundation.layout.Arrangement
@@ -62,6 +63,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
62 import androidx.compose.ui.hapticfeedback.HapticFeedbackType 63 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
63 import androidx.compose.ui.layout.ContentScale 64 import androidx.compose.ui.layout.ContentScale
64 import androidx.compose.ui.platform.LocalHapticFeedback 65 import androidx.compose.ui.platform.LocalHapticFeedback
66+import androidx.compose.ui.res.painterResource
65 import androidx.compose.ui.text.font.FontFamily 67 import androidx.compose.ui.text.font.FontFamily
66 import androidx.compose.ui.text.font.FontWeight 68 import androidx.compose.ui.text.font.FontWeight
67 import androidx.compose.ui.text.style.TextOverflow 69 import androidx.compose.ui.text.style.TextOverflow
@@ -76,6 +78,7 @@ import com.groupeka.ka.core.FavoritesStore
76 import com.groupeka.ka.core.KAItem 78 import com.groupeka.ka.core.KAItem
77 import com.groupeka.ka.core.Prefs 79 import com.groupeka.ka.core.Prefs
78 import com.groupeka.ka.core.Universe 80 import com.groupeka.ka.core.Universe
81+import com.groupeka.ka.core.logoResFor
79 82
80 // MARK: - Palette 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 // MARK: - Icônes d'univers (clé symbol → Material) 250 // MARK: - Icônes d'univers (clé symbol → Material)
237 251
238 fun universeIcon(symbol: String): ImageVector = when (symbol) { 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