new: integrate pages for deepen and about
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ChomBevApp"
|
android:name=".ChomBevApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
<!-- Breton (br) -->
|
<!-- Breton (br) -->
|
||||||
<string name="app_name">Chom bev</string>
|
<string name="app_name">Chom bev</string>
|
||||||
|
|
||||||
|
<string name="nav_records">Dastumoù</string>
|
||||||
|
<string name="nav_deepen">Donaat</string>
|
||||||
|
<string name="nav_about">Diwar-benn</string>
|
||||||
|
|
||||||
|
<string name="web_unavailable_message">N\'haller ket diskouez ar bajenn-mañ amañ.</string>
|
||||||
|
<string name="web_open_in_browser">Digeriñ er merdeer</string>
|
||||||
|
|
||||||
<string name="subtitle_preferences_title">Istitloù</string>
|
<string name="subtitle_preferences_title">Istitloù</string>
|
||||||
<string name="subtitle_preferences_action">Gwellvezioù istitloù</string>
|
<string name="subtitle_preferences_action">Gwellvezioù istitloù</string>
|
||||||
<string name="subtitle_transcription_label">Treuzskrivañ (BZH)</string>
|
<string name="subtitle_transcription_label">Treuzskrivañ (BZH)</string>
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
<!-- Default locale: French -->
|
<!-- Default locale: French -->
|
||||||
<string name="app_name">Chom bev</string>
|
<string name="app_name">Chom bev</string>
|
||||||
|
|
||||||
|
<string name="nav_records">Enregistrements</string>
|
||||||
|
<string name="nav_deepen">Approfondir</string>
|
||||||
|
<string name="nav_about">À propos</string>
|
||||||
|
|
||||||
|
<string name="web_unavailable_message">Impossible d\'afficher cette page ici.</string>
|
||||||
|
<string name="web_open_in_browser">Ouvrir dans le navigateur</string>
|
||||||
|
|
||||||
<string name="subtitle_preferences_title">Sous-titres</string>
|
<string name="subtitle_preferences_title">Sous-titres</string>
|
||||||
<string name="subtitle_preferences_action">Préférences de sous-titrage</string>
|
<string name="subtitle_preferences_action">Préférences de sous-titrage</string>
|
||||||
<string name="subtitle_transcription_label">Transcription (BZH)</string>
|
<string name="subtitle_transcription_label">Transcription (BZH)</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ androidMinSdk = "24"
|
|||||||
androidTargetSdk = "37"
|
androidTargetSdk = "37"
|
||||||
androidxActivity = "1.13.0"
|
androidxActivity = "1.13.0"
|
||||||
androidxLifecycle = "2.11.0-rc01"
|
androidxLifecycle = "2.11.0-rc01"
|
||||||
|
androidxWebkit = "1.16.0"
|
||||||
media3 = "1.10.1"
|
media3 = "1.10.1"
|
||||||
navigation3 = "1.1.1"
|
navigation3 = "1.1.1"
|
||||||
composeMultiplatform = "1.11.1"
|
composeMultiplatform = "1.11.1"
|
||||||
@@ -30,6 +31,7 @@ composeCompiler-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler
|
|||||||
|
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
||||||
androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" }
|
androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" }
|
||||||
|
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidxWebkit" }
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||||
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
||||||
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
|
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
|
implementation(libs.androidx.webkit)
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
// Core modules
|
// Core modules
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package fr.ajaury.gwenedeg.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
|
||||||
|
import androidx.webkit.WebSettingsCompat
|
||||||
|
import androidx.webkit.WebViewFeature
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformWebView(
|
||||||
|
url: String,
|
||||||
|
darkTheme: Boolean,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier,
|
||||||
|
factory = { context ->
|
||||||
|
WebView(context).apply {
|
||||||
|
// Keep navigation inside the embedded view instead of launching an external browser.
|
||||||
|
webViewClient = WebClientDelegatingExtLinks()
|
||||||
|
settings.javaScriptEnabled = false
|
||||||
|
settings.domStorageEnabled = true
|
||||||
|
applyDarkTheme(darkTheme)
|
||||||
|
loadUrl(url)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Re-apply on recomposition so a light/dark switch takes effect without reloading the page.
|
||||||
|
update = { webView -> webView.applyDarkTheme(darkTheme) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets the web view darken content to match the app when [darkTheme] is on: the page's
|
||||||
|
* `prefers-color-scheme: dark` styles are used, falling back to algorithmic darkening.
|
||||||
|
*/
|
||||||
|
private fun WebView.applyDarkTheme(darkTheme: Boolean) {
|
||||||
|
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
|
||||||
|
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, darkTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
|
||||||
|
class WebClientDelegatingExtLinks : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest,
|
||||||
|
): Boolean {
|
||||||
|
val uri = request.url
|
||||||
|
|
||||||
|
// Ouvre tous les liens dans le navigateur
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
|
view.context.startActivity(intent)
|
||||||
|
|
||||||
|
return true // On indique que l'on a géré la navigation
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package fr.ajaury.gwenedeg
|
package fr.ajaury.gwenedeg
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.saveable.rememberSerializable
|
import androidx.compose.runtime.saveable.rememberSerializable
|
||||||
@@ -10,11 +13,15 @@ import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSe
|
|||||||
import fr.ajaury.gwenedeg.core.coroutines.di.coroutinesModule
|
import fr.ajaury.gwenedeg.core.coroutines.di.coroutinesModule
|
||||||
import fr.ajaury.gwenedeg.core.logging.di.loggingModule
|
import fr.ajaury.gwenedeg.core.logging.di.loggingModule
|
||||||
import fr.ajaury.gwenedeg.di.sharedModule
|
import fr.ajaury.gwenedeg.di.sharedModule
|
||||||
|
import fr.ajaury.gwenedeg.navigation.MainBottomBar
|
||||||
|
import fr.ajaury.gwenedeg.navigation.MainTopBar
|
||||||
import fr.ajaury.gwenedeg.navigation.Route
|
import fr.ajaury.gwenedeg.navigation.Route
|
||||||
|
import fr.ajaury.gwenedeg.navigation.WebPage
|
||||||
import fr.ajaury.gwenedeg.player.di.audioPlayerModule
|
import fr.ajaury.gwenedeg.player.di.audioPlayerModule
|
||||||
import fr.ajaury.gwenedeg.player.ui.PlayerScreen
|
import fr.ajaury.gwenedeg.player.ui.PlayerScreen
|
||||||
import fr.ajaury.gwenedeg.records.ui.RecordsScreen
|
import fr.ajaury.gwenedeg.records.ui.RecordsScreen
|
||||||
import fr.ajaury.gwenedeg.subtitle.di.subtitleModule
|
import fr.ajaury.gwenedeg.subtitle.di.subtitleModule
|
||||||
|
import fr.ajaury.gwenedeg.web.ui.WebViewScreen
|
||||||
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||||
import org.koin.compose.KoinApplication
|
import org.koin.compose.KoinApplication
|
||||||
import org.koin.dsl.koinConfiguration
|
import org.koin.dsl.koinConfiguration
|
||||||
@@ -44,7 +51,43 @@ fun App() {
|
|||||||
mutableStateListOf(Route.Records)
|
mutableStateListOf(Route.Records)
|
||||||
}
|
}
|
||||||
|
|
||||||
ChomBevTheme {
|
// Selecting a main tab keeps Records as the root, so system Back always returns home.
|
||||||
|
fun selectMainTab(page: WebPage?) {
|
||||||
|
backStack.clear()
|
||||||
|
backStack.add(Route.Records)
|
||||||
|
if (page != null) {
|
||||||
|
backStack.add(Route.Web(page = page))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val darkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
// The current top destination drives the shared chrome: the top and bottom bars are shown
|
||||||
|
// only on the main screens (Records and the web pages), not on the Player detail screen.
|
||||||
|
val currentRoute = backStack.lastOrNull()
|
||||||
|
val selectedPage = (currentRoute as? Route.Web)?.page
|
||||||
|
val isMainScreen = currentRoute is Route.Records || currentRoute is Route.Web
|
||||||
|
|
||||||
|
ChomBevTheme(darkTheme = darkTheme) {
|
||||||
|
Scaffold(
|
||||||
|
// Let each screen's bars/insets apply; keeps the Player's own scaffold from double-padding.
|
||||||
|
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
|
topBar = {
|
||||||
|
if (isMainScreen) {
|
||||||
|
MainTopBar()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
if (isMainScreen) {
|
||||||
|
MainBottomBar(
|
||||||
|
selectedPage = selectedPage,
|
||||||
|
onRecordsClick = { selectMainTab(null) },
|
||||||
|
onDeepenClick = { selectMainTab(WebPage.DEEPEN) },
|
||||||
|
onAboutClick = { selectMainTab(WebPage.ABOUT) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
onBack = { backStack.removeLastOrNull() },
|
onBack = { backStack.removeLastOrNull() },
|
||||||
@@ -52,6 +95,7 @@ fun App() {
|
|||||||
when (key) {
|
when (key) {
|
||||||
is Route.Records -> NavEntry(key) {
|
is Route.Records -> NavEntry(key) {
|
||||||
RecordsScreen(
|
RecordsScreen(
|
||||||
|
contentPadding = innerPadding,
|
||||||
onRecordClick = { record ->
|
onRecordClick = { record ->
|
||||||
backStack.add(Route.Player(recordId = record.id))
|
backStack.add(Route.Player(recordId = record.id))
|
||||||
},
|
},
|
||||||
@@ -64,9 +108,18 @@ fun App() {
|
|||||||
onBackClicked = { backStack.removeLastOrNull() },
|
onBackClicked = { backStack.removeLastOrNull() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is Route.Web -> NavEntry(key) {
|
||||||
|
WebViewScreen(
|
||||||
|
page = key.page,
|
||||||
|
contentPadding = innerPadding,
|
||||||
|
darkTheme = darkTheme,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package fr.ajaury.gwenedeg.navigation
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Mic
|
||||||
|
import androidx.compose.material.icons.filled.School
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.Res
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.nav_about
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.nav_deepen
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.nav_records
|
||||||
|
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main navigation bar shown on the app's home and web screens. "Records" is the in-app home
|
||||||
|
* destination; "Deepen" and "About" open web pages shown inside the app.
|
||||||
|
*
|
||||||
|
* @param selectedPage the currently shown web page, or `null` when the Records home is selected.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MainBottomBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selectedPage: WebPage? = null,
|
||||||
|
onRecordsClick: () -> Unit = {},
|
||||||
|
onDeepenClick: () -> Unit = {},
|
||||||
|
onAboutClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
NavigationBar(modifier = modifier) {
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selectedPage == null,
|
||||||
|
onClick = onRecordsClick,
|
||||||
|
icon = { Icon(imageVector = Icons.Filled.Mic, contentDescription = null) },
|
||||||
|
label = { Text(text = stringResource(Res.string.nav_records)) },
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selectedPage == WebPage.DEEPEN,
|
||||||
|
onClick = onDeepenClick,
|
||||||
|
icon = { Icon(imageVector = Icons.Filled.School, contentDescription = null) },
|
||||||
|
label = { Text(text = stringResource(Res.string.nav_deepen)) },
|
||||||
|
)
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selectedPage == WebPage.ABOUT,
|
||||||
|
onClick = onAboutClick,
|
||||||
|
icon = { Icon(imageVector = Icons.Outlined.Info, contentDescription = null) },
|
||||||
|
label = { Text(text = stringResource(Res.string.nav_about)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun MainBottomBarPreview() {
|
||||||
|
ChomBevTheme {
|
||||||
|
MainBottomBar(selectedPage = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package fr.ajaury.gwenedeg.navigation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.Res
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.app_icon
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.app_name
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
||||||
|
/** Shared top bar (app icon + name) shown across the main screens: Records, Deepen and About. */
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MainTopBar() {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(Res.drawable.app_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
)
|
||||||
|
Text(text = stringResource(Res.string.app_name))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,4 +12,9 @@ sealed interface Route : NavKey {
|
|||||||
data class Player(
|
data class Player(
|
||||||
val recordId: Int,
|
val recordId: Int,
|
||||||
) : Route
|
) : Route
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Web(
|
||||||
|
val page: WebPage,
|
||||||
|
) : Route
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package fr.ajaury.gwenedeg.navigation
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web destinations reachable from the main bottom navigation bar and shown inside the app.
|
||||||
|
* Placeholder URLs for now.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
enum class WebPage(
|
||||||
|
val url: String,
|
||||||
|
) {
|
||||||
|
DEEPEN("https://ksk.bzh/fr/chom-bev-approfondir/"),
|
||||||
|
ABOUT("https://ksk.bzh/contact"),
|
||||||
|
}
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
package fr.ajaury.gwenedeg.records.ui
|
package fr.ajaury.gwenedeg.records.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -24,56 +16,33 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||||
import fr.ajaury.gwenedeg.records.model.Record
|
import fr.ajaury.gwenedeg.records.model.Record
|
||||||
import fr.ajaury.gwenedeg.records.ui.viewmodel.RecordsViewModel
|
import fr.ajaury.gwenedeg.records.ui.viewmodel.RecordsViewModel
|
||||||
import fr.ajaury.gwenedeg.resources.generated.resources.Res
|
|
||||||
import fr.ajaury.gwenedeg.resources.generated.resources.app_icon
|
|
||||||
import fr.ajaury.gwenedeg.resources.generated.resources.app_name
|
|
||||||
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||||
import org.jetbrains.compose.resources.painterResource
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RecordsScreen(
|
fun RecordsScreen(
|
||||||
viewModel: RecordsViewModel = koinViewModel(),
|
contentPadding: PaddingValues,
|
||||||
onRecordClick: (Record) -> Unit,
|
onRecordClick: (Record) -> Unit,
|
||||||
|
viewModel: RecordsViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val records by viewModel.uiState.collectAsStateWithLifecycle()
|
val records by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
RecordsScreen(
|
RecordsScreen(
|
||||||
records = records,
|
records = records,
|
||||||
|
contentPadding = contentPadding,
|
||||||
onRecordClick = onRecordClick,
|
onRecordClick = onRecordClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RecordsScreen(
|
fun RecordsScreen(
|
||||||
records: List<Record>,
|
records: List<Record>,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
onRecordClick: (Record) -> Unit,
|
onRecordClick: (Record) -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(Res.drawable.app_icon),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
)
|
|
||||||
Text(text = stringResource(Res.string.app_name))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { innerPadding ->
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
.padding(contentPadding)
|
||||||
.padding(innerPadding)
|
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
@@ -94,7 +63,6 @@ fun RecordsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@@ -120,6 +88,7 @@ private fun RecordsScreenPreview() {
|
|||||||
subtitleResourcePath = "files/Chom_bev_02_st_bzh.lrc",
|
subtitleResourcePath = "files/Chom_bev_02_st_bzh.lrc",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
contentPadding = PaddingValues(),
|
||||||
onRecordClick = {},
|
onRecordClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Embeds a live web page. Platforms with a native web view (Android, iOS) render the page inline,
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
expect fun PlatformWebView(
|
||||||
|
url: String,
|
||||||
|
darkTheme: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import fr.ajaury.gwenedeg.navigation.WebPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a [WebPage] inside the app: the embedded page, matching the app's [darkTheme]. The
|
||||||
|
* surrounding top and bottom bars are provided by the app-level scaffold.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun WebViewScreen(
|
||||||
|
page: WebPage,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
) {
|
||||||
|
PlatformWebView(
|
||||||
|
url = page.url,
|
||||||
|
darkTheme = darkTheme,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(contentPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.Res
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.web_open_in_browser
|
||||||
|
import fr.ajaury.gwenedeg.resources.generated.resources.web_unavailable_message
|
||||||
|
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shown on platforms without an embedded web view: explains the page can't be displayed inline and
|
||||||
|
* offers to open it in the system browser.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun WebViewUnavailable(
|
||||||
|
url: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.web_unavailable_message),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Button(onClick = { uriHandler.openUri(url) }) {
|
||||||
|
Text(text = stringResource(Res.string.web_open_in_browser))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun WebViewUnavailablePreview() {
|
||||||
|
ChomBevTheme {
|
||||||
|
WebViewUnavailable(url = "https://ksk.bzh")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.interop.UIKitView
|
||||||
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
|
import platform.CoreGraphics.CGRectZero
|
||||||
|
import platform.Foundation.NSURL
|
||||||
|
import platform.Foundation.NSURLRequest
|
||||||
|
import platform.UIKit.UIUserInterfaceStyle
|
||||||
|
import platform.UIKit.UIUserInterfaceStyle.UIUserInterfaceStyleDark
|
||||||
|
import platform.UIKit.UIUserInterfaceStyle.UIUserInterfaceStyleLight
|
||||||
|
import platform.WebKit.WKWebView
|
||||||
|
import platform.WebKit.WKWebViewConfiguration
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformWebView(
|
||||||
|
url: String,
|
||||||
|
darkTheme: Boolean,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
UIKitView(
|
||||||
|
modifier = modifier,
|
||||||
|
factory = {
|
||||||
|
WKWebView(
|
||||||
|
frame = CGRectZero.readValue(),
|
||||||
|
configuration = WKWebViewConfiguration(),
|
||||||
|
).apply {
|
||||||
|
setOverrideUserInterfaceStyle(darkTheme.toInterfaceStyle())
|
||||||
|
NSURL.URLWithString(url)?.let { nsUrl ->
|
||||||
|
loadRequest(NSURLRequest(uRL = nsUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Re-apply on recomposition so a light/dark switch takes effect without reloading the page.
|
||||||
|
update = { webView ->
|
||||||
|
webView.setOverrideUserInterfaceStyle(darkTheme.toInterfaceStyle())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Boolean.toInterfaceStyle(): UIUserInterfaceStyle =
|
||||||
|
if (this) UIUserInterfaceStyleDark else UIUserInterfaceStyleLight
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformWebView(
|
||||||
|
url: String,
|
||||||
|
darkTheme: Boolean,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
// The Compose canvas can't host an iframe cleanly; open the page in the browser instead.
|
||||||
|
WebViewUnavailable(url = url, modifier = modifier)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformWebView(
|
||||||
|
url: String,
|
||||||
|
darkTheme: Boolean,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
// Desktop has no bundled web view; offer to open the page in the system browser instead.
|
||||||
|
WebViewUnavailable(url = url, modifier = modifier)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package fr.ajaury.gwenedeg.web.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun PlatformWebView(
|
||||||
|
url: String,
|
||||||
|
darkTheme: Boolean,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
// The Compose canvas can't host an iframe cleanly; open the page in the browser instead.
|
||||||
|
WebViewUnavailable(url = url, modifier = modifier)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user