feat: settings option to change the app language

This commit is contained in:
2024-07-12 02:15:33 +02:00
committed by Illusion
parent 7d07433e95
commit edd5b24d92
11 changed files with 144 additions and 29 deletions
@@ -8,6 +8,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -45,10 +46,9 @@ data class NotificationEvent(
val artist: String,
val title: String,
val playbackState: Int,
val duration: Long
)
class MainActivity : ComponentActivity() {
class MainActivity : AppCompatActivity() {
private var lastNotification: MutableStateFlow<NotificationEvent?> = MutableStateFlow(null)
private var isReceiverRunning = false
@@ -175,14 +175,12 @@ class MainActivity : ComponentActivity() {
val artist = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_ARTIST)
val songTitle = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_SONG_TITLE)
val playbackState = intent.getIntExtra(NotificationListener.NOTIFICATION_PACKAGE_PLAYBACK_STATE, -1)
val duration = intent.getLongExtra(NotificationListener.NOTIFICATION_PACKAGE_DURATION, -1)
val notifPackageName = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_NAME)
lastNotification.update { NotificationEvent(
title = songTitle!!,
artist = artist!!,
playbackState = playbackState,
duration = duration,
packageName = notifPackageName!!,
) }
}
@@ -18,6 +18,7 @@ import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
class NotificationListener: NotificationListenerService() {
@@ -113,14 +114,11 @@ class NotificationListener: NotificationListenerService() {
if (ContextCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
return
}
// Apparently the new getParcelable API is buggy and throws an exception if used.
// so we continue to use the deprecated API even on newer versions.
// https://issuetracker.google.com/issues/240585930
val mediaSessionToken = sbn.notification.extras.getParcelable<MediaSession.Token>(Notification.EXTRA_MEDIA_SESSION)
val mediaSessionToken = BundleCompat.getParcelable(sbn.notification.extras, Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java)
?: return
val mediaCtrl = MediaController(context, mediaSessionToken)
val duration = mediaCtrl.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.div(1000)
val playbackState = mediaCtrl.playbackState?.state
val albumArt = mediaCtrl.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
@@ -133,7 +131,6 @@ class NotificationListener: NotificationListenerService() {
intent.putExtra(NOTIFICATION_PACKAGE_NAME, sbn.packageName)
intent.putExtra(NOTIFICATION_PACKAGE_ARTIST, packageArtist)
intent.putExtra(NOTIFICATION_PACKAGE_SONG_TITLE, packageSongTitle)
intent.putExtra(NOTIFICATION_PACKAGE_DURATION, duration)
intent.putExtra(NOTIFICATION_PACKAGE_PLAYBACK_STATE, playbackState)
if (playbackState == PlaybackState.STATE_PAUSED) {
@@ -166,9 +163,8 @@ class NotificationListener: NotificationListenerService() {
const val NOTIFICATION_PACKAGE_ARTIST = "package_artist"
const val NOTIFICATION_PACKAGE_SONG_TITLE = "package_song_title"
const val NOTIFICATION_PACKAGE_PLAYBACK_STATE = "package_playback_state"
const val NOTIFICATION_PACKAGE_DURATION = "package_duration"
const val NOTIFICATION_ID = 1
const val NOTIFICATION_CHANNEL_ID = "Lyrics"
const val START_FOREGROUND_SERVICE_ACTION = "START_SERVICE"
}
}
}
@@ -30,9 +30,9 @@ import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@@ -48,7 +48,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -86,7 +85,6 @@ import com.illusionman1212.lyricsgrabbr.viewmodels.HomeViewModel
import com.illusionman1212.lyricsgrabbr.viewmodels.SearchResult
import kotlinx.coroutines.flow.MutableStateFlow
import coil.compose.AsyncImage
import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.ui.components.forwardingPainter
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
@@ -106,7 +104,6 @@ fun HomePage(
lastNotification: MutableStateFlow<NotificationEvent?>
) {
val context = LocalContext.current
val application = (context as Activity).application as LGApp
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState()
@@ -124,7 +121,7 @@ fun HomePage(
) }
var shouldShowNotificationPermissionRationale by remember { mutableStateOf(
ActivityCompat.shouldShowRequestPermissionRationale(
context,
context as Activity,
POST_NOTIFICATIONS
)
)}
@@ -415,7 +412,7 @@ fun Results(
)
if (it != results.last())
Divider()
HorizontalDivider()
}
}
}
@@ -1,5 +1,6 @@
package com.illusionman1212.lyricsgrabbr.ui.screens
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -12,10 +13,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -32,10 +35,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
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.compose.ui.unit.sp
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.ui.components.LGAlertDialog
@@ -43,7 +48,7 @@ import com.illusionman1212.lyricsgrabbr.ui.components.LGRadioButton
import com.illusionman1212.lyricsgrabbr.ui.theme.ColorSecondaryLight
import com.illusionman1212.lyricsgrabbr.data.Theme
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
import com.illusionman1212.lyricsgrabbr.utils.mirror
import com.illusionman1212.lyricsgrabbr.utils.getLangPreferenceDropdownEntries
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
@@ -72,8 +77,7 @@ fun SettingsPage(
) {
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
Icon(
modifier = Modifier.mirror(),
imageVector = Icons.Filled.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(
id = R.string.go_back
) )
@@ -97,6 +101,7 @@ fun SettingsPage(
@Composable
fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
var themeOpenDialog by remember { mutableStateOf(false) }
var languageOpenDialog by remember { mutableStateOf(false) }
SettingGroup(title = stringResource(id = R.string.appearance)) {
DialogSetting(
@@ -105,6 +110,17 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
icon = Icons.Outlined.Palette,
onClick = { themeOpenDialog = true }
)
DialogSetting(
title = stringResource(id = R.string.language),
value = AppCompatDelegate.getApplicationLocales()[0]?.let {
AppCompatDelegate.getApplicationLocales()[0]?.getDisplayLanguage(
it
)
}
?: stringResource(id = R.string._default),
icon = Icons.Outlined.Translate,
onClick = { languageOpenDialog = true }
)
}
if (themeOpenDialog) {
ThemeDialog(
@@ -115,6 +131,11 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
}
)
}
if (languageOpenDialog) {
LanguageDialog(
onDismiss = { languageOpenDialog = false },
)
}
}
@Composable
@@ -159,6 +180,47 @@ fun ThemeDialog(
}
}
@Composable
fun LanguageDialog(
onDismiss: () -> Unit,
) {
val langs = LocalContext.current.getLangPreferenceDropdownEntries()
LGAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.language),
buttons = {
TextButton(onClick = onDismiss) {
Text(stringResource(id = R.string.cancel))
}
},
modifier = Modifier.fillMaxWidth()
) {
LazyColumn {
item {
LGRadioButton(
text = stringResource(id = R.string._default),
onClick = {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
},
selected = AppCompatDelegate.getApplicationLocales().isEmpty,
)
}
for ((key, value) in langs) {
item {
LGRadioButton(
text = key,
onClick = {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(value))
},
selected = AppCompatDelegate.getApplicationLocales()[0]?.language == value,
)
}
}
}
}
}
@Composable
fun SettingGroup(title: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
@@ -169,7 +231,7 @@ fun SettingGroup(title: String, content: @Composable () -> Unit) {
}
@Composable
fun Header(title: String) {
private fun Header(title: String) {
Row(Modifier.padding(start = 56.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.primary)
}
@@ -1,5 +1,6 @@
package com.illusionman1212.lyricsgrabbr.utils
import android.content.Context
import android.text.Annotation
import android.text.SpannedString
import androidx.annotation.StringRes
@@ -7,6 +8,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.core.os.LocaleListCompat
import com.illusionman1212.lyricsgrabbr.R
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
// Native stylization is coming soon
// https://issuetracker.google.com/issues/139320238
@@ -31,4 +37,38 @@ fun annotatedStringResource(
spanStyles(annotation)?.let { resultBuilder.addStyle(it, spanStart, spanEnd) }
}
return resultBuilder.toAnnotatedString()
}
fun Context.getLocaleListFromXml(): LocaleListCompat {
val tagsList = mutableListOf<CharSequence>()
try {
val xpp: XmlPullParser = resources.getXml(R.xml.locales_config)
while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
if (xpp.eventType == XmlPullParser.START_TAG) {
if (xpp.name == "locale") {
tagsList.add(xpp.getAttributeValue(0))
}
}
xpp.next()
}
} catch (e: XmlPullParserException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return LocaleListCompat.forLanguageTags(tagsList.joinToString(","))
}
fun Context.getLangPreferenceDropdownEntries(): Map<String, String> {
val localeList = getLocaleListFromXml()
val map = mutableMapOf<String, String>()
for (a in 0 until localeList.size()) {
localeList[a].let {
it?.let { it1 -> map.put(it1.getDisplayName(it), it.toLanguageTag()) }
}
}
return map
}
@@ -1,6 +1,5 @@
package com.illusionman1212.lyricsgrabbr.viewmodels
import android.content.Context
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -85,7 +84,7 @@ data class HomeState(
val error: String? = null,
)
class HomeViewModel(private val context: Context, private val homeRepository: HomeRepository): ViewModel() {
class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() {
private val _uiState = MutableStateFlow(HomeState())
val uiState = _uiState.asStateFlow()
private val json = Json {
@@ -134,7 +133,7 @@ class HomeViewModel(private val context: Context, private val homeRepository: Ho
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as LGApp)
HomeViewModel(application.applicationContext, application.homeRepository)
HomeViewModel(application.homeRepository)
}
}
}