mirror of
https://github.com/AntennaPod/AntennaPod.git
synced 2026-08-18 11:05:49 +00:00
Add nicer play/pause button on WearOS (#8564)
### Description Add nicer play/pause button on WearOS. During playback (and shortly after), shows a large playback button instead of a simple "play on phone" button. <img width="200" alt="watch_media_2026-07-02_18_49_28" src="https://github.com/user-attachments/assets/a7fa0734-8317-47bc-afd0-6e1ab7be89e1" /> <img width="200" alt="watch_media_2026-07-02_18_49_54" src="https://github.com/user-attachments/assets/f935d805-e018-4137-809f-dc5c4690d82d" /> ### Checklist <!-- To help us keep the issue tracker clean and work as efficient as possible, please make sure that you have done all of the following. You can tick the boxes below by placing an x inside the brackets like this: [x] --> - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests
This commit is contained in:
committed by
GitHub
parent
43e4f64faf
commit
b8f8426c8c
@ -63,6 +63,7 @@ afterEvaluate {
|
||||
dependencies {
|
||||
implementation project(":model")
|
||||
implementation project(":ui:common")
|
||||
implementation project(":ui:notifications")
|
||||
implementation project(":net:sync:wear-interface")
|
||||
|
||||
// NOTE: DO NOT INCLUDE a dependency on androidx.compose.material:material, the watch version is a replacement
|
||||
|
||||
@ -13,6 +13,7 @@ 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.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -22,6 +23,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.Hyphens
|
||||
import androidx.compose.ui.text.style.LineBreak
|
||||
@ -32,6 +34,9 @@ import androidx.core.content.IntentCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
|
||||
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
|
||||
import androidx.wear.compose.material3.FilledIconButton
|
||||
import androidx.wear.compose.material3.FilledTonalIconButton
|
||||
import androidx.wear.compose.material3.Icon
|
||||
import androidx.wear.compose.material3.LinearProgressIndicator
|
||||
import androidx.wear.compose.material3.MaterialTheme
|
||||
import androidx.wear.compose.material3.ScrollIndicator
|
||||
@ -41,6 +46,7 @@ import de.danoeh.antennapod.model.feed.FeedItem
|
||||
import de.danoeh.antennapod.ui.common.Converter
|
||||
import de.danoeh.antennapod.ui.common.DateFormatter
|
||||
import de.danoeh.antennapod.ui.common.R as CommonR
|
||||
import de.danoeh.antennapod.ui.notifications.R as NotificationsR
|
||||
import de.danoeh.antennapod.wearos.composable.ListItem
|
||||
|
||||
class EpisodeDetailActivity : ComponentActivity() {
|
||||
@ -60,6 +66,8 @@ class EpisodeDetailActivity : ComponentActivity() {
|
||||
uiState = uiState,
|
||||
onPlay = { viewModel.play() },
|
||||
onPause = { viewModel.pause() },
|
||||
onSkipForward = { viewModel.skipForward() },
|
||||
onSkipBackward = { viewModel.skipBackward() },
|
||||
onOpenOnPhone = { viewModel.openOnPhone() }
|
||||
)
|
||||
}
|
||||
@ -76,6 +84,8 @@ fun EpisodeDetailScreen(
|
||||
uiState: EpisodeDetailUiState,
|
||||
onPlay: () -> Unit,
|
||||
onPause: () -> Unit,
|
||||
onSkipForward: () -> Unit,
|
||||
onSkipBackward: () -> Unit,
|
||||
onOpenOnPhone: () -> Unit
|
||||
) {
|
||||
val item = uiState.item
|
||||
@ -112,7 +122,7 @@ fun EpisodeDetailScreen(
|
||||
lineBreak = LineBreak.Paragraph
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = if (titleExpanded) Int.MAX_VALUE else 3,
|
||||
maxLines = if (titleExpanded) Int.MAX_VALUE else 2,
|
||||
overflow = if (titleExpanded) TextOverflow.Clip else TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
@ -131,7 +141,12 @@ fun EpisodeDetailScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp, top = 2.dp, end = 8.dp, bottom = 16.dp),
|
||||
.padding(
|
||||
start = 8.dp,
|
||||
top = 2.dp,
|
||||
end = 8.dp,
|
||||
bottom = if (uiState.hasStartedPlaying) 8.dp else 16.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
@ -149,12 +164,47 @@ fun EpisodeDetailScreen(
|
||||
}
|
||||
|
||||
item {
|
||||
if (uiState.isCurrentlyPlaying) {
|
||||
ListItem(
|
||||
text = stringResource(CommonR.string.pause_label),
|
||||
iconRes = CommonR.drawable.ic_pause_black,
|
||||
onClick = onPause
|
||||
)
|
||||
if (uiState.hasStartedPlaying) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
|
||||
) {
|
||||
FilledTonalIconButton(onClick = onSkipBackward, modifier = Modifier.size(48.dp)) {
|
||||
Icon(
|
||||
painter = painterResource(NotificationsR.drawable.ic_notification_fast_rewind),
|
||||
contentDescription = stringResource(CommonR.string.rewind_label),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
val playIcon = if (uiState.isCurrentlyPlaying) {
|
||||
CommonR.drawable.ic_pause_black
|
||||
} else {
|
||||
CommonR.drawable.ic_play_48dp_black
|
||||
}
|
||||
val playLabel = if (uiState.isCurrentlyPlaying) {
|
||||
CommonR.string.pause_label
|
||||
} else {
|
||||
CommonR.string.play_label
|
||||
}
|
||||
FilledIconButton(
|
||||
onClick = if (uiState.isCurrentlyPlaying) onPause else onPlay,
|
||||
modifier = Modifier.size(60.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(playIcon),
|
||||
contentDescription = stringResource(playLabel),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
FilledTonalIconButton(onClick = onSkipForward, modifier = Modifier.size(48.dp)) {
|
||||
Icon(
|
||||
painter = painterResource(NotificationsR.drawable.ic_notification_fast_forward),
|
||||
contentDescription = stringResource(CommonR.string.fast_forward_label),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ListItem(
|
||||
text = stringResource(CommonR.string.wearos_play_on_phone),
|
||||
|
||||
@ -22,7 +22,8 @@ data class EpisodeDetailUiState(
|
||||
val item: FeedItem,
|
||||
val position: Int = 0,
|
||||
val duration: Int = 0,
|
||||
val isCurrentlyPlaying: Boolean = false
|
||||
val isCurrentlyPlaying: Boolean = false,
|
||||
val hasStartedPlaying: Boolean = false
|
||||
)
|
||||
|
||||
class EpisodeDetailViewModel(application: Application, private val episode: FeedItem) : AndroidViewModel(application) {
|
||||
@ -30,7 +31,9 @@ class EpisodeDetailViewModel(application: Application, private val episode: Feed
|
||||
EpisodeDetailUiState(
|
||||
item = episode,
|
||||
position = episode.media?.position ?: 0,
|
||||
duration = episode.media?.duration ?: 0
|
||||
duration = episode.media?.duration ?: 0,
|
||||
hasStartedPlaying = WearDataRepository.nowPlaying.value
|
||||
?.takeIf { it.item.id == episode.id }?.isPlaying == true
|
||||
)
|
||||
)
|
||||
val uiState: StateFlow<EpisodeDetailUiState> = _uiState
|
||||
@ -50,7 +53,12 @@ class EpisodeDetailViewModel(application: Application, private val episode: Feed
|
||||
val duration = liveData?.item?.media?.duration?.takeIf { it > 0 } ?: episode.media?.duration ?: 0
|
||||
val isCurrentlyPlaying = liveData?.isPlaying == true
|
||||
_uiState.update {
|
||||
it.copy(position = position, duration = duration, isCurrentlyPlaying = isCurrentlyPlaying)
|
||||
it.copy(
|
||||
position = position,
|
||||
duration = duration,
|
||||
isCurrentlyPlaying = isCurrentlyPlaying,
|
||||
hasStartedPlaying = it.hasStartedPlaying || isCurrentlyPlaying
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,6 +74,14 @@ class EpisodeDetailViewModel(application: Application, private val episode: Feed
|
||||
viewModelScope.launch(Dispatchers.IO) { WearMessageSender.send(getApplication(), WearDataPaths.PAUSE) }
|
||||
}
|
||||
|
||||
fun skipForward() {
|
||||
viewModelScope.launch(Dispatchers.IO) { WearMessageSender.send(getApplication(), WearDataPaths.SKIP_FORWARD) }
|
||||
}
|
||||
|
||||
fun skipBackward() {
|
||||
viewModelScope.launch(Dispatchers.IO) { WearMessageSender.send(getApplication(), WearDataPaths.SKIP_BACKWARD) }
|
||||
}
|
||||
|
||||
fun openOnPhone() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
WearMessageSender.send(getApplication(), WearDataPaths.openOnPhonePath(episode.id))
|
||||
|
||||
Reference in New Issue
Block a user