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>
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 | ||