mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Ask for record audio permission when enabling visualizer
This commit is contained in:
parent
c91ac434e0
commit
8aa64c6fef
5 changed files with 184 additions and 28 deletions
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -38,11 +39,15 @@ fun MusicViewOptionsDialog(
|
||||||
appPreferences: AppPreferences,
|
appPreferences: AppPreferences,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onViewOptionsChange: (AppPreferences) -> Unit,
|
onViewOptionsChange: (AppPreferences) -> Unit,
|
||||||
|
onEnableVisualizer: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
val columnState = rememberLazyListState()
|
val columnState = rememberLazyListState()
|
||||||
val items = remember { getMusicPreferences() }
|
val items = remember { getMusicPreferences() }
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
properties =
|
properties =
|
||||||
|
|
@ -55,6 +60,7 @@ fun MusicViewOptionsDialog(
|
||||||
window.setGravity(Gravity.END)
|
window.setGravity(Gravity.END)
|
||||||
window.setDimAmount(0f)
|
window.setDimAmount(0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.view_options),
|
text = stringResource(R.string.view_options),
|
||||||
|
|
@ -79,18 +85,48 @@ fun MusicViewOptionsDialog(
|
||||||
pref as AppPreference<AppPreferences, Any>
|
pref as AppPreference<AppPreferences, Any>
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val value = pref.getter.invoke(appPreferences)
|
val value = pref.getter.invoke(appPreferences)
|
||||||
ComposablePreference(
|
// Using title is a bit hacky
|
||||||
preference = pref,
|
when (pref.title) {
|
||||||
value = value,
|
R.string.show_visualizer -> {
|
||||||
onNavigate = {},
|
ComposablePreference(
|
||||||
onValueChange = { newValue ->
|
preference = pref,
|
||||||
onViewOptionsChange.invoke(pref.setter(appPreferences, newValue))
|
value = value,
|
||||||
},
|
onNavigate = {},
|
||||||
interactionSource = interactionSource,
|
onValueChange = { newValue ->
|
||||||
modifier = Modifier,
|
newValue as Boolean
|
||||||
onClickPreference = { pref ->
|
if (newValue) {
|
||||||
},
|
onEnableVisualizer.invoke()
|
||||||
)
|
} else {
|
||||||
|
onViewOptionsChange.invoke(
|
||||||
|
pref.setter(
|
||||||
|
appPreferences,
|
||||||
|
newValue,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = Modifier,
|
||||||
|
onClickPreference = { pref ->
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
ComposablePreference(
|
||||||
|
preference = pref,
|
||||||
|
value = value,
|
||||||
|
onNavigate = {},
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
onViewOptionsChange.invoke(pref.setter(appPreferences, newValue))
|
||||||
|
},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = Modifier,
|
||||||
|
onClickPreference = { pref ->
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.music
|
package com.github.damontecres.wholphin.ui.detail.music
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
|
@ -41,6 +44,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
|
@ -49,9 +53,11 @@ import com.github.damontecres.wholphin.data.model.AudioItem
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.rememberQueue
|
import com.github.damontecres.wholphin.services.rememberQueue
|
||||||
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.findActivity
|
||||||
import com.github.damontecres.wholphin.ui.playback.BottomDialog
|
import com.github.damontecres.wholphin.ui.playback.BottomDialog
|
||||||
import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
|
import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
|
||||||
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
||||||
|
|
@ -141,6 +147,14 @@ fun NowPlayingPage(
|
||||||
focusRequester.tryRequestFocus()
|
focusRequester.tryRequestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showRationaleDialog by remember { mutableStateOf(false) }
|
||||||
|
val launcher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission(),
|
||||||
|
) { isGranted ->
|
||||||
|
viewModel.startVisualizer(isGranted, true)
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -317,6 +331,34 @@ fun NowPlayingPage(
|
||||||
appPreferences = preferences,
|
appPreferences = preferences,
|
||||||
onDismissRequest = { showViewOptionsDialog = false },
|
onDismissRequest = { showViewOptionsDialog = false },
|
||||||
onViewOptionsChange = { viewModel.updatePreferences(it) },
|
onViewOptionsChange = { viewModel.updatePreferences(it) },
|
||||||
|
onEnableVisualizer = {
|
||||||
|
val showRationale =
|
||||||
|
context
|
||||||
|
.findActivity()
|
||||||
|
?.shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO) == true
|
||||||
|
when {
|
||||||
|
!state.visualizerPermissions && showRationale -> {
|
||||||
|
showRationaleDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
!state.visualizerPermissions -> {
|
||||||
|
launcher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
viewModel.startVisualizer(true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showRationaleDialog) {
|
||||||
|
RecordAudioRationaleDialog(
|
||||||
|
onDismissRequest = { showRationaleDialog = false },
|
||||||
|
onClick = {
|
||||||
|
showRationaleDialog = false
|
||||||
|
launcher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -356,3 +398,31 @@ fun NowPlayingBottomDialog(
|
||||||
gravity = Gravity.START,
|
gravity = Gravity.START,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecordAudioRationaleDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.visualizer_rationale),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.continue_string),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ data class NowPlayingState(
|
||||||
val musicServiceState: MusicServiceState,
|
val musicServiceState: MusicServiceState,
|
||||||
val lyrics: LyricDto? = null,
|
val lyrics: LyricDto? = null,
|
||||||
val currentLyricIndex: Int? = null,
|
val currentLyricIndex: Int? = null,
|
||||||
|
val visualizerPermissions: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val hasLyrics: Boolean get() = lyrics != null && lyrics.lyrics.isNotEmpty()
|
val hasLyrics: Boolean get() = lyrics != null && lyrics.lyrics.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.music
|
package com.github.damontecres.wholphin.ui.detail.music
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.media.audiofx.Visualizer
|
import android.media.audiofx.Visualizer
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -11,6 +15,7 @@ import com.github.damontecres.wholphin.data.model.AudioItem
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.updateMusicPreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.MusicService
|
import com.github.damontecres.wholphin.services.MusicService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
|
@ -24,13 +29,17 @@ import com.mayakapps.kache.InMemoryKache
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.lyricsApi
|
import org.jellyfin.sdk.api.client.extensions.lyricsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
|
|
@ -48,6 +57,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||||
class NowPlayingViewModel
|
class NowPlayingViewModel
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val musicService: MusicService,
|
private val musicService: MusicService,
|
||||||
private val backdropService: BackdropService,
|
private val backdropService: BackdropService,
|
||||||
|
|
@ -62,6 +72,7 @@ class NowPlayingViewModel
|
||||||
fun create(): NowPlayingViewModel
|
fun create(): NowPlayingViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val visualizerMutex = Mutex()
|
||||||
private var visualizer: Visualizer? = null
|
private var visualizer: Visualizer? = null
|
||||||
|
|
||||||
val controllerViewState =
|
val controllerViewState =
|
||||||
|
|
@ -87,25 +98,16 @@ class NowPlayingViewModel
|
||||||
player.removeListener(this)
|
player.removeListener(this)
|
||||||
visualizer?.release()
|
visualizer?.release()
|
||||||
}
|
}
|
||||||
|
val visualizerPermissions =
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.RECORD_AUDIO,
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
startVisualizer(visualizerPermissions, false)
|
||||||
viewModelScope.launchDefault {
|
viewModelScope.launchDefault {
|
||||||
musicService.state.collectLatest { musicServiceState ->
|
musicService.state.collectLatest { musicServiceState ->
|
||||||
if (musicServiceState.status != NowPlayingStatus.IDLE) {
|
if (musicServiceState.status != NowPlayingStatus.IDLE) {
|
||||||
viewModelScope.launchDefault {
|
visualizer?.enabled = musicServiceState.status == NowPlayingStatus.PLAYING
|
||||||
if (visualizer == null) {
|
|
||||||
visualizer =
|
|
||||||
Visualizer(onMain { player.audioSessionId }).apply {
|
|
||||||
captureSize = Visualizer.getCaptureSizeRange()[1]
|
|
||||||
setDataCaptureListener(
|
|
||||||
this@NowPlayingViewModel,
|
|
||||||
Visualizer.getMaxCaptureRate() / 2,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visualizer?.enabled =
|
|
||||||
musicServiceState.status == NowPlayingStatus.PLAYING
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.update { it.copy(musicServiceState = musicServiceState) }
|
state.update { it.copy(musicServiceState = musicServiceState) }
|
||||||
|
|
@ -294,4 +296,49 @@ class NowPlayingViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initVisualizer() {
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
visualizerMutex.withLock {
|
||||||
|
val prefs = preferencesDataStore.data.first()
|
||||||
|
if (visualizer == null &&
|
||||||
|
state.value.visualizerPermissions &&
|
||||||
|
prefs.musicPreferences.showVisualizer
|
||||||
|
) {
|
||||||
|
Timber.v("Creating visualizer")
|
||||||
|
visualizer =
|
||||||
|
Visualizer(onMain { player.audioSessionId }).apply {
|
||||||
|
captureSize = Visualizer.getCaptureSizeRange()[1]
|
||||||
|
setDataCaptureListener(
|
||||||
|
this@NowPlayingViewModel,
|
||||||
|
Visualizer.getMaxCaptureRate() / 2,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startVisualizer(
|
||||||
|
permissionGranted: Boolean,
|
||||||
|
updatePreferences: Boolean,
|
||||||
|
) {
|
||||||
|
Timber.v("startVisualizer: permissionGranted=%s", permissionGranted)
|
||||||
|
state.update {
|
||||||
|
it.copy(
|
||||||
|
visualizerPermissions = permissionGranted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
if (updatePreferences || !permissionGranted) {
|
||||||
|
preferencesDataStore.updateData {
|
||||||
|
it.updateMusicPreferences { showVisualizer = permissionGranted }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initVisualizer()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -742,6 +742,8 @@
|
||||||
<string name="show_visualizer">Show visualizer</string>
|
<string name="show_visualizer">Show visualizer</string>
|
||||||
<string name="show_backdrop">Show backdrop</string>
|
<string name="show_backdrop">Show backdrop</string>
|
||||||
<string name="play_as_type">Play as?</string>
|
<string name="play_as_type">Play as?</string>
|
||||||
|
<string name="visualizer_rationale">Visualizing the currently playing audio requires permission to record audio.</string>
|
||||||
|
<string name="continue_string">Continue</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue