Let users pick media player apps
This commit is contained in:
@@ -29,4 +29,31 @@ fun CheckboxPreference(
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckboxPreference(
|
||||
title: String,
|
||||
icon: @Composable () -> Unit,
|
||||
iconPadding: Boolean = true,
|
||||
summary: String? = null,
|
||||
value: Boolean,
|
||||
onValueChanged: (Boolean) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Preference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
iconPadding = iconPadding,
|
||||
summary = summary,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
onValueChanged(!value)
|
||||
},
|
||||
controls = {
|
||||
Checkbox(
|
||||
enabled = enabled, checked = value, onCheckedChange = onValueChanged,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
+55
-20
@@ -2,52 +2,87 @@ package de.mm20.launcher2.ui.settings.media
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
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.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
|
||||
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 MediaIntegrationSettingsScreen() {
|
||||
val context = LocalContext.current
|
||||
val viewModel: MediaIntegrationSettingsScreenVM = viewModel()
|
||||
val hasPermission by viewModel.hasPermission.observeAsState()
|
||||
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
|
||||
val loading by viewModel.loading
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(null) {
|
||||
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
viewModel.onResume(density = density.density)
|
||||
}
|
||||
}
|
||||
|
||||
PreferenceScreen(
|
||||
stringResource(R.string.preference_screen_musicwidget),
|
||||
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/widgets/clock"
|
||||
stringResource(R.string.preference_media_integration),
|
||||
) {
|
||||
if (loading) {
|
||||
item {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceCategory {
|
||||
AnimatedVisibility(hasPermission == false) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_music_widget),
|
||||
onClick = {
|
||||
viewModel.requestNotificationPermission(context as AppCompatActivity)
|
||||
AnimatedVisibility(hasPermission == false) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_music_widget),
|
||||
onClick = {
|
||||
viewModel.requestNotificationPermission(context as AppCompatActivity)
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
PreferenceCategory(
|
||||
stringResource(R.string.preference_category_media_apps)
|
||||
) {
|
||||
val apps by viewModel.appList
|
||||
for (app in apps) {
|
||||
val icon by app.icon.collectAsState(null)
|
||||
CheckboxPreference(
|
||||
icon = {
|
||||
ShapedLauncherIcon(size = 32.dp, icon = { icon })
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
title = app.label,
|
||||
value = app.isChecked,
|
||||
onValueChanged = {
|
||||
viewModel.onAppChecked(app, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
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) {
|
||||
|
||||
+80
-9
@@ -1,46 +1,117 @@
|
||||
package de.mm20.launcher2.ui.settings.media
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.applications.AppRepository
|
||||
import de.mm20.launcher2.icons.IconService
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.normalize
|
||||
import de.mm20.launcher2.music.MusicService
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val musicService: MusicService by inject()
|
||||
private val appRepository: AppRepository by inject()
|
||||
private val iconService: IconService by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
val hasPermission =
|
||||
permissionsManager.hasPermission(PermissionGroup.Notifications).asLiveData()
|
||||
permissionsManager.hasPermission(PermissionGroup.Notifications)
|
||||
|
||||
val loading = mutableStateOf(false)
|
||||
|
||||
|
||||
fun requestNotificationPermission(activity: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.Notifications)
|
||||
}
|
||||
|
||||
val filterSources = dataStore.data.map { it.musicWidget.filterSources }.asLiveData()
|
||||
fun setFilterSources(filterSources: Boolean) {
|
||||
val appList = mutableStateOf(emptyList<AppListItem>())
|
||||
|
||||
fun resetWidget() {
|
||||
musicService.resetPlayer()
|
||||
}
|
||||
|
||||
fun onResume(density: Float) {
|
||||
loading.value = true
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val musicApps = musicService.getInstalledPlayerPackages()
|
||||
val allApps = appRepository.getAllInstalledApps().first().filter { it.isMainProfile }
|
||||
.distinctBy { it.`package` }
|
||||
val settings = dataStore.data.map { it.musicWidget }.first()
|
||||
val allowList = settings.allowListList
|
||||
val denyList = settings.denyListList
|
||||
|
||||
appList.value = allApps.map {
|
||||
AppListItem(
|
||||
label = it.label,
|
||||
packageName = it.`package`,
|
||||
isMusicApp = musicApps.contains(it.`package`),
|
||||
isChecked = allowList.contains(it.`package`) || (!denyList.contains(it.`package`) && musicApps.contains(
|
||||
it.`package`
|
||||
)),
|
||||
icon = iconService.getIcon(it, (32 * density).roundToInt())
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(10000))
|
||||
)
|
||||
}.sortedBy { it.label.normalize() }
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onAppChecked(app: AppListItem, checked: Boolean) {
|
||||
val list = appList.value.toMutableList()
|
||||
val index = list.indexOf(app)
|
||||
list[index] = app.copy(isChecked = checked)
|
||||
appList.value = list
|
||||
saveState()
|
||||
}
|
||||
|
||||
private fun saveState() {
|
||||
val allowList = mutableListOf<String>()
|
||||
val denyList = mutableListOf<String>()
|
||||
val appList = appList.value
|
||||
|
||||
for (app in appList) {
|
||||
if (app.isChecked && !app.isMusicApp) {
|
||||
allowList.add(app.packageName)
|
||||
} else if (!app.isChecked && app.isMusicApp) {
|
||||
denyList.add(app.packageName)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setMusicWidget(
|
||||
it.musicWidget.toBuilder()
|
||||
.setFilterSources(filterSources)
|
||||
.clearAllowList()
|
||||
.addAllAllowList(allowList)
|
||||
.clearDenyList()
|
||||
.addAllDenyList(denyList)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetWidget() {
|
||||
musicService.resetPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
data class AppListItem(
|
||||
val label: String,
|
||||
val packageName: String,
|
||||
val isMusicApp: Boolean,
|
||||
val isChecked: Boolean,
|
||||
val icon: Flow<LauncherIcon>,
|
||||
)
|
||||
Reference in New Issue
Block a user