mirror of
https://github.com/IllusionMan1212/lyrics-grabbr.git
synced 2026-10-24 18:12:27 +00:00
Compare commits
13
Commits
v1.0.0
..
2f056d452e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f056d452e
|
||
|
|
a1c6008528
|
||
|
|
53c0afc780
|
||
|
|
8bbd3bd52b
|
||
|
|
2063673a0d | ||
|
|
88866f61c8 | ||
|
|
773e0fa657 | ||
|
|
78a97cdc41
|
||
|
|
d2b4cbb535 | ||
|
|
6af9de24f4 | ||
|
|
a0baf0f476 | ||
|
|
edd5b24d92 | ||
|
|
7d07433e95 |
@@ -1,4 +1,3 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/caches
|
/.idea/caches
|
||||||
@@ -7,7 +6,6 @@
|
|||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
.DS_Store
|
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|||||||
@@ -8,17 +8,5 @@ App that listens to your notifications and provides lyrics for the currently pla
|
|||||||
## Supported players
|
## Supported players
|
||||||
Any player that uses the MediaSession API _should_ be supported
|
Any player that uses the MediaSession API _should_ be supported
|
||||||
|
|
||||||
## Using Spotify for lyrics
|
|
||||||
If you plan on using the "Use Spotify for lyrics" feature you will need to create a Spotify developer app.
|
|
||||||
You can do so [here](https://developer.spotify.com/dashboard/applications), and you should follow these steps:
|
|
||||||
- Click on "Create app" in your developer dashboard
|
|
||||||
- Name your app whatever you'd like and give it a description
|
|
||||||
- Add the following redirect URI: `lyricsgrabbr://spotify-auth`
|
|
||||||
- Select `Android` under "Which API/SDKs are you planning to use?"
|
|
||||||
- Save your newly created app
|
|
||||||
- You will be redirected to your app page where you can click on "Settings" and find your Client ID under the "Basic Information" tab
|
|
||||||
|
|
||||||
You can then use the Client ID of your app inside LyricsGrabbr to authenticate with Spotify.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
The project is licensed under the GPL3 license while the assets (icon) are licensed under the creative commons license.
|
The project is licensed under the GPL3 license while the assets (icon) are licensed under the creative commons license.
|
||||||
|
|||||||
+22
-20
@@ -9,8 +9,8 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val keystoreProperties = Properties()
|
val keystoreProperties = Properties()
|
||||||
val keystorePropertiesFile: File? = rootProject.file("keystore.properties")
|
val keystorePropertiesFile: File = rootProject.file("keystore.properties")
|
||||||
if (keystorePropertiesFile?.exists() == true) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,14 +22,17 @@ android {
|
|||||||
applicationId = "com.illusionman1212.lyricsgrabbr"
|
applicationId = "com.illusionman1212.lyricsgrabbr"
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 6
|
versionCode = 8
|
||||||
versionName = "1.0.0"
|
versionName = "1.1.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
}
|
}
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
|
||||||
|
// https://developer.android.com/guide/topics/resources/app-languages#gradle-config
|
||||||
|
resourceConfigurations.plus(listOf("en", "ar", "de"))
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -107,31 +110,30 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
|
||||||
implementation("androidx.activity:activity-compose:1.8.2")
|
implementation("androidx.activity:activity-compose:1.9.0")
|
||||||
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
|
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.material3:material3:1.1.2")
|
implementation("androidx.compose.material3:material3:1.3.0-beta04")
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.5.4")
|
implementation("androidx.compose.material:material-icons-extended:1.6.8")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.navigation:navigation-compose:2.7.6")
|
implementation("androidx.navigation:navigation-compose:2.7.7")
|
||||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||||
implementation("io.coil-kt:coil:2.5.0")
|
implementation("io.coil-kt:coil:2.6.0")
|
||||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
implementation("org.jsoup:jsoup:1.17.2")
|
implementation("org.jsoup:jsoup:1.17.2")
|
||||||
implementation("com.adamratzman:spotify-api-kotlin-core:4.0.2")
|
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
|
androidTestImplementation(platform("androidx.compose:compose-bom:2024.06.00"))
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
|||||||
@@ -6,12 +6,16 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".LGApp"
|
android:name=".LGApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:localeConfig="@xml/locales_config"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
@@ -21,7 +25,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="screenSize|screenLayout|keyboardHidden|orientation"
|
android:configChanges="screenSize|screenLayout|keyboardHidden|orientation|locale"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.LyricsGrabbr.Splash"
|
android:theme="@style/Theme.LyricsGrabbr.Splash"
|
||||||
>
|
>
|
||||||
@@ -31,19 +35,6 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".SpotifyPkceLoginActivityImpl"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="lyricsgrabbr" android:host="spotify-auth" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:label="Lyrics Grabbr Notifications Listener"
|
android:label="Lyrics Grabbr Notifications Listener"
|
||||||
@@ -58,5 +49,15 @@
|
|||||||
android:name="android.app.NOTIFICATION_LISTENER"
|
android:name="android.app.NOTIFICATION_LISTENER"
|
||||||
android:value="Media playback notification listener service" />
|
android:value="Media playback notification listener service" />
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="false"
|
||||||
|
>
|
||||||
|
<meta-data
|
||||||
|
android:name="autoStoreLocales"
|
||||||
|
android:value="true"
|
||||||
|
/>
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -5,7 +5,6 @@ import android.content.Context
|
|||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore
|
|
||||||
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
||||||
import com.illusionman1212.lyricsgrabbr.data.LyricsRepository
|
import com.illusionman1212.lyricsgrabbr.data.LyricsRepository
|
||||||
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
|
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
|
||||||
@@ -15,24 +14,15 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
|||||||
name = PREFERENCES_NAME
|
name = PREFERENCES_NAME
|
||||||
)
|
)
|
||||||
|
|
||||||
const val SPOTIFY_REDIRECT_URI = "lyricsgrabbr://spotify-auth"
|
|
||||||
|
|
||||||
class LGApp: Application() {
|
class LGApp: Application() {
|
||||||
lateinit var settingsPreferencesRepository: SettingsPreferencesRepository
|
lateinit var settingsPreferencesRepository: SettingsPreferencesRepository
|
||||||
lateinit var homeRepository: HomeRepository
|
lateinit var homeRepository: HomeRepository
|
||||||
lateinit var lyricsRepository: LyricsRepository
|
lateinit var lyricsRepository: LyricsRepository
|
||||||
val credentialStore by lazy {
|
|
||||||
SpotifyDefaultCredentialStore(
|
|
||||||
clientId = "",
|
|
||||||
redirectUri = SPOTIFY_REDIRECT_URI,
|
|
||||||
applicationContext = applicationContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
settingsPreferencesRepository = SettingsPreferencesRepository(dataStore)
|
settingsPreferencesRepository = SettingsPreferencesRepository(dataStore)
|
||||||
homeRepository = HomeRepository(applicationContext)
|
homeRepository = HomeRepository(applicationContext)
|
||||||
lyricsRepository = LyricsRepository(applicationContext)
|
lyricsRepository = LyricsRepository()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr
|
package com.illusionman1212.lyricsgrabbr
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -22,7 +21,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
@@ -36,6 +34,7 @@ import com.illusionman1212.lyricsgrabbr.ui.screens.HomePage
|
|||||||
import com.illusionman1212.lyricsgrabbr.ui.screens.LyricsPage
|
import com.illusionman1212.lyricsgrabbr.ui.screens.LyricsPage
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.theme.LyricsGrabbrTheme
|
import com.illusionman1212.lyricsgrabbr.ui.theme.LyricsGrabbrTheme
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.screens.SettingsPage
|
import com.illusionman1212.lyricsgrabbr.ui.screens.SettingsPage
|
||||||
|
import com.illusionman1212.lyricsgrabbr.ui.screens.WhitelistPage
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -47,10 +46,9 @@ data class NotificationEvent(
|
|||||||
val artist: String,
|
val artist: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val playbackState: Int,
|
val playbackState: Int,
|
||||||
val duration: Long
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private var lastNotification: MutableStateFlow<NotificationEvent?> = MutableStateFlow(null)
|
private var lastNotification: MutableStateFlow<NotificationEvent?> = MutableStateFlow(null)
|
||||||
private var isReceiverRunning = false
|
private var isReceiverRunning = false
|
||||||
|
|
||||||
@@ -60,10 +58,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = context as Activity
|
|
||||||
val application = activity.application as LGApp
|
|
||||||
|
|
||||||
val settingsViewModel: SettingsViewModel = viewModel(
|
val settingsViewModel: SettingsViewModel = viewModel(
|
||||||
factory = SettingsViewModel.Factory
|
factory = SettingsViewModel.Factory
|
||||||
)
|
)
|
||||||
@@ -83,7 +77,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
settingsViewModel.updateSpotifyAuthStatus(application.credentialStore.spotifyToken?.refreshToken != null)
|
|
||||||
appTheme.intValue = settingsViewModel.getAppTheme()
|
appTheme.intValue = settingsViewModel.getAppTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +116,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
navController.navigate(Routes.Lyrics.route)
|
navController.navigate(Routes.Lyrics.route)
|
||||||
},
|
},
|
||||||
lyricsViewModel = lyricsViewModel,
|
lyricsViewModel = lyricsViewModel,
|
||||||
|
settingsViewModel = settingsViewModel,
|
||||||
goBack = { finish() },
|
goBack = { finish() },
|
||||||
lastNotification = lastNotification,
|
lastNotification = lastNotification,
|
||||||
)
|
)
|
||||||
@@ -133,10 +127,22 @@ class MainActivity : ComponentActivity() {
|
|||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
lyricsViewModel = lyricsViewModel,
|
lyricsViewModel = lyricsViewModel,
|
||||||
|
settingsViewModel = settingsViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Routes.Settings.route) {
|
composable(Routes.Settings.route) {
|
||||||
SettingsPage(
|
SettingsPage(
|
||||||
|
goBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
navigateToWhitelist = {
|
||||||
|
navController.navigate(Routes.Whitelist.route)
|
||||||
|
},
|
||||||
|
viewModel = settingsViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(Routes.Whitelist.route) {
|
||||||
|
WhitelistPage(
|
||||||
goBack = {
|
goBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
@@ -182,14 +188,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
val artist = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_ARTIST)
|
val artist = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_ARTIST)
|
||||||
val songTitle = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_SONG_TITLE)
|
val songTitle = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_SONG_TITLE)
|
||||||
val playbackState = intent.getIntExtra(NotificationListener.NOTIFICATION_PACKAGE_PLAYBACK_STATE, -1)
|
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)
|
val notifPackageName = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_NAME)
|
||||||
|
|
||||||
lastNotification.update { NotificationEvent(
|
lastNotification.update { NotificationEvent(
|
||||||
title = songTitle!!,
|
title = songTitle!!,
|
||||||
artist = artist!!,
|
artist = artist!!,
|
||||||
playbackState = playbackState,
|
playbackState = playbackState,
|
||||||
duration = duration,
|
|
||||||
packageName = notifPackageName!!,
|
packageName = notifPackageName!!,
|
||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,16 +18,31 @@ import android.service.notification.StatusBarNotification
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NotificationListener: NotificationListenerService() {
|
class NotificationListener: NotificationListenerService() {
|
||||||
private var notificationManager : NotificationManagerCompat? = null
|
private var notificationManager : NotificationManagerCompat? = null
|
||||||
|
private lateinit var settingsPrefsRepo: SettingsPreferencesRepository
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
initNotificationChannels(this)
|
initNotificationChannels(this)
|
||||||
notificationManager = NotificationManagerCompat.from(applicationContext)
|
notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
settingsPrefsRepo = (application as LGApp).settingsPreferencesRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
serviceScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -52,8 +67,12 @@ class NotificationListener: NotificationListenerService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
|
serviceScope.launch {
|
||||||
|
val whitelist = settingsPrefsRepo.preferences.first().whitelist
|
||||||
|
if (sbn.packageName !in whitelist) return@launch
|
||||||
sendNotificationBroadcast(sbn, applicationContext)
|
sendNotificationBroadcast(sbn, applicationContext)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getStopServiceNotificationAction(): PendingIntent {
|
private fun getStopServiceNotificationAction(): PendingIntent {
|
||||||
val intent = Intent(android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
val intent = Intent(android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
@@ -90,10 +109,15 @@ class NotificationListener: NotificationListenerService() {
|
|||||||
|
|
||||||
private fun getCurrentNotifications(context: Context) {
|
private fun getCurrentNotifications(context: Context) {
|
||||||
val sbns = activeNotifications
|
val sbns = activeNotifications
|
||||||
|
serviceScope.launch {
|
||||||
|
val whitelist = settingsPrefsRepo.preferences.first().whitelist
|
||||||
|
|
||||||
for (sbn in sbns) {
|
for (sbn in sbns) {
|
||||||
|
if (sbn.packageName !in whitelist) continue
|
||||||
sendNotificationBroadcast(sbn, context)
|
sendNotificationBroadcast(sbn, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getInitialNotification(): Notification {
|
private fun getInitialNotification(): Notification {
|
||||||
val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||||
@@ -113,14 +137,11 @@ class NotificationListener: NotificationListenerService() {
|
|||||||
if (ContextCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
|
||||||
return
|
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.
|
val mediaSessionToken = BundleCompat.getParcelable(sbn.notification.extras, Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java)
|
||||||
// https://issuetracker.google.com/issues/240585930
|
|
||||||
val mediaSessionToken = sbn.notification.extras.getParcelable<MediaSession.Token>(Notification.EXTRA_MEDIA_SESSION)
|
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
val mediaCtrl = MediaController(context, mediaSessionToken)
|
val mediaCtrl = MediaController(context, mediaSessionToken)
|
||||||
val duration = mediaCtrl.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.div(1000)
|
|
||||||
val playbackState = mediaCtrl.playbackState?.state
|
val playbackState = mediaCtrl.playbackState?.state
|
||||||
val albumArt = mediaCtrl.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
|
val albumArt = mediaCtrl.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
|
||||||
|
|
||||||
@@ -133,7 +154,6 @@ class NotificationListener: NotificationListenerService() {
|
|||||||
intent.putExtra(NOTIFICATION_PACKAGE_NAME, sbn.packageName)
|
intent.putExtra(NOTIFICATION_PACKAGE_NAME, sbn.packageName)
|
||||||
intent.putExtra(NOTIFICATION_PACKAGE_ARTIST, packageArtist)
|
intent.putExtra(NOTIFICATION_PACKAGE_ARTIST, packageArtist)
|
||||||
intent.putExtra(NOTIFICATION_PACKAGE_SONG_TITLE, packageSongTitle)
|
intent.putExtra(NOTIFICATION_PACKAGE_SONG_TITLE, packageSongTitle)
|
||||||
intent.putExtra(NOTIFICATION_PACKAGE_DURATION, duration)
|
|
||||||
intent.putExtra(NOTIFICATION_PACKAGE_PLAYBACK_STATE, playbackState)
|
intent.putExtra(NOTIFICATION_PACKAGE_PLAYBACK_STATE, playbackState)
|
||||||
|
|
||||||
if (playbackState == PlaybackState.STATE_PAUSED) {
|
if (playbackState == PlaybackState.STATE_PAUSED) {
|
||||||
@@ -166,7 +186,6 @@ class NotificationListener: NotificationListenerService() {
|
|||||||
const val NOTIFICATION_PACKAGE_ARTIST = "package_artist"
|
const val NOTIFICATION_PACKAGE_ARTIST = "package_artist"
|
||||||
const val NOTIFICATION_PACKAGE_SONG_TITLE = "package_song_title"
|
const val NOTIFICATION_PACKAGE_SONG_TITLE = "package_song_title"
|
||||||
const val NOTIFICATION_PACKAGE_PLAYBACK_STATE = "package_playback_state"
|
const val NOTIFICATION_PACKAGE_PLAYBACK_STATE = "package_playback_state"
|
||||||
const val NOTIFICATION_PACKAGE_DURATION = "package_duration"
|
|
||||||
const val NOTIFICATION_ID = 1
|
const val NOTIFICATION_ID = 1
|
||||||
const val NOTIFICATION_CHANNEL_ID = "Lyrics"
|
const val NOTIFICATION_CHANNEL_ID = "Lyrics"
|
||||||
const val START_FOREGROUND_SERVICE_ACTION = "START_SERVICE"
|
const val START_FOREGROUND_SERVICE_ACTION = "START_SERVICE"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr
|
package com.illusionman1212.lyricsgrabbr
|
||||||
|
|
||||||
sealed class Routes(val route: String) {
|
sealed class Routes(val route: String) {
|
||||||
object Home : Routes("home")
|
data object Home : Routes("home")
|
||||||
object Lyrics : Routes("lyrics")
|
data object Lyrics : Routes("lyrics")
|
||||||
object Settings : Routes("settings")
|
data object Settings : Routes("settings")
|
||||||
|
data object Whitelist : Routes("settings/whitelist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr
|
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import com.adamratzman.spotify.SpotifyClientApi
|
|
||||||
import com.adamratzman.spotify.SpotifyScope
|
|
||||||
import com.adamratzman.spotify.auth.pkce.AbstractSpotifyPkceLoginActivity
|
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
|
||||||
|
|
||||||
class SpotifyPkceLoginActivityImpl: AbstractSpotifyPkceLoginActivity() {
|
|
||||||
override val clientId: String = CLIENT_ID
|
|
||||||
override val redirectUri: String = SPOTIFY_REDIRECT_URI
|
|
||||||
override val scopes: List<SpotifyScope> = listOf(SpotifyScope.UserReadCurrentlyPlaying)
|
|
||||||
|
|
||||||
override fun onFailure(exception: Exception) {
|
|
||||||
Toast.makeText(this, "Failed to authenticate", Toast.LENGTH_SHORT).show()
|
|
||||||
settingsViewModel?.updateSpotifyAuthStatus(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(api: SpotifyClientApi) {
|
|
||||||
(application as LGApp).credentialStore.setSpotifyApi(api)
|
|
||||||
Toast.makeText(this, "Successfully authenticated", Toast.LENGTH_SHORT).show()
|
|
||||||
settingsViewModel?.updateSpotifyAuthStatus(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var CLIENT_ID = ""
|
|
||||||
var settingsViewModel: SettingsViewModel? = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package com.illusionman1212.lyricsgrabbr.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
import com.illusionman1212.lyricsgrabbr.R
|
||||||
import com.illusionman1212.lyricsgrabbr.data.network.GET
|
import com.illusionman1212.lyricsgrabbr.data.network.GET
|
||||||
@@ -10,6 +11,7 @@ import com.illusionman1212.lyricsgrabbr.utils.await
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
import org.jsoup.SerializationException
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -31,11 +33,9 @@ class HomeRepository(private val context: Context) {
|
|||||||
return pkgs.contains(packageName)
|
return pkgs.contains(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun makeSearchRequest(song: String, artist: String): Pair<String?, Boolean> {
|
suspend fun makeSearchRequest(lContext: Context, baseUrl: String, song: String, artist: String): Pair<String?, Boolean> {
|
||||||
val builder = Uri.Builder()
|
val url = Uri.parse(baseUrl)
|
||||||
val url = builder.scheme("https")
|
.buildUpon()
|
||||||
.authority("api.illusionman1212.com")
|
|
||||||
.appendPath("lyrics")
|
|
||||||
.appendPath("search")
|
.appendPath("search")
|
||||||
.appendQueryParameter("q", "$song $artist")
|
.appendQueryParameter("q", "$song $artist")
|
||||||
.appendQueryParameter("disableFuzzy", "true")
|
.appendQueryParameter("disableFuzzy", "true")
|
||||||
@@ -47,11 +47,23 @@ class HomeRepository(private val context: Context) {
|
|||||||
).await()
|
).await()
|
||||||
|
|
||||||
if (!res.isSuccessful) {
|
if (!res.isSuccessful) {
|
||||||
|
if (res.code >= 500) {
|
||||||
|
return Pair(lContext.resources.getString(R.string.internal_server_error), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.body?.contentType()?.type != "text" && res.body?.contentType()?.subtype != "json") {
|
||||||
|
return Pair(lContext.resources.getString(R.string.invalid_json), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
val errRes = json.decodeFromString(ErrorResponse.serializer(), res.body!!.string())
|
val errRes = json.decodeFromString(ErrorResponse.serializer(), res.body!!.string())
|
||||||
|
|
||||||
// matches is only null if there's an error. otherwise it's 0 when there are no results
|
// matches is only null if there's an error. otherwise it's 0 when there are no results
|
||||||
if (errRes.matches == null) {
|
if (errRes.matches == null) {
|
||||||
return Pair(context.resources.getString(R.string.generic_error), false)
|
return Pair(lContext.resources.getString(R.string.generic_error), false)
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
return Pair(lContext.resources.getString(R.string.failed_to_map_json), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair(null, false)
|
return Pair(null, false)
|
||||||
@@ -60,10 +72,10 @@ class HomeRepository(private val context: Context) {
|
|||||||
return Pair(res.body?.string(), true)
|
return Pair(res.body?.string(), true)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
if (e.cause is SocketTimeoutException) {
|
if (e.cause is SocketTimeoutException) {
|
||||||
return Pair(context.resources.getString(R.string.timeout), false)
|
return Pair(lContext.resources.getString(R.string.timeout), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair(context.resources.getString(R.string.generic_error), false)
|
return Pair(e.message, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.data
|
package com.illusionman1212.lyricsgrabbr.data
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
|
||||||
import com.illusionman1212.lyricsgrabbr.data.network.GET
|
|
||||||
import com.illusionman1212.lyricsgrabbr.data.network.NetworkHelper
|
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.await
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.internal.StringUtil
|
import org.jsoup.internal.StringUtil
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
@@ -15,58 +8,7 @@ import org.jsoup.nodes.Node
|
|||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import org.jsoup.select.NodeVisitor
|
import org.jsoup.select.NodeVisitor
|
||||||
|
|
||||||
@Serializable
|
class LyricsRepository {
|
||||||
data class LyricLine(
|
|
||||||
val startTimeMs: ULong,
|
|
||||||
val words: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Lyrics(
|
|
||||||
val syncType: String,
|
|
||||||
val lines: List<LyricLine>,
|
|
||||||
val language: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SuccessResponse(
|
|
||||||
val lyrics: Lyrics,
|
|
||||||
)
|
|
||||||
|
|
||||||
class LyricsRepository(private val context: Context) {
|
|
||||||
private val networkHelper = NetworkHelper(context)
|
|
||||||
private val json = Json {
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchLyricsFromSpotify(trackId: String): Pair<String, Boolean> {
|
|
||||||
val builder = Uri.Builder()
|
|
||||||
val url = builder.scheme("https")
|
|
||||||
.authority("api.lyricstify.vercel.app")
|
|
||||||
.appendPath("v1")
|
|
||||||
.appendPath("lyrics")
|
|
||||||
.appendPath(trackId)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val res = networkHelper.client.newCall(
|
|
||||||
GET(url.toString())
|
|
||||||
).await()
|
|
||||||
|
|
||||||
val body = res.body?.string()
|
|
||||||
|
|
||||||
if (res.isSuccessful) {
|
|
||||||
return Pair(json.decodeFromString(SuccessResponse.serializer(), body!!)
|
|
||||||
.lyrics
|
|
||||||
.lines.joinToString(separator = "\n") { it.words }, true)
|
|
||||||
} else {
|
|
||||||
if (res.code == 404) {
|
|
||||||
return Pair(context.resources.getString(R.string.spotify_no_lyrics_error), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(context.resources.getString(R.string.generic_error), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchLyricsFromGenius(url: Uri): String {
|
fun fetchLyricsFromGenius(url: Uri): String {
|
||||||
val doc = Jsoup.connect(url.toString()).userAgent(USER_AGENT).get()
|
val doc = Jsoup.connect(url.toString()).userAgent(USER_AGENT).get()
|
||||||
val divs = doc.select("div[data-lyrics-container]")
|
val divs = doc.select("div[data-lyrics-container]")
|
||||||
|
|||||||
+45
@@ -2,8 +2,11 @@ package com.illusionman1212.lyricsgrabbr.data
|
|||||||
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
enum class Theme {
|
enum class Theme {
|
||||||
@@ -14,6 +17,10 @@ enum class Theme {
|
|||||||
|
|
||||||
data class SettingsPreferences(
|
data class SettingsPreferences(
|
||||||
val appTheme: Int = Theme.SYSTEM.ordinal,
|
val appTheme: Int = Theme.SYSTEM.ordinal,
|
||||||
|
val whitelist: Set<String> = emptySet(),
|
||||||
|
val keepScreenOn: Boolean = false,
|
||||||
|
val whitelistPromptSeen: Boolean = false,
|
||||||
|
val geniURLbaseURL: String = "https://api.illusionman1212.com/lyrics",
|
||||||
)
|
)
|
||||||
|
|
||||||
class SettingsPreferencesRepository(
|
class SettingsPreferencesRepository(
|
||||||
@@ -21,6 +28,10 @@ class SettingsPreferencesRepository(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private val APP_THEME = intPreferencesKey("app_theme")
|
private val APP_THEME = intPreferencesKey("app_theme")
|
||||||
|
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) }
|
val preferences = dataStore.data.map { mapSettingsPreferences(it) }
|
||||||
@@ -31,9 +42,43 @@ class SettingsPreferencesRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun addToWhitelist(packageName: String) {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) + packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun toggleKeepScreenOn() {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[KEEP_SCREEN_ON] = !(prefs[KEEP_SCREEN_ON] ?: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeFromWhitelist(packageName: String) {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) - packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun discardWhitelistPrompt() {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[WHITELIST_PROMPT_SEEN] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setGeniURLBaseURL(url: String) {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[GENIURL_BASE_URL] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences {
|
private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences {
|
||||||
return SettingsPreferences(
|
return SettingsPreferences(
|
||||||
prefs[APP_THEME] ?: Theme.SYSTEM.ordinal,
|
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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
|||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AlertDialogDefaults
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -29,9 +30,9 @@ fun LGAlertDialog(
|
|||||||
buttons: @Composable () -> Unit,
|
buttons: @Composable () -> Unit,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
BasicAlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
properties = properties,
|
properties = properties
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.wrapContentHeight().wrapContentWidth(),
|
modifier = Modifier.wrapContentHeight().wrapContentWidth(),
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ package com.illusionman1212.lyricsgrabbr.ui.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.PlainTooltipBox
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TooltipBox
|
||||||
|
import androidx.compose.material3.TooltipDefaults
|
||||||
|
import androidx.compose.material3.rememberTooltipState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
import androidx.compose.ui.layout.ModifierInfo
|
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -22,16 +21,19 @@ fun LGIconButton(
|
|||||||
badge: (@Composable BoxScope.() -> Unit)? = null,
|
badge: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val tooltipState = rememberTooltipState()
|
||||||
|
|
||||||
Layout(
|
Layout(
|
||||||
content = {
|
content = {
|
||||||
PlainTooltipBox(
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
Text(text = tooltip)
|
Text(text = tooltip)
|
||||||
},
|
},
|
||||||
|
state = tooltipState,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
Modifier.tooltipAnchor(),
|
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -30,9 +29,9 @@ import androidx.compose.material.icons.filled.MusicNote
|
|||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -48,7 +47,6 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -59,7 +57,6 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
@@ -75,6 +72,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.compose.currentStateAsState
|
import androidx.lifecycle.compose.currentStateAsState
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -86,7 +84,6 @@ import com.illusionman1212.lyricsgrabbr.viewmodels.HomeViewModel
|
|||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SearchResult
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SearchResult
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.illusionman1212.lyricsgrabbr.LGApp
|
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.forwardingPainter
|
import com.illusionman1212.lyricsgrabbr.ui.components.forwardingPainter
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
||||||
@@ -104,10 +101,10 @@ fun HomePage(
|
|||||||
factory = HomeViewModel.Factory,
|
factory = HomeViewModel.Factory,
|
||||||
),
|
),
|
||||||
lyricsViewModel: LyricsViewModel,
|
lyricsViewModel: LyricsViewModel,
|
||||||
|
settingsViewModel: SettingsViewModel,
|
||||||
lastNotification: MutableStateFlow<NotificationEvent?>
|
lastNotification: MutableStateFlow<NotificationEvent?>
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val application = (context as Activity).application as LGApp
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState()
|
val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState()
|
||||||
@@ -120,12 +117,13 @@ fun HomePage(
|
|||||||
var songUri by remember { mutableStateOf(Uri.EMPTY) }
|
var songUri by remember { mutableStateOf(Uri.EMPTY) }
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var listeningPermission = homeViewModel.getListeningPermission()
|
var listeningPermission = homeViewModel.getListeningPermission()
|
||||||
|
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
var notificationPermission by remember { mutableStateOf(
|
var notificationPermission by remember { mutableStateOf(
|
||||||
ActivityCompat.checkSelfPermission(context, POST_NOTIFICATIONS) == PERMISSION_GRANTED
|
ActivityCompat.checkSelfPermission(context, POST_NOTIFICATIONS) == PERMISSION_GRANTED
|
||||||
) }
|
) }
|
||||||
var shouldShowNotificationPermissionRationale by remember { mutableStateOf(
|
var shouldShowNotificationPermissionRationale by remember { mutableStateOf(
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
context as android.app.Activity,
|
context as Activity,
|
||||||
POST_NOTIFICATIONS
|
POST_NOTIFICATIONS
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -141,7 +139,6 @@ fun HomePage(
|
|||||||
|
|
||||||
val song = lastNotification.collectAsStateWithLifecycle().value
|
val song = lastNotification.collectAsStateWithLifecycle().value
|
||||||
val uiState = homeViewModel.uiState.collectAsStateWithLifecycle().value
|
val uiState = homeViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
var getLyricsFromSpotify by rememberSaveable { mutableStateOf(false)}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (!shouldShowNotificationPermissionRationale)
|
if (!shouldShowNotificationPermissionRationale)
|
||||||
@@ -159,17 +156,9 @@ fun HomePage(
|
|||||||
|
|
||||||
LaunchedEffect(song) {
|
LaunchedEffect(song) {
|
||||||
if (song != null && song.title != uiState.notification?.title && song.artist != uiState.notification?.artist) {
|
if (song != null && song.title != uiState.notification?.title && song.artist != uiState.notification?.artist) {
|
||||||
val spotifyApi = application.credentialStore.getSpotifyClientPkceApi()
|
|
||||||
val useSpotifyApi = song.packageName == "com.spotify.music" && spotifyApi != null
|
|
||||||
getLyricsFromSpotify = useSpotifyApi
|
|
||||||
|
|
||||||
homeViewModel.setSong(song)
|
homeViewModel.setSong(song)
|
||||||
|
|
||||||
if (useSpotifyApi) {
|
homeViewModel.makeRequestToGenius(context, song.title, song.artist)
|
||||||
homeViewModel.makeRequestToSpotify(spotifyApi!!)
|
|
||||||
} else {
|
|
||||||
homeViewModel.makeRequestToGenius(song.title, song.artist)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +240,30 @@ fun HomePage(
|
|||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (!settingsState.whitelistPromptSeen) {
|
||||||
|
LGAlertDialog(
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = true,
|
||||||
|
dismissOnClickOutside = true,
|
||||||
|
),
|
||||||
|
onDismiss = { settingsViewModel.discardWhitelistPrompt() },
|
||||||
|
title = stringResource(id = R.string.whitelist_apps),
|
||||||
|
buttons = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
settingsViewModel.discardWhitelistPrompt()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.understood))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.whitelist_prompt_desc),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -262,7 +275,11 @@ fun HomePage(
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text( text = stringResource(id = R.string.no_supported_player), style = MaterialTheme.typography.headlineSmall )
|
Text(
|
||||||
|
text = stringResource(id = R.string.no_supported_player),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
if (!notificationPermission) {
|
if (!notificationPermission) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.notification_post_permission_not_granted).uppercase(),
|
text = stringResource(id = R.string.notification_post_permission_not_granted).uppercase(),
|
||||||
@@ -319,7 +336,7 @@ fun HomePage(
|
|||||||
)
|
)
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
homeViewModel.makeRequestToGenius(song.title, song.artist)
|
homeViewModel.makeRequestToGenius(context, song.title, song.artist)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(stringResource(id = R.string.retry))
|
Text(stringResource(id = R.string.retry))
|
||||||
@@ -335,7 +352,7 @@ fun HomePage(
|
|||||||
},
|
},
|
||||||
navigateToLyrics = navigateToLyrics,
|
navigateToLyrics = navigateToLyrics,
|
||||||
setLyricsTitle = { id, title, subtitle, url ->
|
setLyricsTitle = { id, title, subtitle, url ->
|
||||||
lyricsViewModel.onSongChange(id, title, subtitle, url, getLyricsFromSpotify)
|
lyricsViewModel.onSongChange(id, title, subtitle, url)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -425,7 +442,7 @@ fun Results(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (it != results.last())
|
if (it != results.last())
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,7 +463,6 @@ fun LongClickBottomSheet(
|
|||||||
dragHandle = {},
|
dragHandle = {},
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
windowInsets = WindowInsets(top = 0.dp),
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
if (artistUri != Uri.EMPTY) {
|
if (artistUri != Uri.EMPTY) {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.ui.screens
|
package com.illusionman1212.lyricsgrabbr.ui.screens
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.WindowManager.LayoutParams
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -10,7 +16,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
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.MusicNote
|
import androidx.compose.material.icons.filled.MusicNote
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -23,10 +29,12 @@ import androidx.compose.material3.TextFieldDefaults
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
@@ -36,12 +44,13 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
import com.illusionman1212.lyricsgrabbr.R
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
||||||
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -50,15 +59,25 @@ fun LyricsPage(
|
|||||||
lyricsViewModel: LyricsViewModel = viewModel(
|
lyricsViewModel: LyricsViewModel = viewModel(
|
||||||
factory = LyricsViewModel.Factory
|
factory = LyricsViewModel.Factory
|
||||||
),
|
),
|
||||||
|
settingsViewModel: SettingsViewModel,
|
||||||
) {
|
) {
|
||||||
val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value
|
val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
|
val settingsState = settingsViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(uiState.song, uiState.artist, uiState.url) {
|
LaunchedEffect(uiState.song, uiState.artist, uiState.url) {
|
||||||
if (uiState.useSpotifyApi) {
|
|
||||||
lyricsViewModel.fetchLyricsFromSpotify()
|
|
||||||
} else {
|
|
||||||
lyricsViewModel.fetchLyricsFromGenius()
|
lyricsViewModel.fetchLyricsFromGenius()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
val window = (context as Activity).window
|
||||||
|
if (settingsState.keepScreenOn) {
|
||||||
|
window.addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
window.clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -86,8 +105,7 @@ fun LyricsPage(
|
|||||||
onClick = goBack
|
onClick = goBack
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.mirror(),
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
id = R.string.go_back
|
id = R.string.go_back
|
||||||
)
|
)
|
||||||
@@ -97,9 +115,19 @@ fun LyricsPage(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
Column(Modifier.padding(padding)) {
|
||||||
|
if (settingsState.keepScreenOn) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.screen_will_stay_on).uppercase(), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(padding)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.then(
|
.then(
|
||||||
if (!uiState.isLoading && uiState.lyrics.isNotEmpty()) Modifier.verticalScroll(
|
if (!uiState.isLoading && uiState.lyrics.isNotEmpty()) Modifier.verticalScroll(
|
||||||
@@ -121,7 +149,6 @@ fun LyricsPage(
|
|||||||
} else if (uiState.lyrics.isEmpty()) {
|
} else if (uiState.lyrics.isEmpty()) {
|
||||||
Instrumental()
|
Instrumental()
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.padding(
|
Modifier.padding(
|
||||||
@@ -152,6 +179,7 @@ fun LyricsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.ui.screens
|
package com.illusionman1212.lyricsgrabbr.ui.screens
|
||||||
|
|
||||||
import android.app.Activity
|
import android.util.Patterns
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -13,13 +14,16 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
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.Apps
|
||||||
import androidx.compose.material.icons.filled.Check
|
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.Palette
|
||||||
|
import androidx.compose.material.icons.outlined.Translate
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
@@ -34,37 +38,23 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.adamratzman.spotify.SpotifyClientApi
|
|
||||||
import com.adamratzman.spotify.SpotifyException
|
|
||||||
import com.adamratzman.spotify.auth.pkce.startSpotifyClientPkceLoginActivity
|
|
||||||
import com.illusionman1212.lyricsgrabbr.LGApp
|
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
import com.illusionman1212.lyricsgrabbr.R
|
||||||
import com.illusionman1212.lyricsgrabbr.SpotifyPkceLoginActivityImpl
|
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGAlertDialog
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGAlertDialog
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGRadioButton
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGRadioButton
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.theme.ColorSecondaryLight
|
import com.illusionman1212.lyricsgrabbr.ui.theme.ColorSecondaryLight
|
||||||
import com.illusionman1212.lyricsgrabbr.data.Theme
|
import com.illusionman1212.lyricsgrabbr.data.Theme
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
import com.illusionman1212.lyricsgrabbr.utils.getLangPreferenceDropdownEntries
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
private val Themes = hashMapOf(
|
private val Themes = hashMapOf(
|
||||||
Theme.LIGHT.ordinal to R.string.theme_light,
|
Theme.LIGHT.ordinal to R.string.theme_light,
|
||||||
@@ -72,59 +62,10 @@ private val Themes = hashMapOf(
|
|||||||
Theme.SYSTEM.ordinal to R.string.theme_system
|
Theme.SYSTEM.ordinal to R.string.theme_system
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: this is here as a reference because I think I will need a guard in the future
|
|
||||||
fun <T> Activity.guardValidSpotifyApi(
|
|
||||||
classBackTo: Class<out Activity>,
|
|
||||||
alreadyTriedToReauthenticate: Boolean = false,
|
|
||||||
block: suspend (api: SpotifyClientApi) -> T
|
|
||||||
): T? {
|
|
||||||
return runBlocking {
|
|
||||||
val app = (application as LGApp)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val token = app.credentialStore.spotifyToken
|
|
||||||
?: throw SpotifyException.ReAuthenticationNeededException()
|
|
||||||
val usesPkceAuth = token.refreshToken != null
|
|
||||||
val api = (if (usesPkceAuth) app.credentialStore.getSpotifyClientPkceApi()
|
|
||||||
else app.credentialStore.getSpotifyImplicitGrantApi())
|
|
||||||
?: throw SpotifyException.ReAuthenticationNeededException()
|
|
||||||
|
|
||||||
block(api)
|
|
||||||
} catch (e: SpotifyException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
val api = app.credentialStore.getSpotifyClientPkceApi()!!
|
|
||||||
if (!alreadyTriedToReauthenticate) {
|
|
||||||
try {
|
|
||||||
api.refreshToken()
|
|
||||||
app.credentialStore.spotifyToken = api.token
|
|
||||||
block(api)
|
|
||||||
} catch (e: SpotifyException.ReAuthenticationNeededException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return@runBlocking guardValidSpotifyApi(
|
|
||||||
classBackTo = classBackTo,
|
|
||||||
alreadyTriedToReauthenticate = true,
|
|
||||||
block = block
|
|
||||||
)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return@runBlocking guardValidSpotifyApi(
|
|
||||||
classBackTo = classBackTo,
|
|
||||||
alreadyTriedToReauthenticate = true,
|
|
||||||
block = block
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//pkceClassBackTo = classBackTo
|
|
||||||
startSpotifyClientPkceLoginActivity(SpotifyPkceLoginActivityImpl::class.java)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsPage(
|
fun SettingsPage(
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
|
navigateToWhitelist: () -> Unit,
|
||||||
viewModel: SettingsViewModel,
|
viewModel: SettingsViewModel,
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
@@ -141,8 +82,7 @@ fun SettingsPage(
|
|||||||
) {
|
) {
|
||||||
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
|
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.mirror(),
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
id = R.string.go_back
|
id = R.string.go_back
|
||||||
) )
|
) )
|
||||||
@@ -159,14 +99,17 @@ fun SettingsPage(
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Appearance(viewModel, uiState)
|
Appearance(viewModel, uiState)
|
||||||
Lyrics(viewModel, uiState)
|
geniURL(viewModel, uiState)
|
||||||
|
Listening(navigateToWhitelist)
|
||||||
|
PowerManagement(viewModel, uiState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
|
private fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
|
||||||
var themeOpenDialog by remember { mutableStateOf(false) }
|
var themeOpenDialog by remember { mutableStateOf(false) }
|
||||||
|
var languageOpenDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
SettingGroup(title = stringResource(id = R.string.appearance)) {
|
SettingGroup(title = stringResource(id = R.string.appearance)) {
|
||||||
DialogSetting(
|
DialogSetting(
|
||||||
@@ -175,6 +118,17 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
|
|||||||
icon = Icons.Outlined.Palette,
|
icon = Icons.Outlined.Palette,
|
||||||
onClick = { themeOpenDialog = true }
|
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) {
|
if (themeOpenDialog) {
|
||||||
ThemeDialog(
|
ThemeDialog(
|
||||||
@@ -185,6 +139,111 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (languageOpenDialog) {
|
||||||
|
LanguageDialog(
|
||||||
|
onDismiss = { languageOpenDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Listening(navigateToWhitelist: () -> Unit) {
|
||||||
|
SettingGroup(title = stringResource(id = R.string.listening)) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { navigateToWhitelist() }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.Apps, contentDescription = null, modifier = Modifier.width(56.dp))
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Text(text = stringResource(id = R.string.whitelist_apps), color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.whitelist_desc),
|
||||||
|
color = ColorSecondaryLight,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(end = 24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PowerManagement(viewModel: SettingsViewModel, uiState: SettingsState) {
|
||||||
|
SettingGroup(title = stringResource(id = R.string.power_management)) {
|
||||||
|
ToggleSetting(
|
||||||
|
title = stringResource(id = R.string.keep_device_awake_title),
|
||||||
|
description = stringResource(id = R.string.keep_device_awake_desc),
|
||||||
|
icon = Icons.Default.ScreenLockPortrait,
|
||||||
|
checked = uiState.keepScreenOn
|
||||||
|
) {
|
||||||
|
viewModel.toggleKeepScreenOn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
@Composable
|
||||||
@@ -230,82 +289,45 @@ fun ThemeDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Lyrics(viewModel: SettingsViewModel, uiState: SettingsState) {
|
fun LanguageDialog(
|
||||||
val context = LocalContext.current
|
onDismiss: () -> Unit,
|
||||||
val activity = context as Activity
|
) {
|
||||||
val application = activity.application as LGApp
|
val langs = LocalContext.current.getLangPreferenceDropdownEntries()
|
||||||
|
|
||||||
var isDialogOpen by remember { mutableStateOf(false) }
|
|
||||||
var spotifyIdValue by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
SettingGroup(stringResource(id = R.string.lyrics)) {
|
|
||||||
ToggleSetting(
|
|
||||||
title = stringResource(id = R.string.use_spotify_api),
|
|
||||||
checked = uiState.isSpotifyAuthed,
|
|
||||||
icon = painterResource(id = R.drawable.spotify),
|
|
||||||
onClick = {
|
|
||||||
if (uiState.isSpotifyAuthed) {
|
|
||||||
application.credentialStore.clear()
|
|
||||||
viewModel.updateSpotifyAuthStatus(false)
|
|
||||||
} else {
|
|
||||||
isDialogOpen = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDialogOpen) {
|
|
||||||
LGAlertDialog(
|
LGAlertDialog(
|
||||||
onDismiss = { isDialogOpen = false },
|
onDismiss = onDismiss,
|
||||||
title = stringResource(id = R.string.spotify_client_id),
|
title = stringResource(id = R.string.language),
|
||||||
buttons = {
|
buttons = {
|
||||||
TextButton(onClick = { isDialogOpen = false }) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text(stringResource(id = R.string.cancel))
|
Text(stringResource(id = R.string.cancel))
|
||||||
}
|
}
|
||||||
TextButton(onClick = {
|
},
|
||||||
SpotifyPkceLoginActivityImpl.CLIENT_ID = spotifyIdValue
|
modifier = Modifier.fillMaxWidth()
|
||||||
SpotifyPkceLoginActivityImpl.settingsViewModel = viewModel
|
|
||||||
activity.startSpotifyClientPkceLoginActivity(SpotifyPkceLoginActivityImpl::class.java)
|
|
||||||
isDialogOpen = false
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.confirm))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
val linkColor = MaterialTheme.colorScheme.surfaceTint
|
LazyColumn {
|
||||||
val spotifyDesc = annotatedStringResource(R.string.spotify_client_id_desc) {
|
item {
|
||||||
when (it.key) {
|
LGRadioButton(
|
||||||
"href" -> SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
|
text = stringResource(id = R.string._default),
|
||||||
"weight" -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
|
onClick = {
|
||||||
else -> null
|
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
||||||
}
|
|
||||||
}
|
|
||||||
val textColor = LocalContentColor.current
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
|
|
||||||
ClickableText(
|
|
||||||
spotifyDesc,
|
|
||||||
onClick = { offset ->
|
|
||||||
spotifyDesc.getStringAnnotations(tag = "href", start = offset, end = offset)
|
|
||||||
.firstOrNull()
|
|
||||||
?.let {
|
|
||||||
uriHandler.openUri(it.item)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(color = textColor),
|
selected = AppCompatDelegate.getApplicationLocales().isEmpty,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
TextField(
|
for ((key, value) in langs) {
|
||||||
value = spotifyIdValue,
|
item {
|
||||||
onValueChange = { spotifyIdValue = it },
|
LGRadioButton(
|
||||||
placeholder = {
|
text = key,
|
||||||
Text(stringResource(id = R.string.not_set))
|
onClick = {
|
||||||
|
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(value))
|
||||||
},
|
},
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
selected = AppCompatDelegate.getApplicationLocales()[0]?.language == value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingGroup(title: String, content: @Composable () -> Unit) {
|
fun SettingGroup(title: String, content: @Composable () -> Unit) {
|
||||||
@@ -317,7 +339,7 @@ fun SettingGroup(title: String, content: @Composable () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Header(title: String) {
|
private fun Header(title: String) {
|
||||||
Row(Modifier.padding(start = 56.dp)) {
|
Row(Modifier.padding(start = 56.dp)) {
|
||||||
Text(text = title, color = MaterialTheme.colorScheme.primary)
|
Text(text = title, color = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
@@ -365,7 +387,8 @@ fun DialogSetting(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ToggleSetting(
|
fun ToggleSetting(
|
||||||
title: String,
|
title: String,
|
||||||
icon: Painter? = null,
|
description: String? = null,
|
||||||
|
icon: ImageVector? = null,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onClick: (Boolean) -> Unit)
|
onClick: (Boolean) -> Unit)
|
||||||
{
|
{
|
||||||
@@ -380,11 +403,20 @@ fun ToggleSetting(
|
|||||||
modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp),
|
modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
Icon(painter = icon, contentDescription = null, Modifier.width(56.dp))
|
Icon(imageVector = icon, contentDescription = null, Modifier.width(56.dp))
|
||||||
} else {
|
} else {
|
||||||
Box(modifier = Modifier.width(56.dp))
|
Box(modifier = Modifier.width(56.dp))
|
||||||
}
|
}
|
||||||
Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f))
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
if (description != null) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
color = ColorSecondaryLight,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = null,
|
onCheckedChange = null,
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.illusionman1212.lyricsgrabbr.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.illusionman1212.lyricsgrabbr.R
|
||||||
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
||||||
|
import com.illusionman1212.lyricsgrabbr.ui.theme.Typography
|
||||||
|
import com.illusionman1212.lyricsgrabbr.viewmodels.AppInfo
|
||||||
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) {
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
Scaffold(topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
TextField(
|
||||||
|
value = uiState.whitelistSearchQuery,
|
||||||
|
onValueChange = {
|
||||||
|
viewModel.filterPackages(it)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
placeholder = {
|
||||||
|
Text(text = stringResource(id = R.string.search), style = Typography.titleLarge)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
LGIconButton(tooltip = stringResource(id = R.string.clear), onClick = { viewModel.clearWhitelistSearch() }) {
|
||||||
|
Icon(Icons.Default.Clear, contentDescription = stringResource(id = R.string.clear))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
|
||||||
|
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
|
||||||
|
id = R.string.go_back
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}) { padding ->
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val refreshState = rememberPullToRefreshState()
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (uiState.applications.isNotEmpty()) {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
PullToRefreshBox(
|
||||||
|
state = refreshState,
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
isRefreshing = true
|
||||||
|
viewModel.refreshPackages(context)
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
) {
|
||||||
|
LazyColumn(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
if (uiState.applications.any { it.value.isWhitelisted }) {
|
||||||
|
for ((packageName, appInfo) in uiState.applications.filter { it.value.isWhitelisted }) {
|
||||||
|
item {
|
||||||
|
Application(packageName, appInfo, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider(
|
||||||
|
Modifier
|
||||||
|
.width(84.dp)
|
||||||
|
.padding(vertical = 12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ((packageName, appInfo) in uiState.applications.filter { !it.value.isWhitelisted }) {
|
||||||
|
item {
|
||||||
|
Application(packageName, appInfo, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Application(packageName: String, app: AppInfo, viewModel: SettingsViewModel) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
role = Role.Checkbox,
|
||||||
|
onClick = {
|
||||||
|
if (app.isWhitelisted) {
|
||||||
|
viewModel.removeFromWhitelist(packageName)
|
||||||
|
} else {
|
||||||
|
viewModel.addToWhitelist(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 5.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AsyncImage(modifier = Modifier.size(32.dp), model = app.icon, contentDescription = null)
|
||||||
|
Text(app.displayName, Modifier.weight(1f))
|
||||||
|
Checkbox(
|
||||||
|
checked = app.isWhitelisted,
|
||||||
|
onCheckedChange = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.utils
|
|
||||||
|
|
||||||
import android.util.LayoutDirection
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.core.text.layoutDirection
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
fun Modifier.mirror(): Modifier {
|
|
||||||
return if (Locale.getDefault().layoutDirection == LayoutDirection.RTL)
|
|
||||||
this.scale(scaleX = -1f, scaleY = 1f)
|
|
||||||
else
|
|
||||||
this
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.utils
|
package com.illusionman1212.lyricsgrabbr.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.text.Annotation
|
import android.text.Annotation
|
||||||
import android.text.SpannedString
|
import android.text.SpannedString
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -7,6 +8,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
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
|
// Native stylization is coming soon
|
||||||
// https://issuetracker.google.com/issues/139320238
|
// https://issuetracker.google.com/issues/139320238
|
||||||
@@ -19,7 +25,7 @@ fun annotatedStringResource(
|
|||||||
val spannedString = SpannedString(resources.getText(id))
|
val spannedString = SpannedString(resources.getText(id))
|
||||||
val resultBuilder = AnnotatedString.Builder()
|
val resultBuilder = AnnotatedString.Builder()
|
||||||
resultBuilder.append(spannedString.toString())
|
resultBuilder.append(spannedString.toString())
|
||||||
spannedString.getSpans<Annotation>(0, spannedString.length, Annotation::class.java).forEach { annotation ->
|
spannedString.getSpans(0, spannedString.length, Annotation::class.java).forEach { annotation ->
|
||||||
val spanStart = spannedString.getSpanStart(annotation)
|
val spanStart = spannedString.getSpanStart(annotation)
|
||||||
val spanEnd = spannedString.getSpanEnd(annotation)
|
val spanEnd = spannedString.getSpanEnd(annotation)
|
||||||
resultBuilder.addStringAnnotation(
|
resultBuilder.addStringAnnotation(
|
||||||
@@ -32,3 +38,37 @@ fun annotatedStringResource(
|
|||||||
}
|
}
|
||||||
return resultBuilder.toAnnotatedString()
|
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
|
||||||
|
}
|
||||||
@@ -2,21 +2,20 @@ package com.illusionman1212.lyricsgrabbr.viewmodels
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import com.adamratzman.spotify.SpotifyClientApi
|
|
||||||
import com.illusionman1212.lyricsgrabbr.LGApp
|
import com.illusionman1212.lyricsgrabbr.LGApp
|
||||||
import com.illusionman1212.lyricsgrabbr.NotificationEvent
|
import com.illusionman1212.lyricsgrabbr.NotificationEvent
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
|
||||||
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
||||||
|
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -88,7 +87,7 @@ data class HomeState(
|
|||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class HomeViewModel(private val context: Context, private val homeRepository: HomeRepository): ViewModel() {
|
class HomeViewModel(private val homeRepository: HomeRepository, private val settingsRepository: SettingsPreferencesRepository): ViewModel() {
|
||||||
private val _uiState = MutableStateFlow(HomeState())
|
private val _uiState = MutableStateFlow(HomeState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
@@ -100,41 +99,18 @@ class HomeViewModel(private val context: Context, private val homeRepository: Ho
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setSong(song: NotificationEvent) {
|
fun setSong(song: NotificationEvent) {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null)
|
_uiState.update { state ->
|
||||||
}
|
state.copy(isLoading = true, notification = song, error = null)
|
||||||
|
|
||||||
fun makeRequestToSpotify(spotifyApi: SpotifyClientApi) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val track = spotifyApi.player.getCurrentlyPlaying()?.item
|
|
||||||
|
|
||||||
if (track?.id == null) {
|
|
||||||
_uiState.value = _uiState.value.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = context.resources.getString(R.string.no_spotify_song_playing)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
_uiState.value = _uiState.value.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = null,
|
|
||||||
results = listOf(SearchResult(
|
|
||||||
title = _uiState.value.notification!!.title,
|
|
||||||
artist = _uiState.value.notification!!.artist,
|
|
||||||
url = track.asTrack?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
|
|
||||||
artistUrl = track.asTrack?.artists?.get(0)?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
|
|
||||||
thumbnail = track.asTrack?.album?.images?.get(0)?.url?.toUri() ?: Uri.EMPTY,
|
|
||||||
id = track.id!!,
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun makeRequestToGenius(song: String, artist: String) {
|
fun makeRequestToGenius(context: Context, song: String, artist: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) }
|
_uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) }
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val (res, isSuccess) = homeRepository.makeSearchRequest(song, artist)
|
val baseUrl = settingsRepository.preferences.first().geniURLbaseURL
|
||||||
|
val (res, isSuccess) = homeRepository.makeSearchRequest(context, baseUrl, song, artist)
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
val results = json.decodeFromString(SearchResultsResponse.serializer(), res!!).all.map {
|
val results = json.decodeFromString(SearchResultsResponse.serializer(), res!!).all.map {
|
||||||
SearchResult(
|
SearchResult(
|
||||||
@@ -163,7 +139,7 @@ class HomeViewModel(private val context: Context, private val homeRepository: Ho
|
|||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
val application = (this[APPLICATION_KEY] as LGApp)
|
val application = (this[APPLICATION_KEY] as LGApp)
|
||||||
HomeViewModel(application.applicationContext, application.homeRepository)
|
HomeViewModel(application.homeRepository, application.settingsPreferencesRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ data class LyricsState(
|
|||||||
val artist: String = "",
|
val artist: String = "",
|
||||||
val url: Uri = Uri.EMPTY,
|
val url: Uri = Uri.EMPTY,
|
||||||
val trackId: String = "",
|
val trackId: String = "",
|
||||||
val useSpotifyApi: Boolean = false,
|
|
||||||
|
|
||||||
// set by Lyrics screen
|
// set by Lyrics screen
|
||||||
val lyrics: String = "",
|
val lyrics: String = "",
|
||||||
@@ -35,34 +34,23 @@ class LyricsViewModel(private val lyricsRepository: LyricsRepository): ViewModel
|
|||||||
private val _uiState = MutableStateFlow(LyricsState())
|
private val _uiState = MutableStateFlow(LyricsState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun onSongChange(trackId: String, title: String, subtitle: String, url: Uri, useSpotifyApi: Boolean) {
|
fun onSongChange(trackId: String, title: String, subtitle: String, url: Uri) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(song = title, artist = subtitle, url = url, useSpotifyApi = useSpotifyApi, trackId = trackId)
|
it.copy(song = title, artist = subtitle, url = url, trackId = trackId)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchLyricsFromSpotify() {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val (lyrics, isSuccess) = lyricsRepository.fetchLyricsFromSpotify(uiState.value.trackId)
|
|
||||||
if (isSuccess) {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
|
|
||||||
} else {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = lyrics, lyrics = "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchLyricsFromGenius() {
|
fun fetchLyricsFromGenius() {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
|
_uiState.update { state ->
|
||||||
|
state.copy(isLoading = true, lyrics = "")
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val lyrics = lyricsRepository.fetchLyricsFromGenius(uiState.value.url)
|
val lyrics = lyricsRepository.fetchLyricsFromGenius(uiState.value.url)
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
|
_uiState.update { state ->
|
||||||
|
state.copy(isLoading = false, lyrics = lyrics)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-6
@@ -1,5 +1,8 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.viewmodels
|
package com.illusionman1212.lyricsgrabbr.viewmodels
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||||
@@ -14,14 +17,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class AppInfo(
|
||||||
|
val displayName: String,
|
||||||
|
val icon: Drawable,
|
||||||
|
var isWhitelisted: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
data class SettingsState(
|
data class SettingsState(
|
||||||
val appTheme: Int = Theme.SYSTEM.ordinal,
|
val appTheme: Int = Theme.SYSTEM.ordinal,
|
||||||
val isSpotifyAuthed: Boolean = false,
|
val applications: Map<String, AppInfo> = emptyMap(),
|
||||||
|
val whitelistSearchQuery: String = "",
|
||||||
|
val keepScreenOn: Boolean = false,
|
||||||
|
val geniURLbaseURL: String = "",
|
||||||
|
val whitelistPromptSeen: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
|
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): ViewModel() {
|
||||||
|
private var immutablePackages: Map<String, AppInfo> = emptyMap()
|
||||||
private val _uiState = MutableStateFlow(SettingsState())
|
private val _uiState = MutableStateFlow(SettingsState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -35,25 +50,143 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSpotifyAuthStatus(isAuthed: Boolean) {
|
fun discardWhitelistPrompt() {
|
||||||
_uiState.value = _uiState.value.copy(isSpotifyAuthed = isAuthed)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
settingsPrefsRepo.discardWhitelistPrompt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getWhitelistedApps(): Set<String> {
|
||||||
|
return settingsPrefsRepo.preferences.first().whitelist
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackages(context: Context) {
|
||||||
|
if (immutablePackages.isNotEmpty()) return
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val pm = context.packageManager
|
||||||
|
val packages = pm.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||||
|
.filter { it.packageName != context.packageName && pm.getLaunchIntentForPackage(it.packageName) != null }
|
||||||
|
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.loadLabel(pm).toString() })
|
||||||
|
|
||||||
|
val apps: MutableMap<String, AppInfo> = mutableMapOf()
|
||||||
|
val whitelistedApps = getWhitelistedApps()
|
||||||
|
|
||||||
|
packages.forEach { pkg ->
|
||||||
|
val label = pkg.loadLabel(pm)
|
||||||
|
val icon = pkg.loadIcon(pm)
|
||||||
|
apps[pkg.packageName] = AppInfo(
|
||||||
|
displayName = label.toString(),
|
||||||
|
icon = icon,
|
||||||
|
isWhitelisted = pkg.packageName in whitelistedApps
|
||||||
|
)
|
||||||
|
}
|
||||||
|
immutablePackages = apps
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
applications = apps
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshPackages(context: Context) {
|
||||||
|
_uiState.update {
|
||||||
|
val apps = uiState.value.applications.toMutableMap()
|
||||||
|
apps.clear()
|
||||||
|
it.copy(whitelistSearchQuery = "", applications = apps)
|
||||||
|
}
|
||||||
|
val apps = immutablePackages.toMutableMap()
|
||||||
|
apps.clear()
|
||||||
|
immutablePackages = apps
|
||||||
|
getPackages(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterPackages(query: String) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
applications = immutablePackages.filter {
|
||||||
|
it.value.displayName.lowercase().contains(query.lowercase())
|
||||||
|
}.toMutableMap(),
|
||||||
|
whitelistSearchQuery = query,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearWhitelistSearch() {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(applications = immutablePackages, whitelistSearchQuery = "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addToWhitelist(packageName: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
val apps = immutablePackages.toMutableMap()
|
||||||
|
apps[packageName] = apps[packageName]!!.copy(isWhitelisted = true)
|
||||||
|
immutablePackages = apps
|
||||||
|
state.copy(
|
||||||
|
applications = immutablePackages.filter {
|
||||||
|
it.value.displayName.lowercase().contains(uiState.value.whitelistSearchQuery.lowercase())
|
||||||
|
}.toMutableMap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
settingsPrefsRepo.addToWhitelist(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFromWhitelist(packageName: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
val apps = immutablePackages.toMutableMap()
|
||||||
|
apps[packageName] = apps[packageName]!!.copy(isWhitelisted = false)
|
||||||
|
immutablePackages = apps
|
||||||
|
state.copy(
|
||||||
|
applications = immutablePackages.filter {
|
||||||
|
it.value.displayName.lowercase().contains(uiState.value.whitelistSearchQuery.lowercase())
|
||||||
|
}.toMutableMap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
settingsPrefsRepo.removeFromWhitelist(packageName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
getPackages(context)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsPrefsRepo.preferences.collectLatest {
|
settingsPrefsRepo.preferences.collectLatest {
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
appTheme = it.appTheme,
|
appTheme = it.appTheme,
|
||||||
|
keepScreenOn = it.keepScreenOn,
|
||||||
|
whitelistPromptSeen = it.whitelistPromptSeen,
|
||||||
|
geniURLbaseURL = it.geniURLbaseURL,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
val application = (this[APPLICATION_KEY] as LGApp)
|
val application = (this[APPLICATION_KEY] as LGApp)
|
||||||
SettingsViewModel(application.settingsPreferencesRepository)
|
SettingsViewModel(application.settingsPreferencesRepository, application.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM16.057,16.406c-0.188,0 -0.282,-0.094 -0.47,-0.188c-1.411,-0.847 -3.198,-1.317 -5.079,-1.317c-1.035,0 -2.163,0.188 -3.104,0.376c-0.188,0 -0.376,0.094 -0.47,0.094c-0.376,0 -0.658,-0.282 -0.658,-0.658c0,-0.376 0.188,-0.658 0.564,-0.658c1.223,-0.282 2.446,-0.47 3.763,-0.47c2.163,0 4.139,0.564 5.832,1.505c0.188,0.188 0.376,0.282 0.376,0.658C16.621,16.218 16.339,16.406 16.057,16.406zM17.091,13.773c-0.188,0 -0.376,-0.094 -0.564,-0.188c-1.599,-1.035 -3.857,-1.599 -6.302,-1.599c-1.223,0 -2.352,0.188 -3.198,0.376c-0.188,0.094 -0.282,0.094 -0.47,0.094c-0.47,0 -0.753,-0.376 -0.753,-0.753c0,-0.47 0.188,-0.658 0.658,-0.847c1.129,-0.282 2.258,-0.564 3.857,-0.564c2.634,0 5.079,0.658 7.149,1.881c0.282,0.188 0.47,0.47 0.47,0.753C17.844,13.396 17.562,13.773 17.091,13.773zM18.314,10.762c-0.188,0 -0.282,-0.094 -0.564,-0.188c-1.881,-1.035 -4.609,-1.693 -7.243,-1.693c-1.317,0 -2.728,0.188 -3.951,0.47c-0.188,0 -0.282,0.094 -0.564,0.094C5.428,9.54 5.051,9.069 5.051,8.505c0,-0.564 0.282,-0.847 0.658,-0.941C7.215,7.188 8.814,7 10.507,7c2.916,0 5.926,0.658 8.278,1.881c0.282,0.188 0.564,0.47 0.564,0.941C19.255,10.386 18.878,10.762 18.314,10.762z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -8,8 +8,10 @@
|
|||||||
<string name="confirm">تأكيد</string>
|
<string name="confirm">تأكيد</string>
|
||||||
<string name="timeout">لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا</string>
|
<string name="timeout">لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا</string>
|
||||||
<string name="generic_error">لقد حدث خطأ، حاول مرة أخرى لاحقاً</string>
|
<string name="generic_error">لقد حدث خطأ، حاول مرة أخرى لاحقاً</string>
|
||||||
|
<string name="internal_server_error">تأكد من تشغيل خادم geniURL الخاص بك إذا كنت تستضيف ذاتيًا أو اتصل بمضيفك.</string>
|
||||||
|
<string name="invalid_json">الاستجابة لم تكن JSON صالحة</string>
|
||||||
|
<string name="failed_to_map_json">فشل تعيين JSON المفكك إلى النوع الداخلي</string>
|
||||||
<string name="retry">أعد المحاولة</string>
|
<string name="retry">أعد المحاولة</string>
|
||||||
<string name="spotify_no_lyrics_error">عذرًا، لا يحتوي Spotify على كلمات لهذه الأغنية</string>
|
|
||||||
|
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<string name="no_supported_player">لا يوجد مشغل مدعوم قيد التشغيل</string>
|
<string name="no_supported_player">لا يوجد مشغل مدعوم قيد التشغيل</string>
|
||||||
@@ -25,7 +27,8 @@
|
|||||||
<string name="notification_post_permission_desc">يتطلب هذا التطبيق أذونات الإشعارات لبدء خدمة مقدمة بغرض الاستماع إلى إشعارات الموسيقى، ولا يمكن أن يعمل بدونها.</string>
|
<string name="notification_post_permission_desc">يتطلب هذا التطبيق أذونات الإشعارات لبدء خدمة مقدمة بغرض الاستماع إلى إشعارات الموسيقى، ولا يمكن أن يعمل بدونها.</string>
|
||||||
<string name="notification_post_permission_not_granted">لم يتم منح إذن الإشعارات، لن يعمل التطبيق بشكل صحيح</string>
|
<string name="notification_post_permission_not_granted">لم يتم منح إذن الإشعارات، لن يعمل التطبيق بشكل صحيح</string>
|
||||||
<string name="grant_permission">منح الإذن</string>
|
<string name="grant_permission">منح الإذن</string>
|
||||||
<string name="no_spotify_song_playing">No song is playing on Spotify</string>
|
<string name="understood">مفهوم</string>
|
||||||
|
<string name="whitelist_prompt_desc">يعمل التطبيق على أساس القائمة البيضاء. وهذا يمنحك التحكم في التطبيقات التي سنستمع إليها ويقلل أيضًا من التشويش على التطبيق.\nيمكنك التحكم في القائمة البيضاء من الإعدادات</string>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="settings">الإعدادات</string>
|
<string name="settings">الإعدادات</string>
|
||||||
@@ -34,13 +37,21 @@
|
|||||||
<string name="theme_light">نهار</string>
|
<string name="theme_light">نهار</string>
|
||||||
<string name="theme_dark">ليلي</string>
|
<string name="theme_dark">ليلي</string>
|
||||||
<string name="theme_system">استخدم مظهر النظام</string>
|
<string name="theme_system">استخدم مظهر النظام</string>
|
||||||
<string name="use_spotify_api">استخدم Spotify للكلمات</string>
|
<string name="language">اللغة</string>
|
||||||
<string name="spotify_client_id">Spotify Client ID</string>
|
<string name="_default">استخدم لغة النظام</string>
|
||||||
<string name="not_set">خالي</string>
|
<string name="listening">الإستماع</string>
|
||||||
<string name="spotify_client_id_desc">يمكن لهذا التطبيق استخدام واجهة برمجة تطبيقات الويب الخاصة بـ Spotify للحصول على كلمات الأغاني. تحتاج إلى تقديم Spotify Client ID لاستخدام هذه الميزة. يمكنك الحصول على واحدة من <annotation href="https://developer.spotify.com/dashboard/applications">هنا</annotation>.\n<annotation weight="bold">لا يعمل هذا إلا إذا كنت تقوم بتشغيل الموسيقى من مشغل Spotify.</annotation></string>
|
<string name="whitelist_apps">تطبيقات القائمة البيضاء</string>
|
||||||
|
<string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string>
|
||||||
|
<string name="search">بحث</string>
|
||||||
|
<string name="clear">مسح</string>
|
||||||
|
<string name="power_management">إدارة إستخدام الطافة</string>
|
||||||
|
<string name="keep_device_awake_title">أبق الجهاز مستيقظا</string>
|
||||||
|
<string name="keep_device_awake_desc">أبق الجهاز مستيقظا على صفحة كلمات الأغاني (يزيد من استخدام البطارية)</string>
|
||||||
|
<string name="screen_will_stay_on">ستبقى الشاشة على قيد التشغيل</string>
|
||||||
|
<string name="api_base_url">عنوان URL الأساسي لواجهة برمجة التطبيقات (API)</string>
|
||||||
|
<string name="base_url">عنوان URL الأساسي</string>
|
||||||
|
|
||||||
<!-- Lyrics -->
|
<!-- Lyrics -->
|
||||||
<string name="lyrics">كلمات الأغنية</string>
|
|
||||||
<string name="instrumental">هذه الأغنية موسيقية</string>
|
<string name="instrumental">هذه الأغنية موسيقية</string>
|
||||||
|
|
||||||
<!-- Notification -->
|
<!-- Notification -->
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">Lyrics Grabbr</string>
|
||||||
|
|
||||||
|
<!-- General -->
|
||||||
|
<string name="go_back">Zurück</string>
|
||||||
|
<string name="cancel">Abbrechen</string>
|
||||||
|
<string name="timeout">Anfrage ist abgelaufen, bitte versuche es später erneut</string>
|
||||||
|
<string name="generic_error">Ein Fehler ist aufgetreten, bitte versuche es später erneut</string>
|
||||||
|
<string name="retry">Erneut versuchen</string>
|
||||||
|
|
||||||
|
<!-- Home -->
|
||||||
|
<string name="no_supported_player">Keinen unterstützten Player gefunden</string>
|
||||||
|
<string name="open_settings">Einstellungen öffnen</string>
|
||||||
|
<string name="notification_listen_permission">Berechtigung zum Auslesen von Benachrichtigungen</string>
|
||||||
|
<string name="notification_listen_desc">Diese App benötigt die Berechtigung zum Auslesen von Benachrichtigungen, um ordnungsgemäß zu funktionieren. Bitte erteile diese Berechtigung auf dem nächsten Bildschirm.\nBITTE BEACHTEN: der einzige Weg den Dienst zu stoppen ist die Berechtigung zum Auslesen von Benachrichtigungen zu widerrufen.</string>
|
||||||
|
<string name="no_results">Keine Ergebnisse gefunden</string>
|
||||||
|
<string name="found_results">%1$d Ergebnis(se) gefunden</string>
|
||||||
|
<string name="powered_by">Verwendet <annotation href="https://github.com/sv443/geniURL">geniURL</annotation></string>
|
||||||
|
<string name="open_song_page">Seite des Songs öffnen</string>
|
||||||
|
<string name="open_artist_page">Seite des Künstlers öffnen</string>
|
||||||
|
<string name="notification_post_permission">Berechtigung für Benachrichtigungen erforderlich</string>
|
||||||
|
<string name="notification_post_permission_desc">Lyrics Grabbr benötigt die Berechtigung Benachrichtigungen zu lesen, um einen Vordergrunddienst zum Abhören zu starten und kann nicht ohne diese funktionieren.</string>
|
||||||
|
<string name="notification_post_permission_not_granted">Berechtigung zum Abhören von Benachrichtigungen nicht erteilt, daher wird die App wird nicht ordnungsgemäß funktionieren.</string>
|
||||||
|
<string name="grant_permission">Berechtigung erteilen</string>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="settings">Einstellungen</string>
|
||||||
|
<string name="appearance">Aussehen</string>
|
||||||
|
<string name="theme">Farbschema</string>
|
||||||
|
<string name="theme_light">Hell</string>
|
||||||
|
<string name="theme_dark">Dunkel</string>
|
||||||
|
<string name="theme_system">Systemstandard</string>
|
||||||
|
<string name="language">Sprache</string>
|
||||||
|
<string name="_default">Systemsprache</string>
|
||||||
|
<string name="listening">Erlaubt</string>
|
||||||
|
<string name="whitelist_apps">Erlaubte Apps</string>
|
||||||
|
<string name="whitelist_desc">Wähle aus, welchen Apps Lyrics Grabbr zuhören soll</string>
|
||||||
|
<string name="search">Suche</string>
|
||||||
|
<string name="clear">Löschen</string>
|
||||||
|
<string name="power_management">Energieeinstellungen</string>
|
||||||
|
<string name="keep_device_awake_title">Gerät wachhalten</string>
|
||||||
|
<string name="keep_device_awake_desc">Gerät auf der Songtext-Seite wachhalten (erhöht den Batterieverbrauch)</string>
|
||||||
|
<string name="screen_will_stay_on">Bildschirm bleibt an</string>
|
||||||
|
|
||||||
|
<!-- Lyrics -->
|
||||||
|
<string name="instrumental">Dies ist ein instrumentaler Song</string>
|
||||||
|
|
||||||
|
<!-- Notification -->
|
||||||
|
<string name="service_running">Dienst läuft</string>
|
||||||
|
<string name="stop_service">Dienst anhalten</string>
|
||||||
|
<string name="service_notif_desc_init">Spiele Songs mit einem unterstützten Player, um Songtexte zu erhalten</string>
|
||||||
|
<string name="service_notif_desc_playing">Für den Songtext antippen</string>
|
||||||
|
<string name="now_playing">Es läuft %1$s von %2$s</string>
|
||||||
|
</resources>
|
||||||
@@ -7,8 +7,10 @@
|
|||||||
<string name="confirm">Confirm</string>
|
<string name="confirm">Confirm</string>
|
||||||
<string name="timeout">Request has timed out, try again later</string>
|
<string name="timeout">Request has timed out, try again later</string>
|
||||||
<string name="generic_error">An error has occurred, try again later</string>
|
<string name="generic_error">An error has occurred, try again later</string>
|
||||||
|
<string name="internal_server_error">Internal server error. Make sure your geniURL server is running if self-hosting or contact your hoster.</string>
|
||||||
|
<string name="invalid_json">Response was not valid JSON</string>
|
||||||
|
<string name="failed_to_map_json">Failed to map decoded JSON to internal type.</string>
|
||||||
<string name="retry">Retry</string>
|
<string name="retry">Retry</string>
|
||||||
<string name="spotify_no_lyrics_error">Sorry, Spotify doesn\'t have lyrics for this song</string>
|
|
||||||
|
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<string name="no_supported_player">No supported player is running</string>
|
<string name="no_supported_player">No supported player is running</string>
|
||||||
@@ -24,7 +26,8 @@
|
|||||||
<string name="notification_post_permission_desc">LyricsGrabbr requires notification permissions to start a foreground service for the purpose of listening to music notifications, and cannot function without it.</string>
|
<string name="notification_post_permission_desc">LyricsGrabbr requires notification permissions to start a foreground service for the purpose of listening to music notifications, and cannot function without it.</string>
|
||||||
<string name="notification_post_permission_not_granted">Notification permission is not granted, the app will not function properly</string>
|
<string name="notification_post_permission_not_granted">Notification permission is not granted, the app will not function properly</string>
|
||||||
<string name="grant_permission">Grant Permission</string>
|
<string name="grant_permission">Grant Permission</string>
|
||||||
<string name="no_spotify_song_playing">No song is playing on Spotify</string>
|
<string name="understood">Understood</string>
|
||||||
|
<string name="whitelist_prompt_desc">The application operates on a whitelist basis. This gives you control over which apps we\'ll listen to and also reduces noise for the application.\nYou may control the whitelist from the settings.</string>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
@@ -33,13 +36,21 @@
|
|||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</string>
|
<string name="theme_dark">Dark</string>
|
||||||
<string name="theme_system">Use System Theme</string>
|
<string name="theme_system">Use System Theme</string>
|
||||||
<string name="use_spotify_api">Use Spotify for lyrics</string>
|
<string name="language">Language</string>
|
||||||
<string name="spotify_client_id" translatable="false">Spotify Client ID</string>
|
<string name="_default">Use System Language</string>
|
||||||
<string name="not_set">Not Set</string>
|
<string name="listening">Listening</string>
|
||||||
<string name="spotify_client_id_desc">This app can use Spotify\'s Web API to get lyrics. You need to provide a Spotify Client ID to use this feature. You can get one from <annotation href="https://developer.spotify.com/dashboard/applications">here</annotation>.\n<annotation weight="bold">This only works if you\'re playing music from the Spotify player.</annotation></string>
|
<string name="whitelist_apps">Whitelist Apps</string>
|
||||||
|
<string name="whitelist_desc">Choose which apps Lyrics Grabbr will listen to</string>
|
||||||
|
<string name="search">Search</string>
|
||||||
|
<string name="clear">Clear</string>
|
||||||
|
<string name="power_management">Power Management</string>
|
||||||
|
<string name="keep_device_awake_title">Keep device awake</string>
|
||||||
|
<string name="keep_device_awake_desc">Keep device awake on lyrics page (Increases battery usage)</string>
|
||||||
|
<string name="screen_will_stay_on">Screen Will Stay On</string>
|
||||||
|
<string name="api_base_url">API Base URL</string>
|
||||||
|
<string name="base_url">Base URL</string>
|
||||||
|
|
||||||
<!-- Lyrics -->
|
<!-- Lyrics -->
|
||||||
<string name="lyrics">Lyrics</string>
|
|
||||||
<string name="instrumental">This song is an instrumental</string>
|
<string name="instrumental">This song is an instrumental</string>
|
||||||
|
|
||||||
<!-- Notification -->
|
<!-- Notification -->
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<locale android:name="en" />
|
||||||
|
<locale android:name="ar" />
|
||||||
|
<locale android:name="de" />
|
||||||
|
</locale-config>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
## Veröffentlichung auf F-droid
|
||||||
|
Initiale Veröffentlichung auf F-droid
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- App mit Kotlin und Jetpack Compose neu geschrieben
|
||||||
|
- Möglichkeit hinzugefügt, Spotify für Songtexte zu verwenden
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Eine Whitelist von Apps ist jetzt erforderlich, damit die App Benachrichtigungen von ihnen abrufen kann
|
||||||
|
- Spotify wurde aufgrund des nicht-FOSS-SDKs vollständig entfernt
|
||||||
|
- Spracheinstellungen (Hilfe bei Übersetzungen ist sehr willkommen)
|
||||||
|
- Einstellung hinzugefügt, um das Gerät wach zu halten, während Songtexte angezeigt werden
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 550 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 583 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
@@ -0,0 +1 @@
|
|||||||
|
Lyrics Grabbr bietet Songtexte für das aktuell abgespielte Lied anhand von Benachrichtigungen an
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Lyrics Grabbr
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- A whitelist of apps is now required in order for the app to pick up on notifications from them
|
||||||
|
- Completely remove Spotify due to non-FOSS SDK
|
||||||
|
- Language settings (help with translations is much appreciated)
|
||||||
|
- Added a setting to keep the device awake when reading lyrics
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Fix app crash when geniURL instance is down.
|
||||||
|
- Show a one-time notice related to whitelisting apps.
|
||||||
|
- Added a setting to allow for changing the geniURL instance.
|
||||||
|
- German translation
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 294 KiB |
Reference in New Issue
Block a user