From f6a12ad84af30d70dfe78653384e715c789cfea2 Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Wed, 8 Jul 2026 12:47:38 +0200 Subject: [PATCH] new: add offline fallback support for WebView --- .../composeResources/files/offline_about.html | 310 ++++++++++++++++++ .../files/offline_deepen.html | 277 ++++++++++++++++ .../chombev/web/ui/PlatformWebView.android.kt | 14 +- .../web/ui/WebClientDelegatingExtLinks.kt | 9 + .../kotlin/bzh/ajaury/chombev/di/Modules.kt | 2 + .../bzh/ajaury/chombev/navigation/WebPage.kt | 15 +- .../ajaury/chombev/web/ui/PlatformWebView.kt | 4 + .../ajaury/chombev/web/ui/WebViewScreen.kt | 49 ++- .../chombev/web/ui/viewmodel/WebUiState.kt | 16 + .../chombev/web/ui/viewmodel/WebViewModel.kt | 42 +++ .../chombev/web/ui/PlatformWebView.ios.kt | 26 +- .../chombev/web/ui/PlatformWebView.js.kt | 1 + .../chombev/web/ui/PlatformWebView.jvm.kt | 1 + .../chombev/web/ui/PlatformWebView.wasmJs.kt | 1 + 14 files changed, 747 insertions(+), 20 deletions(-) create mode 100644 data/resources/src/commonMain/composeResources/files/offline_about.html create mode 100644 data/resources/src/commonMain/composeResources/files/offline_deepen.html create mode 100644 shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebUiState.kt create mode 100644 shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebViewModel.kt diff --git a/data/resources/src/commonMain/composeResources/files/offline_about.html b/data/resources/src/commonMain/composeResources/files/offline_about.html new file mode 100644 index 0000000..6466523 --- /dev/null +++ b/data/resources/src/commonMain/composeResources/files/offline_about.html @@ -0,0 +1,310 @@ + + + + + + + + + App Chom Bev – À propos – Kerlenn Sten Kidna + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + +
+ + + + +
+ +

ℹ️ À propos

+ + + +
"Chom Bev e Brezhoneg" est une application qui vous est proposée gratuitement par l'association Kerlenn Sten Kidna dans le but de vous aider à vous initier au Breton, et particulièrement aux sonorités et tournures typiques du Breton Vannetais.
+ + + +

🎙️ Contributeurs

+ + + +

Contenu

+ + + +

Cette application vous est proposée par l’association Kerlenn Sten Kidna (KSK). « Chom Bev e Brezhoneg » est une oeuvre créée par KSK, initialement sous la forme d’un livret imprimé et accompagné d’un CD audio. Il est adapté ici au format numérique. L’association KSK a notamment pour mission de faire vivre le patrimoine culturel breton en pays d’Auray. Elle propose un ensemble de ressources (collectages audio, vidéo, écrits, …) mais aussi des rencontres (stages, soirées jeux, cafés pain-beurre, club lecteur …) et des cours hebdomadaires de breton (en journée & en soirée, en présentiel & à distance).

+ + + +

Le contenu, les textes et enregistrements ont été réalisés par l’association, et restent la propriété de l’association. Pour une réutilisation en dehors de l’application, merci de vous rapprocher de l’association.

+ + + +

Sur les enregistrement, nous retrouvons deux voix :

+ + + +
    +
  • Patrig Dréan, actuel président de Kerlenn Sten Kidna, formateur et auteur de nombreuses œuvres en breton.
  • + + + +
  • ?
  • +
+ + + +

Conception de l’app, pilotage & développement

+ + + +

La conception et le développement de l’application sont pilotés et réalisés par Antoine Jaury, développeur professionnel d’applications mobiles. Le code source de l’application est ouvert (Open Source), et utilise Kotlin Multi-Platform (KMP) avec Compose Multi-Platform (CMP) afin de proposer une application fonctionnant sur Android, iOS, navigateur Web, mais aussi en application bureau.

+ + + +

Le code source de l’application est disponible sur : https://…/chombev

+ + + +

💬 C’est quoi le Breton Vannetais (Gwenedeg) ?

+ + + +

Le breton Vannetais regroupe une variété de bretons parlée dans la quasi-totalité de la partie bretonnante du Morbihan, et un peu au-delà. C’est le cas notamment du Pays d’Auray actuel.

+ + + +

+ ajout d’une carte ?

+ + + +

🤙🏼 Contact

+ + + +

Vous souhaitez contacter l’association Kerlenn Sten Kidna : ksk@ksk.bzh

+ + + +

Vous avez une question, une suggestion concernant l’application : antoine@jaury.eu

+ + + +

🔒 Politique de confidentialité

+ + + +

Cette application ne demande ni ne collecte de données personnelles telles que votre nom, votre adresse électronique ou votre position géographique.

+ + + +

Les contenus audio sont intégrés à l’application et restent accessibles hors connexion.

+ + + +

Les sections « Approfondir » et « À propos » affichent des pages web hébergées sous WordPress. Lors de leur consultation, des données techniques nécessaires au fonctionnement du serveur web peuvent être traitées (par exemple l’adresse IP, les informations du navigateur ou les journaux de connexion).

+ + + +

Afin d’améliorer la stabilité de l’application, un service de collecte des erreurs (GlitchTip) est utilisé. En cas de plantage ou d’erreur inattendue, des informations techniques (telles que la version de l’application, le modèle de l’appareil, la version du système d’exploitation et la trace de l’erreur) peuvent être transmises. Ces informations sont utilisées uniquement pour diagnostiquer et corriger les problèmes techniques.

+ + + +

L’ensemble de ces données sont stockées sur des serveurs hébergés en Europe :

+ + + +
    +
  • En France pour les pages web, hébergées chez l’Association LA KAZ,
  • + + + +
  • En Allemagne pour les données de stabilité avec GlitchTip Europe.
  • +
+ + + +

Pour plus de détails, veuillez consulter notre : politique de confidentialité.

+ + + +

🧑🏽‍⚖️ Mentions légales

+ + + +

Les mentions légales de l’application sont disponibles via ce lien.

+ + + +
+
+ + + + + +
+ + + +
+ +
+ +
+ + + + + + + diff --git a/data/resources/src/commonMain/composeResources/files/offline_deepen.html b/data/resources/src/commonMain/composeResources/files/offline_deepen.html new file mode 100644 index 0000000..1fc8a43 --- /dev/null +++ b/data/resources/src/commonMain/composeResources/files/offline_deepen.html @@ -0,0 +1,277 @@ + + + + + + + + + App Chom Bev -​ Approfondir – Kerlenn Sten Kidna + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + +
+ + + + +
+ +

📚 Approfondir

+ + + +
Voici quelques propositions pour aller plus loin dans votre apprentissage du Breton, et particulièrement du Breton Vannetais.

Pour trouver des ressources concernant la culture et la langue bretonnes sur le Pays d'Auray, suivez les publications de Ti Douar Alre.
+ + + +

🧑🏼‍🏫 Cours hebdo.

+ + + +

Sur le Pays d’Auray

+ + + +

Kerlenn Sten Kidna (KSK) propose des cours hebdomadaires : en journée, mais aussi en soirée ; en présentiel, mais aussi une ligne. Différents niveaux sont proposés, de quoi permettre à chacun·e de trouver un créneau qui lui convienne.

+ + + +

Locoal-Mendon, Plouhinec

+ + + +

L’association Ar Vammenn propose des cours du soir pour les débutant·e·s à Locoal-Mendon.

+ + + +

🍻 Les rencontres

+ + + +

bev.bzh permet aux personnes souhaitant parler Breton entre elles de se retrouver, quelque soit le lieu. Une carte vous permettra de voir les animations et rencontres autour de chez vous, et de proposer vos propres activités.

+ + + +

💻 L’apprentissage en ligne

+ + + +

Desketa.bzh est une application en ligne, proposée gratuitement par l’Office Public de la Langue Bretonne afin d’apprendre et perfectionner le Breton unifié (KLT). Si vous souhaitez apprendre le Breton du Pays d’Auray, cette ressource pourra vous être d’une bonne aide pour appréhender la construction des phrases, enrichir votre vocabulaire et pouvoir interagir facilement avec d’autres bretonnant·e·s qui ne sont pas familiers avec le Vannetais. L’écoute et les échanges en Vannetais vous permettront de compléter vos acquis et de pratiquer pleinement le Vannetais.

+ + + +

L’écoute

+ + + +
+

📻 Radio Bro Gwened

+
+ + + +

Sur le Pays de Vannes (Bro-Gwened), vous avez certainement déjà croisé les ondes de Radio Bro Gwened (RBG.bzh). Si ce n’est pas le cas, allez vite faire un tour sur la radio en ligne. Vous y trouverez une écoute en direct et en rediffusion d’émissions en Breton et en français, ainsi qu’un bouillon de culture bretonne. Pour le Vannetais, à vous de creuser pour trouver les bonnes émissions.

+ + + +

🎧​ Kleweled

+ + + +

Kleweled.bzh est un portail qui référence un nombre important de ressources audiovisuelles en langue bretonne. Elle regroupe des podcasts des différentes radios bretonnantes, des films, des séries, des émissions, des documentaires, des dessins animés … et plus on creuse, plus on trouve de contenus !

+ + + +

🎬 La chaîne Youtube KSK

+ + + +

La chaîne Youtube de Kerlenn Sten Kidna diffuse un nombre important de collectage vidéos essentiellement réalisés sur le Pays d’Auray. Vous y trouverez plus de 250 vidéos !

+ + + +

👋🏻 Votre voisin·e

+ + + +

Des fois, il ne faut pas aller bien loin ! Un « Demat » à l’occasion de croiser une tête que l’on connait crée parfois de belles surprises. Et quoi de mieux que de pratiquer le breton dans son quotidien pour progresser. Les meilleurs collectages de breton que l’on puisse entendre sont ceux que l’on vit !

+ + + +
+
+ + + + + +
+ + + +
+ +
+ +
+ + + + + + + diff --git a/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.android.kt b/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.android.kt index 2dd9364..7ccc39e 100644 --- a/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.android.kt +++ b/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.android.kt @@ -1,10 +1,7 @@ package bzh.ajaury.chombev.web.ui import android.annotation.SuppressLint -import android.content.Intent -import android.webkit.WebResourceRequest import android.webkit.WebView -import android.webkit.WebViewClient import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView @@ -15,6 +12,7 @@ import androidx.webkit.WebViewFeature @Composable actual fun PlatformWebView( url: String, + offlineHtml: String?, darkTheme: Boolean, onLoadingStateChange: (Boolean) -> Unit, modifier: Modifier, @@ -24,7 +22,15 @@ actual fun PlatformWebView( factory = { context -> WebView(context).apply { // Keep navigation inside the embedded view instead of launching an external browser. - webViewClient = WebClientDelegatingExtLinks(onLoadingStateChange = onLoadingStateChange) + webViewClient = WebClientDelegatingExtLinks( + onLoadingStateChange = onLoadingStateChange, + onMainFrameError = { + // Swap in the bundled offline copy, keeping the real URL as base so links resolve. + offlineHtml?.let { html -> + loadDataWithBaseURL(url, html, "text/html", "utf-8", null) + } + }, + ) settings.javaScriptEnabled = false settings.domStorageEnabled = true applyDarkTheme(darkTheme) diff --git a/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/WebClientDelegatingExtLinks.kt b/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/WebClientDelegatingExtLinks.kt index 2a3e31c..4f1e178 100644 --- a/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/WebClientDelegatingExtLinks.kt +++ b/shared/src/androidMain/kotlin/bzh/ajaury/chombev/web/ui/WebClientDelegatingExtLinks.kt @@ -10,9 +10,13 @@ import android.webkit.WebViewClient /** * Keeps the initial page inside the embedded web view but opens any link the user taps in the system * browser. Reports page load start/finish through [onLoadingStateChange] so the UI can show a loader. + * + * [onMainFrameError] fires when the top-level page itself fails to load (not a sub-resource), so the + * caller can swap in an offline fallback. */ class WebClientDelegatingExtLinks( private val onLoadingStateChange: (Boolean) -> Unit = {}, + private val onMainFrameError: () -> Unit = {}, ) : WebViewClient() { override fun onPageStarted( view: WebView?, @@ -34,6 +38,11 @@ class WebClientDelegatingExtLinks( request: WebResourceRequest?, error: WebResourceError?, ) { + // Only fall back for the top-level page; a failed sub-resource (image, etc.) shouldn't + // replace the whole page. + if (request?.isForMainFrame == true) { + onMainFrameError() + } // Stop the loader even if the page failed, so the user isn't stuck on the spinner. onLoadingStateChange(false) } diff --git a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/di/Modules.kt b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/di/Modules.kt index ad972c7..9660eec 100644 --- a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/di/Modules.kt +++ b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/di/Modules.kt @@ -7,6 +7,7 @@ import bzh.ajaury.chombev.preferences.di.preferencesModule import bzh.ajaury.chombev.records.di.recordsModule import bzh.ajaury.chombev.records.ui.viewmodel.RecordsViewModel import bzh.ajaury.chombev.resources.di.resourcesModule +import bzh.ajaury.chombev.web.ui.viewmodel.WebViewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module @@ -18,4 +19,5 @@ val sharedModule = module { includes(resourcesModule) viewModelOf(::RecordsViewModel) viewModelOf(::PlayerViewModel) + viewModelOf(::WebViewModel) } diff --git a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/navigation/WebPage.kt b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/navigation/WebPage.kt index d674116..8ae91cc 100644 --- a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/navigation/WebPage.kt +++ b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/navigation/WebPage.kt @@ -4,12 +4,21 @@ import kotlinx.serialization.Serializable /** * Web destinations reachable from the main bottom navigation bar and shown inside the app. - * Placeholder URLs for now. + * + * [offlineAsset] is the resource path of a bundled HTML snapshot shown when the online [url] cannot + * be reached, so the page still has content offline. */ @Serializable enum class WebPage( val url: String, + val offlineAsset: String, ) { - DEEPEN("https://ksk.bzh/fr/chom-bev-approfondir/"), - ABOUT("https://ksk.bzh/fr/chom-bev-a-propos/"), + DEEPEN( + url = "https://ksk.bzh/fr/chom-bev-approfondir/", + offlineAsset = "files/offline_deepen.html", + ), + ABOUT( + url = "https://ksk.bzh/fr/chom-bev-a-propos/", + offlineAsset = "files/offline_about.html", + ), } diff --git a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.kt b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.kt index 214203f..3c8442e 100644 --- a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.kt +++ b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.kt @@ -8,12 +8,16 @@ import androidx.compose.ui.Modifier * honouring [darkTheme] so the page matches the app's light/dark appearance; the others fall back to * [WebViewUnavailable], which offers to open the page in the system browser. * + * When [url] cannot be reached, the native web views swap in [offlineHtml] — a bundled offline + * snapshot — so the page still shows content. When it is `null` no fallback is loaded. + * * [onLoadingStateChange] reports whether a page is currently loading, so the caller can show a * loader. Fallback platforms report `false` immediately since there is nothing to load. */ @Composable expect fun PlatformWebView( url: String, + offlineHtml: String?, darkTheme: Boolean, onLoadingStateChange: (Boolean) -> Unit = {}, modifier: Modifier = Modifier, diff --git a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/WebViewScreen.kt b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/WebViewScreen.kt index 58387bb..93f9bc6 100644 --- a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/WebViewScreen.kt +++ b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/WebViewScreen.kt @@ -11,33 +11,62 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import bzh.ajaury.chombev.navigation.WebPage +import bzh.ajaury.chombev.web.ui.viewmodel.WebUiState +import bzh.ajaury.chombev.web.ui.viewmodel.WebViewModel +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf /** - * Displays a [WebPage] inside the app: the embedded page, matching the app's [darkTheme]. A - * [WebViewLoader] covers the page while it loads. The surrounding top and bottom bars are provided by - * the app-level scaffold. + * Displays a [WebPage] inside the app. Injects a [WebViewModel] keyed by the page so each page keeps + * its own online URL and bundled offline snapshot. */ @Composable fun WebViewScreen( page: WebPage, contentPadding: PaddingValues, darkTheme: Boolean = isSystemInDarkTheme(), + viewModel: WebViewModel = koinViewModel(key = page.name) { parametersOf(page) }, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + WebViewScreen( + uiState = uiState, + contentPadding = contentPadding, + darkTheme = darkTheme, + ) +} + +/** + * Renders the embedded page for [uiState], matching the app's [darkTheme]. A [WebViewLoader] covers + * the page while it loads; if the online page fails, the web view swaps in the offline snapshot. The + * surrounding top and bottom bars are provided by the app-level scaffold. + */ +@Composable +fun WebViewScreen( + uiState: WebUiState?, + contentPadding: PaddingValues, + darkTheme: Boolean = isSystemInDarkTheme(), ) { // Start in the loading state; each distinct page loads afresh. - var isLoading by remember(page) { mutableStateOf(true) } + var isLoading by remember(uiState) { mutableStateOf(true) } Box( modifier = Modifier .padding(contentPadding) .fillMaxSize(), ) { - PlatformWebView( - url = page.url, - darkTheme = darkTheme, - onLoadingStateChange = { isLoading = it }, - modifier = Modifier.fillMaxSize(), - ) + if (uiState != null) { + PlatformWebView( + url = uiState.url, + offlineHtml = uiState.offlineHtml, + darkTheme = darkTheme, + onLoadingStateChange = { isLoading = it }, + modifier = Modifier.fillMaxSize(), + ) + } + if (isLoading) { WebViewLoader(modifier = Modifier.fillMaxSize()) } diff --git a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebUiState.kt b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebUiState.kt new file mode 100644 index 0000000..2c234b1 --- /dev/null +++ b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebUiState.kt @@ -0,0 +1,16 @@ +package bzh.ajaury.chombev.web.ui.viewmodel + +import androidx.compose.runtime.Immutable + +/** + * UI state for [bzh.ajaury.chombev.web.ui.WebViewScreen]. + * + * [url] is the online page to display. [offlineHtml] is the bundled fallback shown when [url] fails + * to load; it is `null` until the snapshot has been read (or if reading it failed), in which case no + * offline fallback is available. + */ +@Immutable +data class WebUiState( + val url: String = "", + val offlineHtml: String? = null, +) diff --git a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebViewModel.kt b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebViewModel.kt new file mode 100644 index 0000000..0a3ca0c --- /dev/null +++ b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/web/ui/viewmodel/WebViewModel.kt @@ -0,0 +1,42 @@ +package bzh.ajaury.chombev.web.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import bzh.ajaury.chombev.core.logging.domain.Logger +import bzh.ajaury.chombev.navigation.WebPage +import bzh.ajaury.chombev.resources.domain.ResourceReader +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** + * Owns the state for a single [page]: the online URL to display and its bundled offline snapshot, + * read from resources on init so it is ready as a fallback the moment the online page fails to load. + */ +class WebViewModel( + private val page: WebPage, + private val resourceReader: ResourceReader, + private val logger: Logger, +) : ViewModel() { + private val _uiState = MutableStateFlow(null) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadOfflineSnapshot() + } + + private fun loadOfflineSnapshot() { + viewModelScope.launch { + val html = runCatching { resourceReader.read(page.offlineAsset) } + .onFailure { + logger.error( + "Failed to read offline snapshot ${page.offlineAsset}", + it, + ) + }.getOrNull() + _uiState.update { WebUiState(url = page.url, offlineHtml = html) } + } + } +} diff --git a/shared/src/iosMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.ios.kt b/shared/src/iosMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.ios.kt index a331fc0..be80148 100644 --- a/shared/src/iosMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.ios.kt +++ b/shared/src/iosMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.ios.kt @@ -23,6 +23,7 @@ import platform.darwin.NSObject @Composable actual fun PlatformWebView( url: String, + offlineHtml: String?, darkTheme: Boolean, onLoadingStateChange: (Boolean) -> Unit, modifier: Modifier, @@ -30,6 +31,8 @@ actual fun PlatformWebView( // WKWebView holds its navigationDelegate weakly, so keep a strong reference across recompositions. val navigationDelegate = remember { WebViewNavigationDelegate() } navigationDelegate.onLoadingStateChange = onLoadingStateChange + navigationDelegate.offlineHtml = offlineHtml + navigationDelegate.baseUrl = url UIKitView( modifier = modifier, @@ -54,12 +57,19 @@ actual fun PlatformWebView( /** * Forwards WKWebView load lifecycle events to [onLoadingStateChange] so the UI can show a loader - * while a page is loading and hide it once loading finishes (or fails). + * while a page is loading and hide it once loading finishes. + * + * When a navigation fails, it swaps in [offlineHtml] (loaded against [baseUrl] so links resolve) so + * the page still shows content offline. [showingOffline] guards against re-entering the fallback. */ private class WebViewNavigationDelegate : NSObject(), WKNavigationDelegateProtocol { var onLoadingStateChange: (Boolean) -> Unit = {} + var offlineHtml: String? = null + var baseUrl: String = "" + + private var showingOffline = false override fun webView( webView: WKWebView, @@ -82,7 +92,7 @@ private class WebViewNavigationDelegate : didFailNavigation: WKNavigation?, withError: NSError, ) { - onLoadingStateChange(false) + handleFailure(webView) } // Shares the (WKWebView, WKNavigation?, NSError) Kotlin signature with didFailNavigation above. @@ -92,7 +102,17 @@ private class WebViewNavigationDelegate : didFailProvisionalNavigation: WKNavigation?, withError: NSError, ) { - onLoadingStateChange(false) + handleFailure(webView) + } + + private fun handleFailure(webView: WKWebView) { + val html = offlineHtml + if (html != null && !showingOffline) { + showingOffline = true + webView.loadHTMLString(string = html, baseURL = NSURL.URLWithString(baseUrl)) + } else { + onLoadingStateChange(false) + } } } diff --git a/shared/src/jsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.js.kt b/shared/src/jsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.js.kt index f1fbd4c..f4a76c5 100644 --- a/shared/src/jsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.js.kt +++ b/shared/src/jsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.js.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier @Composable actual fun PlatformWebView( url: String, + offlineHtml: String?, darkTheme: Boolean, onLoadingStateChange: (Boolean) -> Unit, modifier: Modifier, diff --git a/shared/src/jvmMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.jvm.kt b/shared/src/jvmMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.jvm.kt index d3a656b..f5c9d2f 100644 --- a/shared/src/jvmMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.jvm.kt +++ b/shared/src/jvmMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.jvm.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier @Composable actual fun PlatformWebView( url: String, + offlineHtml: String?, darkTheme: Boolean, onLoadingStateChange: (Boolean) -> Unit, modifier: Modifier, diff --git a/shared/src/wasmJsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.wasmJs.kt b/shared/src/wasmJsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.wasmJs.kt index f1fbd4c..f4a76c5 100644 --- a/shared/src/wasmJsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.wasmJs.kt +++ b/shared/src/wasmJsMain/kotlin/bzh/ajaury/chombev/web/ui/PlatformWebView.wasmJs.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier @Composable actual fun PlatformWebView( url: String, + offlineHtml: String?, darkTheme: Boolean, onLoadingStateChange: (Boolean) -> Unit, modifier: Modifier,