refactor: improve player accessibility
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
id("gwenedeg.kmp.library")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
androidLibrary {
|
||||||
|
namespace = "bzh.ajaury.chombev.core.accessibility"
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.accessibility.AccessibilityManager
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
|
|
||||||
|
internal class AndroidAccessibilityRepository(
|
||||||
|
context: Context,
|
||||||
|
) : AccessibilityRepository {
|
||||||
|
private val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
|
||||||
|
|
||||||
|
// Screen readers such as TalkBack turn on touch exploration. Other accessibility services
|
||||||
|
// (password managers, automation tools) do not, so `isEnabled` alone would misreport them.
|
||||||
|
override fun isScreenReaderEnabled(): Boolean = accessibilityManager?.isTouchExplorationEnabled == true
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.di
|
||||||
|
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.data.AndroidAccessibilityRepository
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.module.dsl.bind
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
actual val accessibilityModule: Module = module {
|
||||||
|
singleOf(::AndroidAccessibilityRepository) { bind<AccessibilityRepository>() }
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.data
|
||||||
|
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
|
|
||||||
|
internal class UndetectableAccessibilityRepository : AccessibilityRepository {
|
||||||
|
override fun isScreenReaderEnabled(): Boolean = false
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.di
|
||||||
|
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
|
||||||
|
expect val accessibilityModule: Module
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.domain
|
||||||
|
|
||||||
|
interface AccessibilityRepository {
|
||||||
|
fun isScreenReaderEnabled(): Boolean
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.data
|
||||||
|
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
|
import platform.UIKit.UIAccessibilityIsVoiceOverRunning
|
||||||
|
|
||||||
|
internal class IosAccessibilityRepository : AccessibilityRepository {
|
||||||
|
override fun isScreenReaderEnabled(): Boolean = UIAccessibilityIsVoiceOverRunning()
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.di
|
||||||
|
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.data.IosAccessibilityRepository
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.module.dsl.bind
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
actual val accessibilityModule: Module = module {
|
||||||
|
singleOf(::IosAccessibilityRepository) { bind<AccessibilityRepository>() }
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.di
|
||||||
|
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.data.UndetectableAccessibilityRepository
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.module.dsl.bind
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
actual val accessibilityModule: Module = module {
|
||||||
|
singleOf(::UndetectableAccessibilityRepository) { bind<AccessibilityRepository>() }
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package bzh.ajaury.chombev.core.accessibility.di
|
||||||
|
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.data.UndetectableAccessibilityRepository
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.module.dsl.bind
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
actual val accessibilityModule: Module = module {
|
||||||
|
singleOf(::UndetectableAccessibilityRepository) { bind<AccessibilityRepository>() }
|
||||||
|
}
|
||||||
+11
-5
@@ -138,13 +138,16 @@ internal class PlaybackRepositoryImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(record: Record) {
|
override suspend fun load(
|
||||||
|
record: Record,
|
||||||
|
autoPlay: Boolean,
|
||||||
|
) {
|
||||||
phraseBreakJob?.cancel()
|
phraseBreakJob?.cancel()
|
||||||
cancelRunningBreak()
|
cancelRunningBreak()
|
||||||
pendingPhraseStart = null
|
pendingPhraseStart = null
|
||||||
pendingSeekTarget = null
|
pendingSeekTarget = null
|
||||||
|
|
||||||
playAudio(filePath = record.audioResourcePath)
|
loadAudio(filePath = record.audioResourcePath, autoPlay = autoPlay)
|
||||||
subtitle.value = loadSubtitle(record = record)
|
subtitle.value = loadSubtitle(record = record)
|
||||||
reachedPhraseIndex = upcomingPhraseIndex(position = Duration.ZERO)
|
reachedPhraseIndex = upcomingPhraseIndex(position = Duration.ZERO)
|
||||||
recordTitle.value = record.title
|
recordTitle.value = record.title
|
||||||
@@ -264,12 +267,15 @@ internal class PlaybackRepositoryImpl(
|
|||||||
audioPlayer.seekTo(position)
|
audioPlayer.seekTo(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playAudio(filePath: String) {
|
private fun loadAudio(
|
||||||
|
filePath: String,
|
||||||
|
autoPlay: Boolean,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
audioPlayer.load(uri = resourceReader.uri(filePath))
|
audioPlayer.load(uri = resourceReader.uri(filePath))
|
||||||
audioPlayer.play()
|
if (autoPlay) audioPlayer.play()
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
logger.error(message = "Failed to play audio: $filePath", throwable = exception)
|
logger.error(message = "Failed to load audio: $filePath", throwable = exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,10 @@ import kotlin.time.Duration
|
|||||||
interface PlaybackRepository {
|
interface PlaybackRepository {
|
||||||
val playerState: Flow<PlayerState>
|
val playerState: Flow<PlayerState>
|
||||||
|
|
||||||
suspend fun load(record: Record)
|
suspend fun load(
|
||||||
|
record: Record,
|
||||||
|
autoPlay: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun play()
|
suspend fun play()
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<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="settings_open_action_description">accéder aux paramètres</string>
|
||||||
<string name="subtitle_transcription_label">Transcription (BZH)</string>
|
<string name="subtitle_transcription_label">Transcription (BZH)</string>
|
||||||
<string name="subtitle_transcription_description">Afficher la transcription bretonne</string>
|
<string name="subtitle_transcription_description">Afficher la transcription bretonne</string>
|
||||||
<string name="subtitle_translation_label">Traduction (FR)</string>
|
<string name="subtitle_translation_label">Traduction (FR)</string>
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
<string name="records_chapter_number">Chapitre %1$d</string>
|
<string name="records_chapter_number">Chapitre %1$d</string>
|
||||||
|
|
||||||
<string name="player_back">Retour</string>
|
<string name="player_back">Retour</string>
|
||||||
|
<string name="player_back_action_label">revenir à la liste des contenus</string>
|
||||||
<string name="player_previous_sentence">Phrase précédente</string>
|
<string name="player_previous_sentence">Phrase précédente</string>
|
||||||
<string name="player_next_sentence">Phrase suivante</string>
|
<string name="player_next_sentence">Phrase suivante</string>
|
||||||
<string name="player_play">Lire</string>
|
<string name="player_play">Lire</string>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
// Core modules
|
// Core modules
|
||||||
|
implementation(projects.core.accessibility)
|
||||||
implementation(projects.core.model)
|
implementation(projects.core.model)
|
||||||
implementation(projects.core.logging)
|
implementation(projects.core.logging)
|
||||||
implementation(projects.core.audioplayer)
|
implementation(projects.core.audioplayer)
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.onClick
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
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
|
||||||
@@ -43,6 +47,8 @@ import bzh.ajaury.chombev.player.ui.viewmodel.PlayerViewModel
|
|||||||
import bzh.ajaury.chombev.preferences.ui.SubtitlePreferencesBottomSheet
|
import bzh.ajaury.chombev.preferences.ui.SubtitlePreferencesBottomSheet
|
||||||
import bzh.ajaury.chombev.resources.generated.resources.Res
|
import bzh.ajaury.chombev.resources.generated.resources.Res
|
||||||
import bzh.ajaury.chombev.resources.generated.resources.player_back
|
import bzh.ajaury.chombev.resources.generated.resources.player_back
|
||||||
|
import bzh.ajaury.chombev.resources.generated.resources.player_back_action_label
|
||||||
|
import bzh.ajaury.chombev.resources.generated.resources.settings_open_action_description
|
||||||
import bzh.ajaury.chombev.resources.generated.resources.subtitle_preferences_action
|
import bzh.ajaury.chombev.resources.generated.resources.subtitle_preferences_action
|
||||||
import bzh.ajaury.chombev.subtitle.model.SubtitleLine
|
import bzh.ajaury.chombev.subtitle.model.SubtitleLine
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
@@ -198,6 +204,9 @@ private fun PlayerTopBar(
|
|||||||
label = "PlayerTitle",
|
label = "PlayerTitle",
|
||||||
) { (emoji, title) ->
|
) { (emoji, title) ->
|
||||||
Row(
|
Row(
|
||||||
|
modifier = Modifier.clearAndSetSemantics {
|
||||||
|
contentDescription = title.translation ?: title.transcription
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
@@ -227,15 +236,20 @@ private fun PlayerTopBar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBackClicked) {
|
BackButton(onBackClicked = onBackClicked)
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(Res.string.player_back),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = showPreferences) {
|
val openSettingsLabel = stringResource(Res.string.settings_open_action_description)
|
||||||
|
IconButton(
|
||||||
|
// Names the action: "double-tap to open settings" rather than the generic "to activate".
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
onClick(label = openSettingsLabel) {
|
||||||
|
showPreferences()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = showPreferences,
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Subtitles,
|
imageVector = Icons.Filled.Subtitles,
|
||||||
contentDescription = stringResource(Res.string.subtitle_preferences_action),
|
contentDescription = stringResource(Res.string.subtitle_preferences_action),
|
||||||
@@ -245,6 +259,26 @@ private fun PlayerTopBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BackButton(onBackClicked: () -> Unit) {
|
||||||
|
val backActionLabel = stringResource(Res.string.player_back_action_label)
|
||||||
|
IconButton(
|
||||||
|
onClick = onBackClicked,
|
||||||
|
modifier = Modifier
|
||||||
|
.semantics {
|
||||||
|
onClick(label = backActionLabel) {
|
||||||
|
onBackClicked()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(Res.string.player_back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val previewSubtitleLines =
|
private val previewSubtitleLines =
|
||||||
listOf(
|
listOf(
|
||||||
SubtitleLine(
|
SubtitleLine(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -97,7 +98,9 @@ fun SubtitleList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
BoxWithConstraints(modifier = modifier) {
|
// Subtitles transcribe the audio being played: hide them from screen readers, which would
|
||||||
|
// otherwise read the same phrases a second time.
|
||||||
|
BoxWithConstraints(modifier = modifier.clearAndSetSemantics {}) {
|
||||||
// Half-height padding so the first and last lines can reach the vertical center. Captured here
|
// Half-height padding so the first and last lines can reach the vertical center. Captured here
|
||||||
// because `maxHeight` (BoxWithConstraintsScope) is not in scope inside the provider lambda.
|
// because `maxHeight` (BoxWithConstraintsScope) is not in scope inside the provider lambda.
|
||||||
val verticalPadding = maxHeight / 2
|
val verticalPadding = maxHeight / 2
|
||||||
|
|||||||
+2
-1
@@ -17,10 +17,11 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
@Composable
|
@Composable
|
||||||
fun NextButton(
|
fun NextButton(
|
||||||
canGoToNextSentence: Boolean,
|
canGoToNextSentence: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onNextSentenceClicked: () -> Unit = {},
|
onNextSentenceClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.size(60.dp),
|
modifier = modifier.size(60.dp),
|
||||||
enabled = canGoToNextSentence,
|
enabled = canGoToNextSentence,
|
||||||
onClick = onNextSentenceClicked,
|
onClick = onNextSentenceClicked,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-1
@@ -38,6 +38,7 @@ private val BreakRingSize = 68.dp
|
|||||||
fun PlayButton(
|
fun PlayButton(
|
||||||
playbackState: PlaybackState,
|
playbackState: PlaybackState,
|
||||||
breakDuration: Duration = Duration.ZERO,
|
breakDuration: Duration = Duration.ZERO,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onMainPlayActionButtonClicked: () -> Unit = {},
|
onMainPlayActionButtonClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val isBreaking = playbackState == PlaybackState.BREAKING && breakDuration > Duration.ZERO
|
val isBreaking = playbackState == PlaybackState.BREAKING && breakDuration > Duration.ZERO
|
||||||
@@ -61,7 +62,7 @@ fun PlayButton(
|
|||||||
// The ring reserves its footprint at all times so the transport row does not jump when a break
|
// The ring reserves its footprint at all times so the transport row does not jump when a break
|
||||||
// starts; the indicator itself is only drawn while breaking.
|
// starts; the indicator itself is only drawn while breaking.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(BreakRingSize),
|
modifier = modifier.size(BreakRingSize),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (isBreaking) {
|
if (isBreaking) {
|
||||||
|
|||||||
+4
-1
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import bzh.ajaury.chombev.player.model.PlaybackTiming
|
import bzh.ajaury.chombev.player.model.PlaybackTiming
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
@@ -33,7 +34,9 @@ fun PlaybackProgress(
|
|||||||
}
|
}
|
||||||
seekProgress = null
|
seekProgress = null
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clearAndSetSemantics { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -17,10 +17,11 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
@Composable
|
@Composable
|
||||||
fun PreviousButton(
|
fun PreviousButton(
|
||||||
canGoToPreviousSentence: Boolean,
|
canGoToPreviousSentence: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onPreviousSentenceClicked: () -> Unit = {},
|
onPreviousSentenceClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.size(60.dp),
|
modifier = modifier.size(60.dp),
|
||||||
enabled = canGoToPreviousSentence,
|
enabled = canGoToPreviousSentence,
|
||||||
onClick = onPreviousSentenceClicked,
|
onClick = onPreviousSentenceClicked,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+13
-1
@@ -9,10 +9,13 @@ import androidx.compose.material3.LocalContentColor
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.onClick
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
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
|
||||||
import bzh.ajaury.chombev.resources.generated.resources.Res
|
import bzh.ajaury.chombev.resources.generated.resources.Res
|
||||||
import bzh.ajaury.chombev.resources.generated.resources.playback_settings_action
|
import bzh.ajaury.chombev.resources.generated.resources.playback_settings_action
|
||||||
|
import bzh.ajaury.chombev.resources.generated.resources.settings_open_action_description
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,10 +25,19 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
@Composable
|
@Composable
|
||||||
fun SpeedPreferenceButton(
|
fun SpeedPreferenceButton(
|
||||||
isActive: Boolean,
|
isActive: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onSpeedClicked: () -> Unit = {},
|
onSpeedClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val openSettingsLabel = stringResource(Res.string.settings_open_action_description)
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.size(60.dp),
|
modifier = modifier
|
||||||
|
.size(60.dp)
|
||||||
|
.semantics {
|
||||||
|
onClick(label = openSettingsLabel) {
|
||||||
|
onSpeedClicked()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
onClick = onSpeedClicked,
|
onClick = onSpeedClicked,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
+7
-2
@@ -2,6 +2,7 @@ package bzh.ajaury.chombev.player.ui.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository
|
||||||
import bzh.ajaury.chombev.core.logging.domain.Logger
|
import bzh.ajaury.chombev.core.logging.domain.Logger
|
||||||
import bzh.ajaury.chombev.core.model.Phrase
|
import bzh.ajaury.chombev.core.model.Phrase
|
||||||
import bzh.ajaury.chombev.core.model.Record
|
import bzh.ajaury.chombev.core.model.Record
|
||||||
@@ -29,6 +30,7 @@ class PlayerViewModel(
|
|||||||
private val playbackRepository: PlaybackRepository,
|
private val playbackRepository: PlaybackRepository,
|
||||||
private val preferencesRepository: PreferencesRepository,
|
private val preferencesRepository: PreferencesRepository,
|
||||||
private val audioSessionManager: AudioSessionManager,
|
private val audioSessionManager: AudioSessionManager,
|
||||||
|
private val accessibilityRepository: AccessibilityRepository,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val record = MutableStateFlow<Record?>(null)
|
private val record = MutableStateFlow<Record?>(null)
|
||||||
@@ -62,11 +64,14 @@ class PlayerViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
playbackRepository.stop()
|
playbackRepository.stop()
|
||||||
|
|
||||||
|
// A screen reader speaks over the audio: let its user start playback once oriented.
|
||||||
|
val autoPlay = !accessibilityRepository.isScreenReaderEnabled()
|
||||||
|
|
||||||
logger.debug("Loading record $recordId")
|
logger.debug("Loading record $recordId")
|
||||||
val alreadyLoadedRecord = record.value
|
val alreadyLoadedRecord = record.value
|
||||||
if (alreadyLoadedRecord != null) {
|
if (alreadyLoadedRecord != null) {
|
||||||
logger.debug("Record $recordId already loaded")
|
logger.debug("Record $recordId already loaded")
|
||||||
playbackRepository.load(record = alreadyLoadedRecord)
|
playbackRepository.load(record = alreadyLoadedRecord, autoPlay = autoPlay)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +81,7 @@ class PlayerViewModel(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackRepository.load(record = loadedRecord)
|
playbackRepository.load(record = loadedRecord, autoPlay = autoPlay)
|
||||||
record.value = loadedRecord
|
record.value = loadedRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -1,5 +1,6 @@
|
|||||||
package bzh.ajaury.chombev.records.ui.di
|
package bzh.ajaury.chombev.records.ui.di
|
||||||
|
|
||||||
|
import bzh.ajaury.chombev.core.accessibility.di.accessibilityModule
|
||||||
import bzh.ajaury.chombev.player.di.audioPlayerModule
|
import bzh.ajaury.chombev.player.di.audioPlayerModule
|
||||||
import bzh.ajaury.chombev.player.di.playbackModule
|
import bzh.ajaury.chombev.player.di.playbackModule
|
||||||
import bzh.ajaury.chombev.player.ui.viewmodel.PlayerViewModel
|
import bzh.ajaury.chombev.player.ui.viewmodel.PlayerViewModel
|
||||||
@@ -23,6 +24,7 @@ val recordsFeatureModule: Module = module {
|
|||||||
recordsModule,
|
recordsModule,
|
||||||
resourcesModule,
|
resourcesModule,
|
||||||
audioPlayerModule,
|
audioPlayerModule,
|
||||||
|
accessibilityModule,
|
||||||
subtitleModule,
|
subtitleModule,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ include(":webApp")
|
|||||||
include(":shared")
|
include(":shared")
|
||||||
|
|
||||||
// Core modules
|
// Core modules
|
||||||
|
include(":core:accessibility")
|
||||||
include(":core:audioplayer")
|
include(":core:audioplayer")
|
||||||
include(":core:coroutines")
|
include(":core:coroutines")
|
||||||
include(":core:logging")
|
include(":core:logging")
|
||||||
|
|||||||
Reference in New Issue
Block a user