Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,12 @@ fun SettingScreen(
}
}
}
var showLastFmDialog by rememberSaveable { mutableStateOf(false) }
val lastFmUsername by viewModel.lastFmUsername.collectAsStateWithLifecycle()
val lastFmApiKey by viewModel.lastFmApiKey.collectAsStateWithLifecycle()
val lastFmSecret by viewModel.lastFmSecret.collectAsStateWithLifecycle()
val lastFmSessionKey by viewModel.lastFmSessionKey.collectAsStateWithLifecycle()
val lastFmEnabled by viewModel.lastFmEnabled.collectAsStateWithLifecycle()
var showYouTubeAccountDialog by rememberSaveable {
mutableStateOf(false)
}
Expand Down Expand Up @@ -635,6 +641,13 @@ fun SettingScreen(
showYouTubeAccountDialog = true
},
)
val isLastFmAuth by viewModel.isLastFmAuthenticating.collectAsStateWithLifecycle()
SettingItem(
title = "Last.fm Scrobbling",
subtitle = if (lastFmUsername.isNotBlank()) "Connected as $lastFmUsername" else if (isLastFmAuth) "Waiting for browser approval..." else "Tap to log in with your Last.fm account",
switch = if (lastFmUsername.isNotBlank()) (lastFmEnabled to { viewModel.setLastFmEnabled(it) }) else null,
onClick = { showLastFmDialog = true },
)
SettingItem(
title = stringResource(Res.string.language),
subtitle = SUPPORTED_LANGUAGE.getLanguageFromCode(language ?: "en-US"),
Expand Down Expand Up @@ -2339,6 +2352,89 @@ fun SettingScreen(
},
)
}
if (showLastFmDialog) {
var inputUser by rememberSaveable { mutableStateOf(lastFmUsername) }
var inputKey by rememberSaveable { mutableStateOf(lastFmApiKey) }
var inputSecret by rememberSaveable { mutableStateOf(lastFmSecret) }
var inputSessionKey by rememberSaveable { mutableStateOf(lastFmSessionKey) }
val isLastFmAuth by viewModel.isLastFmAuthenticating.collectAsStateWithLifecycle()
val uriHandler = LocalUriHandler.current

AlertDialog(
onDismissRequest = { showLastFmDialog = false },
title = { Text(text = "Last.fm Account Integration", style = typo().titleMedium) },
text = {
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
Text(
text = "Enter your Last.fm Username, API Key, and Secret from last.fm/api/account/create:",
style = typo().bodySmall,
modifier = Modifier.padding(bottom = 12.dp)
)
androidx.compose.material3.OutlinedTextField(
value = inputUser,
onValueChange = { inputUser = it },
label = { Text("Last.fm Username") },
singleLine = true,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
)
androidx.compose.material3.OutlinedTextField(
value = inputKey,
onValueChange = { inputKey = it },
label = { Text("Last.fm API Key") },
singleLine = true,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
)
androidx.compose.material3.OutlinedTextField(
value = inputSecret,
onValueChange = { inputSecret = it },
label = { Text("Last.fm Shared Secret") },
singleLine = true,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
)

if (lastFmSessionKey.isNotBlank()) {
Text(
text = "Authenticated Session Key Active ✅",
style = typo().bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 8.dp)
)
}
}
},
confirmButton = {
TextButton(
onClick = {
viewModel.loginLastFmCustom(inputUser, inputKey, inputSecret) { url ->
try {
uriHandler.openUri(url)
} catch (e: Exception) {
try {
val os = System.getProperty("os.name").lowercase()
if (os.contains("win")) {
Runtime.getRuntime().exec(arrayOf("cmd.exe", "/c", "start", url))
} else if (os.contains("mac")) {
Runtime.getRuntime().exec(arrayOf("open", url))
} else {
Runtime.getRuntime().exec(arrayOf("xdg-open", url))
}
} catch (_: Exception) {}
}
}
},
enabled = !isLastFmAuth
) {
Text(if (isLastFmAuth) "Waiting for browser approval..." else "Save & Authenticate")
}
},
dismissButton = {
TextButton(onClick = { showLastFmDialog = false }) {
Text("Cancel")
}
}
)
}

if (showYouTubeAccountDialog) {
BasicAlertDialog(
onDismissRequest = { },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ class SettingsViewModel(
private val _localTrackingEnabled = MutableStateFlow<Boolean>(false)
val localTrackingEnabled: StateFlow<Boolean> = _localTrackingEnabled

private val _lastFmEnabled = MutableStateFlow<Boolean>(false)
val lastFmEnabled: StateFlow<Boolean> = _lastFmEnabled

private val _lastFmUsername = MutableStateFlow<String>("")
val lastFmUsername: StateFlow<String> = _lastFmUsername

private val _lastFmApiKey = MutableStateFlow<String>("")
val lastFmApiKey: StateFlow<String> = _lastFmApiKey

private val _lastFmSecret = MutableStateFlow<String>("")
val lastFmSecret: StateFlow<String> = _lastFmSecret

private val _lastFmSessionKey = MutableStateFlow<String>("")
val lastFmSessionKey: StateFlow<String> = _lastFmSessionKey

private val _blogNotificationEnabled = MutableStateFlow(true)
val blogNotificationEnabled: StateFlow<Boolean> = _blogNotificationEnabled

Expand Down Expand Up @@ -279,6 +294,8 @@ class SettingsViewModel(
getDownloadQuality()
getVideoDownloadQuality()
getLocalTrackingEnabled()
getLastFmEnabled()
getLastFmAccount()
getBlogNotificationEnabled()
getAutoBackupEnabled()
getAutoBackupFrequency()
Expand All @@ -305,6 +322,117 @@ class SettingsViewModel(
viewModelScope.launch {
dataStoreManager.setLocalTrackingEnabled(enabled)
getLocalTrackingEnabled()
getLastFmEnabled()
getLastFmAccount()
}
}

private fun getLastFmEnabled() {
viewModelScope.launch {
dataStoreManager.lastFmEnabled.collect { enabled ->
_lastFmEnabled.value = enabled == DataStoreManager.TRUE
}
}
}

fun setLastFmEnabled(enabled: Boolean) {
viewModelScope.launch {
dataStoreManager.setLastFmEnabled(enabled)
getLastFmEnabled()
getLastFmAccount()
}
}

private fun getLastFmAccount() {
viewModelScope.launch {
dataStoreManager.lastFmUsername.collect { _lastFmUsername.value = it }
}
viewModelScope.launch {
dataStoreManager.lastFmApiKey.collect { _lastFmApiKey.value = it }
}
viewModelScope.launch {
dataStoreManager.lastFmSecret.collect { _lastFmSecret.value = it }
}
viewModelScope.launch {
dataStoreManager.lastFmSessionKey.collect { _lastFmSessionKey.value = it }
}
}

private val _isLastFmAuthenticating = MutableStateFlow<Boolean>(false)
val isLastFmAuthenticating: StateFlow<Boolean> = _isLastFmAuthenticating

fun loginLastFm(openUrl: (String) -> Unit) {
viewModelScope.launch {
_isLastFmAuthenticating.value = true
val token = com.maxrave.data.scrobbler.LastFmScrobbler.fetchAuthToken()
if (token != null) {
openUrl("https://www.last.fm/api/auth/?api_key=${com.maxrave.data.scrobbler.LastFmScrobbler.DEFAULT_API_KEY}&token=$token")
for (i in 1..30) {
kotlinx.coroutines.delay(2000)
val result = com.maxrave.data.scrobbler.LastFmScrobbler.fetchSession(token)
if (result != null) {
val (username, sessionKey) = result
dataStoreManager.setLastFmUsername(username)
dataStoreManager.setLastFmSessionKey(sessionKey)
dataStoreManager.setLastFmApiKey(com.maxrave.data.scrobbler.LastFmScrobbler.DEFAULT_API_KEY)
dataStoreManager.setLastFmSecret(com.maxrave.data.scrobbler.LastFmScrobbler.DEFAULT_API_SECRET)
dataStoreManager.setLastFmEnabled(true)
getLastFmAccount()
getLastFmEnabled()
break
}
}
}
_isLastFmAuthenticating.value = false
}
}

fun loginLastFmCustom(username: String, apiKey: String, secret: String, openUrl: (String) -> Unit) {
viewModelScope.launch {
_isLastFmAuthenticating.value = true
val effectiveKey = apiKey.ifBlank { com.maxrave.data.scrobbler.LastFmScrobbler.DEFAULT_API_KEY }
val effectiveSecret = secret.ifBlank { com.maxrave.data.scrobbler.LastFmScrobbler.DEFAULT_API_SECRET }
val token = com.maxrave.data.scrobbler.LastFmScrobbler.fetchAuthToken(effectiveKey)
if (token != null) {
openUrl("https://www.last.fm/api/auth/?api_key=${effectiveKey}&token=$token")
for (i in 1..30) {
kotlinx.coroutines.delay(2000)
val result = com.maxrave.data.scrobbler.LastFmScrobbler.fetchSession(token, effectiveKey, effectiveSecret)
if (result != null) {
val (fetchedUser, sessionKey) = result
val finalUser = fetchedUser.ifBlank { username }
dataStoreManager.setLastFmUsername(finalUser)
dataStoreManager.setLastFmSessionKey(sessionKey)
dataStoreManager.setLastFmApiKey(effectiveKey)
dataStoreManager.setLastFmSecret(effectiveSecret)
dataStoreManager.setLastFmEnabled(true)
getLastFmAccount()
getLastFmEnabled()
break
}
}
}
_isLastFmAuthenticating.value = false
}
}

fun logoutLastFm() {
viewModelScope.launch {
dataStoreManager.setLastFmUsername("")
dataStoreManager.setLastFmSessionKey("")
dataStoreManager.setLastFmEnabled(false)
getLastFmAccount()
getLastFmEnabled()
}
}

fun saveLastFmCredentials(username: String, apiKey: String, secret: String, sessionKey: String) {
viewModelScope.launch {
dataStoreManager.setLastFmUsername(username)
dataStoreManager.setLastFmApiKey(apiKey)
dataStoreManager.setLastFmSecret(secret)
dataStoreManager.setLastFmSessionKey(sessionKey)
getLastFmAccount()
}
}

Expand Down
2 changes: 1 addition & 1 deletion core
Submodule core updated from ccaa2d to 99708a