Add option to disable music widget filtering

This commit is contained in:
MM20
2022-01-06 23:43:50 +01:00
parent 51e9370dd5
commit d3358fa7b5
16 changed files with 477 additions and 198 deletions
@@ -1,5 +1,7 @@
package de.mm20.launcher2.ui.widget
package de.mm20.launcher2.ui.launcher.widgets.music
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
@@ -10,6 +12,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Audiotrack
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.SkipNext
import androidx.compose.material.icons.rounded.SkipPrevious
@@ -24,13 +27,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.music.PlaybackState
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.ktx.conditional
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidgetVM
@OptIn(
ExperimentalFoundationApi::class,
@@ -49,107 +53,152 @@ fun MusicWidget() {
val context = LocalContext.current
Row(
modifier = Modifier.height(IntrinsicSize.Min)
Column(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxHeight()
.weight(2f),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = title ?: "---",
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = artist ?: "---",
modifier = Modifier.padding(vertical = 2.dp),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = album ?: "---",
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp, end = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(
onClick = {
viewModel.skipPrevious()
}) {
Icon(
imageVector = Icons.Rounded.SkipPrevious,
null
)
val hasPermission by viewModel.hasPermission.observeAsState()
AnimatedVisibility(hasPermission == false) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_music_widget),
onClick = {
viewModel.requestPermission(context as AppCompatActivity)
}
val playPauseIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause)
IconButton(onClick = { viewModel.togglePause() }) {
Icon(
painter = rememberAnimatedVectorPainter(playPauseIcon, atEnd = playbackState == PlaybackState.Playing),
contentDescription = ""
)
}
IconButton(onClick = {
viewModel.skipNext()
}) {
Icon(
imageVector = Icons.Rounded.SkipNext,
null
)
}
}
)
}
Box(
modifier = Modifier
.size(144.dp)
.combinedClickable(
onClick = {
viewModel.openPlayer()
},
onLongClick = {
viewModel.openPlayerSelector(context)
if (title == null && artist == null && album == null) {
NoData()
} else {
Row(
modifier = Modifier.height(IntrinsicSize.Min)
) {
Column(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxHeight()
.weight(2f),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = title ?: "",
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = artist ?: "",
modifier = Modifier.padding(vertical = 2.dp),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = album ?: "",
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
.conditional(
albumArt == null,
Modifier.background(
MaterialTheme.colorScheme.primaryContainer,
)
),
contentAlignment = Alignment.Center
) {
if (albumArt != null) {
albumArt?.let {
Image(
bitmap = it.asImageBitmap(),
Row(
modifier = Modifier
.fillMaxSize(),
contentDescription = null
)
.fillMaxWidth()
.padding(bottom = 4.dp, end = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(
onClick = {
viewModel.skipPrevious()
}) {
Icon(
imageVector = Icons.Rounded.SkipPrevious,
null
)
}
val playPauseIcon =
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause)
IconButton(onClick = { viewModel.togglePause() }) {
Icon(
painter = rememberAnimatedVectorPainter(
playPauseIcon,
atEnd = playbackState == PlaybackState.Playing
),
contentDescription = ""
)
}
IconButton(onClick = {
viewModel.skipNext()
}) {
Icon(
imageVector = Icons.Rounded.SkipNext,
null
)
}
}
}
Box(
modifier = Modifier
.size(144.dp)
.combinedClickable(
onClick = {
viewModel.openPlayer()
},
onLongClick = {
viewModel.openPlayerSelector(context)
}
)
.conditional(
albumArt == null,
Modifier.background(
MaterialTheme.colorScheme.primaryContainer,
)
),
contentAlignment = Alignment.Center
) {
if (albumArt != null) {
albumArt?.let {
Image(
bitmap = it.asImageBitmap(),
modifier = Modifier
.fillMaxSize(),
contentDescription = null
)
}
} else {
Icon(
imageVector = Icons.Rounded.MusicNote,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(56.dp)
)
}
}
} else {
Icon(
imageVector = Icons.Rounded.MusicNote,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(56.dp)
)
}
}
}
}
@Composable
fun NoData() {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.Audiotrack,
contentDescription = "",
modifier = Modifier
.padding(24.dp)
.size(32.dp),
tint = MaterialTheme.colorScheme.secondary
)
Text(
text = stringResource(id = R.string.music_widget_no_data),
style = MaterialTheme.typography.bodySmall
)
}
}
@@ -3,17 +3,21 @@ package de.mm20.launcher2.ui.launcher.widgets.music
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.music.MusicRepository
import de.mm20.launcher2.music.PlaybackState
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class MusicWidgetVM: ViewModel(), KoinComponent {
private val musicRepository: MusicRepository by inject()
private val permissionsManager: PermissionsManager by inject()
val title: LiveData<String?> = musicRepository.title.asLiveData()
val artist: LiveData<String?> = musicRepository.artist.asLiveData()
@@ -21,6 +25,8 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
val albumArt: LiveData<Bitmap?> = musicRepository.albumArt.asLiveData()
val playbackState: LiveData<PlaybackState> = musicRepository.playbackState.asLiveData()
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Notifications).asLiveData()
fun skipPrevious() {
musicRepository.previous()
}
@@ -35,7 +41,8 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
fun openPlayer() {
try {
musicRepository.getLaunchIntent().send()
musicRepository.
openPlayer()?.send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
@@ -44,4 +51,8 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
fun openPlayerSelector(context: Context) {
musicRepository.openPlayerChooser(context)
}
fun requestPermission(context: AppCompatActivity) {
permissionsManager.requestPermission(context, PermissionGroup.Notifications)
}
}
@@ -10,7 +10,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.widget.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
class MusicWidget : LauncherWidget {
@@ -26,6 +26,7 @@ import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
@@ -89,6 +90,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/widgets/weather") {
WeatherWidgetSettingsScreen()
}
composable("settings/widgets/music") {
MusicWidgetSettingsScreen()
}
composable("settings/about") {
AboutSettingsScreen()
}
@@ -0,0 +1,66 @@
package de.mm20.launcher2.ui.settings.musicwidget
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.BuildConfig
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@Composable
fun MusicWidgetSettingsScreen() {
val context = LocalContext.current
val viewModel: MusicWidgetSettingsScreenVM = viewModel()
val hasPermission by viewModel.hasPermission.observeAsState()
PreferenceScreen(
stringResource(R.string.preference_screen_musicwidget)
) {
item {
PreferenceCategory {
AnimatedVisibility(hasPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_music_widget),
onClick = {
viewModel.requestNotificationPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
val filterSources by viewModel.filterSources.observeAsState(false)
SwitchPreference(
title = stringResource(R.string.preference_music_filter_sources),
summary = stringResource(R.string.preference_music_filter_sources_summary),
value = filterSources,
onValueChanged = {
viewModel.setFilterSources(it)
}
)
}
}
if (BuildConfig.DEBUG) {
item {
PreferenceCategory(stringResource(R.string.preference_category_debug)) {
Preference(
title = "Reset widget",
summary = "Clear all music data",
onClick = {
viewModel.resetWidget()
}
)
}
}
}
}
}
@@ -0,0 +1,46 @@
package de.mm20.launcher2.ui.settings.musicwidget
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.music.MusicRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class MusicWidgetSettingsScreenVM : ViewModel(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val musicRepository: MusicRepository by inject()
private val dataStore: LauncherDataStore by inject()
val hasPermission =
permissionsManager.hasPermission(PermissionGroup.Notifications).asLiveData()
fun requestNotificationPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Notifications)
}
val filterSources = dataStore.data.map { it.musicWidget.filterSources }.asLiveData()
fun setFilterSources(filterSources: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setMusicWidget(
it.musicWidget.toBuilder()
.setFilterSources(filterSources)
)
.build()
}
}
}
fun resetWidget() {
musicRepository.resetPlayer()
}
}
@@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.settings.widgets
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Audiotrack
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Today
import androidx.compose.runtime.Composable
@@ -22,6 +23,13 @@ fun WidgetsSettingsScreen() {
navController?.navigate("settings/widgets/weather")
}
)
Preference(
title = stringResource(R.string.preference_screen_musicwidget),
icon = Icons.Rounded.Audiotrack,
onClick = {
navController?.navigate("settings/widgets/music")
}
)
Preference(
title = stringResource(R.string.preference_screen_calendarwidget),
icon = Icons.Rounded.Today
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType