new: extract WebView feature module
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
plugins {
|
||||
id("gwenedeg.kmp.library")
|
||||
id("gwenedeg.compose")
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidLibrary {
|
||||
namespace = "bzh.ajaury.chombev.webview"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.uiToolingPreview)
|
||||
implementation(libs.androidx.webkit)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
// Core modules
|
||||
implementation(projects.core.logging)
|
||||
|
||||
// Reads the bundled offline page snapshots and web strings
|
||||
implementation(projects.data.resources)
|
||||
|
||||
// Serialization: WebPage is carried in the serializable navigation back stack
|
||||
implementation(libs.kotlinx.serializationJson)
|
||||
|
||||
// UI - Compose
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.uiToolingPreview)
|
||||
implementation(libs.compose.components.resources)
|
||||
|
||||
// Lifecycle
|
||||
implementation(libs.lifecycle.viewmodelCompose)
|
||||
implementation(libs.lifecycle.runtimeCompose)
|
||||
|
||||
// DI
|
||||
implementation(project.dependencies.platform(libs.koin.bom))
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.core.viewmodel)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidRuntimeClasspath(libs.compose.uiTooling)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package bzh.ajaury.chombev.web.di
|
||||
|
||||
import bzh.ajaury.chombev.web.ui.viewmodel.WebViewModel
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val webviewModule: Module = module {
|
||||
viewModelOf(::WebViewModel)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package bzh.ajaury.chombev.web.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Web page with a distant and local fallback.
|
||||
*
|
||||
* [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
|
||||
data class WebPage(
|
||||
val url: String,
|
||||
val offlineAsset: String,
|
||||
)
|
||||
+1
-2
@@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import bzh.ajaury.chombev.theme.ChomBevTheme
|
||||
|
||||
/**
|
||||
* Full-surface loading overlay shown on top of the embedded web view while a page is loading. The
|
||||
@@ -29,7 +28,7 @@ internal fun WebViewLoader(modifier: Modifier = Modifier) {
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WebViewLoaderPreview() {
|
||||
ChomBevTheme {
|
||||
MaterialTheme {
|
||||
WebViewLoader(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -12,7 +12,7 @@ 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.model.WebPage
|
||||
import bzh.ajaury.chombev.web.ui.viewmodel.WebUiState
|
||||
import bzh.ajaury.chombev.web.ui.viewmodel.WebViewModel
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
@@ -27,7 +27,7 @@ fun WebViewScreen(
|
||||
page: WebPage,
|
||||
contentPadding: PaddingValues,
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
viewModel: WebViewModel = koinViewModel(key = page.name) { parametersOf(page) },
|
||||
viewModel: WebViewModel = koinViewModel(key = page.url) { parametersOf(page) },
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ import androidx.compose.ui.unit.dp
|
||||
import bzh.ajaury.chombev.resources.generated.resources.Res
|
||||
import bzh.ajaury.chombev.resources.generated.resources.web_open_in_browser
|
||||
import bzh.ajaury.chombev.resources.generated.resources.web_unavailable_message
|
||||
import bzh.ajaury.chombev.theme.ChomBevTheme
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
@@ -52,7 +51,7 @@ internal fun WebViewUnavailable(
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WebViewUnavailablePreview() {
|
||||
ChomBevTheme {
|
||||
MaterialTheme {
|
||||
WebViewUnavailable(url = "https://ksk.bzh")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,8 +3,8 @@ 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 bzh.ajaury.chombev.web.model.WebPage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
+8
-3
@@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.interop.UIKitView
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.ObjCSignatureOverride
|
||||
import kotlinx.cinterop.readValue
|
||||
import platform.CoreGraphics.CGRectZero
|
||||
import platform.Foundation.NSError
|
||||
import platform.Foundation.NSURL
|
||||
@@ -41,7 +42,8 @@ actual fun PlatformWebView(
|
||||
frame = CGRectZero.readValue(),
|
||||
configuration = WKWebViewConfiguration(),
|
||||
).apply {
|
||||
navigationDelegate = navigationDelegate
|
||||
// Qualify with `this` so it targets the web view's property, not the local val above.
|
||||
this.navigationDelegate = navigationDelegate
|
||||
setOverrideUserInterfaceStyle(darkTheme.toInterfaceStyle())
|
||||
NSURL.URLWithString(url)?.let { nsUrl ->
|
||||
loadRequest(NSURLRequest(uRL = nsUrl))
|
||||
@@ -71,6 +73,10 @@ private class WebViewNavigationDelegate :
|
||||
|
||||
private var showingOffline = false
|
||||
|
||||
// The four delegate callbacks below collapse to two Kotlin signatures — (WKWebView, WKNavigation?)
|
||||
// and (WKWebView, WKNavigation?, NSError) — so every colliding override is marked
|
||||
// @ObjCSignatureOverride.
|
||||
@ObjCSignatureOverride
|
||||
override fun webView(
|
||||
webView: WKWebView,
|
||||
didStartProvisionalNavigation: WKNavigation?,
|
||||
@@ -78,7 +84,6 @@ private class WebViewNavigationDelegate :
|
||||
onLoadingStateChange(true)
|
||||
}
|
||||
|
||||
// Shares the (WKWebView, WKNavigation?) Kotlin signature with didStartProvisionalNavigation above.
|
||||
@ObjCSignatureOverride
|
||||
override fun webView(
|
||||
webView: WKWebView,
|
||||
@@ -87,6 +92,7 @@ private class WebViewNavigationDelegate :
|
||||
onLoadingStateChange(false)
|
||||
}
|
||||
|
||||
@ObjCSignatureOverride
|
||||
override fun webView(
|
||||
webView: WKWebView,
|
||||
didFailNavigation: WKNavigation?,
|
||||
@@ -95,7 +101,6 @@ private class WebViewNavigationDelegate :
|
||||
handleFailure(webView)
|
||||
}
|
||||
|
||||
// Shares the (WKWebView, WKNavigation?, NSError) Kotlin signature with didFailNavigation above.
|
||||
@ObjCSignatureOverride
|
||||
override fun webView(
|
||||
webView: WKWebView,
|
||||
@@ -50,3 +50,6 @@ include(":data:preferences")
|
||||
include(":data:records")
|
||||
include(":data:resources")
|
||||
include(":data:subtitle")
|
||||
|
||||
// Feature modules
|
||||
include(":feature:webview")
|
||||
|
||||
@@ -30,7 +30,6 @@ kotlin {
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.uiToolingPreview)
|
||||
implementation(libs.androidx.webkit)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
// Core modules
|
||||
@@ -46,6 +45,9 @@ kotlin {
|
||||
implementation(projects.data.resources)
|
||||
implementation(projects.data.subtitle)
|
||||
|
||||
// Feature modules
|
||||
implementation(projects.feature.webview)
|
||||
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.material3)
|
||||
|
||||
@@ -16,12 +16,13 @@ import bzh.ajaury.chombev.di.sharedModule
|
||||
import bzh.ajaury.chombev.navigation.MainBottomBar
|
||||
import bzh.ajaury.chombev.navigation.MainTopBar
|
||||
import bzh.ajaury.chombev.navigation.Route
|
||||
import bzh.ajaury.chombev.navigation.WebPage
|
||||
import bzh.ajaury.chombev.navigation.WebPages
|
||||
import bzh.ajaury.chombev.player.di.audioPlayerModule
|
||||
import bzh.ajaury.chombev.player.ui.PlayerScreen
|
||||
import bzh.ajaury.chombev.records.ui.RecordsScreen
|
||||
import bzh.ajaury.chombev.subtitle.di.subtitleModule
|
||||
import bzh.ajaury.chombev.theme.ChomBevTheme
|
||||
import bzh.ajaury.chombev.web.model.WebPage
|
||||
import bzh.ajaury.chombev.web.ui.WebViewScreen
|
||||
import org.koin.compose.KoinApplication
|
||||
import org.koin.dsl.koinConfiguration
|
||||
@@ -81,8 +82,8 @@ fun App() {
|
||||
MainBottomBar(
|
||||
selectedPage = selectedPage,
|
||||
onRecordsClick = { selectMainTab(null) },
|
||||
onDeepenClick = { selectMainTab(WebPage.DEEPEN) },
|
||||
onAboutClick = { selectMainTab(WebPage.ABOUT) },
|
||||
onDeepenClick = { selectMainTab(WebPages.DEEPEN) },
|
||||
onAboutClick = { selectMainTab(WebPages.ABOUT) },
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +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 bzh.ajaury.chombev.web.di.webviewModule
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
@@ -17,7 +17,7 @@ val sharedModule = module {
|
||||
includes(preferencesModule)
|
||||
includes(recordsModule)
|
||||
includes(resourcesModule)
|
||||
includes(webviewModule)
|
||||
viewModelOf(::RecordsViewModel)
|
||||
viewModelOf(::PlayerViewModel)
|
||||
viewModelOf(::WebViewModel)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import bzh.ajaury.chombev.resources.generated.resources.nav_about
|
||||
import bzh.ajaury.chombev.resources.generated.resources.nav_deepen
|
||||
import bzh.ajaury.chombev.resources.generated.resources.nav_records
|
||||
import bzh.ajaury.chombev.theme.ChomBevTheme
|
||||
import bzh.ajaury.chombev.web.model.WebPage
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
@@ -40,13 +41,13 @@ fun MainBottomBar(
|
||||
label = { Text(text = stringResource(Res.string.nav_records)) },
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedPage == WebPage.DEEPEN,
|
||||
selected = selectedPage == WebPages.DEEPEN,
|
||||
onClick = onDeepenClick,
|
||||
icon = { Icon(imageVector = Icons.Filled.School, contentDescription = null) },
|
||||
label = { Text(text = stringResource(Res.string.nav_deepen)) },
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedPage == WebPage.ABOUT,
|
||||
selected = selectedPage == WebPages.ABOUT,
|
||||
onClick = onAboutClick,
|
||||
icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) },
|
||||
label = { Text(text = stringResource(Res.string.nav_about)) },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package bzh.ajaury.chombev.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import bzh.ajaury.chombev.web.model.WebPage
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
||||
+6
-13
@@ -1,24 +1,17 @@
|
||||
package bzh.ajaury.chombev.navigation
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import bzh.ajaury.chombev.web.model.WebPage
|
||||
|
||||
/**
|
||||
* Web destinations reachable from the main bottom navigation bar and shown inside the app.
|
||||
*
|
||||
* [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(
|
||||
object WebPages {
|
||||
val DEEPEN = WebPage(
|
||||
url = "https://ksk.bzh/fr/chom-bev-approfondir/",
|
||||
offlineAsset = "files/offline_deepen.html",
|
||||
),
|
||||
ABOUT(
|
||||
)
|
||||
val ABOUT = WebPage(
|
||||
url = "https://ksk.bzh/fr/chom-bev-a-propos/",
|
||||
offlineAsset = "files/offline_about.html",
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package bzh.ajaury.chombev.di
|
||||
|
||||
import bzh.ajaury.chombev.appModule
|
||||
import bzh.ajaury.chombev.player.di.audioPlayerModule
|
||||
import bzh.ajaury.chombev.web.model.WebPage
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.test.verify.verify
|
||||
import kotlin.test.Test
|
||||
@@ -18,6 +19,10 @@ class KoinModulesTest {
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
@Test
|
||||
fun koinConfigurationIsValid() {
|
||||
appModule.verify()
|
||||
// WebPage is supplied at call time via parametersOf (WebViewModel), so declare it as a known
|
||||
// extra type rather than a resolvable definition.
|
||||
appModule.verify(
|
||||
extraTypes = listOf(WebPage::class),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user