diff --git a/.gitignore b/.gitignore index 2f80f28..fbfb549 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ data/resources/src/commonMain/composeResources/files/records.json keystore.properties *.jks *.keystore + +# Secrets — see docs/sentry-glitchtip.md (the .example template stays versioned) +.sentryclirc diff --git a/.sentryclirc.example b/.sentryclirc.example new file mode 100644 index 0000000..93e32b9 --- /dev/null +++ b/.sentryclirc.example @@ -0,0 +1,17 @@ +; Template for the local `.sentryclirc` used by sentry-cli to upload debug symbols +; to GlitchTip. Copy it to `.sentryclirc` at the repository root and fill in the token: +; +; cp .sentryclirc.example .sentryclirc +; chmod 600 .sentryclirc +; +; `.sentryclirc` is git-ignored and must never be committed — it holds a credential. +; See docs/sentry-glitchtip.md for the full procedure. + +[auth] +; Created in GlitchTip under your user menu > Profile > Auth Tokens. +token= + +[defaults] +url=https://eu.glitchtip.com/ +org=kerlenn-sten-kidna +project=chom-bev-e-brezhoneg diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e8730..3a5114e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- show on boarding as a modal on large screens +- limit player controller size +- browse the web pages inside the app on web, instead of only linking to them +- embed web pages inside the app on web + +### Fixed + +- Desktop: audio playback failed with *Failed to play audio* on every record, + because the JDK cannot decode the `.m4a` (AAC) recordings on its own. +- add emoji fallback for web +- correct seeking to a sentence on web + ## [1.0.0-alpha02] - 2026-08-04 ### Added @@ -31,13 +45,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0-alpha01] - 2026-07-08 -First public release of **Chom Bev e Brezhoneg**. +First public release of **Donemat - Breton d'Auray**. ### Added #### Listening -- Record list of the *Chom Bev* audio course, with an emoji, the Breton title and +- Record list of the *Donemat - Breton d'Auray* audio course, with an emoji, the Breton title and its French translation for each record. - Audio player with play/pause/replay, a seekable progress bar, and previous/next-sentence navigation. @@ -64,7 +78,7 @@ First public release of **Chom Bev e Brezhoneg**. - Targets Android, iOS, Desktop (JVM) and Web (JS + Wasm), all sharing the same Compose Multiplatform UI. Only Android has been deeply tested for this version. - Audio playback backed by ExoPlayer (Android), AVFoundation (iOS), the browser - audio APIs (Web) and VorbisSPI (Desktop). + audio APIs (Web) and `javax.sound.sampled` (Desktop). - Light and dark themes; interface localised in French and Breton. - Modular architecture: `core`, `data` and `feature` modules glued by a thin `shared` module, with Koin dependency injection validated at build time. diff --git a/README.md b/README.md index 6a007f8..88306ce 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Chom Bev e Brezhoneg - Survive in Breton - Survive en Breton +# "Donemat - Breton d'Auray" - Survive in Breton This is a Compose Multiplatform project to listen to Breton from Vannes country. It uses an audio player adapted for each platform and synchronised subtitles. @@ -93,8 +93,24 @@ See [Resource's README.md](data/resources/README.md) to handle the resource file audio files, and subtitles). +## Documentation + +- [Crash reporting — Sentry SDK on GlitchTip](./docs/sentry-glitchtip.md): DSN configuration, + `.sentryclirc` setup, and the iOS Archive procedure for uploading dSYM files. + +## Crash reporting + +Crashes are reported with the Sentry Kotlin Multiplatform SDK to a [GlitchTip](https://glitchtip.com) +instance. The DSN is read from `local.properties` and never committed, so a fresh clone builds and +runs with crash reporting disabled until you configure it. See +[docs/sentry-glitchtip.md](./docs/sentry-glitchtip.md).R + ## License +The web build bundles a subset of [Noto Color Emoji](https://github.com/googlefonts/noto-emoji), +licensed under the SIL Open Font License 1.1 +([data/resources/licenses/NotoColorEmoji-OFL.txt](./data/resources/licenses/NotoColorEmoji-OFL.txt)). + This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. ``` diff --git a/androidApp/README.md b/androidApp/README.md index fe30b45..3e6dcaf 100644 --- a/androidApp/README.md +++ b/androidApp/README.md @@ -1,10 +1,5 @@ # Android app -## Crash logging - -GlitchTip is configured in the project. It uses the open source solution Senstry to log crashes -generated by the app and send them to GlitchTip on a European server. - ## StrictMode (debug builds) The app enables Android [StrictMode](https://developer.android.com/reference/android/os/StrictMode) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a4550b8..9fe7896 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -12,7 +12,11 @@ plugins { // Apply Google Services only when the Firebase config is present: google-services.json is not // tracked in git (see google-services.example.json), so builds must work without it. if (file("google-services.json").exists()) { - apply(plugin = libs.plugins.googleServices.get().pluginId) + apply( + plugin = libs.plugins.googleServices + .get() + .pluginId, + ) } kotlin { @@ -27,9 +31,6 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.compose.uiToolingPreview) debugImplementation(libs.compose.uiTooling) - - // Crash logging - implementation(libs.glitchtips) } android { @@ -40,7 +41,7 @@ android { .toInt() defaultConfig { - applicationId = "bzh.ajaury.chombev" + applicationId = "bzh.ksk.donemat" minSdk = libs.versions.androidMinSdk .get() @@ -49,12 +50,8 @@ android { libs.versions.androidTargetSdk .get() .toInt() - versionCode = 1 - versionName = "1.0.0-alpha01" - - // Crash-reporting DSN, kept out of the public repo: read from local.properties - // (glitchtip.dsn=...). Empty when absent, which disables the Sentry SDK. - manifestPlaceholders["glitchtipDsn"] = localProperty("glitchtip.dsn") ?: "" + versionCode = 3 + versionName = "1.0.0-beta02" } packaging { resources { @@ -72,12 +69,3 @@ android { targetCompatibility = JavaVersion.VERSION_11 } } - -/** Reads a key from the root local.properties, or null when the file or key is absent. */ -fun localProperty(key: String): String? { - val file = rootProject.file("local.properties") - if (!file.exists()) return null - return Properties() - .apply { file.inputStream().use { load(it) } } - .getProperty(key) -} diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 5d94f36..4ef8eef 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -20,11 +20,6 @@ - - - - - \ No newline at end of file diff --git a/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt b/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt index 8dca7e3..f334338 100644 --- a/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt +++ b/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt @@ -10,10 +10,11 @@ class ChomBevApp : Application() { if (isDebuggable()) { enableStrictMode() } + + SentryHelper.initializeSentry() } - private fun isDebuggable(): Boolean = - applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 + private fun isDebuggable(): Boolean = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 /** * Enables StrictMode for debug builds only. All violations are logged diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png index a571e60..a6fb4f2 100644 Binary files a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png index 61da551..77bc1dc 100644 Binary files a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png index c41dd28..3c29a89 100644 Binary files a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png index db5080a..c0c6163 100644 Binary files a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png index 6dba46d..7b02a24 100644 Binary files a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png index da31a87..750e012 100644 Binary files a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png index 15ac681..0c9195b 100644 Binary files a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index b216f2d..5e2e822 100644 Binary files a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png index f25a419..250ab1e 100644 Binary files a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index e96783c..2fc95c6 100644 Binary files a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index 6c09e95..44620c8 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - Chom Bev + Donemat - Breton d\'Auray \ No newline at end of file diff --git a/build-logic/src/main/kotlin/gwenedeg.secrets.gradle.kts b/build-logic/src/main/kotlin/gwenedeg.secrets.gradle.kts new file mode 100644 index 0000000..5454b8a --- /dev/null +++ b/build-logic/src/main/kotlin/gwenedeg.secrets.gradle.kts @@ -0,0 +1,84 @@ +import java.util.Properties + +/** + * Injects build-time secrets into `commonMain` so they stay out of version control. + * + * Each secret is read from the environment first (CI), then from `local.properties` + * (local development), and defaults to an empty string when neither provides it — + * consumers are expected to degrade gracefully rather than fail. + * + * The values are emitted as a generated `BuildSecrets` object, available to every + * Kotlin target of the module. + */ + +plugins { + id("org.jetbrains.kotlin.multiplatform") +} + +val secretsPackage = "bzh.ajaury.chombev" + +private val localProperties: Provider = + providers + .fileContents(layout.settingsDirectory.file("local.properties")) + .asText + .map { text -> Properties().apply { load(text.reader()) } } + +/** Resolves a secret from [environmentVariable], falling back to [localPropertyKey], then to an empty string. */ +fun secret( + environmentVariable: String, + localPropertyKey: String, +): Provider = + providers + .environmentVariable(environmentVariable) + .orElse(localProperties.map { properties -> properties.getProperty(localPropertyKey).orEmpty() }) + .orElse("") + +// GlitchTip DSN, consumed by initializeSentry() in the shared module. +val glitchtipDsn: Provider = secret(environmentVariable = "GLITCHTIP_DSN", localPropertyKey = "glitchtip.dsn") + +val generateBuildSecrets = + tasks.register("generateBuildSecrets") { + description = "Generates the BuildSecrets object from local.properties and environment variables." + + // Escaped here so the value can be inlined as-is in a Kotlin string literal. + val dsn = + glitchtipDsn.map { value -> + value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("$", "\${'$'}") + } + val outputDir = layout.buildDirectory.dir("generated/secrets/commonMain/kotlin") + // Kept in locals so the execution-time action never captures the script instance. + val packageName = secretsPackage + val packagePath = packageName.replace('.', '/') + + inputs.property("glitchtipDsn", dsn) + outputs.dir(outputDir) + + doLast { + val file = + outputDir + .get() + .asFile + .resolve("$packagePath/BuildSecrets.kt") + file.parentFile.mkdirs() + file.writeText( + """ + package $packageName + + // Generated by the `gwenedeg.secrets` convention plugin. Do not edit. + internal object BuildSecrets { + const val GLITCHTIP_DSN: String = "${dsn.get()}" + } + + """.trimIndent(), + ) + } + } + +kotlin { + sourceSets.commonMain { + kotlin.srcDir(generateBuildSecrets) + } +} diff --git a/core/audioplayer/build.gradle.kts b/core/audioplayer/build.gradle.kts index 11e4918..44d0c5e 100644 --- a/core/audioplayer/build.gradle.kts +++ b/core/audioplayer/build.gradle.kts @@ -14,8 +14,8 @@ kotlin { implementation(libs.media3.common) } jvmMain.dependencies { - // Desktop audio: Ogg Vorbis SPI so javax.sound.sampled can decode .ogg - implementation(libs.vorbisspi) + // Desktop audio: AAC/MP4 SPI so javax.sound.sampled can decode .m4a + implementation(libs.jaad) } // webMain is the js + wasmJs intermediate source set from the default hierarchy template. webMain.dependencies { diff --git a/core/audioplayer/src/iosMain/kotlin/bzh/ajaury/chombev/player/di/AudioPlayerModule.ios.kt b/core/audioplayer/src/iosMain/kotlin/bzh/ajaury/chombev/player/di/AudioPlayerModule.ios.kt index d42a63b..683e42b 100644 --- a/core/audioplayer/src/iosMain/kotlin/bzh/ajaury/chombev/player/di/AudioPlayerModule.ios.kt +++ b/core/audioplayer/src/iosMain/kotlin/bzh/ajaury/chombev/player/di/AudioPlayerModule.ios.kt @@ -1,6 +1,5 @@ package bzh.ajaury.chombev.player.di -import bzh.ajaury.chombev.core.logging.di.loggingModule import bzh.ajaury.chombev.player.data.IosAudioPlayer import bzh.ajaury.chombev.player.data.IosAudioSessionManager import bzh.ajaury.chombev.player.domain.AudioPlayer @@ -11,7 +10,6 @@ import org.koin.core.module.dsl.singleOf import org.koin.dsl.module actual val audioPlayerModule: Module = module { - includes(loggingModule) singleOf(::IosAudioPlayer) { bind() } singleOf(::IosAudioSessionManager) { bind() } } diff --git a/core/audioplayer/src/jvmMain/kotlin/bzh/ajaury/chombev/player/data/JvmAudioPlayer.kt b/core/audioplayer/src/jvmMain/kotlin/bzh/ajaury/chombev/player/data/JvmAudioPlayer.kt index 5100852..f1af5d4 100644 --- a/core/audioplayer/src/jvmMain/kotlin/bzh/ajaury/chombev/player/data/JvmAudioPlayer.kt +++ b/core/audioplayer/src/jvmMain/kotlin/bzh/ajaury/chombev/player/data/JvmAudioPlayer.kt @@ -18,9 +18,9 @@ import kotlin.time.Duration.Companion.microseconds /** * Desktop [AudioPlayer] backed by [javax.sound.sampled]. * - * The Ogg Vorbis recordings are decoded to PCM by the VorbisSPI providers on the classpath, which - * the JDK [AudioSystem] discovers automatically, so loading works the same as any natively - * supported format. + * The JDK decodes neither AAC nor its MP4 container, so the `.m4a` recordings are decoded to PCM by + * the jaad AAC provider on the classpath, which [AudioSystem] discovers automatically. Loading then + * works the same as for any natively supported format. */ internal class JvmAudioPlayer : AudioPlayer { private var clip: Clip? = null diff --git a/core/audioplayer/src/webMain/kotlin/bzh/ajaury/chombev/player/data/WebAudioPlayer.kt b/core/audioplayer/src/webMain/kotlin/bzh/ajaury/chombev/player/data/WebAudioPlayer.kt index 8b391e2..b8f6213 100644 --- a/core/audioplayer/src/webMain/kotlin/bzh/ajaury/chombev/player/data/WebAudioPlayer.kt +++ b/core/audioplayer/src/webMain/kotlin/bzh/ajaury/chombev/player/data/WebAudioPlayer.kt @@ -24,7 +24,8 @@ internal class WebAudioPlayer : AudioPlayer { get() = (audio?.duration?.takeIf { it.isFinite() } ?: 0.0).seconds override fun seekTo(position: Duration) { - audio?.currentTime = position.inWholeMicroseconds.toDouble() + audio?.currentTime = position.inWholeMilliseconds.toDouble() / 1000 + audio?.play() } override fun load(uri: String) { diff --git a/core/logging/src/commonMain/kotlin/bzh/ajaury/chombev/core/logging/data/KermitLogger.kt b/core/logging/src/commonMain/kotlin/bzh/ajaury/chombev/core/logging/data/KermitLogger.kt index 37da580..67efde2 100644 --- a/core/logging/src/commonMain/kotlin/bzh/ajaury/chombev/core/logging/data/KermitLogger.kt +++ b/core/logging/src/commonMain/kotlin/bzh/ajaury/chombev/core/logging/data/KermitLogger.kt @@ -3,7 +3,7 @@ package bzh.ajaury.chombev.core.logging.data import bzh.ajaury.chombev.core.logging.domain.Logger internal class KermitLogger : Logger { - private val tag: String = "Chom Bev" + private val tag: String = "Donemat" override fun debug( message: String, diff --git a/data/resources/licenses/NotoColorEmoji-OFL.txt b/data/resources/licenses/NotoColorEmoji-OFL.txt new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/data/resources/licenses/NotoColorEmoji-OFL.txt @@ -0,0 +1,92 @@ +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/data/resources/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/data/resources/src/commonMain/composeResources/drawable/compose-multiplatform.xml deleted file mode 100644 index 1ffc948..0000000 --- a/data/resources/src/commonMain/composeResources/drawable/compose-multiplatform.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/data/resources/src/commonMain/composeResources/files/offline_about.html b/data/resources/src/commonMain/composeResources/files/offline_about.html index 6466523..d2e326d 100644 --- a/data/resources/src/commonMain/composeResources/files/offline_about.html +++ b/data/resources/src/commonMain/composeResources/files/offline_about.html @@ -6,7 +6,7 @@ - App Chom Bev – À propos – Kerlenn Sten Kidna + App Donemat - Breton d'Auray – À propos – Kerlenn Sten Kidna @@ -164,7 +164,7 @@ -
"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.
+
"Donemat - Breton d'Auray" 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.
@@ -176,7 +176,7 @@ -

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

+

Cette application vous est proposée par l’association Kerlenn Sten Kidna (KSK). «Donemat - Breton d'Auray» est une oeuvre créée par KSK, initialement sous la forme d’un livret imprimé et accompagné d’un CD audio ("Chom Bev e Brezhoneg"). 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).

diff --git a/data/resources/src/commonMain/composeResources/files/offline_deepen.html b/data/resources/src/commonMain/composeResources/files/offline_deepen.html index 1fc8a43..ec45ce5 100644 --- a/data/resources/src/commonMain/composeResources/files/offline_deepen.html +++ b/data/resources/src/commonMain/composeResources/files/offline_deepen.html @@ -6,7 +6,7 @@ - App Chom Bev -​ Approfondir – Kerlenn Sten Kidna + App Donemat - Breton d'Auray -​ Approfondir – Kerlenn Sten Kidna diff --git a/data/resources/src/commonMain/composeResources/values-br/strings.xml b/data/resources/src/commonMain/composeResources/values-br/strings.xml index 2a7a3d3..684d5c1 100644 --- a/data/resources/src/commonMain/composeResources/values-br/strings.xml +++ b/data/resources/src/commonMain/composeResources/values-br/strings.xml @@ -1,6 +1,6 @@ - Chom bev + Donemat - Breton d'Auray Dastumoù Donaat @@ -28,7 +28,7 @@ Ehan emgefreek Ehanañ goude pep frazenn - Degemer mat e Chom bev + Degemer mat e "Donemat - Breton d'Auray" Deskit brezhoneg en ur selaou enrolladennoù, frazenn ha frazenn, evit gwellaat ho komprenidigezh hag ho tistagadur. Istitloù diouzh ho tro Adalek al lenner, diskouezit pe kuzhit an treuzskrivadur brezhonek hag an droidigezh c\'hallek, cheñchit ment an destenn hag ar merkoù distagañ. @@ -42,6 +42,7 @@ Tizh boas, hep ehan etre ar frazennoù War-lerc\'h Kregiñ + Serriñ Distreiñ Frazenn a-raok diff --git a/data/resources/src/commonMain/composeResources/values/strings.xml b/data/resources/src/commonMain/composeResources/values/strings.xml index f8a3fb6..310f12e 100644 --- a/data/resources/src/commonMain/composeResources/values/strings.xml +++ b/data/resources/src/commonMain/composeResources/values/strings.xml @@ -1,12 +1,12 @@ - Chom bev + Donemat - Breton d'Auray Enregistrements Approfondir À propos - Impossible d\'afficher cette page ici. + Impossible d'afficher cette page ici. Ouvrir dans le navigateur Sous-titres @@ -28,7 +28,7 @@ Pause automatique Mettre en pause après chaque phrase - Bienvenue dans\n Chom bev e Brezhoneg + Bienvenue dans\n Donemat - Breton d'Auray Apprenez le breton en écoutant des enregistrements, phrase par phrase, pour améliorer votre compréhension et votre prononciation du Breton Vannetais. Des sous-titres à votre rythme Depuis le lecteur, affichez ou masquez la transcription bretonne et la traduction française, ajustez la taille du texte et les marques de prononciation. @@ -42,6 +42,7 @@ Vitesse normale, sans pause Suivant Commencer + Fermer Retour Phrase précédente diff --git a/data/resources/src/webMain/composeResources/font/noto_color_emoji.ttf b/data/resources/src/webMain/composeResources/font/noto_color_emoji.ttf new file mode 100644 index 0000000..374200b Binary files /dev/null and b/data/resources/src/webMain/composeResources/font/noto_color_emoji.ttf differ diff --git a/desktopApp/src/main/kotlin/bzh/ajaury/chombev/main.kt b/desktopApp/src/main/kotlin/bzh/ajaury/chombev/main.kt index 22c4e88..e0e87d9 100644 --- a/desktopApp/src/main/kotlin/bzh/ajaury/chombev/main.kt +++ b/desktopApp/src/main/kotlin/bzh/ajaury/chombev/main.kt @@ -7,7 +7,7 @@ fun main() { application { Window( onCloseRequest = ::exitApplication, - title = "Chom Bev", + title = "Donemat - Breton d'Auray", ) { App() } diff --git a/docs/sentry-glitchtip.md b/docs/sentry-glitchtip.md new file mode 100644 index 0000000..6b99249 --- /dev/null +++ b/docs/sentry-glitchtip.md @@ -0,0 +1,164 @@ +# Crash reporting — Sentry SDK on GlitchTip + +"Donemat - Breton d'Auray" reports crashes and handled errors with the +[Sentry Kotlin Multiplatform SDK](https://docs.sentry.io/platforms/kotlin/guides/kotlin-multiplatform/), +but sends them to [GlitchTip](https://glitchtip.com) instead of Sentry. GlitchTip is an +open-source, Sentry-API-compatible backend, so the Sentry SDKs and `glitchtip-cli` work against +it unchanged — only the DSN (and the CLI's `url`) point somewhere else. + +Our instance is the hosted EU one: . + +| | | +|-----------------------|------------------------------------------------------------------------------------------------------| +| SDK | `io.sentry.kotlin.multiplatform` (see `sentry` in `gradle/libs.versions.toml`) | +| Gradle plugin | `io.sentry.kotlin.multiplatform.gradle`, applied in `shared/build.gradle.kts` | +| iOS native dependency | `sentry-cocoa` via Swift Package Manager, declared in the Xcode project | +| Initialisation | `initializeSentry()` in `shared/src/commonMain/kotlin/bzh/ajaury/chombev/SentryHelper.kt` | +| Called from | `ChomBevApp.onCreate()` (Android), `AppDelegate.application(_:didFinishLaunchingWithOptions:)` (iOS) | + +There are two distinct pieces of configuration, and they are easy to confuse: + +1. **The DSN** — where the *app* sends events at runtime. Injected at build time from + `local.properties`. See [Runtime configuration](#1-runtime-configuration--the-dsn). +2. **The auth token** — how *your machine or CI* uploads debug symbols to GlitchTip. + Stored in `.sentryclirc`. See [CLI configuration](#2-cli-configuration--sentryclirc). + +Neither is committed. + +--- + +## 1. Runtime configuration — the DSN + +The DSN is a write-only endpoint URL identifying the GlitchTip project. It is kept out of +version control by the `gwenedeg.secrets` convention plugin +(`build-logic/src/main/kotlin/gwenedeg.secrets.gradle.kts`), which reads it at build +time and generates an internal `BuildSecrets` object into `commonMain`. + +### Setup + +Add the DSN to `local.properties` at the repository root (this file is git-ignored): + +```properties +glitchtip.dsn=https://@eu.glitchtip.com/ +``` + +Find the value in GlitchTip under **Settings → Projects → _Donemat_ → Client Keys (DSN)**. + +On CI, set the `GLITCHTIP_DSN` environment variable instead — it takes precedence over +`local.properties`. + +### Behaviour when it is missing + +`initializeSentry()` returns early when the DSN is blank, so a fresh clone builds and runs +with crash reporting simply disabled. No placeholder or dummy DSN is needed. + +### Adding other secrets + +Extend the `secret(environmentVariable, localPropertyKey)` helper in the convention plugin +rather than hardcoding values in Kotlin sources. + +--- + +## 2. CLI configuration — `.sentryclirc` + +`sentry-cli` needs an auth token to upload debug symbols. It reads its settings from a +`.sentryclirc` INI file in the current working directory (and from `~/.sentryclirc`), +or from environment variables. + +> **`.sentryclirc` contains a credential and must never be committed.** +> It is listed in `.gitignore`. Only `.sentryclirc.example` is versioned. + +### Setup + +```bash +cp .sentryclirc.example .sentryclirc +chmod 600 .sentryclirc +``` + +Then edit `.sentryclirc` and replace the token placeholder: + +```ini +[auth] +token= + +[defaults] +url=https://eu.glitchtip.com/ +org=kerlenn-sten-kidna +project=chom-bev-e-brezhoneg +``` + +- `url` is mandatory: without it `glitchtip-cli` talks to sentry.io, not GlitchTip. +- `org` and `project` are the *slugs* shown in the GlitchTip URLs, not the display names. + +### Creating the token + +In GlitchTip: **user menu → Profile → Auth Tokens → Create New Token**. Grant it at least +`project:read`, `project:write` and `project:releases`. Copy the token immediately — it is +shown only once. + +### On CI + +Do not write the file. Export the equivalent environment variables, sourced from your CI +secret store: + +```bash +export SENTRY_URL=https://eu.glitchtip.com/ +export SENTRY_ORG=kerlenn-sten-kidna +export SENTRY_PROJECT=chom-bev-e-brezhoneg +export SENTRY_AUTH_TOKEN=*** +``` + +### Installing glitchtip-cli + +```bash +curl -fsSL https://glitchtip.com/install.sh | sh +``` + +Check that the configuration resolves: + +```bash +glitchtip-cli info +``` + +It should print the GlitchTip URL and report the token as valid. + +--- + +## 3. iOS Archive — uploading dSYM files + +### Why this is needed + +A release iOS binary ships without symbol names. Without the matching dSYM, GlitchTip shows +crash frames as raw memory addresses. Uploading the dSYM lets it resolve them back to +function names and line numbers. + +The shared Kotlin framework is linked **statically** (`isStatic = true` in +`shared/build.gradle.kts`), so Kotlin/Native frames end up in the app's own dSYM — there is +no separate framework dSYM to upload. + +### Prerequisites + +You need `glitchtip-cli` installed and `.sentryclirc` filled in ([section 2](#2-cli-configuration--sentryclirc)). + +### Upload after an archive + +1. **Product → Archive** in Xcode. +2. In the Organizer, right-click the archive → **Show in Finder**. +3. Right-click the `.xcarchive` → **Show Package Contents**. +4. Upload the `dSYMs` folder: + +```bash +cd /path/to/Gwenedeg +glitchtip-cli debug-files upload /path/to/Gwenedeg.xcarchive/dSYMs +``` + +Add `--include-sources` if you also want source context attached to native frames. + +## 4. References + +- [Sentry — Kotlin Multiplatform SDK](https://docs.sentry.io/platforms/kotlin/guides/kotlin-multiplatform/) +- [Sentry — Uploading debug symbols for Apple platforms](https://docs.sentry.io/platforms/apple/guides/ios/dsym/) +- [Sentry — data collected by the SDK](https://docs.sentry.io/platforms/kotlin/guides/kotlin-multiplatform/data-management/data-collected/) +- [GlitchTip documentation](https://glitchtip.com/documentation) +- [GlitchTip CLI documentation](https://glitchtip.com/documentation/cli) +- [Kotlin — symbolicating iOS crash reports](https://kotlinlang.org/docs/native-ios-symbolication.html) diff --git a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt index 836fe15..c88af1f 100644 --- a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt +++ b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt @@ -1,20 +1,27 @@ package bzh.ajaury.chombev.onboarding.ui +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Hearing import androidx.compose.material.icons.filled.Speed import androidx.compose.material.icons.filled.Subtitles import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -23,6 +30,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import bzh.ajaury.chombev.onboarding.model.OnboardingLevel import bzh.ajaury.chombev.onboarding.ui.components.OnboardingInfoPage @@ -31,6 +40,7 @@ import bzh.ajaury.chombev.onboarding.ui.components.OnboardingPageIndicator import bzh.ajaury.chombev.onboarding.ui.viewmodel.OnboardingUiState import bzh.ajaury.chombev.onboarding.ui.viewmodel.OnboardingViewModel import bzh.ajaury.chombev.resources.generated.resources.Res +import bzh.ajaury.chombev.resources.generated.resources.onboarding_close import bzh.ajaury.chombev.resources.generated.resources.onboarding_goal_description import bzh.ajaury.chombev.resources.generated.resources.onboarding_goal_title import bzh.ajaury.chombev.resources.generated.resources.onboarding_next @@ -46,93 +56,156 @@ import org.koin.compose.viewmodel.koinViewModel private const val PAGE_COUNT = 4 private const val LEVEL_PAGE_INDEX = PAGE_COUNT - 1 +private val DIALOG_MAX_WIDTH = 560.dp +private val DIALOG_MAX_HEIGHT = 680.dp +private const val DIALOG_WINDOW_FRACTION = 0.9f + @Composable -fun OnboardingScreen(viewModel: OnboardingViewModel = koinViewModel()) { +fun OnboardingScreen( + showAsDialog: Boolean = false, + viewModel: OnboardingViewModel = koinViewModel(), +) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() OnboardingScreen( uiState = uiState, + showAsDialog = showAsDialog, onLevelSelected = viewModel::selectLevel, onFinish = viewModel::completeOnboarding, + onDismiss = viewModel::skipOnboarding, ) } @Composable fun OnboardingScreen( uiState: OnboardingUiState, + showAsDialog: Boolean = false, onLevelSelected: (OnboardingLevel) -> Unit = {}, onFinish: () -> Unit = {}, + onDismiss: () -> Unit = {}, +) { + if (showAsDialog) { + Dialog( + onDismissRequest = onDismiss, + // The content sizes itself against the window rather than the platform default width. + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface( + modifier = Modifier + .sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT) + .fillMaxSize(fraction = DIALOG_WINDOW_FRACTION), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + OnboardingContent( + uiState = uiState, + onLevelSelected = onLevelSelected, + onFinish = onFinish, + onClose = onDismiss, + ) + } + } + } else { + Scaffold { innerPadding -> + OnboardingContent( + uiState = uiState, + modifier = Modifier.padding(innerPadding), + onLevelSelected = onLevelSelected, + onFinish = onFinish, + ) + } + } +} + +@Composable +private fun OnboardingContent( + uiState: OnboardingUiState, + modifier: Modifier = Modifier, + onLevelSelected: (OnboardingLevel) -> Unit = {}, + onFinish: () -> Unit = {}, + onClose: (() -> Unit)? = null, ) { val pagerState = rememberPagerState(pageCount = { PAGE_COUNT }) val scope = rememberCoroutineScope() val isLastPage = pagerState.currentPage == LEVEL_PAGE_INDEX - Scaffold { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - HorizontalPager( - state = pagerState, - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - ) { page -> - when (page) { - 0 -> OnboardingInfoPage( - icon = Icons.Filled.Hearing, - title = stringResource(Res.string.onboarding_goal_title), - description = stringResource(Res.string.onboarding_goal_description), - ) - - 1 -> OnboardingInfoPage( - icon = Icons.Filled.Subtitles, - title = stringResource(Res.string.onboarding_subtitles_title), - description = stringResource(Res.string.onboarding_subtitles_description), - ) - - 2 -> OnboardingInfoPage( - icon = Icons.Filled.Speed, - title = stringResource(Res.string.onboarding_playback_title), - description = stringResource(Res.string.onboarding_playback_description), - ) - - else -> OnboardingLevelPage( - selectedLevel = uiState.selectedLevel, - onLevelSelected = onLevelSelected, + Column( + modifier = modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (onClose != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(Res.string.onboarding_close), ) } } + } - OnboardingPageIndicator( - pageCount = PAGE_COUNT, - currentPage = pagerState.currentPage, - ) + HorizontalPager( + state = pagerState, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { page -> + when (page) { + 0 -> OnboardingInfoPage( + icon = Icons.Filled.Hearing, + title = stringResource(Res.string.onboarding_goal_title), + description = stringResource(Res.string.onboarding_goal_description), + ) - Spacer(modifier = Modifier.height(24.dp)) + 1 -> OnboardingInfoPage( + icon = Icons.Filled.Subtitles, + title = stringResource(Res.string.onboarding_subtitles_title), + description = stringResource(Res.string.onboarding_subtitles_description), + ) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - if (isLastPage) { - onFinish() - } else { - scope.launch { - pagerState.animateScrollToPage(pagerState.currentPage + 1) - } - } - }, - ) { - Text( - text = stringResource( - if (isLastPage) Res.string.onboarding_start else Res.string.onboarding_next, - ), + 2 -> OnboardingInfoPage( + icon = Icons.Filled.Speed, + title = stringResource(Res.string.onboarding_playback_title), + description = stringResource(Res.string.onboarding_playback_description), + ) + + else -> OnboardingLevelPage( + selectedLevel = uiState.selectedLevel, + onLevelSelected = onLevelSelected, ) } } + + OnboardingPageIndicator( + pageCount = PAGE_COUNT, + currentPage = pagerState.currentPage, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + if (isLastPage) { + onFinish() + } else { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + }, + ) { + Text( + text = stringResource( + if (isLastPage) Res.string.onboarding_start else Res.string.onboarding_next, + ), + ) + } } } @@ -148,3 +221,23 @@ private fun OnboardingScreenPreview() { ) } } + +@Preview +@Composable +private fun OnboardingContentAsDialogPreview() { + MaterialTheme { + Surface( + modifier = Modifier.sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + OnboardingContent( + uiState = OnboardingUiState( + isCompleted = false, + selectedLevel = OnboardingLevel.BEGINNER, + ), + onClose = {}, + ) + } + } +} diff --git a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/components/OnboardingInfoPage.kt b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/components/OnboardingInfoPage.kt index 5545b1b..2216f18 100644 --- a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/components/OnboardingInfoPage.kt +++ b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/components/OnboardingInfoPage.kt @@ -58,7 +58,7 @@ private fun OnboardingInfoPagePreview() { MaterialTheme { OnboardingInfoPage( icon = Icons.Filled.Hearing, - title = "Bienvenue dans Chom bev", + title = "Bienvenue dans Donemat - Breton d'Auray", description = "Apprenez le breton en écoutant des enregistrements, phrase par phrase.", ) } diff --git a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/viewmodel/OnboardingViewModel.kt b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/viewmodel/OnboardingViewModel.kt index eaee4f0..1875dd6 100644 --- a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/viewmodel/OnboardingViewModel.kt +++ b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/viewmodel/OnboardingViewModel.kt @@ -36,6 +36,12 @@ class OnboardingViewModel( selectedLevel.value = level } + fun skipOnboarding() { + viewModelScope.launch { + preferencesRepository.setOnboardingCompleted() + } + } + fun completeOnboarding() { val level = selectedLevel.value viewModelScope.launch { diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt index 91078e8..2f08c9c 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt @@ -8,7 +8,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -40,7 +41,7 @@ fun PlaybackSpeedBottomSheet( onBreakChange: (Float) -> Unit = {}, onAutoPauseChange: (Boolean) -> Unit = {}, onDismiss: () -> Unit = {}, - sheetState: SheetState = rememberModalBottomSheetState(), + sheetState: SheetState = rememberBottomSheetState(initialValue = SheetValue.Expanded), ) { ModalBottomSheet( onDismissRequest = onDismiss, diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt index eb41a74..e2742fc 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerControl.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -48,7 +49,7 @@ fun PlayerControl( ) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.widthIn(max = 350.dp).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { // Left region: the speed button is centered in the space left of the transport group. diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt index 95283ed..0a3ff36 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt @@ -14,8 +14,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,7 +51,7 @@ fun SubtitlePreferencesBottomSheet( onTextSizeDecrease: () -> Unit = {}, onTextSizeIncrease: () -> Unit = {}, onDismiss: () -> Unit = {}, - sheetState: SheetState = rememberModalBottomSheetState(), + sheetState: SheetState = rememberBottomSheetState(initialValue = SheetValue.Expanded), ) { ModalBottomSheet( onDismissRequest = onDismiss, diff --git a/feature/webview/build.gradle.kts b/feature/webview/build.gradle.kts index 091d58a..4ef76a8 100644 --- a/feature/webview/build.gradle.kts +++ b/feature/webview/build.gradle.kts @@ -14,6 +14,15 @@ kotlin { implementation(libs.compose.uiToolingPreview) implementation(libs.androidx.webkit) } + // webMain is the js + wasmJs intermediate source set from the default hierarchy template. + webMain.dependencies { + // DOM access (document, navigator) to embed the page in an