From a1c6008528c7f7d30988b3f84487b29a32698e68 Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Mon, 26 Aug 2024 22:25:28 +0200 Subject: [PATCH] feat: added setting to configure custom API endpoint --- app/src/main/AndroidManifest.xml | 1 + .../lyricsgrabbr/data/HomeRepository.kt | 13 ++-- .../data/SettingsPreferencesRepository.kt | 10 +++ .../lyricsgrabbr/ui/screens/Settings.kt | 61 +++++++++++++++++++ .../lyricsgrabbr/viewmodels/HomeViewModel.kt | 9 ++- .../viewmodels/SettingsViewModel.kt | 11 ++++ app/src/main/res/values-ar/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 8 files changed, 101 insertions(+), 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 623e1a3..ef0d756 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ { - val builder = Uri.Builder() - val url = builder.scheme("https") - .authority("api.illusionman1212.com") - .appendPath("lyrics") + suspend fun makeSearchRequest(lContext: Context, baseUrl: String, song: String, artist: String): Pair { + val url = Uri.parse(baseUrl) + .buildUpon() .appendPath("search") .appendQueryParameter("q", "$song $artist") .appendQueryParameter("disableFuzzy", "true") @@ -76,7 +75,7 @@ class HomeRepository(private val context: Context) { return Pair(lContext.resources.getString(R.string.timeout), false) } - return Pair(lContext.resources.getString(R.string.generic_error), false) + return Pair(e.message, false) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/data/SettingsPreferencesRepository.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/data/SettingsPreferencesRepository.kt index a49e4e1..6dcbd9f 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/data/SettingsPreferencesRepository.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/data/SettingsPreferencesRepository.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import kotlinx.coroutines.flow.map @@ -19,6 +20,7 @@ data class SettingsPreferences( val whitelist: Set = emptySet(), val keepScreenOn: Boolean = false, val whitelistPromptSeen: Boolean = false, + val geniURLbaseURL: String = "https://api.illusionman1212.com/lyrics", ) class SettingsPreferencesRepository( @@ -29,6 +31,7 @@ class SettingsPreferencesRepository( private val WHITELIST = stringSetPreferencesKey("apps_whitelist") private val KEEP_SCREEN_ON = booleanPreferencesKey("keep_screen_on") private val WHITELIST_PROMPT_SEEN = booleanPreferencesKey("whitelist_prompt_seen") + private val GENIURL_BASE_URL = stringPreferencesKey("geniurl_base_url") } val preferences = dataStore.data.map { mapSettingsPreferences(it) } @@ -63,12 +66,19 @@ class SettingsPreferencesRepository( } } + suspend fun setGeniURLBaseURL(url: String) { + dataStore.edit { prefs -> + prefs[GENIURL_BASE_URL] = url + } + } + private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences { return SettingsPreferences( prefs[APP_THEME] ?: Theme.SYSTEM.ordinal, prefs[WHITELIST] ?: emptySet(), prefs[KEEP_SCREEN_ON] ?: false, prefs[WHITELIST_PROMPT_SEEN] ?: false, + prefs[GENIURL_BASE_URL] ?: "https://api.illusionman1212.com/lyrics", ) } } \ No newline at end of file diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt index d50634f..cd05e98 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt @@ -1,5 +1,6 @@ package com.illusionman1212.lyricsgrabbr.ui.screens +import android.util.Patterns import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,6 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.ScreenLockPortrait +import androidx.compose.material.icons.outlined.Cloud import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Translate import androidx.compose.material3.Icon @@ -27,6 +29,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -96,6 +99,7 @@ fun SettingsPage( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Appearance(viewModel, uiState) + geniURL(viewModel, uiState) Listening(navigateToWhitelist) PowerManagement(viewModel, uiState) } @@ -185,6 +189,63 @@ private fun PowerManagement(viewModel: SettingsViewModel, uiState: SettingsState } } +@Composable +private fun geniURL(viewModel: SettingsViewModel, uiState: SettingsState) { + var dialogOpen by remember { mutableStateOf(false) } + + SettingGroup(title = "geniURL") { + DialogSetting( + title = stringResource(id = R.string.api_base_url), + value = uiState.geniURLbaseURL, + icon = Icons.Outlined.Cloud, + onClick = { dialogOpen = true } + ) + } + + if (dialogOpen) { + BaseURLDialog( + uiState = uiState, + viewModel = viewModel, + onDismiss = { dialogOpen = false }, + ) + } +} + +@Composable +private fun BaseURLDialog(uiState: SettingsState, viewModel: SettingsViewModel, onDismiss: () -> Unit) { + var value by remember { mutableStateOf(uiState.geniURLbaseURL) } + var isValidUrl by remember { mutableStateOf(true) } + + LGAlertDialog( + onDismiss = onDismiss, + title = stringResource(id = R.string.base_url), + buttons = { + TextButton( + onClick = { + viewModel.setGeniURLBaseURL(value) + onDismiss() + }, + enabled = isValidUrl + ) { + Text(stringResource(id = R.string.confirm)) + } + TextButton(onClick = onDismiss) { + Text(stringResource(id = R.string.cancel)) + } + }, + modifier = Modifier.fillMaxWidth() + ) { + TextField( + value = value, + isError = !isValidUrl, + onValueChange = { newValue -> + isValidUrl = Patterns.WEB_URL.matcher(newValue.lowercase()).matches() && newValue.startsWith("http", true) + value = newValue + }, + ) + } +} + @Composable fun ThemeDialog( uiState: SettingsState, diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt index 6f9c0a1..0789df2 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt @@ -11,9 +11,11 @@ import androidx.lifecycle.viewmodel.viewModelFactory import com.illusionman1212.lyricsgrabbr.LGApp import com.illusionman1212.lyricsgrabbr.NotificationEvent import com.illusionman1212.lyricsgrabbr.data.HomeRepository +import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -85,7 +87,7 @@ data class HomeState( val error: String? = null, ) -class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() { +class HomeViewModel(private val homeRepository: HomeRepository, private val settingsRepository: SettingsPreferencesRepository): ViewModel() { private val _uiState = MutableStateFlow(HomeState()) val uiState = _uiState.asStateFlow() private val json = Json { @@ -107,7 +109,8 @@ class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() { _uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) } withContext(Dispatchers.IO) { - val (res, isSuccess) = homeRepository.makeSearchRequest(context, song, artist) + val baseUrl = settingsRepository.preferences.first().geniURLbaseURL + val (res, isSuccess) = homeRepository.makeSearchRequest(context, baseUrl, song, artist) if (isSuccess) { val results = json.decodeFromString(SearchResultsResponse.serializer(), res!!).all.map { SearchResult( @@ -136,7 +139,7 @@ class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as LGApp) - HomeViewModel(application.homeRepository) + HomeViewModel(application.homeRepository, application.settingsPreferencesRepository) } } } diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt index 694dcb3..288e89c 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt @@ -31,6 +31,7 @@ data class SettingsState( val applications: Map = emptyMap(), val whitelistSearchQuery: String = "", val keepScreenOn: Boolean = false, + val geniURLbaseURL: String = "", val whitelistPromptSeen: Boolean = true, ) @@ -55,6 +56,15 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi } } + fun setGeniURLBaseURL(baseUrl: String) { + viewModelScope.launch(Dispatchers.IO) { + settingsPrefsRepo.setGeniURLBaseURL(baseUrl) + _uiState.update { state -> + state.copy(geniURLbaseURL = baseUrl) + } + } + } + fun toggleKeepScreenOn() { viewModelScope.launch(Dispatchers.IO) { settingsPrefsRepo.toggleKeepScreenOn() @@ -165,6 +175,7 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi appTheme = it.appTheme, keepScreenOn = it.keepScreenOn, whitelistPromptSeen = it.whitelistPromptSeen, + geniURLbaseURL = it.geniURLbaseURL, ) } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 013c500..d6cf9ae 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -5,6 +5,7 @@ رجوع إلغاء + تأكيد لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا لقد حدث خطأ، حاول مرة أخرى لاحقاً تأكد من تشغيل خادم geniURL الخاص بك إذا كنت تستضيف ذاتيًا أو اتصل بمضيفك. @@ -47,6 +48,8 @@ أبق الجهاز مستيقظا أبق الجهاز مستيقظا على صفحة كلمات الأغاني (يزيد من استخدام البطارية) ستبقى الشاشة على قيد التشغيل + عنوان URL الأساسي لواجهة برمجة التطبيقات (API) + عنوان URL الأساسي هذه الأغنية موسيقية diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34cf146..92898d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Go Back Cancel + Confirm Request has timed out, try again later An error has occurred, try again later Internal server error. Make sure your geniURL server is running if self-hosting or contact your hoster. @@ -46,6 +47,8 @@ Keep device awake Keep device awake on lyrics page (Increases battery usage) Screen Will Stay On + API Base URL + Base URL This song is an instrumental