diff --git a/app/build.gradle b/app/build.gradle index 83af00b..befdd77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,8 +47,8 @@ android { applicationId "io.musicorum.mobile" minSdk 28 targetSdk 34 - versionCode 66 - versionName "1.21-release" + versionCode 67 + versionName "1.22-release" //compileSdkPreview = "UpsideDownCake" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 58e509b..a53e1ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,9 @@ + @@ -32,8 +35,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Musicorum" android:testOnly="false" + android:theme="@style/Theme.Musicorum" tools:targetApi="31"> + android:theme="@style/Theme.Musicorum.SplashScreenTheme" + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/io/musicorum/mobile/MainActivity.kt b/app/src/main/java/io/musicorum/mobile/MainActivity.kt index 9ddfc7a..11ee40a 100644 --- a/app/src/main/java/io/musicorum/mobile/MainActivity.kt +++ b/app/src/main/java/io/musicorum/mobile/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -29,7 +30,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -63,11 +63,12 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.ktx.remoteConfig import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import dagger.hilt.android.AndroidEntryPoint +import io.musicorum.mobile.datastore.ScrobblePreferences +import io.musicorum.mobile.datastore.UserData import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.models.FetchPeriod import io.musicorum.mobile.repositories.LocalUserRepository import io.musicorum.mobile.router.BottomNavBar -import io.musicorum.mobile.serialization.User import io.musicorum.mobile.ui.theme.KindaBlack import io.musicorum.mobile.ui.theme.MusicorumMobileTheme import io.musicorum.mobile.utils.CrowdinUtils @@ -100,11 +101,9 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.serialization.json.Json -val Context.userData: DataStore by preferencesDataStore(name = "userdata") -val Context.scrobblePrefs by preferencesDataStore(name = "scrobblePrefs") -val LocalUser = compositionLocalOf { null } +val Context.userData: DataStore by preferencesDataStore(UserData.DataStoreName) +val Context.scrobblePrefs by preferencesDataStore(ScrobblePreferences.DataStoreName) val LocalNavigation = compositionLocalOf { null } -val MutableUserState = mutableStateOf(null) val LocalAnalytics = compositionLocalOf { null } @AndroidEntryPoint @@ -130,6 +129,7 @@ class MainActivity : ComponentActivity() { super.attachBaseContext(Crowdin.wrapContext(newBase)) } + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -199,7 +199,9 @@ class MainActivity : ComponentActivity() { } if (!BuildConfig.DEBUG) { try { - SentryAndroid.init(this) + SentryAndroid.init(this) { opts -> + opts.isAnrEnabled + } } catch (_: Exception) { } } @@ -214,27 +216,25 @@ class MainActivity : ComponentActivity() { val systemUiController = rememberSystemUiController() if (intent?.data == null) { - if (MutableUserState.value == null) { - LaunchedEffect(Unit) { - val sessionKey = ctx.applicationContext.userData.data.map { prefs -> - prefs[stringPreferencesKey("session_key")] - }.firstOrNull() - if (sessionKey == null) { - navController.navigate("login") { - popUpTo("home") { - inclusive = true - } + LaunchedEffect(Unit) { + val sessionKey = ctx.applicationContext.userData.data.map { prefs -> + prefs[stringPreferencesKey("session_key")] + }.firstOrNull() + if (sessionKey == null) { + navController.navigate("login") { + popUpTo("home") { + inclusive = true } - } else { + } + } else { + val localUser = LocalUserRepository(applicationContext) + if (localUser.getUser().username.isEmpty()) { val userReq = UserEndpoint.getSessionUser(sessionKey) - val localUser = LocalUserRepository(applicationContext) - if (localUser.getUser().username.isEmpty()) { - localUser.create(userReq?.user) - } - MutableUserState.value = userReq + localUser.create(userReq?.user) } } } + } DisposableEffect(systemUiController, useDarkIcons) { @@ -255,7 +255,6 @@ class MainActivity : ComponentActivity() { } CompositionLocalProvider( - LocalUser provides MutableUserState.value, LocalSnackbar provides LocalSnackbarContext(snackHostState), LocalNavigation provides navController, LocalAnalytics provides firebaseAnalytics @@ -350,18 +349,16 @@ class MainActivity : ComponentActivity() { } composable("mostListened") { - MostListened(mostListenedViewModel = mostListenedViewModel) + MostListened(viewModel = mostListenedViewModel) } composable( - "user/{username}", - arguments = listOf(navArgument("username") { + "user/{usernameArg}", + arguments = listOf(navArgument("usernameArg") { type = NavType.StringType }) ) { - User( - username = it.arguments?.getString("username")!! - ) + User() } composable( diff --git a/app/src/main/java/io/musicorum/mobile/database/CachedScrobblesDb.kt b/app/src/main/java/io/musicorum/mobile/database/CachedScrobblesDb.kt index 075ff4c..3f845ff 100644 --- a/app/src/main/java/io/musicorum/mobile/database/CachedScrobblesDb.kt +++ b/app/src/main/java/io/musicorum/mobile/database/CachedScrobblesDb.kt @@ -4,8 +4,8 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import io.musicorum.mobile.database.daos.CachedScrobblesDao import io.musicorum.mobile.models.CachedScrobble +import io.musicorum.mobile.repositories.daos.CachedScrobblesDao import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.internal.synchronized diff --git a/app/src/main/java/io/musicorum/mobile/database/PendingScrobblesDb.kt b/app/src/main/java/io/musicorum/mobile/database/PendingScrobblesDb.kt index 1a68dda..496e278 100644 --- a/app/src/main/java/io/musicorum/mobile/database/PendingScrobblesDb.kt +++ b/app/src/main/java/io/musicorum/mobile/database/PendingScrobblesDb.kt @@ -4,8 +4,8 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import io.musicorum.mobile.database.daos.PendingScrobblesDao import io.musicorum.mobile.models.PendingScrobble +import io.musicorum.mobile.repositories.daos.PendingScrobblesDao import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.internal.synchronized diff --git a/app/src/main/java/io/musicorum/mobile/datastore/AnalyticsConsent.kt b/app/src/main/java/io/musicorum/mobile/datastore/AnalyticsConsent.kt new file mode 100644 index 0000000..25c7541 --- /dev/null +++ b/app/src/main/java/io/musicorum/mobile/datastore/AnalyticsConsent.kt @@ -0,0 +1,8 @@ +package io.musicorum.mobile.datastore + +import androidx.datastore.preferences.core.booleanPreferencesKey + +object AnalyticsConsent { + const val DataStoreName = "analyticsConsent" + val CONSENT_KEY = booleanPreferencesKey("consent") +} \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/datastore/LocalUser.kt b/app/src/main/java/io/musicorum/mobile/datastore/LocalUser.kt new file mode 100644 index 0000000..ae313ee --- /dev/null +++ b/app/src/main/java/io/musicorum/mobile/datastore/LocalUser.kt @@ -0,0 +1,11 @@ +package io.musicorum.mobile.datastore + +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey + +object LocalUser { + const val DataStoreName = "partialUser" + val USERNAME_KEY = stringPreferencesKey("usernameArg") + val PROFILE_ICON_KEY = stringPreferencesKey("profilePictureUrl") + val EXPIRES_IN_KEY = longPreferencesKey("expires_in") +} \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/datastore/ScrobblePreferences.kt b/app/src/main/java/io/musicorum/mobile/datastore/ScrobblePreferences.kt new file mode 100644 index 0000000..c8d8ed3 --- /dev/null +++ b/app/src/main/java/io/musicorum/mobile/datastore/ScrobblePreferences.kt @@ -0,0 +1,13 @@ +package io.musicorum.mobile.datastore + +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.stringSetPreferencesKey + +object ScrobblePreferences { + const val DataStoreName = "scrobblePrefs" + val SCROBBLE_POINT_KEY = floatPreferencesKey("scrobblePoint") + val ENABLED_KEY = booleanPreferencesKey("enabled") + val ALLOWED_APPS_KEY = stringSetPreferencesKey("enabledApps") + val UPDATED_NOWPLAYING_KEY = booleanPreferencesKey("updateNowPlaying") +} \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/datastore/UserData.kt b/app/src/main/java/io/musicorum/mobile/datastore/UserData.kt new file mode 100644 index 0000000..1c14cbe --- /dev/null +++ b/app/src/main/java/io/musicorum/mobile/datastore/UserData.kt @@ -0,0 +1,8 @@ +package io.musicorum.mobile.datastore + +import androidx.datastore.preferences.core.stringPreferencesKey + +object UserData { + const val DataStoreName = "userdata" + val SESSION_KEY = stringPreferencesKey("session_key") +} \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/ArtistEndpoint.kt b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/ArtistEndpoint.kt index 6d9d79f..74d4548 100644 --- a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/ArtistEndpoint.kt +++ b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/ArtistEndpoint.kt @@ -17,7 +17,7 @@ object ArtistEndpoint { parameter("method", "artist.getInfo") parameter("name", artist) parameter("artist", artist) - parameter("username", username) + parameter("usernameArg", username) } return if (res.status.isSuccess()) { return res.body() diff --git a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/TrackEndpoint.kt b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/TrackEndpoint.kt index 900c56b..d9a1fb1 100644 --- a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/TrackEndpoint.kt +++ b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/TrackEndpoint.kt @@ -27,7 +27,7 @@ object TrackEndpoint { val autoCorrectValue = if (autoCorrect == true) 1 else 0 parameter("track", trackName) parameter("method", "track.getInfo") - parameter("username", username) + parameter("usernameArg", username) parameter("artist", artist) parameter("autocorrect", autoCorrectValue) } diff --git a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/UserEndpoint.kt b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/UserEndpoint.kt index 5aa459d..ee4d07e 100644 --- a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/UserEndpoint.kt +++ b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/UserEndpoint.kt @@ -91,7 +91,7 @@ object UserEndpoint { val result = kotlin.runCatching { val res = KtorConfiguration.lastFmClient.get { parameter("method", "user.getFriends") - parameter("username", user) + parameter("user", user) parameter("limit", limit) headers.remove("Cache-Control") } diff --git a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/musicorum/generator/Generator.kt b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/musicorum/generator/Generator.kt index 23eb696..9b0face 100644 --- a/app/src/main/java/io/musicorum/mobile/ktor/endpoints/musicorum/generator/Generator.kt +++ b/app/src/main/java/io/musicorum/mobile/ktor/endpoints/musicorum/generator/Generator.kt @@ -70,7 +70,7 @@ object Generator { /** * Generates a grid collage - * @param username The last.fm username + * @param username The last.fm usernameArg * @param rowCount Row count * @param colCount Column count * @param entity Entity, either artist, album or track diff --git a/app/src/main/java/io/musicorum/mobile/repositories/CachedScrobblesRepository.kt b/app/src/main/java/io/musicorum/mobile/repositories/CachedScrobblesRepository.kt index 6e9c874..5714928 100644 --- a/app/src/main/java/io/musicorum/mobile/repositories/CachedScrobblesRepository.kt +++ b/app/src/main/java/io/musicorum/mobile/repositories/CachedScrobblesRepository.kt @@ -1,7 +1,7 @@ package io.musicorum.mobile.repositories -import io.musicorum.mobile.database.daos.CachedScrobblesDao import io.musicorum.mobile.models.CachedScrobble +import io.musicorum.mobile.repositories.daos.CachedScrobblesDao class CachedScrobblesRepository(private val cachedScrobblesDao: CachedScrobblesDao) { diff --git a/app/src/main/java/io/musicorum/mobile/repositories/IPendingScrobbles.kt b/app/src/main/java/io/musicorum/mobile/repositories/IPendingScrobbles.kt new file mode 100644 index 0000000..feeacc5 --- /dev/null +++ b/app/src/main/java/io/musicorum/mobile/repositories/IPendingScrobbles.kt @@ -0,0 +1,12 @@ +package io.musicorum.mobile.repositories + +import io.musicorum.mobile.models.PendingScrobble +import kotlinx.coroutines.flow.Flow + +interface IPendingScrobbles { + suspend fun getAllScrobblesStream(): Flow> + + suspend fun deleteScrobble(scrobble: PendingScrobble) + + suspend fun insertScrobble(scrobble: PendingScrobble) +} \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/repositories/LocalUserRepository.kt b/app/src/main/java/io/musicorum/mobile/repositories/LocalUserRepository.kt index 7e32d4a..acd5c1c 100644 --- a/app/src/main/java/io/musicorum/mobile/repositories/LocalUserRepository.kt +++ b/app/src/main/java/io/musicorum/mobile/repositories/LocalUserRepository.kt @@ -5,9 +5,8 @@ import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import io.musicorum.mobile.datastore.LocalUser import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.models.PartialUser import io.musicorum.mobile.serialization.User @@ -22,9 +21,9 @@ val Context.localUser: DataStore by preferencesDataStore("partialUs class LocalUserRepository(val context: Context) { private val userFlow = context.localUser.data.map { PartialUser( - it[usernameKey] ?: "", - it[pfpKey] ?: "", - it[expiresKey] ?: 0L + it[LocalUser.USERNAME_KEY] ?: "", + it[LocalUser.PROFILE_ICON_KEY] ?: "", + it[LocalUser.EXPIRES_IN_KEY] ?: 0L ) } @@ -71,14 +70,11 @@ class LocalUserRepository(val context: Context) { suspend fun updateUser(partialUser: PartialUser) { context.localUser.edit { - it[usernameKey] = partialUser.username - it[pfpKey] = partialUser.imageUrl - it[expiresKey] = partialUser.expiresIn + it[LocalUser.USERNAME_KEY] = partialUser.username + it[LocalUser.PROFILE_ICON_KEY] = partialUser.imageUrl + it[LocalUser.EXPIRES_IN_KEY] = partialUser.expiresIn } } - private val usernameKey = stringPreferencesKey("username") - private val pfpKey = stringPreferencesKey("profilePictureUrl") - private val expiresKey = longPreferencesKey("expires_in") private val cacheTime = Date(Date().time + (1000 * 60 * 60 * 48)).time } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/repositories/OfflineScrobblesRepository.kt b/app/src/main/java/io/musicorum/mobile/repositories/OfflineScrobblesRepository.kt deleted file mode 100644 index 9d69983..0000000 --- a/app/src/main/java/io/musicorum/mobile/repositories/OfflineScrobblesRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.musicorum.mobile.repositories - -import io.musicorum.mobile.database.daos.PendingScrobblesDao -import io.musicorum.mobile.models.PendingScrobble -import kotlinx.coroutines.flow.Flow - -class OfflineScrobblesRepository(private val scrobblesDao: PendingScrobblesDao) : - PendingScrobblesRepository { - override suspend fun getAllScrobblesStream(): Flow> = - scrobblesDao.getAll() - - override suspend fun deleteScrobble(scrobble: PendingScrobble) = scrobblesDao.delete(scrobble) - - override suspend fun insertScrobble(scrobble: PendingScrobble) = scrobblesDao.insert(scrobble) -} \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/repositories/PendingScrobblesRepository.kt b/app/src/main/java/io/musicorum/mobile/repositories/PendingScrobblesRepository.kt index e905991..cee4181 100644 --- a/app/src/main/java/io/musicorum/mobile/repositories/PendingScrobblesRepository.kt +++ b/app/src/main/java/io/musicorum/mobile/repositories/PendingScrobblesRepository.kt @@ -1,12 +1,15 @@ package io.musicorum.mobile.repositories import io.musicorum.mobile.models.PendingScrobble +import io.musicorum.mobile.repositories.daos.PendingScrobblesDao import kotlinx.coroutines.flow.Flow -interface PendingScrobblesRepository { - suspend fun getAllScrobblesStream(): Flow> +class PendingScrobblesRepository(private val scrobblesDao: PendingScrobblesDao) : + IPendingScrobbles { + override suspend fun getAllScrobblesStream(): Flow> = + scrobblesDao.getAll() - suspend fun deleteScrobble(scrobble: PendingScrobble) + override suspend fun deleteScrobble(scrobble: PendingScrobble) = scrobblesDao.delete(scrobble) - suspend fun insertScrobble(scrobble: PendingScrobble) + override suspend fun insertScrobble(scrobble: PendingScrobble) = scrobblesDao.insert(scrobble) } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/database/daos/CachedScrobblesDao.kt b/app/src/main/java/io/musicorum/mobile/repositories/daos/CachedScrobblesDao.kt similarity index 94% rename from app/src/main/java/io/musicorum/mobile/database/daos/CachedScrobblesDao.kt rename to app/src/main/java/io/musicorum/mobile/repositories/daos/CachedScrobblesDao.kt index 9b1d59a..e5fe75c 100644 --- a/app/src/main/java/io/musicorum/mobile/database/daos/CachedScrobblesDao.kt +++ b/app/src/main/java/io/musicorum/mobile/repositories/daos/CachedScrobblesDao.kt @@ -1,4 +1,4 @@ -package io.musicorum.mobile.database.daos +package io.musicorum.mobile.repositories.daos import androidx.room.Dao import androidx.room.Delete diff --git a/app/src/main/java/io/musicorum/mobile/database/daos/PendingScrobblesDao.kt b/app/src/main/java/io/musicorum/mobile/repositories/daos/PendingScrobblesDao.kt similarity index 92% rename from app/src/main/java/io/musicorum/mobile/database/daos/PendingScrobblesDao.kt rename to app/src/main/java/io/musicorum/mobile/repositories/daos/PendingScrobblesDao.kt index 56a5989..85df167 100644 --- a/app/src/main/java/io/musicorum/mobile/database/daos/PendingScrobblesDao.kt +++ b/app/src/main/java/io/musicorum/mobile/repositories/daos/PendingScrobblesDao.kt @@ -1,4 +1,4 @@ -package io.musicorum.mobile.database.daos +package io.musicorum.mobile.repositories.daos import androidx.room.Dao import androidx.room.Delete diff --git a/app/src/main/java/io/musicorum/mobile/router/BottomNavbar.kt b/app/src/main/java/io/musicorum/mobile/router/BottomNavbar.kt index 4d4abe9..71d6b7b 100644 --- a/app/src/main/java/io/musicorum/mobile/router/BottomNavbar.kt +++ b/app/src/main/java/io/musicorum/mobile/router/BottomNavbar.kt @@ -3,10 +3,10 @@ package io.musicorum.mobile.router import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.QueueMusic import androidx.compose.material.icons.rounded.BarChart import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Person -import androidx.compose.material.icons.rounded.QueueMusic import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar @@ -31,7 +31,7 @@ fun BottomNavBar(nav: NavHostController) { val icons = listOf( Icons.Rounded.Home, Icons.Rounded.Search, - Icons.Rounded.QueueMusic, + Icons.AutoMirrored.Rounded.QueueMusic, Icons.Rounded.BarChart, Icons.Rounded.Person ) @@ -44,12 +44,8 @@ fun BottomNavBar(nav: NavHostController) { val navBackStackEntry by nav.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination - Box(modifier = Modifier - .background(LighterGray) - ) { - NavigationBar( - containerColor = Color.Transparent - ) { + Box(modifier = Modifier.background(LighterGray)) { + NavigationBar(containerColor = Color.Transparent) { items.forEachIndexed { index, s -> NavigationBarItem( selected = currentDestination?.hierarchy?.any { it.route?.lowercase() == s.lowercase() } == true, diff --git a/app/src/main/java/io/musicorum/mobile/router/Controller.kt b/app/src/main/java/io/musicorum/mobile/router/Controller.kt index b18042a..4501510 100644 --- a/app/src/main/java/io/musicorum/mobile/router/Controller.kt +++ b/app/src/main/java/io/musicorum/mobile/router/Controller.kt @@ -48,12 +48,12 @@ fun NavigationRouter(controller: NavHostController) { }*/ composable( - "user/{username}", - arguments = listOf(navArgument("username") { + "user/{usernameArg}", + arguments = listOf(navArgument("usernameArg") { type = NavType.StringType }) ) { - User(username = it.arguments?.getString("username")!!) + User() } composable( diff --git a/app/src/main/java/io/musicorum/mobile/services/NotificationListener.kt b/app/src/main/java/io/musicorum/mobile/services/NotificationListener.kt index 12301ac..01b774a 100644 --- a/app/src/main/java/io/musicorum/mobile/services/NotificationListener.kt +++ b/app/src/main/java/io/musicorum/mobile/services/NotificationListener.kt @@ -22,7 +22,7 @@ import io.ktor.http.isSuccess import io.musicorum.mobile.database.PendingScrobblesDb import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.models.PendingScrobble -import io.musicorum.mobile.repositories.OfflineScrobblesRepository +import io.musicorum.mobile.repositories.PendingScrobblesRepository import io.musicorum.mobile.scrobblePrefs import io.musicorum.mobile.userData import kotlinx.coroutines.CoroutineScope @@ -48,12 +48,16 @@ class NotificationListener : NotificationListenerService() { p[stringPreferencesKey("session_key")] }.first() ?: return@launch - val scrobbles = offlineScrobblesRepo.getAllScrobblesStream() - val list = scrobbles.first() - if (list.isEmpty()) { + if (!this@NotificationListener::offlineScrobblesRepo::isInitialized.get()) { + Log.w(tag, "Couldn't init offline scrobbles repo, aborting.") + return@launch + } + + val scrobbles = offlineScrobblesRepo.getAllScrobblesStream().first() + if (scrobbles.isEmpty()) { Log.d(tag, "no scrobbles to sync") } else { - for (scrobble in list) { + for (scrobble in scrobbles) { val res = UserEndpoint.scrobble( track = scrobble.trackName, artist = scrobble.artistName, @@ -74,7 +78,7 @@ class NotificationListener : NotificationListenerService() { Log.d(tag, "internet connection lost") } } - private lateinit var offlineScrobblesRepo: OfflineScrobblesRepository + private lateinit var offlineScrobblesRepo: PendingScrobblesRepository override fun onListenerConnected() { val networkRequest = NetworkRequest.Builder() @@ -86,7 +90,7 @@ class NotificationListener : NotificationListenerService() { val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager connectivityManager.requestNetwork(networkRequest, networkCallback) - offlineScrobblesRepo = OfflineScrobblesRepository( + offlineScrobblesRepo = PendingScrobblesRepository( PendingScrobblesDb.getDatabase(applicationContext).pendingScrobblesDao() ) } diff --git a/app/src/main/java/io/musicorum/mobile/utils/HandleAuth.kt b/app/src/main/java/io/musicorum/mobile/utils/HandleAuth.kt index 0b4a65f..6742325 100644 --- a/app/src/main/java/io/musicorum/mobile/utils/HandleAuth.kt +++ b/app/src/main/java/io/musicorum/mobile/utils/HandleAuth.kt @@ -1,14 +1,11 @@ package io.musicorum.mobile.utils import android.content.Context -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey import io.musicorum.mobile.ktor.endpoints.AuthEndpoint import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.models.PartialUser import io.musicorum.mobile.repositories.LocalUserRepository import io.musicorum.mobile.serialization.User -import io.musicorum.mobile.userData import java.util.Date suspend fun handleAuth( @@ -32,10 +29,3 @@ suspend fun handleAuth( return userBlock(user, s) } } - -suspend fun commitUser(sessionKey: String, context: Context) { - val dataStoreKey = stringPreferencesKey("session_key") - context.userData.edit { prefs -> - prefs[dataStoreKey] = sessionKey - } -} diff --git a/app/src/main/java/io/musicorum/mobile/utils/MessagingService.kt b/app/src/main/java/io/musicorum/mobile/utils/MessagingService.kt index ba104ec..e10cc26 100644 --- a/app/src/main/java/io/musicorum/mobile/utils/MessagingService.kt +++ b/app/src/main/java/io/musicorum/mobile/utils/MessagingService.kt @@ -1,9 +1,12 @@ package io.musicorum.mobile.utils +import android.Manifest import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import android.content.pm.PackageManager import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat.getSystemService @@ -41,6 +44,11 @@ class MessagingService : FirebaseMessagingService() { .build() with(NotificationManagerCompat.from(this.applicationContext)) { + if (ActivityCompat.checkSelfPermission( + this@MessagingService.applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) return notify(message.messageId?.toIntOrNull() ?: 0, builder) } } diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/AlbumViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/AlbumViewModel.kt index 969e7c8..0348a03 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/AlbumViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/AlbumViewModel.kt @@ -1,23 +1,26 @@ package io.musicorum.mobile.viewmodels +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.musicorum.mobile.ktor.endpoints.AlbumEndpoint import io.musicorum.mobile.ktor.endpoints.InnerAlbum import io.musicorum.mobile.ktor.endpoints.musicorum.MusicorumArtistEndpoint -import io.musicorum.mobile.serialization.User +import io.musicorum.mobile.repositories.LocalUserRepository import io.musicorum.mobile.serialization.entities.Artist import kotlinx.coroutines.launch -class AlbumViewModel : ViewModel() { +class AlbumViewModel(application: Application) : AndroidViewModel(application) { val album by lazy { MutableLiveData() } val artistImage by lazy { MutableLiveData() } val errored by lazy { MutableLiveData(false) } + val ctx = application - fun getAlbum(albumName: String, artistName: String, user: User?) { + fun getAlbum(albumName: String, artistName: String) { viewModelScope.launch { - val res = AlbumEndpoint.getInfo(albumName, artistName, user?.user?.name) + val user = LocalUserRepository(ctx).getUser() + val res = AlbumEndpoint.getInfo(albumName, artistName, user.username) album.value = res if (res == null) errored.value = true } diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/ArtistViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/ArtistViewModel.kt index c45f61c..54ba999 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/ArtistViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/ArtistViewModel.kt @@ -1,25 +1,30 @@ package io.musicorum.mobile.viewmodels +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.palette.graphics.Palette import io.musicorum.mobile.ktor.endpoints.ArtistEndpoint import io.musicorum.mobile.ktor.endpoints.musicorum.MusicorumArtistEndpoint +import io.musicorum.mobile.repositories.LocalUserRepository import io.musicorum.mobile.serialization.TopAlbum import io.musicorum.mobile.serialization.entities.Artist import io.musicorum.mobile.serialization.entities.Track import kotlinx.coroutines.launch -class ArtistViewModel : ViewModel() { +class ArtistViewModel(application: Application) : AndroidViewModel(application) { val artist by lazy { MutableLiveData() } val palette by lazy { MutableLiveData() } val topTracks by lazy { MutableLiveData>() } val topAlbums by lazy { MutableLiveData>() } + val ctx = application - fun fetchArtist(artistName: String, username: String?) { + fun fetchArtist(artistName: String) { viewModelScope.launch { - val res = ArtistEndpoint.getInfo(artistName, username) + val user = LocalUserRepository(ctx).getUser() + val res = ArtistEndpoint.getInfo(artistName, user.username) + if (res != null) { val artists = mutableListOf(res.artist) artists.addAll(res.artist.similar?.artist!!) diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/ChartsViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/ChartsViewModel.kt index 261297e..f00ed86 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/ChartsViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/ChartsViewModel.kt @@ -1,7 +1,6 @@ package io.musicorum.mobile.viewmodels import android.app.Application -import android.content.Context import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.lifecycle.AndroidViewModel @@ -35,9 +34,10 @@ class ChartsViewModel(application: Application) : AndroidViewModel(application) } } - fun getColor(image: String, ctx: Context) { + fun getUserColor() { viewModelScope.launch { - val bmp = getBitmap(image, ctx) + val user = LocalUserRepository(_application).getUser() + val bmp = getBitmap(user.imageUrl, _application) val palette = createPalette(bmp) if (palette.vibrantSwatch == null) { preferredColor.value = Color(palette.getDominantColor(Color.Gray.toArgb())) @@ -93,4 +93,8 @@ class ChartsViewModel(application: Application) : AndroidViewModel(application) fetchAll(user.username) } } + + init { + getUserColor() + } } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/CollageViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/CollageViewModel.kt index be36fe1..9b436b9 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/CollageViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/CollageViewModel.kt @@ -1,12 +1,16 @@ package io.musicorum.mobile.viewmodels +import android.Manifest import android.app.Application import android.app.DownloadManager import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.os.Environment import android.widget.Toast +import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData @@ -99,9 +103,20 @@ class CollageViewModel(application: Application) : AndroidViewModel(application) } } - fun downloadFile() { + fun downloadFile(): Boolean { val manager = ctx.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val uri = imageUrl.value ?: return + val uri = imageUrl.value ?: return false + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + if (ContextCompat.checkSelfPermission( + ctx, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_DENIED + ) { + return false + } + } + Toast.makeText(ctx, ctx.getString(R.string.starting_download), Toast.LENGTH_SHORT).show() val request = DownloadManager.Request(Uri.parse(uri)) @@ -111,6 +126,7 @@ class CollageViewModel(application: Application) : AndroidViewModel(application) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) manager.enqueue(request) + return true } fun shareFile() { diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/HomeViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/HomeViewModel.kt index 5aebf32..44686a4 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/HomeViewModel.kt @@ -9,6 +9,8 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.palette.graphics.Palette +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase import com.google.firebase.remoteconfig.FirebaseRemoteConfig import dagger.hilt.android.lifecycle.HiltViewModel import io.musicorum.mobile.database.CachedScrobblesDb @@ -20,7 +22,7 @@ import io.musicorum.mobile.models.FetchPeriod import io.musicorum.mobile.models.PartialUser import io.musicorum.mobile.repositories.CachedScrobblesRepository import io.musicorum.mobile.repositories.LocalUserRepository -import io.musicorum.mobile.repositories.OfflineScrobblesRepository +import io.musicorum.mobile.repositories.PendingScrobblesRepository import io.musicorum.mobile.repositories.ScrobbleRepository import io.musicorum.mobile.serialization.Image import io.musicorum.mobile.serialization.RecentTracks @@ -57,6 +59,7 @@ class HomeViewModel @Inject constructor( val showRewindCard = MutableLiveData(false) val rewindCardMessage = MutableLiveData("") private val remoteConfig = FirebaseRemoteConfig.getInstance() + val showSettingsBadge = MutableLiveData(false) fun refresh() { @@ -109,7 +112,7 @@ class HomeViewModel @Inject constructor( } if (res.exceptionOrNull() is UnknownHostException) { val pendingDao = PendingScrobblesDb.getDatabase(ctx).pendingScrobblesDao() - val pendingRepo = OfflineScrobblesRepository(pendingDao) + val pendingRepo = PendingScrobblesRepository(pendingDao) val pendingScrobbles = pendingRepo.getAllScrobblesStream().first() isOffline.value = true val list = mutableListOf() @@ -223,5 +226,6 @@ class HomeViewModel @Inject constructor( init { init() + showSettingsBadge.value = Firebase.crashlytics.didCrashOnPreviousExecution() } } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/MostListenedViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/MostListenedViewModel.kt index 2b1309e..96f8ace 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/MostListenedViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/MostListenedViewModel.kt @@ -1,21 +1,27 @@ package io.musicorum.mobile.viewmodels +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.ktor.endpoints.musicorum.MusicorumTrackEndpoint import io.musicorum.mobile.models.FetchPeriod -import io.musicorum.mobile.serialization.entities.TopTracks +import io.musicorum.mobile.repositories.LocalUserRepository +import io.musicorum.mobile.serialization.entities.Track +import kotlinx.coroutines.Job import kotlinx.coroutines.launch -class MostListenedViewModel : ViewModel() { - val mosListenedTracks by lazy { MutableLiveData() } - val error by lazy { MutableLiveData(null) } +class MostListenedViewModel(application: Application) : AndroidViewModel(application) { + val mosListenedTracks = MutableLiveData>(emptyList()) + val error = MutableLiveData(null) + lateinit var job: Job + val ctx = application - suspend fun fetchMostListened(username: String, period: FetchPeriod?, limit: Int?) { - viewModelScope.launch { - val res = UserEndpoint.getTopTracks(username, period, limit) + fun fetchMostListened(period: FetchPeriod?, limit: Int?) { + job = viewModelScope.launch { + val localUser = LocalUserRepository(ctx).getUser() + val res = UserEndpoint.getTopTracks(localUser.username, period, limit) if (res == null) { error.value = true return@launch @@ -25,7 +31,11 @@ class MostListenedViewModel : ViewModel() { val url = tr?.resources?.getOrNull(0)?.bestImageUrl res.topTracks.tracks[i].bestImageUrl = url ?: "" } - mosListenedTracks.value = res + mosListenedTracks.value = res.topTracks.tracks } } + + init { + fetchMostListened(FetchPeriod.WEEK, null) + } } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobbleSettingsVm.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobbleSettingsVm.kt index 28963f2..040d741 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobbleSettingsVm.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobbleSettingsVm.kt @@ -5,15 +5,13 @@ import android.app.Application import android.content.Context import android.content.pm.ApplicationInfo import androidx.core.app.NotificationManagerCompat -import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.floatPreferencesKey -import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase +import io.musicorum.mobile.datastore.ScrobblePreferences import io.musicorum.mobile.scrobblePrefs import io.musicorum.mobile.views.settings.MediaApp import kotlinx.coroutines.flow.first @@ -33,12 +31,6 @@ class ScrobbleSettingsVm(application: Application) : AndroidViewModel(applicatio val hasPermission = MutableLiveData(false) val showSpotifyModal = MutableLiveData(false) - private val scrobblePointKey = floatPreferencesKey("scrobblePoint") - private val enabledKey = booleanPreferencesKey("enabled") - - // val newAppsKey = booleanPreferencesKey("newApps") - private val enabledAppsKey = stringSetPreferencesKey("enabledApps") - private val updateNowPlayingKey = booleanPreferencesKey("updateNowPlaying") fun updateScrobbling(value: Boolean) { isEnabled.value = value @@ -49,7 +41,7 @@ class ScrobbleSettingsVm(application: Application) : AndroidViewModel(applicatio } viewModelScope.launch { ctx.scrobblePrefs.edit { p -> - p[enabledKey] = value + p[ScrobblePreferences.ENABLED_KEY] = value } } } @@ -58,7 +50,7 @@ class ScrobbleSettingsVm(application: Application) : AndroidViewModel(applicatio updateNowPlaying.value = value viewModelScope.launch { ctx.scrobblePrefs.edit { p -> - p[updateNowPlayingKey] = value + p[ScrobblePreferences.UPDATED_NOWPLAYING_KEY] = value } } } @@ -66,7 +58,7 @@ class ScrobbleSettingsVm(application: Application) : AndroidViewModel(applicatio fun updateScrobblePoint() { viewModelScope.launch { ctx.applicationContext.scrobblePrefs.edit { p -> - p[scrobblePointKey] = scrobblePoint.value!! + p[ScrobblePreferences.SCROBBLE_POINT_KEY] = scrobblePoint.value!! } } } @@ -76,7 +68,7 @@ class ScrobbleSettingsVm(application: Application) : AndroidViewModel(applicatio newSet.add("com.spotify.music") viewModelScope.launch { ctx.scrobblePrefs.edit { p -> - p[enabledAppsKey] = newSet + p[ScrobblePreferences.ALLOWED_APPS_KEY] = newSet } } enabledPackageSet.value = newSet @@ -96,7 +88,7 @@ class ScrobbleSettingsVm(application: Application) : AndroidViewModel(applicatio viewModelScope.launch { ctx.scrobblePrefs.edit { p -> - p[enabledAppsKey] = newSet + p[ScrobblePreferences.ALLOWED_APPS_KEY] = newSet } enabledPackageSet.value = newSet } @@ -104,14 +96,17 @@ class ScrobbleSettingsVm(application: Application) : AndroidViewModel(applicatio private fun init() { viewModelScope.launch { - val scrobblePointData = ctx.scrobblePrefs.data.map { p -> p[scrobblePointKey] } - .first() - val enabled = ctx.scrobblePrefs.data.map { p -> p[enabledKey] } - .first() - val enabledApps = ctx.scrobblePrefs.data.map { p -> p[enabledAppsKey] } - .first() - val updateNowPlayingData = ctx.scrobblePrefs.data.map { p -> p[updateNowPlayingKey] } + val scrobblePointData = + ctx.scrobblePrefs.data.map { p -> p[ScrobblePreferences.SCROBBLE_POINT_KEY] } + .first() + val enabled = ctx.scrobblePrefs.data.map { p -> p[ScrobblePreferences.ENABLED_KEY] } .first() + val enabledApps = + ctx.scrobblePrefs.data.map { p -> p[ScrobblePreferences.ALLOWED_APPS_KEY] } + .first() + val updateNowPlayingData = + ctx.scrobblePrefs.data.map { p -> p[ScrobblePreferences.UPDATED_NOWPLAYING_KEY] } + .first() if (scrobblePointData == null) { scrobblePoint.value = 50f diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobblingViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobblingViewModel.kt index 58acadf..c36cb9f 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobblingViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/ScrobblingViewModel.kt @@ -16,7 +16,7 @@ import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.models.CachedScrobble import io.musicorum.mobile.repositories.CachedScrobblesRepository import io.musicorum.mobile.repositories.LocalUserRepository -import io.musicorum.mobile.repositories.OfflineScrobblesRepository +import io.musicorum.mobile.repositories.PendingScrobblesRepository import io.musicorum.mobile.repositories.ScrobbleRepository import io.musicorum.mobile.serialization.entities.Track import kotlinx.coroutines.flow.first @@ -74,7 +74,7 @@ class ScrobblingViewModel @Inject constructor( if (result.exceptionOrNull() is UnknownHostException) { val pendingScrobblesDao = PendingScrobblesDb.getDatabase(ctx).pendingScrobblesDao() - val pendingRepo = OfflineScrobblesRepository(pendingScrobblesDao) + val pendingRepo = PendingScrobblesRepository(pendingScrobblesDao) val list = mutableListOf() val cached = cachedRepo.getAllFromCache().first() diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/SettingsVm.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/SettingsVm.kt index d7a4f65..96ca9b5 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/SettingsVm.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/SettingsVm.kt @@ -1,8 +1,6 @@ package io.musicorum.mobile.viewmodels -import android.annotation.SuppressLint import android.app.Application -import android.content.Context import android.content.Intent import android.net.Uri import androidx.datastore.preferences.core.booleanPreferencesKey @@ -12,6 +10,10 @@ import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase +import io.musicorum.mobile.models.PartialUser +import io.musicorum.mobile.repositories.LocalUserRepository import io.musicorum.mobile.scrobblePrefs import io.musicorum.mobile.userData import kotlinx.coroutines.flow.first @@ -21,13 +23,13 @@ import kotlinx.coroutines.launch class SettingsVm(application: Application) : AndroidViewModel(application) { val enabledApps = MutableLiveData>() val deviceScrobble = MutableLiveData(false) - - @SuppressLint("StaticFieldLeak") - private val context = application as Context + val user = MutableLiveData(null) + val showReport = MutableLiveData(false) + val ctx = application fun logout(onLogout: () -> Unit) { viewModelScope.launch { - context.applicationContext.userData.edit { + ctx.applicationContext.userData.edit { it.remove(stringPreferencesKey("session_key")) } onLogout() @@ -38,20 +40,28 @@ class SettingsVm(application: Application) : AndroidViewModel(application) { val uri = Uri.parse(url) val intent = Intent(Intent.ACTION_VIEW, uri) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) + ctx.startActivity(intent) } fun getScrobbleInfo() { viewModelScope.launch { - val enabled = context.scrobblePrefs.data.map { p -> + val enabled = ctx.scrobblePrefs.data.map { p -> p[booleanPreferencesKey("enabled")] }.first() deviceScrobble.value = enabled - val apps = context.scrobblePrefs.data.map { p -> + val apps = ctx.scrobblePrefs.data.map { p -> p[stringSetPreferencesKey("enabledApps")] }.first() enabledApps.value = apps } } + + init { + showReport.value = Firebase.crashlytics.didCrashOnPreviousExecution() + viewModelScope.launch { + val localUser = LocalUserRepository(ctx).getUser() + user.value = localUser + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/TrackViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/TrackViewModel.kt index d5475fb..277ec14 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/TrackViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/TrackViewModel.kt @@ -1,14 +1,15 @@ package io.musicorum.mobile.viewmodels -import android.content.Context +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.ktor.client.plugins.ServerResponseException import io.musicorum.mobile.ktor.endpoints.TrackEndpoint import io.musicorum.mobile.ktor.endpoints.musicorum.MusicorumAlbumEndpoint import io.musicorum.mobile.ktor.endpoints.musicorum.MusicorumArtistEndpoint import io.musicorum.mobile.ktor.endpoints.musicorum.MusicorumTrackEndpoint +import io.musicorum.mobile.repositories.LocalUserRepository import io.musicorum.mobile.serialization.Image import io.musicorum.mobile.serialization.SimilarTrack import io.musicorum.mobile.serialization.entities.Album @@ -16,21 +17,21 @@ import io.musicorum.mobile.serialization.entities.Artist import io.musicorum.mobile.serialization.entities.Track import kotlinx.coroutines.launch -class TrackViewModel : ViewModel() { +class TrackViewModel(application: Application) : AndroidViewModel(application) { val track by lazy { MutableLiveData() } val similar by lazy { MutableLiveData() } val artistCover by lazy { MutableLiveData() } val error by lazy { MutableLiveData(null) } + val ctx = application suspend fun fetchTrack( trackName: String, artist: String, - username: String?, autoCorrect: Boolean? ) { viewModelScope.launch { - val res = TrackEndpoint.getTrack(trackName, artist, username, autoCorrect) - + val user = LocalUserRepository(ctx).getUser() + val res = TrackEndpoint.getTrack(trackName, artist, user.username, autoCorrect) val musRes = MusicorumTrackEndpoint.fetchTracks(listOf(res!!.track)) musRes.getOrNull(0)?.bestResource?.bestImageUrl?.let { res.track.album = @@ -90,7 +91,7 @@ class TrackViewModel : ViewModel() { artistCover.value = res[0].resources.getOrNull(0)?.bestImageUrl } - fun updateFavoritePreference(track: Track, ctx: Context) { + fun updateFavoritePreference(track: Track) { viewModelScope.launch { TrackEndpoint.updateFavoritePreference(track, ctx) } diff --git a/app/src/main/java/io/musicorum/mobile/viewmodels/UserViewModel.kt b/app/src/main/java/io/musicorum/mobile/viewmodels/UserViewModel.kt index a0721c5..ef51b2f 100644 --- a/app/src/main/java/io/musicorum/mobile/viewmodels/UserViewModel.kt +++ b/app/src/main/java/io/musicorum/mobile/viewmodels/UserViewModel.kt @@ -1,41 +1,58 @@ package io.musicorum.mobile.viewmodels +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.ktor.endpoints.musicorum.MusicorumArtistEndpoint import io.musicorum.mobile.models.FetchPeriod +import io.musicorum.mobile.repositories.LocalUserRepository import io.musicorum.mobile.serialization.RecentTracks import io.musicorum.mobile.serialization.TopAlbumsResponse import io.musicorum.mobile.serialization.TopArtistsResponse import io.musicorum.mobile.serialization.User import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class UserViewModel : ViewModel() { +class UserViewModel @Inject constructor( + application: Application, + savedStateHandle: SavedStateHandle +) : AndroidViewModel(application) { val user by lazy { MutableLiveData() } val topArtists by lazy { MutableLiveData() } val recentTracks by lazy { MutableLiveData() } val topAlbums by lazy { MutableLiveData() } val isRefreshing by lazy { MutableStateFlow(false) } val errored by lazy { MutableLiveData(false) } + val ctx = application + private val usernameArg = savedStateHandle.get("usernameArg") + private val localUser = LocalUserRepository(ctx) fun refresh() { isRefreshing.value = true recentTracks.value = null + getRecentTracks(limit = 4, null) } - fun getUser(username: String) = kotlin.runCatching { + fun getUser() = kotlin.runCatching { viewModelScope.launch { - val userInfo = UserEndpoint.getUser(username) + if (usernameArg == null) { + val localUser = LocalUserRepository(ctx).fetch() + user.value = localUser + return@launch + } + val userInfo = UserEndpoint.getUser(usernameArg) user.value = userInfo if (userInfo == null) errored.value = true } } - fun getTopArtists(username: String, limit: Int?, period: FetchPeriod?) = kotlin.runCatching { + fun getTopArtists(limit: Int?, period: FetchPeriod?) = kotlin.runCatching { viewModelScope.launch { + val username = usernameArg ?: localUser.getUser().username val res = UserEndpoint.getTopArtists(username, limit, period) if (res != null) { val musRes = @@ -49,18 +66,27 @@ class UserViewModel : ViewModel() { } } - fun getRecentTracks(username: String, limit: Int?, extended: Boolean?) = kotlin.runCatching { + fun getRecentTracks(limit: Int?, extended: Boolean?) = kotlin.runCatching { viewModelScope.launch { + val username = usernameArg ?: localUser.getUser().username val res = UserEndpoint.getRecentTracks(username, null, limit, extended) recentTracks.value = res isRefreshing.value = false } } - fun getTopAlbums(username: String, period: FetchPeriod?, limit: Int?) = kotlin.runCatching { + fun getTopAlbums(period: FetchPeriod?, limit: Int?) = kotlin.runCatching { viewModelScope.launch { + val username = usernameArg ?: localUser.getUser().username val res = UserEndpoint.getTopAlbums(username, period, limit) topAlbums.value = res } } + + init { + getUser() + getTopArtists(null, FetchPeriod.MONTH) + getRecentTracks(limit = 4, null) + getTopAlbums(FetchPeriod.MONTH, null) + } } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/views/Account.kt b/app/src/main/java/io/musicorum/mobile/views/Account.kt index 1ee8383..bac08a3 100644 --- a/app/src/main/java/io/musicorum/mobile/views/Account.kt +++ b/app/src/main/java/io/musicorum/mobile/views/Account.kt @@ -15,6 +15,6 @@ fun Account(model: AccountVm = viewModel()) { if (user == null) { CenteredLoadingSpinner() } else { - User(username = user.username) + User() } } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/views/Collage.kt b/app/src/main/java/io/musicorum/mobile/views/Collage.kt index ac78ced..676bcdc 100644 --- a/app/src/main/java/io/musicorum/mobile/views/Collage.kt +++ b/app/src/main/java/io/musicorum/mobile/views/Collage.kt @@ -1,6 +1,9 @@ package io.musicorum.mobile.views +import android.Manifest import android.os.Bundle +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -88,6 +91,11 @@ fun Collage(viewModel: CollageViewModel = viewModel(), args: Bundle) { val ready = viewModel.ready.observeAsState().value!! val isGenerating = viewModel.isGenerating.observeAsState().value!! val nav = LocalNavigation.current + val permLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) viewModel.downloadFile() + } val themeOptions = listOf("Grid" to MusicorumTheme.GRID, "Duotone" to MusicorumTheme.DUOTONE) val entityOptions = @@ -346,7 +354,12 @@ fun Collage(viewModel: CollageViewModel = viewModel(), args: Bundle) { } Spacer(modifier = Modifier.width(20.dp)) Button( - onClick = { viewModel.downloadFile() }, + onClick = { + val result = viewModel.downloadFile() + if (!result) { + permLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + }, modifier = Modifier.weight(.5f, true) ) { Icon(Icons.Rounded.Download, null) diff --git a/app/src/main/java/io/musicorum/mobile/views/Home.kt b/app/src/main/java/io/musicorum/mobile/views/Home.kt index 3578cce..bd35136 100644 --- a/app/src/main/java/io/musicorum/mobile/views/Home.kt +++ b/app/src/main/java/io/musicorum/mobile/views/Home.kt @@ -25,6 +25,9 @@ import androidx.compose.material.icons.rounded.ChevronRight import androidx.compose.material.icons.rounded.CloudOff import androidx.compose.material.icons.rounded.Error import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -44,7 +47,6 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavHostController import androidx.palette.graphics.Palette import coil.compose.AsyncImage import com.google.accompanist.placeholder.PlaceholderHighlight @@ -63,16 +65,19 @@ import io.musicorum.mobile.components.RewindCard import io.musicorum.mobile.components.skeletons.GenericCardPlaceholder import io.musicorum.mobile.models.PartialUser import io.musicorum.mobile.router.Route +import io.musicorum.mobile.router.Routes import io.musicorum.mobile.ui.theme.ContentSecondary import io.musicorum.mobile.ui.theme.EvenLighterGray import io.musicorum.mobile.ui.theme.KindaBlack import io.musicorum.mobile.ui.theme.LighterGray +import io.musicorum.mobile.ui.theme.MostlyRed import io.musicorum.mobile.ui.theme.SkeletonSecondaryColor import io.musicorum.mobile.ui.theme.Subtitle1 import io.musicorum.mobile.ui.theme.Typography import io.musicorum.mobile.utils.getDarkenGradient import io.musicorum.mobile.viewmodels.HomeViewModel +@OptIn(ExperimentalMaterial3Api::class) @Composable @Route("home") fun Home(vm: HomeViewModel = hiltViewModel()) { @@ -89,6 +94,7 @@ fun Home(vm: HomeViewModel = hiltViewModel()) { val isOffline = vm.isOffline.observeAsState(initial = false) val hasPendingScrobbles by vm.hasPendingScrobbles.observeAsState(false) val showRewindCard by vm.showRewindCard.observeAsState(initial = false) + val crashBadge by vm.showSettingsBadge.observeAsState(false) SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing.value), @@ -111,8 +117,20 @@ fun Home(vm: HomeViewModel = hiltViewModel()) { modifier = Modifier.padding(start = 20.dp) ) - IconButton(onClick = { nav.navigate("settings") }) { + BadgedBox( + modifier = Modifier + .clip(CircleShape) + .clickable { + nav.navigate(Routes.settings) + } + .padding(12.dp), + badge = { + if (crashBadge) Badge(containerColor = MostlyRed) + } + ) { + //IconButton(onClick = { nav.navigate("settings") }) { Icon(Icons.Rounded.Settings, contentDescription = null) + //} } } @@ -125,7 +143,7 @@ fun Home(vm: HomeViewModel = hiltViewModel()) { } if (user != null && palette != null) { - UserCard(user, palette!!, weeklyScrobbles, nav) + UserCard(user, palette!!, weeklyScrobbles) } else { Box( modifier = Modifier @@ -160,8 +178,14 @@ fun Home(vm: HomeViewModel = hiltViewModel()) { .size(30.dp) ) Column(modifier = Modifier.padding(start = 22.dp)) { - Text(stringResource(id = R.string.youre_offline), style = Typography.titleMedium) - Text(stringResource(R.string.outdated_data_notice), style = Typography.bodySmall) + Text( + stringResource(id = R.string.youre_offline), + style = Typography.titleMedium + ) + Text( + stringResource(R.string.outdated_data_notice), + style = Typography.bodySmall + ) } } } @@ -283,14 +307,14 @@ fun Home(vm: HomeViewModel = hiltViewModel()) { private fun UserCard( user: PartialUser, palette: Palette, - weeklyScrobbles: Int?, - nav: NavHostController + weeklyScrobbles: Int? ) { var vibrant = Color(palette.getVibrantColor(0)) if (palette.vibrantSwatch == null) { vibrant = Color(palette.getDominantColor(0)) } val gradient = getDarkenGradient(vibrant) + val nav = LocalNavigation.current Box( modifier = Modifier @@ -299,7 +323,7 @@ private fun UserCard( .padding(20.dp, 20.dp, 20.dp) .clip(RoundedCornerShape(15.dp)) .background(brush = Brush.linearGradient(gradient)) - .clickable { nav.navigate("profile") } + .clickable { nav?.navigate("profile") } ) { Row( diff --git a/app/src/main/java/io/musicorum/mobile/views/charts/Charts.kt b/app/src/main/java/io/musicorum/mobile/views/charts/Charts.kt index de67fb7..31ca915 100644 --- a/app/src/main/java/io/musicorum/mobile/views/charts/Charts.kt +++ b/app/src/main/java/io/musicorum/mobile/views/charts/Charts.kt @@ -64,7 +64,6 @@ import com.google.accompanist.placeholder.material.shimmer import com.google.accompanist.placeholder.placeholder import io.ktor.util.escapeHTML import io.musicorum.mobile.LocalNavigation -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.R import io.musicorum.mobile.coil.defaultImageRequestBuilder import io.musicorum.mobile.components.CenteredLoadingSpinner @@ -87,8 +86,6 @@ import io.musicorum.mobile.viewmodels.ChartsViewModel fun Charts() { val model: ChartsViewModel = viewModel() val period by model.period.observeAsState() - val user = LocalUser.current ?: return - val ctx = LocalContext.current val userColor by model.preferredColor.observeAsState(Color.Gray) val topArtists by model.topArtists.observeAsState() val topAlbums by model.topAlbums.observeAsState() @@ -97,10 +94,6 @@ fun Charts() { val busy by model.busy.observeAsState() val nav = LocalNavigation.current - LaunchedEffect(Unit) { - model.getColor(user.user.bestImageUrl, ctx) - } - if (showBottomSheet.value) { PeriodBottomSheet(state = showBottomSheet) { model.updatePeriod(it) diff --git a/app/src/main/java/io/musicorum/mobile/views/charts/PeriodPicker.kt b/app/src/main/java/io/musicorum/mobile/views/charts/PeriodPicker.kt index d37b98e..bf0cc73 100644 --- a/app/src/main/java/io/musicorum/mobile/views/charts/PeriodPicker.kt +++ b/app/src/main/java/io/musicorum/mobile/views/charts/PeriodPicker.kt @@ -1,6 +1,5 @@ package io.musicorum.mobile.views.charts -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -17,8 +16,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -77,26 +74,18 @@ fun PeriodPicker( } } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun PeriodComponent( period: Pair, active: Boolean, onClick: (FetchPeriod) -> Unit ) { - val colorState = remember { mutableStateOf(Color.Transparent) } val colorAnimation = animateColorAsState( if (active) { MostlyRed } else Color.Transparent, ) - val activeMod = Modifier - .padding(end = 15.dp) - .clip(RoundedCornerShape(100)) - .background(MostlyRed) - .padding(horizontal = 9.dp, vertical = 1.dp) - val normalMod = Modifier .padding(end = 15.dp) .clip(RoundedCornerShape(100)) diff --git a/app/src/main/java/io/musicorum/mobile/views/individual/Album.kt b/app/src/main/java/io/musicorum/mobile/views/individual/Album.kt index cb8ab77..16b94e2 100644 --- a/app/src/main/java/io/musicorum/mobile/views/individual/Album.kt +++ b/app/src/main/java/io/musicorum/mobile/views/individual/Album.kt @@ -26,7 +26,6 @@ import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.logEvent import io.ktor.http.* import io.musicorum.mobile.LocalAnalytics -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.R import io.musicorum.mobile.coil.PlaceholderType import io.musicorum.mobile.components.* @@ -68,7 +67,6 @@ fun Album( val ctx = LocalContext.current val paletteReady = remember { mutableStateOf(false) } val palette: MutableState = remember { mutableStateOf(null) } - val user = LocalUser.current val errored = albumViewModel.errored.observeAsState().value val localSnack = LocalSnackbar.current @@ -80,7 +78,7 @@ fun Album( LaunchedEffect(album) { if (album == null) { - albumViewModel.getAlbum(partialAlbum.name, partialAlbum.artist, user) + albumViewModel.getAlbum(partialAlbum.name, partialAlbum.artist) } } diff --git a/app/src/main/java/io/musicorum/mobile/views/individual/AlbumTracklist.kt b/app/src/main/java/io/musicorum/mobile/views/individual/AlbumTracklist.kt index 08da3ea..6ad1cee 100644 --- a/app/src/main/java/io/musicorum/mobile/views/individual/AlbumTracklist.kt +++ b/app/src/main/java/io/musicorum/mobile/views/individual/AlbumTracklist.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.components.AlbumTrack import io.musicorum.mobile.components.CenteredLoadingSpinner import io.musicorum.mobile.components.MusicorumTopBar @@ -21,10 +20,9 @@ import io.musicorum.mobile.viewmodels.AlbumViewModel @Composable fun AlbumTracklist(partialAlbum: PartialAlbum, model: AlbumViewModel = viewModel()) { val album = model.album.observeAsState().value?.album - val user = LocalUser.current!! LaunchedEffect(Unit) { - model.getAlbum(partialAlbum.name, partialAlbum.artist, user) + model.getAlbum(partialAlbum.name, partialAlbum.artist) } if (album == null) { diff --git a/app/src/main/java/io/musicorum/mobile/views/individual/Artist.kt b/app/src/main/java/io/musicorum/mobile/views/individual/Artist.kt index 9ef2453..7c603f9 100644 --- a/app/src/main/java/io/musicorum/mobile/views/individual/Artist.kt +++ b/app/src/main/java/io/musicorum/mobile/views/individual/Artist.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -23,7 +24,6 @@ import androidx.palette.graphics.Palette import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.logEvent import io.musicorum.mobile.LocalAnalytics -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.R import io.musicorum.mobile.coil.PlaceholderType import io.musicorum.mobile.components.* @@ -37,23 +37,22 @@ import io.musicorum.mobile.viewmodels.ArtistViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun Artist(artistName: String, artistViewModel: ArtistViewModel = viewModel()) { - val analytics = LocalAnalytics.current!! + val analytics = LocalAnalytics.current LaunchedEffect(Unit) { - analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) { + analytics?.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) { param(FirebaseAnalytics.Param.SCREEN_NAME, "artist") } } val artist = artistViewModel.artist.observeAsState().value - val topAlbums = artistViewModel.topAlbums.observeAsState().value + val topAlbums by artistViewModel.topAlbums.observeAsState() val topTracks = artistViewModel.topTracks.observeAsState().value - val user = LocalUser.current!!.user val palette = remember { mutableStateOf(null) } val paletteReady = remember { mutableStateOf(false) } val ctx = LocalContext.current LaunchedEffect(artist) { if (artist == null) { - artistViewModel.fetchArtist(artistName, user.name) + artistViewModel.fetchArtist(artistName) artistViewModel.fetchTopAlbums(artistName) artistViewModel.fetchTopTracks(artistName) } else { diff --git a/app/src/main/java/io/musicorum/mobile/views/individual/Track.kt b/app/src/main/java/io/musicorum/mobile/views/individual/Track.kt index 05da525..a8e7245 100644 --- a/app/src/main/java/io/musicorum/mobile/views/individual/Track.kt +++ b/app/src/main/java/io/musicorum/mobile/views/individual/Track.kt @@ -25,7 +25,6 @@ import androidx.palette.graphics.Palette import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.logEvent import io.musicorum.mobile.LocalAnalytics -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.R import io.musicorum.mobile.coil.PlaceholderType import io.musicorum.mobile.components.* @@ -68,7 +67,6 @@ fun Track( var paletteReady by remember { mutableStateOf(false) } val similarTracks = trackViewModel.similar.observeAsState().value val artistCover = trackViewModel.artistCover.observeAsState().value - val user = LocalUser.current!! val errored = trackViewModel.error.observeAsState().value val localSnack = LocalSnackbar.current @@ -83,7 +81,6 @@ fun Track( trackViewModel.fetchTrack( partialTrack.trackName, partialTrack.trackArtist, - user.user.name, null ) } else { @@ -122,7 +119,7 @@ fun Track( IconButton( onClick = { analytics.logEvent("topbar_like_pressed", null) - trackViewModel.updateFavoritePreference(track, ctx) + trackViewModel.updateFavoritePreference(track) loved.value = !loved.value }) { if (loved.value) { diff --git a/app/src/main/java/io/musicorum/mobile/views/individual/User.kt b/app/src/main/java/io/musicorum/mobile/views/individual/User.kt index 7e80376..7aabd72 100644 --- a/app/src/main/java/io/musicorum/mobile/views/individual/User.kt +++ b/app/src/main/java/io/musicorum/mobile/views/individual/User.kt @@ -10,12 +10,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,7 +25,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.R import io.musicorum.mobile.coil.PlaceholderType import io.musicorum.mobile.components.CenteredLoadingSpinner @@ -36,7 +35,6 @@ import io.musicorum.mobile.components.TopAlbumsRow import io.musicorum.mobile.components.TopArtistsRow import io.musicorum.mobile.components.TrackListItem import io.musicorum.mobile.components.skeletons.GenericListItemSkeleton -import io.musicorum.mobile.models.FetchPeriod import io.musicorum.mobile.ui.theme.ContentSecondary import io.musicorum.mobile.ui.theme.Heading4 import io.musicorum.mobile.ui.theme.KindaBlack @@ -47,37 +45,20 @@ import io.musicorum.mobile.viewmodels.UserViewModel @Composable fun User( - username: String, userViewModel: UserViewModel = viewModel() ) { - val user = if (username == LocalUser.current?.user?.name) { - LocalUser.current!! - } else { - userViewModel.user.observeAsState().value - } - val topArtists = userViewModel.topArtists.observeAsState().value + val user by userViewModel.user.observeAsState() + + val topArtists by userViewModel.topArtists.observeAsState() val recentScrobbles = userViewModel.recentTracks.observeAsState().value?.recentTracks?.tracks - val topAlbums = userViewModel.topAlbums.observeAsState().value + val topAlbums by userViewModel.topAlbums.observeAsState() val isRefreshing = rememberSwipeRefreshState(isRefreshing = userViewModel.isRefreshing.collectAsState().value) - val errored = userViewModel.errored.observeAsState().value + val errored by userViewModel.errored.observeAsState() val scrollState = rememberScrollState() val localSnack = LocalSnackbar.current - LaunchedEffect(user, recentScrobbles, errored) { - if (user == null) { - userViewModel.getUser(username) - } else { - if (topArtists == null) { - userViewModel.getTopArtists(username, null, FetchPeriod.MONTH) - } - if (recentScrobbles == null) { - userViewModel.getRecentTracks(user.user.name, limit = 4, null) - } - if (topAlbums == null) { - userViewModel.getTopAlbums(user.user.name, FetchPeriod.MONTH, null) - } - } + LaunchedEffect(errored) { if (errored == true) { localSnack.showSnackbar("Failed to get some information") } @@ -97,14 +78,14 @@ fun User( ) { GradientHeader( backgroundUrl = topArtists?.topArtists?.artists?.getOrNull(0)?.bestImageUrl, - coverUrl = user.user.bestImageUrl, + coverUrl = user?.user?.bestImageUrl, shape = CircleShape, placeholderType = PlaceholderType.USER ) - Text(text = user.user.name, style = Typography.displaySmall) + Text(text = user?.user!!.name, style = Typography.displaySmall) Row { - user.user.realName?.let { + user?.user!!.realName?.let { Text( text = "$it • ", color = ContentSecondary, @@ -112,7 +93,7 @@ fun User( ) } Text( - text = "Scrobbling since ${user.user.registered.asParsedDate}", + text = "Scrobbling since ${user?.user?.registered?.asParsedDate}", style = Typography.bodyLarge, color = ContentSecondary ) @@ -121,9 +102,9 @@ fun User( HorizontalDivider(modifier = Modifier.run { padding(vertical = 20.dp) }) StatisticRow( short = false, - stringResource(R.string.scrobbles) to user.user.scrobbles.toLong(), - stringResource(R.string.artists) to user.user.artistCount?.toLongOrNull(), - stringResource(R.string.albums) to user.user.albumCount?.toLongOrNull() + stringResource(R.string.scrobbles) to user?.user?.scrobbles?.toLong(), + stringResource(R.string.artists) to user?.user?.artistCount?.toLongOrNull(), + stringResource(R.string.albums) to user?.user?.albumCount?.toLongOrNull() ) HorizontalDivider(modifier = Modifier.run { padding(vertical = 20.dp) }) diff --git a/app/src/main/java/io/musicorum/mobile/views/login/Login.kt b/app/src/main/java/io/musicorum/mobile/views/login/Login.kt index 15e4abd..9009c5a 100644 --- a/app/src/main/java/io/musicorum/mobile/views/login/Login.kt +++ b/app/src/main/java/io/musicorum/mobile/views/login/Login.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController import io.musicorum.mobile.BuildConfig -import io.musicorum.mobile.MutableUserState import io.musicorum.mobile.R import io.musicorum.mobile.ui.theme.KindaBlack import io.musicorum.mobile.utils.handleAuth @@ -50,7 +49,6 @@ fun Login(nav: NavController, deepLinkToken: String?) { loading.value = true handleAuth(deepLinkToken, ctx = ctx) { user, sk -> if (user != null) { - MutableUserState.value = user nav.navigate("user_confirmation/${sk}") } } diff --git a/app/src/main/java/io/musicorum/mobile/views/login/LoginGraph.kt b/app/src/main/java/io/musicorum/mobile/views/login/LoginGraph.kt index c7b7847..d027b88 100644 --- a/app/src/main/java/io/musicorum/mobile/views/login/LoginGraph.kt +++ b/app/src/main/java/io/musicorum/mobile/views/login/LoginGraph.kt @@ -21,7 +21,10 @@ fun NavGraphBuilder.loginGraph(navController: NavController) { "user_confirmation/{session_key}", arguments = listOf(navArgument("session_key") { type = NavType.StringType }) ) { - UserConfirmation(nav = navController, it.arguments?.getString("session_key")!!) + UserConfirmation( + nav = navController, + sessionKey = it.arguments?.getString("session_key")!! + ) } composable("deviceScrobble") { Scrobble() } diff --git a/app/src/main/java/io/musicorum/mobile/views/login/UserConfirmation.kt b/app/src/main/java/io/musicorum/mobile/views/login/UserConfirmation.kt index 00bd3af..8f2c4af 100644 --- a/app/src/main/java/io/musicorum/mobile/views/login/UserConfirmation.kt +++ b/app/src/main/java/io/musicorum/mobile/views/login/UserConfirmation.kt @@ -10,13 +10,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Checkbox import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -24,29 +23,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil.compose.AsyncImage -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase -import io.musicorum.mobile.BuildConfig -import io.musicorum.mobile.LocalUser -import io.musicorum.mobile.MutableUserState import io.musicorum.mobile.R import io.musicorum.mobile.coil.PlaceholderType import io.musicorum.mobile.coil.defaultImageRequestBuilder -import io.musicorum.mobile.ktor.endpoints.UserEndpoint import io.musicorum.mobile.ui.theme.Heading2 import io.musicorum.mobile.ui.theme.KindaBlack import io.musicorum.mobile.ui.theme.Typography -import io.musicorum.mobile.utils.commitUser import kotlinx.coroutines.launch @Composable -fun UserConfirmation(nav: NavController, sessionKey: String) { +fun UserConfirmation( + viewModel: UserConfirmationViewModel = viewModel(), + nav: NavController, + sessionKey: String +) { Column( modifier = Modifier .background(KindaBlack) @@ -57,15 +52,9 @@ fun UserConfirmation(nav: NavController, sessionKey: String) { horizontalAlignment = Alignment.CenterHorizontally ) { val checkboxChecked = remember { mutableStateOf(true) } - val dialogOpened = remember { mutableStateOf(false) } - val user = LocalUser.current - val ctx = LocalContext.current + val user by viewModel.user.observeAsState(null) val coroutine = rememberCoroutineScope() user?.let { user1 -> - if (dialogOpened.value) { - AnalyticsDialog(open = dialogOpened, checkBoxState = checkboxChecked) - } - AsyncImage( model = defaultImageRequestBuilder( url = user1.user.bestImageUrl, @@ -80,11 +69,7 @@ fun UserConfirmation(nav: NavController, sessionKey: String) { Checkbox( checked = checkboxChecked.value, onCheckedChange = { - if (checkboxChecked.value) { - dialogOpened.value = true - } else { - checkboxChecked.value = true - } + checkboxChecked.value = it } ) Text( @@ -96,42 +81,13 @@ fun UserConfirmation(nav: NavController, sessionKey: String) { Spacer(modifier = Modifier.width(25.dp)) Button(onClick = { coroutine.launch { - if (!BuildConfig.DEBUG) { - Firebase.crashlytics.setCrashlyticsCollectionEnabled(checkboxChecked.value) - Firebase.analytics.setAnalyticsCollectionEnabled(checkboxChecked.value) - } - commitUser(sessionKey, ctx) - val sessionUser = UserEndpoint.getSessionUser(sessionKey) - sessionUser?.let { - MutableUserState.value = it - nav.navigate("deviceScrobble") - } + viewModel.setAnalyticsPreferences(checkboxChecked.value) + viewModel.saveSession(sessionKey) + nav.navigate("deviceScrobble") } }) { Text(text = stringResource(id = R.string.confirmation_continue)) } } } -} - - -@Composable -private fun AnalyticsDialog(open: MutableState, checkBoxState: MutableState) { - AlertDialog( - onDismissRequest = { open.value = false }, - title = { Text(text = stringResource(id = R.string.hold_on)) }, - confirmButton = { - TextButton(onClick = { checkBoxState.value = false; open.value = false }) { - Text(text = stringResource(id = R.string.disable_anyway)) - } - }, - dismissButton = { - TextButton(onClick = { open.value = false }) { - Text(text = stringResource(id = R.string.cancel)) - } - }, - text = { - Text(text = stringResource(id = R.string.analytics_warning)) - } - ) } \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/views/login/UserConfirmationViewModel.kt b/app/src/main/java/io/musicorum/mobile/views/login/UserConfirmationViewModel.kt new file mode 100644 index 0000000..97ea561 --- /dev/null +++ b/app/src/main/java/io/musicorum/mobile/views/login/UserConfirmationViewModel.kt @@ -0,0 +1,59 @@ +package io.musicorum.mobile.views.login + +import android.app.Application +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase +import io.musicorum.mobile.BuildConfig +import io.musicorum.mobile.datastore.AnalyticsConsent +import io.musicorum.mobile.datastore.UserData +import io.musicorum.mobile.ktor.endpoints.UserEndpoint +import io.musicorum.mobile.serialization.User +import io.musicorum.mobile.userData +import kotlinx.coroutines.launch +import javax.inject.Inject + +class UserConfirmationViewModel @Inject constructor( + application: Application, + savedStateHandle: SavedStateHandle +) : AndroidViewModel(application) { + private val Context.analyticsConsent: DataStore by preferencesDataStore(name = AnalyticsConsent.DataStoreName) + val ctx = application + val user = MutableLiveData(null) + + fun setAnalyticsPreferences(consent: Boolean) { + if (BuildConfig.DEBUG) return + Firebase.analytics.setAnalyticsCollectionEnabled(consent) + Firebase.crashlytics.setCrashlyticsCollectionEnabled(consent) + viewModelScope.launch { + ctx.analyticsConsent.edit { + it[AnalyticsConsent.CONSENT_KEY] = consent + } + } + } + + fun saveSession(token: String) { + viewModelScope.launch { + ctx.userData.edit { + it[UserData.SESSION_KEY] = token + } + } + } + + init { + val sessionKey = savedStateHandle.get("session_key")!! + viewModelScope.launch { + val sessionUser = UserEndpoint.getSessionUser(sessionKey) + user.value = sessionUser + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/musicorum/mobile/views/mostListened/MostListened.kt b/app/src/main/java/io/musicorum/mobile/views/mostListened/MostListened.kt index 40331cc..252eca6 100644 --- a/app/src/main/java/io/musicorum/mobile/views/mostListened/MostListened.kt +++ b/app/src/main/java/io/musicorum/mobile/views/mostListened/MostListened.kt @@ -8,18 +8,22 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -27,16 +31,13 @@ import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.logEvent import io.musicorum.mobile.LocalAnalytics import io.musicorum.mobile.LocalNavigation -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.components.TrackListItem -import io.musicorum.mobile.models.FetchPeriod import io.musicorum.mobile.ui.theme.LighterGray -import io.musicorum.mobile.utils.LocalSnackbar import io.musicorum.mobile.viewmodels.MostListenedViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MostListened(mostListenedViewModel: MostListenedViewModel) { +fun MostListened(viewModel: MostListenedViewModel) { val analytics = LocalAnalytics.current!! LaunchedEffect(Unit) { analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) { @@ -44,19 +45,14 @@ fun MostListened(mostListenedViewModel: MostListenedViewModel) { } } - val mostListened = mostListenedViewModel.mosListenedTracks.observeAsState() - val snack = LocalSnackbar.current - val user = LocalUser.current!! + val mostListened by viewModel.mosListenedTracks.observeAsState(emptyList()) + val snackHost = remember { SnackbarHostState() } val nav = LocalNavigation.current!! - LaunchedEffect(Unit) { - if (mostListenedViewModel.mosListenedTracks.value == null) { - mostListenedViewModel.fetchMostListened(user.user.name, FetchPeriod.WEEK, null) - } - } - LaunchedEffect(mostListenedViewModel.error.value) { - if (mostListenedViewModel.error.value == true) { - snack.showSnackbar("Failed to fetch") + + LaunchedEffect(viewModel.error.value) { + if (viewModel.error.value == true) { + snackHost.showSnackbar("Failed to fetch") } } @@ -73,14 +69,15 @@ fun MostListened(mostListenedViewModel: MostListenedViewModel) { colors = appBarColors, navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { - Icon(Icons.Rounded.ArrowBack, null) + Icon(Icons.AutoMirrored.Rounded.ArrowBack, null) } } ) }, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { SnackbarHost(snackHost) } ) { - if (mostListened.value == null) { + if (viewModel.job.isActive) { Row( modifier = Modifier .fillMaxSize() @@ -97,7 +94,7 @@ fun MostListened(mostListenedViewModel: MostListenedViewModel) { modifier = Modifier .padding(it), ) { - items(mostListened.value!!.topTracks.tracks) { track -> + items(mostListened) { track -> TrackListItem(track = track) } } diff --git a/app/src/main/java/io/musicorum/mobile/views/settings/Settings.kt b/app/src/main/java/io/musicorum/mobile/views/settings/Settings.kt index b80271e..49fa209 100644 --- a/app/src/main/java/io/musicorum/mobile/views/settings/Settings.kt +++ b/app/src/main/java/io/musicorum/mobile/views/settings/Settings.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,7 +49,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import io.musicorum.mobile.BuildConfig import io.musicorum.mobile.LocalNavigation -import io.musicorum.mobile.LocalUser import io.musicorum.mobile.R import io.musicorum.mobile.coil.PlaceholderType import io.musicorum.mobile.coil.defaultImageRequestBuilder @@ -60,11 +60,12 @@ import io.musicorum.mobile.viewmodels.SettingsVm @Composable fun Settings(viewModel: SettingsVm = viewModel()) { - val user = LocalUser.current + val user by viewModel.user.observeAsState(null) val ctx = LocalContext.current val nav = LocalNavigation.current - val enabledApps = viewModel.enabledApps.observeAsState().value - val enabled = viewModel.deviceScrobble.observeAsState().value + val enabledApps by viewModel.enabledApps.observeAsState(emptySet()) + val enabled by viewModel.deviceScrobble.observeAsState() + val showReport by viewModel.showReport.observeAsState(false) val patreonUrl = "https://www.patreon.com/musicorumapp" val discordInvite = "https://discord.gg/7shqxp9Mg4" @@ -110,7 +111,7 @@ fun Settings(viewModel: SettingsVm = viewModel()) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage( model = defaultImageRequestBuilder( - url = user?.user?.bestImageUrl, + url = user?.imageUrl, placeholderType = PlaceholderType.USER ), contentDescription = null, @@ -119,7 +120,7 @@ fun Settings(viewModel: SettingsVm = viewModel()) { .size(43.dp) ) Spacer(Modifier.width(10.dp)) - Text(text = user?.user?.name ?: "", style = Typography.titleMedium) + Text(text = user?.username ?: "", style = Typography.titleMedium) } IconButton(onClick = { viewModel.logout { @@ -191,7 +192,18 @@ fun Settings(viewModel: SettingsVm = viewModel()) { leadingContent = { Image(painterResource(id = R.drawable.discord_logo), null) }, - modifier = Modifier.clickable { viewModel.launchUrl(discordInvite) } + modifier = Modifier.clickable { viewModel.launchUrl(discordInvite) }, + supportingContent = { + if (showReport) { + Row(verticalAlignment = Alignment.CenterVertically) { + Canvas(modifier = Modifier, onDraw = { + drawCircle(MostlyRed, 6f) + }) + Spacer(modifier = Modifier.width(5.dp)) + Text(stringResource(R.string.report_crash)) + } + } + } ) ListItem( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1598b3..3fc9104 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -147,6 +147,7 @@ An internal error occurred while generating your image.\n%1$s Sharing item… Starting download… + Report what happened when the app crashed Enabled • %1$d app Enabled • %1$d apps