diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 4cd16b6..c2622ee 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -13,13 +13,7 @@
-
-
-
-
-
-
-
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 5fc23f8..8177882 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -14,10 +14,10 @@
+
-
diff --git a/.idea/ktfmt.xml b/.idea/ktfmt.xml
new file mode 100644
index 0000000..1859451
--- /dev/null
+++ b/.idea/ktfmt.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e88f74f..b889184 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,36 +11,32 @@ plugins {
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.ktfmt.gradle)
}
-val localProperties = Properties().apply {
- load(project.rootDir.resolve("local.properties").inputStream())
-}
+val localProperties =
+ Properties().apply { load(project.rootDir.resolve("local.properties").inputStream()) }
android {
namespace = "com.bobbyesp.metadator"
- compileSdk = 35
+ compileSdk = 36
defaultConfig {
applicationId = "com.bobbyesp.metadator"
minSdk = 24
- targetSdk = 35
+ targetSdk = 36
versionCode = rootProject.extra["versionCode"] as Int
versionName = rootProject.extra["versionName"] as String
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary = true
- }
+ vectorDrawables { useSupportLibrary = true }
manifestPlaceholders["redirectHostName"] = "metadator"
manifestPlaceholders["redirectSchemeName"] = "metadator"
}
- androidResources {
- generateLocaleConfig = true
- }
+ androidResources { generateLocaleConfig = true }
signingConfigs {
create("release") {
@@ -56,14 +52,19 @@ android {
buildTypes {
release {
buildConfigField(
- "String", "CLIENT_ID", "\"${localProperties.getProperty("CLIENT_ID")}\""
+ "String",
+ "CLIENT_ID",
+ "\"${localProperties.getProperty("CLIENT_ID")}\"",
)
buildConfigField(
- "String", "CLIENT_SECRET", "\"${localProperties.getProperty("CLIENT_SECRET")}\""
+ "String",
+ "CLIENT_SECRET",
+ "\"${localProperties.getProperty("CLIENT_SECRET")}\"",
)
isMinifyEnabled = true
proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
)
if (System.getenv("RELEASE_STORE_FILE") != null) {
signingConfig = signingConfigs["release"]
@@ -71,13 +72,17 @@ android {
}
debug {
buildConfigField(
- "String", "CLIENT_ID", "\"${localProperties.getProperty("CLIENT_ID")}\""
+ "String",
+ "CLIENT_ID",
+ "\"${localProperties.getProperty("CLIENT_ID")}\"",
)
buildConfigField(
- "String", "CLIENT_SECRET", "\"${localProperties.getProperty("CLIENT_SECRET")}\""
+ "String",
+ "CLIENT_SECRET",
+ "\"${localProperties.getProperty("CLIENT_SECRET")}\"",
)
isMinifyEnabled = false
-// applicationIdSuffix = ".debug"
+ // applicationIdSuffix = ".debug"
signingConfig = signingConfigs["debug"]
}
}
@@ -96,59 +101,46 @@ android {
nativeSymbolUploadEnabled = true
}
}
-
}
- create("foss") {
- dimension = "version"
- }
+ create("foss") { dimension = "version" }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
- kotlinOptions {
- jvmTarget = "21"
- //freeCompilerArgs = listOf("-Xcontext-receivers", "-XXLanguage:+ExplicitBackingFields")
- }
buildFeatures {
compose = true
buildConfig = true
}
- composeCompiler {
- reportsDestination = layout.buildDirectory.dir("compose_compiler")
- }
- kotlin {
- sourceSets.all {
- languageSettings {
- languageVersion = "2.0"
- }
- }
- }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
- }
+ composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") }
+ kotlin { sourceSets.all { languageSettings { languageVersion = "2.0" } } }
+ packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
applicationVariants.all {
val variantName = name
sourceSets {
- getByName("main") {
- java.srcDir(File("build/generated/ksp/$variantName/kotlin"))
- }
+ getByName("main") { java.srcDir(File("build/generated/ksp/$variantName/kotlin")) }
}
outputs.all {
if (githubBuild) {
- (this as com.android.build.gradle.internal.api.BaseVariantOutputImpl).outputFileName =
- "Metadator-${defaultConfig.versionName}-${name}_(GitHub).apk"
+ (this as com.android.build.gradle.internal.api.BaseVariantOutputImpl)
+ .outputFileName = "Metadator-${defaultConfig.versionName}-${name}_(GitHub).apk"
} else {
- (this as com.android.build.gradle.internal.api.BaseVariantOutputImpl).outputFileName =
- "Metadator-${defaultConfig.versionName}-${name}.apk"
+ (this as com.android.build.gradle.internal.api.BaseVariantOutputImpl)
+ .outputFileName = "Metadator-${defaultConfig.versionName}-${name}.apk"
}
}
}
+}
+ktfmt {
+ // Google style - 2 space indentation & automatically adds/removes trailing commas
+ // googleStyle()
+
+ // KotlinLang style - 4 space indentation - From
+ // https://kotlinlang.org/docs/coding-conventions.html
+ kotlinLangStyle()
}
ksp {
@@ -158,69 +150,72 @@ ksp {
dependencies {
implementation(project(":app:utilities"))
+ implementation(project(":core-utilities"))
implementation(project(":app:ui"))
-//---------------Core----------------//
- implementation(libs.bundles.core) //⚠️ This contains core kotlinx libraries, lifecycle runtime and Activity Compose support
+ // ---------------Core----------------//
+ implementation(
+ libs.bundles.core
+ ) // ⚠️ This contains core kotlinx libraries, lifecycle runtime and Activity Compose support
implementation(libs.bundles.coroutines)
-//---------------User Interface---------------//
-//Core UI libraries
+ // ---------------User Interface---------------//
+ // Core UI libraries
api(platform(libs.compose.bom))
-//Accompanist libraries
+ // Accompanist libraries
implementation(libs.bundles.accompanist)
-//Compose libraries
+ // Compose libraries
implementation(libs.bundles.compose)
implementation(libs.materialKolor)
-//Pagination
+ // Pagination
implementation(libs.bundles.pagination)
-//-------------------Network-------------------//
+ // -------------------Network-------------------//
implementation(libs.bundles.ktor)
- //---------------Media3---------------//
+ // ---------------Media3---------------//
implementation(libs.bundles.media3)
implementation(project(":app:mediaplayer"))
-//---------------Dependency Injection---------------//
+ // ---------------Dependency Injection---------------//
implementation(libs.bundles.koin)
-//-------------------Database-------------------//
+ // -------------------Database-------------------//
implementation(libs.room.runtime)
implementation(libs.room.ktx)
implementation(libs.room.paging)
annotationProcessor(libs.room.compiler)
-//-------------------Key-value Storage-------------------//
+ // -------------------Key-value Storage-------------------//
implementation(libs.datastore.preferences)
-//-------------------Image Loading-------------------//
+ // -------------------Image Loading-------------------//
implementation(libs.landscapist.coil)
-//-------------------FIREBASE-------------------//
+ // -------------------FIREBASE-------------------//
"playstoreApi"(platform(libs.firebase.bom))
"playstoreImplementation"(libs.firebase.analytics)
"playstoreImplementation"(libs.firebase.crashlytics)
-//-------------------Utilities-------------------//
+ // -------------------Utilities-------------------//
implementation(libs.kotlinx.collections.immutable)
implementation(libs.profileinstaller)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
- implementation(libs.taglib)
+ implementation(files("libs/taglib_1.0.2.aar"))
implementation(libs.scrollbar)
implementation(libs.sonner)
implementation(libs.spotify.api.android)
implementation(project(":crashhandler"))
-//-------------------Testing-------------------//
-//Android testing libraries
+ // -------------------Testing-------------------//
+ // Android testing libraries
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
-//Compose testing and tooling libraries
+ // Compose testing and tooling libraries
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.compose.test.junit4)
implementation(libs.compose.tooling.preview)
@@ -240,4 +235,4 @@ class RoomSchemaArgProvider(
}
return listOf("room.schemaLocation=${schemaDir.path}")
}
-}
\ No newline at end of file
+}
diff --git a/app/libs/taglib-release-1.0.3.aar b/app/libs/taglib-release-1.0.3.aar
new file mode 100644
index 0000000..1766170
Binary files /dev/null and b/app/libs/taglib-release-1.0.3.aar differ
diff --git a/app/libs/taglib_1.0.2.aar b/app/libs/taglib_1.0.2.aar
new file mode 100644
index 0000000..9aed234
Binary files /dev/null and b/app/libs/taglib_1.0.2.aar differ
diff --git a/app/mediaplayer/build.gradle.kts b/app/mediaplayer/build.gradle.kts
index 663bc66..c5e6eed 100644
--- a/app/mediaplayer/build.gradle.kts
+++ b/app/mediaplayer/build.gradle.kts
@@ -2,12 +2,15 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.android.kotlin)
alias(libs.plugins.kotlin.ksp)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.ktfmt.gradle)
}
android {
namespace = "com.bobbyesp.mediaplayer"
- compileSdk = 35
+ compileSdk = 36
defaultConfig {
minSdk = 24
@@ -21,46 +24,54 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
libraryVariants.all {
val variantName = name
sourceSets {
- getByName("main") {
- java.srcDir(File("build/generated/ksp/$variantName/kotlin"))
- }
+ getByName("main") { java.srcDir(File("build/generated/ksp/$variantName/kotlin")) }
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
- kotlinOptions {
- jvmTarget = "21"
- }
}
-ksp {
- arg("KOIN_CONFIG_CHECK", "true")
+ktfmt {
+ // Google style - 2 space indentation & automatically adds/removes trailing commas
+ // googleStyle()
+
+ // KotlinLang style - 4 space indentation - From
+ // https://kotlinlang.org/docs/coding-conventions.html
+ kotlinLangStyle()
}
+ksp { arg("KOIN_CONFIG_CHECK", "true") }
+
dependencies {
implementation(libs.core.ktx)
implementation(libs.core.appcompat)
- implementation(libs.androidx.legacy.support.v4) // Needed MediaSessionCompat.Token
+ implementation(libs.androidx.legacy.support.v4)
+ implementation(project(":core-utilities")) // Needed MediaSessionCompat.Token
- //DI (Dependency Injection - Koin)
+ // Todo create a top level utilities
+
+ // DI (Dependency Injection - Koin)
implementation(libs.bundles.koin)
- //Media3
+ // Media3
implementation(libs.bundles.media3)
- //Coil
+ // KotlinX Serialization
+ implementation(libs.kotlinx.serialization.json)
+
+ // Coil
implementation(libs.coil)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
-}
\ No newline at end of file
+}
diff --git a/app/mediaplayer/src/androidTest/java/com/bobbyesp/mediaplayer/ExampleInstrumentedTest.kt b/app/mediaplayer/src/androidTest/java/com/bobbyesp/mediaplayer/ExampleInstrumentedTest.kt
index b48b919..5e2f889 100644
--- a/app/mediaplayer/src/androidTest/java/com/bobbyesp/mediaplayer/ExampleInstrumentedTest.kt
+++ b/app/mediaplayer/src/androidTest/java/com/bobbyesp/mediaplayer/ExampleInstrumentedTest.kt
@@ -19,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.bobbyesp.mediaplayer.test", appContext.packageName)
}
-}
\ No newline at end of file
+}
diff --git a/app/mediaplayer/src/main/AndroidManifest.xml b/app/mediaplayer/src/main/AndroidManifest.xml
index d46789c..487a25d 100644
--- a/app/mediaplayer/src/main/AndroidManifest.xml
+++ b/app/mediaplayer/src/main/AndroidManifest.xml
@@ -3,17 +3,18 @@
xmlns:tools="http://schemas.android.com/tools">
+
-
-
+
+
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/repository/MediaStoreFolderScannerImpl.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/repository/MediaStoreFolderScannerImpl.kt
new file mode 100644
index 0000000..d769284
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/repository/MediaStoreFolderScannerImpl.kt
@@ -0,0 +1,46 @@
+package com.bobbyesp.mediaplayer.data.repository
+
+import android.content.Context
+import android.media.MediaScannerConnection
+import com.bobbyesp.mediaplayer.domain.model.MusicTrack
+import com.bobbyesp.mediaplayer.domain.repository.MediaStoreFolderScanner
+import java.io.File
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class MediaStoreFolderScannerImpl : MediaStoreFolderScanner {
+ override suspend fun scanCustomFolder(path: String): List =
+ withContext(Dispatchers.IO) {
+ val folder = File(path)
+ if (!folder.exists() || !folder.isDirectory) return@withContext emptyList()
+
+ return@withContext folder
+ .walkTopDown()
+ .filter { it.isFile && it.extension.lowercase() in allowedExtensions }
+ .map { file ->
+ MusicTrack(
+ id = file.hashCode().toLong(),
+ title = file.nameWithoutExtension,
+ path = file.absolutePath,
+ )
+ }
+ .toList()
+ }
+
+ override fun forceMediaStoreFolderScanning(context: Context, path: String) {
+ val file = File(path)
+ if (!file.exists() || !file.isDirectory)
+ throw IllegalArgumentException(
+ "An invalid path was provided to be scanned by MediaStore"
+ )
+
+ val paths =
+ file.walkTopDown().filter { it.isFile }.map { it.absolutePath }.toList().toTypedArray()
+
+ if (paths.isNotEmpty()) {
+ MediaScannerConnection.scanFile(context, paths, arrayOf("audio/*")) { audioPath, uri ->
+ println("Scanned: $audioPath, URI: $uri")
+ }
+ }
+ }
+}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/repository/MusicLibraryRepositoryImpl.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/repository/MusicLibraryRepositoryImpl.kt
new file mode 100644
index 0000000..0a81460
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/repository/MusicLibraryRepositoryImpl.kt
@@ -0,0 +1,358 @@
+package com.bobbyesp.mediaplayer.data.repository
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import android.util.Log
+import androidx.core.database.getStringOrNull
+import androidx.core.net.toUri
+import com.bobbyesp.coreutilities.observe
+import com.bobbyesp.mediaplayer.domain.enums.MediaStoreSearchFilter
+import com.bobbyesp.mediaplayer.domain.model.Genre
+import com.bobbyesp.mediaplayer.domain.model.MusicTrack
+import com.bobbyesp.mediaplayer.domain.repository.MusicLibraryRepository
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+private const val TAG = "MusicLibraryRepositoryImpl"
+
+class MusicLibraryRepositoryImpl(private val context: Context) : MusicLibraryRepository {
+
+ override suspend fun getMusicLibrary(
+ query: String?,
+ searchFilters: List?,
+ ): List =
+ withContext(Dispatchers.IO) {
+ val musicList = mutableListOf()
+ val projection = projectionBasedOnVersion()
+ val selection = buildSelection(query, searchFilters)
+ val selectionArgs = buildSelectionArgs(query, searchFilters)
+ val sortOrder = MediaStore.Audio.Media.TITLE
+
+ context.contentResolver
+ .advancedQuery(
+ uri = musicUri,
+ projection = projection,
+ selection = selection,
+ args = selectionArgs,
+ order = sortOrder,
+ ascending = true,
+ )
+ ?.use { cursor -> musicList.addAll(parseCursor(cursor)) }
+ musicList.toList() // Convert to immutable list
+ }
+
+ override fun observeMusicLibrary(
+ query: String?,
+ searchFilters: List?,
+ ): Flow> =
+ context.contentResolver
+ .observe(musicUri)
+ .map { getMusicLibrary(query, searchFilters) }
+ .flowOn(Dispatchers.IO)
+
+ override fun getGenres(): List {
+ val projection = arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)
+
+ val query = context.contentResolver.query(musicUri, projection, null, null, null)
+
+ val genres = mutableListOf()
+ query?.use { cursor ->
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
+ val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
+
+ while (cursor.moveToNext()) {
+ val id = getLong(cursor, idColumn) ?: continue
+ val name = getString(cursor, nameColumn) ?: ""
+
+ genres.add(Genre(id = id, name = name))
+ }
+ }
+
+ return genres
+ }
+
+ override fun getTrackIdMapToGenreName(): Map {
+ val trackIdToGenreMap = mutableMapOf()
+
+ getGenres().forEach { genre ->
+ val collection = MediaStore.Audio.Genres.Members.getContentUri("external", genre.id)
+ val projection = arrayOf(MediaStore.Audio.Genres.Members.AUDIO_ID)
+
+ context.contentResolver.query(collection, projection, null, null, null)?.use { cursor ->
+ val audioIdColumn =
+ cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members.AUDIO_ID)
+ while (cursor.moveToNext()) {
+ trackIdToGenreMap[cursor.getLong(audioIdColumn)] = genre.name
+ }
+ }
+ }
+
+ return trackIdToGenreMap
+ }
+
+ override fun getFoldersWithAudio(): Set {
+ val projection = arrayOf(MediaStore.Audio.Media.DATA)
+
+ val selection = "${MediaStore.Audio.Media.DURATION} >= ?"
+
+ // 30 seconds will be the minimum duration for the audios to be considered tracks.
+ // This may be considered a preference in the future.
+ val selectionArgs = arrayOf(TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS).toString())
+
+ val query =
+ context.contentResolver.query(
+ musicUri,
+ projection,
+ selection, // .takeIf { settings.ignoreShortTracks },
+ selectionArgs, // . takeIf { settings.ignoreShortTracks },
+ null,
+ )
+
+ val paths = mutableSetOf()
+ query?.use { cursor ->
+ val dataColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA)
+ if (dataColumn < 0) return emptySet()
+
+ while (cursor.moveToNext()) {
+ val data = cursor.getStringOrNull(dataColumn) ?: continue
+ paths += data.substringBeforeLast('/')
+ }
+ }
+
+ return paths
+ }
+
+ private fun projectionBasedOnVersion(): Array =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ arrayOf(
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.ARTIST,
+ MediaStore.Audio.Media.ALBUM,
+ MediaStore.Audio.Media.ALBUM_ID,
+ MediaStore.Audio.Media.DURATION,
+ MediaStore.Audio.Media.TRACK,
+ MediaStore.Audio.Media.YEAR,
+ MediaStore.Audio.Media.DATA,
+ MediaStore.Audio.Media.SIZE,
+ MediaStore.Audio.Media.DISC_NUMBER,
+ MediaStore.Audio.Media.DATE_ADDED,
+ MediaStore.Audio.Media.DATE_MODIFIED,
+ MediaStore.Audio.Media.GENRE,
+ )
+ } else {
+ arrayOf(
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.ARTIST,
+ MediaStore.Audio.Media.ALBUM,
+ MediaStore.Audio.Media.ALBUM_ID,
+ MediaStore.Audio.Media.DURATION,
+ MediaStore.Audio.Media.TRACK,
+ MediaStore.Audio.Media.YEAR,
+ MediaStore.Audio.Media.DATA,
+ MediaStore.Audio.Media.SIZE,
+ MediaStore.Audio.Media.DISC_NUMBER,
+ MediaStore.Audio.Media.DATE_ADDED,
+ MediaStore.Audio.Media.DATE_MODIFIED,
+ )
+ }
+
+ // TODO: Add support for more filters (like ignore some folders, genres, etc...)
+ private fun buildSelection(
+ searchTerm: String?,
+ filters: List?,
+ ): String {
+ val selection = StringBuilder(MUSIC_SELECTION)
+
+ searchTerm?.let { term ->
+ selection.append(" AND (")
+ if (!filters.isNullOrEmpty()) {
+ val filterSelection = filters.joinToString(" OR ") { "${it.column} LIKE '%$term%'" }
+ selection.append(filterSelection)
+ } else {
+ selection.append(
+ "${MediaStore.Audio.Media.TITLE} LIKE '%$term%' OR " +
+ "${MediaStore.Audio.Media.ARTIST} LIKE '%$term%' OR " +
+ "${MediaStore.Audio.Media.ALBUM} LIKE '%$term%'"
+ )
+ }
+ selection.append(")")
+ }
+ return selection.toString()
+ }
+
+ private fun buildSelectionArgs(
+ searchTerm: String?,
+ filters: List?,
+ ): Array? =
+ searchTerm?.let { term ->
+ Array(if (filters.isNullOrEmpty()) 3 else filters.size) { "%$term%" }
+ }
+
+ private fun parseCursor(cursor: Cursor): List {
+ val musicList = mutableListOf()
+
+ val columnIndices = MusicColumnIndices(cursor)
+
+ while (cursor.moveToNext()) {
+ val id =
+ getLong(cursor, columnIndices.id)
+ ?: continue // throw IllegalStateException("ID cannot be null")
+ val albumId = getLong(cursor, columnIndices.albumId)
+ val path =
+ getString(cursor, columnIndices.path)
+ ?: continue // throw IllegalStateException("Path cannot be null")
+ val title = getString(cursor, columnIndices.title) ?: path.substringAfterLast("/")
+ val artist = getString(cursor, columnIndices.artist)
+ val album = getString(cursor, columnIndices.album)
+ val duration = getLong(cursor, columnIndices.duration)
+ val trackNumber = getInt(cursor, columnIndices.trackNumber)
+ val discNumber = getInt(cursor, columnIndices.discNumber)
+ val year = getInt(cursor, columnIndices.year)
+ val addedTimestamp = getLong(cursor, columnIndices.addedTimestamp)
+ val modifiedTimestamp = getLong(cursor, columnIndices.modifiedTimestamp)
+ val genre = columnIndices.genre?.let { getString(cursor, it) }
+ val size = getLong(cursor, columnIndices.size)
+ val artworkUrl =
+ ContentUris.withAppendedId(
+ "content://media/external/audio/albumart".toUri(),
+ albumId ?: 0,
+ )
+
+ musicList.add(
+ MusicTrack(
+ id = id,
+ title = title,
+ artist = artist,
+ album = album,
+ duration = duration,
+ trackNumber = trackNumber,
+ discNumber = discNumber,
+ year = year,
+ path = path,
+ addedTimestamp = addedTimestamp,
+ modifiedTimestamp = modifiedTimestamp,
+ genre = genre,
+ size = size,
+ artworkUri = artworkUrl.toString(),
+ )
+ )
+ }
+ return musicList.toList() // Convert to immutable list
+ }
+
+ private data class MusicColumnIndices(val cursor: Cursor) {
+ val id = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
+ val title = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
+ val artist = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
+ val album = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
+ val albumId = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
+ val duration = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
+ val trackNumber = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK)
+ val discNumber = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISC_NUMBER)
+ val year = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.YEAR)
+ val path = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
+ val addedTimestamp = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED)
+ val modifiedTimestamp = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_MODIFIED)
+ val genre =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ cursor.getColumnIndex(
+ MediaStore.Audio.Media.GENRE
+ ) // Use getColumnIndex to avoid exception if not present
+ } else null
+ val size = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
+ }
+
+ private fun getString(cursor: Cursor, columnIndex: Int): String? {
+ return try {
+ cursor.getString(columnIndex)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error getting String from cursor at index $columnIndex", e)
+ null
+ }
+ }
+
+ private fun getLong(cursor: Cursor, columnIndex: Int): Long? {
+ return try {
+ cursor.getLong(columnIndex)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error getting Long from cursor at index $columnIndex", e)
+ null
+ }
+ }
+
+ private fun getInt(cursor: Cursor, columnIndex: Int): Int? {
+ return try {
+ cursor.getInt(columnIndex)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error getting Int from cursor at index $columnIndex", e)
+ null
+ }
+ }
+
+ /**
+ * Performs an advanced query on the content resolver.
+ *
+ * @param uri The URI to query.
+ * @param projection The list of columns to put into the cursor.
+ * @param selection A filter declaring which rows to return.
+ * @param args You may include ?s in selection, which will be replaced by the values from
+ * selectionArgs.
+ * @param order How to order the rows, formatted as an SQL ORDER BY clause.
+ * @param ascending Whether the results should be in ascending order.
+ * @param offset The offset of the first row to return.
+ * @param limit The maximum number of rows to return.
+ * @return A Cursor object, which is positioned before the first entry.
+ */
+ @SuppressLint("Recycle")
+ private suspend fun ContentResolver.advancedQuery(
+ uri: Uri,
+ projection: Array? = null,
+ selection: String,
+ args: Array? = null,
+ order: String = MediaStore.MediaColumns._ID,
+ ascending: Boolean = true,
+ offset: Int = 0,
+ limit: Int = Int.MAX_VALUE,
+ ): Cursor? =
+ withContext(Dispatchers.IO) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val queryArgs =
+ Bundle().apply {
+ putInt(ContentResolver.QUERY_ARG_LIMIT, limit)
+ putInt(ContentResolver.QUERY_ARG_OFFSET, offset)
+ putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(order))
+ putInt(
+ ContentResolver.QUERY_ARG_SORT_DIRECTION,
+ if (ascending) ContentResolver.QUERY_SORT_DIRECTION_ASCENDING
+ else ContentResolver.QUERY_SORT_DIRECTION_DESCENDING,
+ )
+ args?.let {
+ putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, it)
+ }
+ putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection)
+ }
+ query(uri, projection, queryArgs, null)
+ } else {
+ val orderClause =
+ "$order ${if (ascending) "ASC" else "DESC"} LIMIT $limit OFFSET $offset"
+ query(uri, projection, selection, args, orderClause)
+ }
+ }
+
+ companion object {
+ private const val MUSIC_SELECTION = "${MediaStore.Audio.Media.IS_MUSIC} != 0"
+ }
+}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/service/PlaybackService.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/service/PlaybackService.kt
new file mode 100644
index 0000000..3c305f5
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/data/service/PlaybackService.kt
@@ -0,0 +1,56 @@
+package com.bobbyesp.mediaplayer.data.service
+
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaLibraryService
+import androidx.media3.session.MediaSession
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import org.koin.core.parameter.parametersOf
+
+class PlaybackService : KoinComponent, MediaLibraryService() {
+
+ // We inject the ExoPlayer instance
+ private val player: ExoPlayer by inject()
+
+ // Callback for MediaLibrarySession
+ private val callback =
+ object : MediaLibrarySession.Callback {
+ // Whatever we need...
+ }
+
+ // MediaLibrarySession has to be lazily created because it needs the callback
+ private var mediaLibrarySession: MediaLibrarySession? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ mediaLibrarySession =
+ getKoin().get {
+ parametersOf(callback)
+ } // We pass the callback to the MediaLibrarySession constructor
+ // player.addListener(
+ // object : Player.Listener {
+ // override fun onPlaybackStateChanged(playbackState: Int) {
+ // if (playbackState == Player.STATE_READY || playbackState ==
+ // Player.STATE_BUFFERING) {
+ // val audioSessionId = player.audioSessionId
+ // if (audioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+ // equalizerController.updateEqualizer(audioSessionId)
+ // }
+ // }
+ // }
+ // }
+ // )
+ }
+
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? =
+ mediaLibrarySession
+
+ override fun onDestroy() {
+ mediaLibrarySession?.run {
+ release()
+ mediaLibrarySession = null
+ }
+ player.release()
+ super.onDestroy()
+ }
+}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/di/MediaPlayerModule.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/di/MediaPlayerModule.kt
index 0ee3eb4..97f0b99 100644
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/di/MediaPlayerModule.kt
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/di/MediaPlayerModule.kt
@@ -1,18 +1,13 @@
-import android.app.PendingIntent
-import android.os.Build
import androidx.annotation.OptIn
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
+import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaLibraryService.MediaLibrarySession
-import com.bobbyesp.mediaplayer.service.ConnectionHandler
-import com.bobbyesp.mediaplayer.service.MediaLibrarySessionCallback
-import com.bobbyesp.mediaplayer.service.MediaServiceHandler
-import com.bobbyesp.mediaplayer.service.notifications.MediaNotificationManager
-import com.bobbyesp.mediaplayer.service.notifications.customLayout.MediaSessionLayoutHandler
-import com.bobbyesp.mediaplayer.service.notifications.customLayout.MediaSessionLayoutHandlerImpl
+import com.bobbyesp.mediaplayer.data.repository.MusicLibraryRepositoryImpl
+import com.bobbyesp.mediaplayer.domain.repository.MusicLibraryRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.Module
import org.koin.dsl.module
@@ -30,52 +25,15 @@ val mediaplayerInternalsModule: Module = module {
ExoPlayer.Builder(androidContext())
.setSeekBackIncrementMs(5000)
.setSeekForwardIncrementMs(5000)
- .setAudioAttributes(get(), true)
.setHandleAudioBecomingNoisy(true)
.setTrackSelector(DefaultTrackSelector(androidContext()))
- .setAudioAttributes(
- get(),
- true
- )
+ .setAudioAttributes(get(), true)
.build()
}
- single {
- MediaServiceHandler(
- player = get()
- )
- }
-
- single { ConnectionHandler() }
-
- single {
- MediaLibrarySession.Builder(
- androidContext(),
- get(),
- MediaLibrarySessionCallback(androidContext())
- ).setSessionActivity(
- PendingIntent.getActivity(
- androidContext(),
- 0,
- androidContext().packageManager.getLaunchIntentForPackage(androidContext().packageName),
- PendingIntent.FLAG_IMMUTABLE
- )
- ).build()
- }
-
- single {
- MediaSessionLayoutHandlerImpl(
- androidContext(),
- get()
- )
+ single { (callback: MediaLibrarySession.Callback) ->
+ MediaLibrarySession.Builder(get(), get(), callback).build()
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- single {
- MediaNotificationManager(
- context = androidContext(),
- player = get()
- )
- }
- }
+ single { MusicLibraryRepositoryImpl(context = androidContext()) }
}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/enums/AudioChannels.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/enums/AudioChannels.kt
new file mode 100644
index 0000000..35c0df6
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/enums/AudioChannels.kt
@@ -0,0 +1,8 @@
+package com.bobbyesp.mediaplayer.domain.enums
+
+enum class AudioChannels(val value: Int) {
+ MONO(1),
+ STEREO(2),
+ SURROUND_5_1(6),
+ SURROUND_7_1(8),
+}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/enums/MediaStoreSearchFilter.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/enums/MediaStoreSearchFilter.kt
new file mode 100644
index 0000000..152c79e
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/enums/MediaStoreSearchFilter.kt
@@ -0,0 +1,10 @@
+package com.bobbyesp.mediaplayer.domain.enums
+
+import android.provider.MediaStore
+
+enum class MediaStoreSearchFilter(val column: String) {
+ TITLE(MediaStore.Audio.Media.TITLE),
+ ARTIST(MediaStore.Audio.Media.ARTIST),
+ YEAR(MediaStore.Audio.Media.YEAR),
+ ALBUM(MediaStore.Audio.Media.ALBUM),
+}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/Genre.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/Genre.kt
new file mode 100644
index 0000000..b083063
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/Genre.kt
@@ -0,0 +1,8 @@
+package com.bobbyesp.mediaplayer.domain.model
+
+import android.os.Parcelable
+import androidx.compose.runtime.Immutable
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+@Immutable @Serializable @Parcelize data class Genre(val id: Long, val name: String) : Parcelable
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/MusicTrack.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/MusicTrack.kt
new file mode 100644
index 0000000..7534e21
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/MusicTrack.kt
@@ -0,0 +1,48 @@
+package com.bobbyesp.mediaplayer.domain.model
+
+import android.os.Parcelable
+import androidx.compose.runtime.Immutable
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class representing a music track.
+ *
+ * @property id Unique ID (can be generated by MediaStore or a hash).
+ * @property title Title of the song.
+ * @property artist Artist of the song.
+ * @property album Album of the song.
+ * @property genre Genre of the song (can be null if not available).
+ * @property duration Duration of the song in milliseconds.
+ * @property trackNumber Track number in the album (if available).
+ * @property discNumber Disc number (for albums with multiple discs).
+ * @property year Year of release.
+ * @property lyrics Lyrics of the song (if available).
+ * @property path Full path of the file in storage.
+ * @property artworkUri URI of the album artwork.
+ * @property addedTimestamp Date when the song was added to the library.
+ * @property modifiedTimestamp Last modification date of the file.
+ * @property size Size of the file in bytes.
+ * @property trackTechnicalDetails Technical details of the track (bitrate, sample rate, channels).
+ */
+@Parcelize
+@Immutable
+@Serializable
+data class MusicTrack(
+ val id: Long,
+ val title: String,
+ val artist: String? = null,
+ val album: String? = null,
+ val genre: String? = null,
+ val duration: Long? = null,
+ val trackNumber: Int? = null,
+ val discNumber: Int? = null,
+ val year: Int? = null,
+ val lyrics: String? = null,
+ val path: String,
+ val artworkUri: String? = null,
+ val addedTimestamp: Long? = null,
+ val modifiedTimestamp: Long? = null,
+ val size: Long? = null,
+ val trackTechnicalDetails: TrackTechnicalDetails? = null,
+) : Parcelable
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/TrackTechnicalDetails.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/TrackTechnicalDetails.kt
new file mode 100644
index 0000000..4049ab5
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/model/TrackTechnicalDetails.kt
@@ -0,0 +1,16 @@
+package com.bobbyesp.mediaplayer.domain.model
+
+import android.os.Parcelable
+import androidx.compose.runtime.Immutable
+import com.bobbyesp.mediaplayer.domain.enums.AudioChannels
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+@Parcelize
+@Immutable
+@Serializable
+data class TrackTechnicalDetails(
+ val bitrate: Int,
+ val sampleRate: Int,
+ val channels: AudioChannels,
+) : Parcelable
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/repository/MediaStoreFolderScanner.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/repository/MediaStoreFolderScanner.kt
new file mode 100644
index 0000000..49e8bc8
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/repository/MediaStoreFolderScanner.kt
@@ -0,0 +1,13 @@
+package com.bobbyesp.mediaplayer.domain.repository
+
+import android.content.Context
+import com.bobbyesp.mediaplayer.domain.model.MusicTrack
+
+interface MediaStoreFolderScanner {
+ val allowedExtensions: Set
+ get() = setOf("mp3", "flac", "wav", "ogg", "m4a", "aac", "opus")
+
+ suspend fun scanCustomFolder(path: String): List
+
+ fun forceMediaStoreFolderScanning(context: Context, path: String)
+}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/repository/MusicLibraryRepository.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/repository/MusicLibraryRepository.kt
new file mode 100644
index 0000000..f20250c
--- /dev/null
+++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/domain/repository/MusicLibraryRepository.kt
@@ -0,0 +1,29 @@
+package com.bobbyesp.mediaplayer.domain.repository
+
+import android.net.Uri
+import android.provider.MediaStore
+import com.bobbyesp.mediaplayer.domain.enums.MediaStoreSearchFilter
+import com.bobbyesp.mediaplayer.domain.model.Genre
+import com.bobbyesp.mediaplayer.domain.model.MusicTrack
+import kotlinx.coroutines.flow.Flow
+
+interface MusicLibraryRepository {
+ val musicUri: Uri
+ get() = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+
+ suspend fun getMusicLibrary(
+ query: String? = null,
+ searchFilters: List? = null,
+ ): List
+
+ fun observeMusicLibrary(
+ query: String?,
+ searchFilters: List?,
+ ): Flow>
+
+ fun getGenres(): List
+
+ fun getTrackIdMapToGenreName(): Map
+
+ fun getFoldersWithAudio(): Set
+}
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/ext/MediaMetadata.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/ext/MediaMetadata.kt
deleted file mode 100644
index a1dbb62..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/ext/MediaMetadata.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.bobbyesp.mediaplayer.ext
-
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaMetadata
-
-fun MediaMetadata.toMediaItem(): MediaItem {
- return MediaItem.Builder()
- .setMediaMetadata(this)
- .build()
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/ConnectionState.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/ConnectionState.kt
deleted file mode 100644
index 5e4faab..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/ConnectionState.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.bobbyesp.mediaplayer.service
-
-import android.util.Log
-import androidx.annotation.OptIn
-import androidx.media3.common.util.UnstableApi
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
-
-@OptIn(UnstableApi::class)
-sealed class ConnectionState {
- data object Disconnected : ConnectionState()
- data class Connected(val serviceHandler: MediaServiceHandler) : ConnectionState()
-}
-
-@OptIn(UnstableApi::class)
-class ConnectionHandler {
- private val _connectionState = MutableStateFlow(ConnectionState.Disconnected)
- val connectionState: StateFlow = _connectionState
- .onEach { newState ->
- Log.d("ConnectionHandler", "Connection state changed: $newState")
- }
- .stateIn(
- CoroutineScope(Dispatchers.Default),
- started = SharingStarted.WhileSubscribed(5000),
- initialValue = ConnectionState.Disconnected
- )
-
- fun connect(serviceHandler: MediaServiceHandler) {
- _connectionState.update {
- ConnectionState.Connected(serviceHandler)
- }
- }
-
- fun disconnect() {
- _connectionState.update {
- ConnectionState.Disconnected
- }
- }
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaLibrarySessionCallback.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaLibrarySessionCallback.kt
deleted file mode 100644
index 1a584a5..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaLibrarySessionCallback.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.bobbyesp.mediaplayer.service
-
-import android.content.Context
-import android.os.Bundle
-import androidx.annotation.OptIn
-import androidx.media3.common.Player.REPEAT_MODE_ALL
-import androidx.media3.common.Player.REPEAT_MODE_OFF
-import androidx.media3.common.Player.REPEAT_MODE_ONE
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.session.MediaLibraryService.MediaLibrarySession
-import androidx.media3.session.MediaSession
-import androidx.media3.session.SessionCommand
-import androidx.media3.session.SessionResult
-import com.bobbyesp.mediaplayer.service.MediaSessionConstants.ACTION_TOGGLE_REPEAT_MODE
-import com.bobbyesp.mediaplayer.service.MediaSessionConstants.ACTION_TOGGLE_SHUFFLE
-import com.bobbyesp.mediaplayer.service.MediaSessionConstants.CommandToggleRepeatMode
-import com.bobbyesp.mediaplayer.service.MediaSessionConstants.CommandToggleShuffle
-import com.google.common.util.concurrent.Futures
-import com.google.common.util.concurrent.ListenableFuture
-import org.koin.core.component.KoinComponent
-
-class MediaLibrarySessionCallback(
- val context: Context,
-) : KoinComponent, MediaLibrarySession.Callback {
-
- private val availableCommands = listOf(
- CommandToggleShuffle,
- CommandToggleRepeatMode
- )
-
- override fun onConnect(
- session: MediaSession, //I've checked the MediaSession class and it is correct. Not the problem of the player
- controller: MediaSession.ControllerInfo
- ): MediaSession.ConnectionResult {
- val connectionResult = super.onConnect(session, controller)
- val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
-
- availableCommands.forEach {
- availableSessionCommands.add(it)
- }
-
- return MediaSession.ConnectionResult.accept(
- availableSessionCommands.build(),
- connectionResult.availablePlayerCommands
- )
- }
-
- @OptIn(UnstableApi::class)
- override fun onCustomCommand(
- session: MediaSession,
- controller: MediaSession.ControllerInfo,
- customCommand: SessionCommand,
- args: Bundle,
- ): ListenableFuture {
- when (customCommand.customAction) {
- ACTION_TOGGLE_SHUFFLE -> session.player.shuffleModeEnabled =
- !session.player.shuffleModeEnabled
-
- ACTION_TOGGLE_REPEAT_MODE -> session.player.repeatMode =
- when (session.player.repeatMode) {
- REPEAT_MODE_OFF -> REPEAT_MODE_ONE
- REPEAT_MODE_ONE -> REPEAT_MODE_ALL
- REPEAT_MODE_ALL -> REPEAT_MODE_OFF
- else -> REPEAT_MODE_OFF
- }
- }
- return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
- }
-
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaServiceHandler.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaServiceHandler.kt
deleted file mode 100644
index 58e3e6f..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaServiceHandler.kt
+++ /dev/null
@@ -1,323 +0,0 @@
-package com.bobbyesp.mediaplayer.service
-
-import androidx.media3.common.MediaItem
-import androidx.media3.common.Player
-import androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY
-import androidx.media3.common.Player.EVENT_TIMELINE_CHANGED
-import androidx.media3.common.Player.REPEAT_MODE_ALL
-import androidx.media3.common.Player.REPEAT_MODE_OFF
-import androidx.media3.common.Player.REPEAT_MODE_ONE
-import androidx.media3.common.Player.STATE_IDLE
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.exoplayer.analytics.AnalyticsListener
-import androidx.media3.exoplayer.analytics.PlaybackStats
-import androidx.media3.exoplayer.analytics.PlaybackStatsListener
-import androidx.media3.exoplayer.source.ShuffleOrder
-import com.bobbyesp.mediaplayer.ext.toMediaItem
-import com.bobbyesp.mediaplayer.service.notifications.customLayout.MediaSessionLayoutHandler
-import com.bobbyesp.mediaplayer.service.queue.EmptyQueue
-import com.bobbyesp.mediaplayer.service.queue.Queue
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.koin.core.component.KoinComponent
-
-/**
- * This class is responsible for handling media playback events and managing the state of the media player.
- * It provides methods to control the media player such as play, pause, stop, and seek.
- * It also provides methods to manage the media queue such as set, add, and remove media items.
- */
-@UnstableApi
-class MediaServiceHandler(
- private val player: ExoPlayer
-) : KoinComponent, Player.Listener, PlaybackStatsListener.Callback {
-
- private val _mediaState = MutableStateFlow(MediaState.Idle)
- val mediaState = _mediaState.asStateFlow()
-
- private var currentQueue: Queue = EmptyQueue
- var queueTitle: String? = null
-
- val currentMediaItem = MutableStateFlow(null)
-
- val isPlaying: MutableStateFlow = MutableStateFlow(false)
- val shuffleModeEnabled = MutableStateFlow(false)
- val repeatMode = MutableStateFlow(REPEAT_MODE_OFF)
- val canSkipNext: Boolean
- get() = player.hasNextMediaItem()
-
- val canSkipPrevious: Boolean = false
-
- private var job: Job? = null
- private val scope = CoroutineScope(Dispatchers.Main)
- private lateinit var mediaSessionInterface: MediaSessionLayoutHandler
-
- fun setMediaSessionInterface(mediaSessionInterface: MediaSessionLayoutHandler) {
- this.mediaSessionInterface = mediaSessionInterface
- }
-
- init {
- player.addListener(this)
- repeatMode.update { player.repeatMode }
- shuffleModeEnabled.update { player.shuffleModeEnabled }
- job = Job()
- }
-
- override fun onIsPlayingChanged(isPlaying: Boolean) {
- mediaSessionInterface.updateNotificationLayout()
- _mediaState.update {
- MediaState.Playing(isPlaying)
- }
- this.isPlaying.update {
- isPlaying
- }
- super.onIsPlayingChanged(isPlaying)
- }
-
- /**
- * Stops the player, clears the media queue, and releases resources.
- */
- fun killPlayer() {
- player.removeListener(this)
- player.stop()
- player.clearMediaItems()
- player.release()
- }
-
- /**
- * Sets a single media item as the current item and prepares the player for playback.
- * @param mediaItem The media item to be set.
- */
- fun setMediaItem(mediaItem: MediaItem) {
- player.setMediaItem(mediaItem)
- player.prepare()
- }
-
- fun playQueue(queue: Queue, playWhenReady: Boolean = true) {
- currentQueue = queue
- queueTitle = null
- if (queue.preloadItem != null) {
- setMediaItem(queue.preloadItem!!.toMediaItem())
- player.playWhenReady = playWhenReady
- }
-
- // Launch a new coroutine in the main thread
- scope.launch {
- // Get the initial state of the queue in the IO thread
- // This is a suspending operation and will not block the main thread
- val initialState = withContext(Dispatchers.IO) { queue.getInitialData() }
-
- // Set the title of the queue
- queueTitle = initialState.title
-
- // Check if the initial state has any items and if the player is not idle or the preload item is null
- // If both conditions are met, proceed with the rest of the code
- if (initialState.items.isNotEmpty() && !(queue.preloadItem != null && player.playbackState == STATE_IDLE)) {
- // If the preload item is not null, add media items to the player
- if (queue.preloadItem != null) {
- // Add media items from the start of the list to the current media item index
- player.addMediaItems(
- 0,
- initialState.items.subList(0, initialState.mediaItemIndex)
- )
- // Add media items from the current media item index to the end of the list
- player.addMediaItems(
- initialState.items.subList(
- initialState.mediaItemIndex + 1,
- initialState.items.size
- )
- )
- } else {
- // If the preload item is null, set media items to the player
- // If the media item index is greater than 0, use it as the start position
- // Otherwise, use 0 as the start position
- player.setMediaItems(
- initialState.items,
- if (initialState.mediaItemIndex > 0) initialState.mediaItemIndex else 0,
- initialState.position
- )
- // Prepare the player for playback
- player.prepare()
- // Set the player to start playback when it's ready
- player.playWhenReady = playWhenReady
- }
- }
- }
- }
-
- /**
- * Seeks to a specific position in the current media item.
- * @param positionMs The position to seek to, in milliseconds.
- */
- fun seekTo(positionMs: Long) {
- player.seekTo(positionMs)
- }
-
- suspend fun onPlayerEvent(playerEvent: PlayerEvent) {
- when (playerEvent) {
- is PlayerEvent.PlayPause -> {
- if (player.isPlaying) {
- player.pause()
- stopProgressUpdate()
- } else {
- player.play()
- startProgressUpdate()
- }
- }
-
- is PlayerEvent.Stop -> {
- killPlayer()
- stopProgressUpdate()
- }
-
- is PlayerEvent.Next -> {
- player.seekToNext()
- }
-
- is PlayerEvent.Previous -> {
- player.seekToPrevious()
- }
-
- is PlayerEvent.UpdateProgress -> {
- player.seekTo(playerEvent.updatedProgress)
- }
-
- PlayerEvent.ToggleRepeat -> {
- val repeatMode = when (player.repeatMode) {
- REPEAT_MODE_OFF -> REPEAT_MODE_ONE
- REPEAT_MODE_ONE -> REPEAT_MODE_ALL
- REPEAT_MODE_ALL -> REPEAT_MODE_OFF
- else -> REPEAT_MODE_OFF
- }
- player.repeatMode = repeatMode
- }
-
- PlayerEvent.ToggleShuffle -> {
- player.shuffleModeEnabled = !player.shuffleModeEnabled
- }
- }
- }
-
- override fun onEvents(player: Player, events: Player.Events) {
- super.onEvents(player, events)
- if (events.containsAny(EVENT_TIMELINE_CHANGED, EVENT_POSITION_DISCONTINUITY)) {
- currentMediaItem.value = player.currentMediaItem
- }
- }
-
- override fun onPlaybackStateChanged(playbackState: Int) {
- when (playbackState) {
- ExoPlayer.STATE_BUFFERING -> _mediaState.update {
- MediaState.Buffering(player.duration)
- }
-
- ExoPlayer.STATE_READY -> _mediaState.update {
- MediaState.Ready(player.duration)
- }
-
- ExoPlayer.STATE_ENDED -> _mediaState.update {
- MediaState.Idle
- }
-
- ExoPlayer.STATE_IDLE -> _mediaState.update {
- MediaState.Idle
- }
- }
- }
-
- override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
- super.onPlayWhenReadyChanged(playWhenReady, reason)
- if (reason == STATE_IDLE) {
- currentQueue = EmptyQueue
- queueTitle = null
- }
- }
-
- /**
- * This method is triggered when the shuffle mode is enabled or disabled in the player.
- * It updates the notification layout and, if shuffle mode is enabled, it shuffles the media items in the player.
- *
- * @param shuffleModeEnabled A boolean indicating whether shuffle mode is enabled.
- */
- override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
- // Update the notification layout
- mediaSessionInterface.updateNotificationLayout()
- this.shuffleModeEnabled.update { shuffleModeEnabled }
-
- // If shuffle mode is enabled
- if (shuffleModeEnabled) {
- // Always put current playing item at first
- // Create an array of indices representing the media items in the player
- val shuffledIndices = IntArray(player.mediaItemCount) { it }
-
- // Shuffle the indices
- shuffledIndices.shuffle()
-
- // Swap the current media item index with the first index
- shuffledIndices[shuffledIndices.indexOf(player.currentMediaItemIndex)] =
- shuffledIndices[0]
- shuffledIndices[0] = player.currentMediaItemIndex
-
- // Set the shuffle order in the player using the shuffled indices
- player.setShuffleOrder(
- ShuffleOrder.DefaultShuffleOrder(
- shuffledIndices,
- System.currentTimeMillis()
- )
- )
- }
- }
-
- override fun onRepeatModeChanged(repeatMode: Int) {
- mediaSessionInterface.updateNotificationLayout()
- this.repeatMode.update { repeatMode }
- }
-
- private suspend fun startProgressUpdate() = job.run {
- while (true) {
- delay(250)
- _mediaState.update {
- MediaState.Progress(player.currentPosition)
- }
- }
- }
-
- private fun stopProgressUpdate() {
- job?.cancel()
- _mediaState.update {
- MediaState.Playing(false)
- }
- }
-
- override fun onPlaybackStatsReady(
- eventTime: AnalyticsListener.EventTime,
- playbackStats: PlaybackStats
- ) {
- TODO("Not yet implemented")
- }
-}
-
-sealed class PlayerEvent {
- data object PlayPause : PlayerEvent()
- data object Stop : PlayerEvent()
- data object Next : PlayerEvent()
- data object Previous : PlayerEvent()
- data object ToggleShuffle : PlayerEvent()
- data object ToggleRepeat : PlayerEvent()
- data class UpdateProgress(val updatedProgress: Long) : PlayerEvent()
-}
-
-sealed class MediaState { //TODO: NOT USE SEALED CLASSES
- data object Idle : MediaState()
- data class Ready(val duration: Long) : MediaState()
- data class Progress(val progress: Long) : MediaState()
- data class Buffering(val progress: Long) : MediaState()
- data class Playing(val isPlaying: Boolean) : MediaState()
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaSessionConstants.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaSessionConstants.kt
deleted file mode 100644
index 27c63d4..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaSessionConstants.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.bobbyesp.mediaplayer.service
-
-import android.os.Bundle
-import androidx.media3.session.SessionCommand
-
-object MediaSessionConstants {
- const val ACTION_TOGGLE_LIBRARY = "TOGGLE_LIBRARY"
- const val ACTION_TOGGLE_LIKE = "TOGGLE_LIKE"
- const val ACTION_TOGGLE_SHUFFLE = "TOGGLE_SHUFFLE"
- const val ACTION_TOGGLE_REPEAT_MODE = "TOGGLE_REPEAT_MODE"
- val CommandToggleLibrary = SessionCommand(ACTION_TOGGLE_LIBRARY, Bundle.EMPTY)
- val CommandToggleLike = SessionCommand(ACTION_TOGGLE_LIKE, Bundle.EMPTY)
- val CommandToggleShuffle = SessionCommand(ACTION_TOGGLE_SHUFFLE, Bundle.EMPTY)
- val CommandToggleRepeatMode = SessionCommand(ACTION_TOGGLE_REPEAT_MODE, Bundle.EMPTY)
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt
deleted file mode 100644
index c78160b..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.bobbyesp.mediaplayer.service
-
-import android.content.Intent
-import android.os.Binder
-import androidx.media3.common.Player
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.session.MediaController
-import androidx.media3.session.MediaLibraryService
-import androidx.media3.session.MediaSession
-import com.bobbyesp.mediaplayer.service.notifications.MediaNotificationManager
-import com.bobbyesp.mediaplayer.service.notifications.customLayout.MediaSessionLayoutHandler
-import com.google.common.util.concurrent.MoreExecutors
-import org.koin.android.ext.android.inject
-
-@UnstableApi
-class MediaplayerService : MediaLibraryService(), MediaController.Listener {
-
- val mediaSession: MediaLibrarySession by inject()
- val mediaServiceHandler: MediaServiceHandler by inject()
- val notificationManager: MediaNotificationManager by inject()
- val mediaSessionLayoutHandler: MediaSessionLayoutHandler by inject()
- val connectionHandler: ConnectionHandler by inject()
-
- /**
- * This method is called by the system every time a client explicitly starts the service by calling
- * [android.content.Context.startService], providing the arguments it supplied and a unique integer
- * token representing the start request.
- */
- @UnstableApi
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- // Connects the media service handler to the connection handler
- connectionHandler.connect(mediaServiceHandler)
-
- // Starts the notification service with the current media session service and media session
- notificationManager.startNotificationService(
- mediaSessionService = this, mediaSession = mediaSession
- )
-
- // Sets the media session interface for the media service handler
- mediaServiceHandler.setMediaSessionInterface(mediaSessionLayoutHandler)
-
- // Builds a media controller asynchronously with the current media session token
- val controllerFuture = MediaController.Builder(this, mediaSession.token).buildAsync()
-
- // Adds a listener to the controller future that gets the result of the future when it's ready
- controllerFuture.addListener({ controllerFuture.get() }, MoreExecutors.directExecutor())
-
- // Calls the super implementation of onStartCommand
- return super.onStartCommand(intent, flags, startId)
- }
-
- override fun onDestroy() {
- connectionHandler.disconnect()
- mediaSession.run {
- release()
- clearListener()
- if (player.playbackState != Player.STATE_IDLE) {
- player.seekTo(0)
- player.clearMediaItems()
- player.playWhenReady = false
- player.stop()
- }
- }
- super.onDestroy()
- }
-
- override fun onTaskRemoved(rootIntent: Intent?) {
- super.onTaskRemoved(rootIntent)
- if (!mediaSession.player.playWhenReady || mediaSession.player.mediaItemCount == 0) {
- stopSelf()
- }
- }
-
- override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession =
- mediaSession
-
- inner class MusicBinder : Binder() {
- val service: MediaplayerService
- get() = this@MediaplayerService
- }
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaCustomActionReceiver.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaCustomActionReceiver.kt
deleted file mode 100644
index bcddc9e..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaCustomActionReceiver.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.bobbyesp.mediaplayer.service.notifications
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import androidx.core.app.NotificationCompat
-import androidx.media3.common.Player
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.ui.PlayerNotificationManager.CustomActionReceiver
-import com.bobbyesp.mediaplayer.R
-
-@UnstableApi
-class MediaCustomActionReceiver : CustomActionReceiver {
- companion object {
- const val CUSTOM_ACTION_SHUFFLE =
- "com.bobbyesp.mediaplayer.service.notifications.ACTION_SHUFFLE"
- const val CUSTOM_ACTION_REPEAT =
- "com.bobbyesp.mediaplayer.service.notifications.ACTION_REPEAT"
- }
-
- override fun createCustomActions(
- context: Context,
- instanceId: Int
- ): MutableMap {
- val shuffleAction = NotificationCompat.Action.Builder(
- R.drawable.shuffle,
- context.getString(R.string.action_shuffle_on),
- PendingIntent.getBroadcast(
- context,
- instanceId,
- Intent(CUSTOM_ACTION_SHUFFLE).setPackage(context.packageName),
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
- ).build()
-
- val repeatAction = NotificationCompat.Action.Builder(
- R.drawable.repeat,
- context.getString(R.string.repeat_mode_all),
- PendingIntent.getBroadcast(
- context,
- instanceId,
- Intent(CUSTOM_ACTION_REPEAT).setPackage(context.packageName),
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
- ).build()
-
- return mutableMapOf(
- CUSTOM_ACTION_SHUFFLE to shuffleAction,
- CUSTOM_ACTION_REPEAT to repeatAction
- )
- }
-
- override fun getCustomActions(player: Player): MutableList {
- return mutableListOf(CUSTOM_ACTION_SHUFFLE, CUSTOM_ACTION_REPEAT)
- }
-
- override fun onCustomAction(player: Player, action: String, intent: Intent) {
- when (action) {
- CUSTOM_ACTION_SHUFFLE -> player.shuffleModeEnabled = !player.shuffleModeEnabled
- CUSTOM_ACTION_REPEAT -> {
- when (player.repeatMode) {
- Player.REPEAT_MODE_OFF -> player.repeatMode = Player.REPEAT_MODE_ALL
- Player.REPEAT_MODE_ALL -> player.repeatMode = Player.REPEAT_MODE_ONE
- Player.REPEAT_MODE_ONE -> player.repeatMode = Player.REPEAT_MODE_OFF
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaNotificationAdapter.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaNotificationAdapter.kt
deleted file mode 100644
index 5d6a831..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaNotificationAdapter.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.bobbyesp.mediaplayer.service.notifications
-
-import android.app.PendingIntent
-import android.content.Context
-import android.graphics.Bitmap
-import androidx.core.graphics.drawable.toBitmap
-import androidx.media3.common.Player
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.ui.PlayerNotificationManager
-import coil.ImageLoader
-
-@UnstableApi
-class MediaNotificationAdapter(
- private val context: Context,
- private val pendingIntent: PendingIntent?
-) : PlayerNotificationManager.MediaDescriptionAdapter {
-
- override fun getCurrentContentTitle(player: Player): CharSequence {
- return player.mediaMetadata.albumTitle
- ?: context.getString(androidx.media3.ui.R.string.exo_track_unknown)
- }
-
- override fun createCurrentContentIntent(player: Player): PendingIntent? {
- return pendingIntent
- }
-
- override fun getCurrentContentText(player: Player): CharSequence {
- return player.mediaMetadata.displayTitle
- ?: context.getString(androidx.media3.ui.R.string.exo_track_unknown)
- }
-
- override fun getCurrentLargeIcon(
- player: Player,
- callback: PlayerNotificationManager.BitmapCallback
- ): Bitmap? {
- val loader = ImageLoader(context)
- val request = coil.request.ImageRequest.Builder(context)
- .data(player.mediaMetadata.artworkData)
- .target { drawable ->
- callback.onBitmap(drawable.toBitmap())
- }
- .build()
- loader.enqueue(request)
-
- return null
- }
-
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaNotificationManager.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaNotificationManager.kt
deleted file mode 100644
index d581655..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/MediaNotificationManager.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package com.bobbyesp.mediaplayer.service.notifications
-
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.content.Context
-import android.os.Build
-import android.util.Log
-import androidx.annotation.OptIn
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.common.util.Util
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.session.MediaSession
-import androidx.media3.session.MediaSessionService
-import androidx.media3.ui.PlayerNotificationManager
-import com.bobbyesp.mediaplayer.R
-import org.koin.core.component.KoinComponent
-
-@UnstableApi
-class MediaNotificationManager @OptIn(UnstableApi::class) constructor(
- private val context: Context,
- private val player: ExoPlayer
-) : KoinComponent {
- private val notificationManager: NotificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- val notificationManagerCompat = NotificationManagerCompat.from(context)
-
- init {
- ensureNotificationChannel(notificationManagerCompat)
- }
-
- val notificationListener = object : PlayerNotificationManager.NotificationListener {
- override fun onNotificationCancelled(
- notificationId: Int,
- dismissedByUser: Boolean
- ) {
- Log.d(
- "MediaNotificationManager",
- "onNotificationCancelled: notification cancelled"
- )
- }
-
- override fun onNotificationPosted(
- notificationId: Int,
- notification: Notification,
- ongoing: Boolean
- ) {
- Log.d(
- "MediaNotificationManager",
- "onNotificationPosted: notification posted"
- )
- }
- }
-
- @UnstableApi
- fun startNotificationService(
- mediaSessionService: MediaSessionService,
- mediaSession: MediaSession
- ) {
- buildNotification(mediaSession)
- startForegroundNotification(mediaSessionService)
- }
-
- @UnstableApi
- private fun buildNotification(mediaSession: MediaSession) {
- PlayerNotificationManager.Builder(context, NOTIFICATION_ID, NOTIFICATION_CHANNEL_ID)
- .setMediaDescriptionAdapter(
- MediaNotificationAdapter(
- context = context,
- pendingIntent = mediaSession.sessionActivity
- )
- )
- .setCustomActionReceiver(MediaCustomActionReceiver())
- .setSmallIconResourceId(R.drawable.metadator_logo_player)
- .setNotificationListener(notificationListener)
- .build()
- .also {
- with(it) {
- setMediaSessionToken(mediaSession.sessionCompatToken)
- setPriority(NotificationCompat.PRIORITY_LOW)
- setPlayer(mediaSession.player)
- }
- }
- }
-
- private fun startForegroundNotification(mediaSessionService: MediaSessionService) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- Log.d(
- "MediaNotificationManager",
- "startForegroundNotification: creating notification for API >= 26"
- )
- val notification = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
- .setCategory(Notification.CATEGORY_SERVICE)
- .build()
-
- mediaSessionService.startForeground(NOTIFICATION_ID, notification)
- } else {
- Log.d(
- "MediaNotificationManager",
- "startForegroundNotification: creating notification for API < 26"
- )
- val notification = NotificationCompat.Builder(context)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .build()
-
- mediaSessionService.startForeground(NOTIFICATION_ID, notification)
- }
- }
-
- @OptIn(UnstableApi::class)
- private fun ensureNotificationChannel(notificationManagerCompat: NotificationManagerCompat) {
- if (Util.SDK_INT < 26 || notificationManagerCompat.getNotificationChannel(
- NOTIFICATION_CHANNEL_ID
- ) != null
- ) {
- return
- }
-
- val channel = NotificationChannel(
- NOTIFICATION_CHANNEL_ID,
- NOTIFICATION_CHANNEL_NAME,
- NotificationManager.IMPORTANCE_DEFAULT
- )
- notificationManagerCompat.createNotificationChannel(channel)
- }
-
-
- companion object {
- private const val NOTIFICATION_ID = 200
- private const val NOTIFICATION_CHANNEL_NAME = "notification_channel_1"
- private const val NOTIFICATION_CHANNEL_ID = "notification_channel_id_1"
- }
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/customLayout/MediaSessionLayoutHandler.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/customLayout/MediaSessionLayoutHandler.kt
deleted file mode 100644
index ca4c381..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/customLayout/MediaSessionLayoutHandler.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.bobbyesp.mediaplayer.service.notifications.customLayout
-
-interface MediaSessionLayoutHandler {
- fun updateNotificationLayout()
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/customLayout/MediaSessionLayoutHandlerImpl.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/customLayout/MediaSessionLayoutHandlerImpl.kt
deleted file mode 100644
index 1833883..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/notifications/customLayout/MediaSessionLayoutHandlerImpl.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.bobbyesp.mediaplayer.service.notifications.customLayout
-
-import android.content.Context
-import androidx.core.content.ContextCompat.getString
-import androidx.media3.common.Player.REPEAT_MODE_ALL
-import androidx.media3.common.Player.REPEAT_MODE_OFF
-import androidx.media3.common.Player.REPEAT_MODE_ONE
-import androidx.media3.session.CommandButton
-import androidx.media3.session.MediaLibraryService
-import com.bobbyesp.mediaplayer.R
-import com.bobbyesp.mediaplayer.service.MediaSessionConstants.CommandToggleRepeatMode
-import com.bobbyesp.mediaplayer.service.MediaSessionConstants.CommandToggleShuffle
-import com.google.common.collect.ImmutableList
-import org.koin.core.component.KoinComponent
-
-class MediaSessionLayoutHandlerImpl(
- private val context: Context,
- private val mediaSession: MediaLibraryService.MediaLibrarySession,
-) : KoinComponent, MediaSessionLayoutHandler {
-
- override fun updateNotificationLayout() {
- val commandButtons = ImmutableList.of(
- CommandButton.Builder()
- .setDisplayName(
- getString(
- context,
- if (mediaSession.player.shuffleModeEnabled) R.string.action_shuffle_on else R.string.action_shuffle_off
- )
- )
- .setIconResId(if (mediaSession.player.shuffleModeEnabled) R.drawable.shuffle_on else R.drawable.shuffle)
- .setSessionCommand(CommandToggleShuffle).build(),
- CommandButton.Builder()
- .setDisplayName(
- getString(
- context,
- when (mediaSession.player.repeatMode) {
- REPEAT_MODE_OFF -> R.string.repeat_mode_off
- REPEAT_MODE_ONE -> R.string.repeat_mode_one
- REPEAT_MODE_ALL -> R.string.repeat_mode_all
- else -> throw IllegalStateException()
- }
- )
- ).setIconResId(
- when (mediaSession.player.repeatMode) {
- REPEAT_MODE_OFF -> R.drawable.repeat
- REPEAT_MODE_ONE -> R.drawable.repeat_one_on
- REPEAT_MODE_ALL -> R.drawable.repeat_on
- else -> throw IllegalStateException()
- }
- ).setSessionCommand(CommandToggleRepeatMode).build()
- )
-
- mediaSession.setCustomLayout(commandButtons)
- }
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt
deleted file mode 100644
index cdf97db..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/EmptyQueue.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.bobbyesp.mediaplayer.service.queue
-
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaMetadata
-
-object EmptyQueue : Queue {
- override val preloadItem: MediaMetadata?
- get() = null
-
- override suspend fun getInitialData(): Queue.Data = Queue.Data.empty()
- override fun hasNextPage(): Boolean = false
- override suspend fun nextPage(): List = emptyList()
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt
deleted file mode 100644
index 138a47d..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/Queue.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.bobbyesp.mediaplayer.service.queue
-
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaMetadata
-
-interface Queue {
- val preloadItem: MediaMetadata?
- suspend fun getInitialData(): Data
- fun hasNextPage(): Boolean
- suspend fun nextPage(): List
-
- data class Data(
- val title: String?,
- val items: List,
- val mediaItemIndex: Int,
- val position: Long = 0L,
- ) {
- companion object {
- fun empty() = Data(null, emptyList(), -1, 0L)
- }
- }
-}
\ No newline at end of file
diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt
deleted file mode 100644
index 2a5894f..0000000
--- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/queue/SongsQueue.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.bobbyesp.mediaplayer.service.queue
-
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaMetadata
-
-data class SongsQueue(
- val title: String? = null,
- val items: List,
- val startIndex: Int = 0,
- val position: Long = 0L,
-) : Queue {
- override val preloadItem: MediaMetadata? = null
- override suspend fun getInitialData(): Queue.Data =
- Queue.Data(title, items, startIndex, position)
-
- override fun hasNextPage(): Boolean = false
- override suspend fun nextPage() = throw UnsupportedOperationException()
-}
diff --git a/app/mediaplayer/src/test/java/com/bobbyesp/mediaplayer/ExampleUnitTest.kt b/app/mediaplayer/src/test/java/com/bobbyesp/mediaplayer/ExampleUnitTest.kt
index dd5f3c5..f566943 100644
--- a/app/mediaplayer/src/test/java/com/bobbyesp/mediaplayer/ExampleUnitTest.kt
+++ b/app/mediaplayer/src/test/java/com/bobbyesp/mediaplayer/ExampleUnitTest.kt
@@ -13,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/com/bobbyesp/metadator/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/bobbyesp/metadator/ExampleInstrumentedTest.kt
index f40b1f2..5f3cca6 100644
--- a/app/src/androidTest/java/com/bobbyesp/metadator/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/bobbyesp/metadator/ExampleInstrumentedTest.kt
@@ -19,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.bobbyesp.metadator", appContext.packageName)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/foss/kotlin/FirebaseSetup.kt b/app/src/foss/kotlin/FirebaseSetup.kt
index bd4e66b..dc4c3ca 100644
--- a/app/src/foss/kotlin/FirebaseSetup.kt
+++ b/app/src/foss/kotlin/FirebaseSetup.kt
@@ -1,14 +1,11 @@
import com.bobbyesp.metadator.App
import com.bobbyesp.metadator.MainActivity
-/**
- * Initialize Firebase services.
- * EMPTY Because this is part of the FOSS flavour of the app.
- */
+/** Initialize Firebase services. EMPTY Because this is part of the FOSS flavour of the app. */
fun App.initializeFirebase() {}
/**
- * Setup Crashlytics collection to the app.
- * EMPTY Because this is part of the FOSS flavour of the app.
+ * Setup Crashlytics collection to the app. EMPTY Because this is part of the FOSS flavour of the
+ * app.
*/
-fun MainActivity.setCrashlyticsCollection() {}
\ No newline at end of file
+fun MainActivity.setCrashlyticsCollection() {}
diff --git a/app/src/main/java/com/bobbyesp/metadator/App.kt b/app/src/main/java/com/bobbyesp/metadator/App.kt
index 17d0b7f..043d917 100644
--- a/app/src/main/java/com/bobbyesp/metadator/App.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/App.kt
@@ -9,16 +9,17 @@ import com.bobbyesp.crashhandler.ReportInfo
import com.bobbyesp.metadator.core.di.appCoroutinesScope
import com.bobbyesp.metadator.core.di.appSystemManagers
import com.bobbyesp.metadator.core.di.coreFunctionalitiesModule
-import com.bobbyesp.metadator.mediastore.di.mediaStoreViewModelsModule
-import com.bobbyesp.metadator.mediaplayer.di.mediaplayerViewModels
-import com.bobbyesp.metadator.tageditor.di.tagEditorViewModelsModule
import com.bobbyesp.metadator.features.spotify.di.spotifyMainModule
import com.bobbyesp.metadator.features.spotify.di.spotifyServicesModule
+import com.bobbyesp.metadator.mediaplayer.di.mediaplayerViewModels
+import com.bobbyesp.metadator.mediastore.di.mediaStoreViewModelsModule
+import com.bobbyesp.metadator.tageditor.di.tagEditorModule
+import com.bobbyesp.metadator.tageditor.di.tagEditorViewModelsModule
+import kotlin.properties.Delegates
import mediaplayerInternalsModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext.startKoin
-import kotlin.properties.Delegates
class App : Application() {
override fun onCreate() {
@@ -28,34 +29,38 @@ class App : Application() {
modules(appSystemManagers, appCoroutinesScope, coreFunctionalitiesModule)
modules(mediaplayerInternalsModule)
modules(mediaStoreViewModelsModule, tagEditorViewModelsModule, mediaplayerViewModels)
+ modules(tagEditorModule)
modules(spotifyMainModule, spotifyServicesModule)
}
- packageInfo = packageManager.run {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) getPackageInfo(
- packageName, PackageManager.PackageInfoFlags.of(0)
- ) else
- getPackageInfo(packageName, 0)
- }
- isPlayStoreBuild = BuildConfig.FLAVOR == "playstore"
+ packageInfo =
+ packageManager.run {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
+ else getPackageInfo(packageName, 0)
+ }
+ isPlayStoreBuild = (BuildConfig.FLAVOR == "playstore")
super.onCreate()
- if (!isPlayStoreBuild) setupCrashHandler(
- reportInfo = ReportInfo(
- androidVersion = true,
- deviceInfo = true,
- supportedABIs = true
+ if (!isPlayStoreBuild)
+ setupCrashHandler(
+ reportInfo =
+ ReportInfo(androidVersion = true, deviceInfo = true, supportedABIs = true),
+ reportUrl = CRASH_REPORT_URL,
)
- )
}
companion object {
lateinit var packageInfo: PackageInfo
var isPlayStoreBuild by Delegates.notNull()
- val appVersion: String get() = packageInfo.versionName.toString()
+ val appVersion: String
+ get() = packageInfo.versionName.toString()
const val APP_PACKAGE_NAME = "com.bobbyesp.metadator"
const val PREFERENCES_NAME = "${APP_PACKAGE_NAME}_preferences"
const val APP_FILE_PROVIDER = "$APP_PACKAGE_NAME.fileprovider"
+
+ const val CRASH_REPORT_URL =
+ "https://github.com/BobbyESP/Metadator/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=%5BApp%20crash%5D"
}
}
diff --git a/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt b/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt
index c272aa0..e50c474 100644
--- a/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt
@@ -1,23 +1,17 @@
package com.bobbyesp.metadator
-import android.content.Intent
import android.os.Bundle
-import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import coil.ImageLoader
-import com.bobbyesp.mediaplayer.service.ConnectionHandler
-import com.bobbyesp.mediaplayer.service.MediaplayerService
import com.bobbyesp.metadator.core.data.local.preferences.AppPreferences
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.COMPLETED_ONBOARDING
import com.bobbyesp.metadator.core.data.local.preferences.UserPreferences.Companion.emptyUserPreferences
@@ -25,7 +19,6 @@ import com.bobbyesp.metadator.core.presentation.common.AppLocalSettingsProvider
import com.bobbyesp.metadator.core.presentation.common.LocalNavController
import com.bobbyesp.metadator.core.presentation.common.Route
import com.bobbyesp.metadator.core.presentation.theme.MetadatorTheme
-import com.bobbyesp.metadator.mediaplayer.data.local.MediaplayerServiceConnection
import com.dokar.sonner.Toaster
import com.dokar.sonner.rememberToasterState
import kotlinx.coroutines.Dispatchers
@@ -38,10 +31,8 @@ import setCrashlyticsCollection
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class MainActivity : ComponentActivity(), KoinComponent {
- private var isMusicPlayerServiceStarted = false
private var startDestination: Route? = null
- private val connectionHandler: ConnectionHandler by inject()
private val appPreferences: AppPreferences by inject()
private val imageLoader: ImageLoader by inject()
@@ -50,18 +41,18 @@ class MainActivity : ComponentActivity(), KoinComponent {
val splashscreen = installSplashScreen()
lifecycleScope.launch(Dispatchers.IO) {
- val completedOnboarding =
- async { appPreferences.getSetting(COMPLETED_ONBOARDING, false) }
- startDestination = if (completedOnboarding.await()) {
- Route.MetadatorNavigator
- } else {
- Route.OnboardingNavigator
+ val completedOnboarding = async {
+ appPreferences.getSetting(COMPLETED_ONBOARDING, false)
}
+ startDestination =
+ if (completedOnboarding.await()) {
+ Route.MetadatorNavigator
+ } else {
+ Route.OnboardingNavigator
+ }
}
- splashscreen.setKeepOnScreenCondition {
- startDestination == null
- }
+ splashscreen.setKeepOnScreenCondition { startDestination == null }
enableEdgeToEdge()
super.onCreate(savedInstanceState)
@@ -75,30 +66,30 @@ class MainActivity : ComponentActivity(), KoinComponent {
emptyUserPreferences()
)
- CompositionLocalProvider(
- LocalNavController provides navController,
- ) {
+ CompositionLocalProvider(LocalNavController provides navController) {
KoinContext {
val windowSizeClass = calculateWindowSizeClass(this)
AppLocalSettingsProvider(
windowWidthSize = windowSizeClass.widthSizeClass,
- playerConnectionHandler = connectionHandler,
sonner = sonner,
appPreferences = appPreferences,
- imageLoader = imageLoader
+ imageLoader = imageLoader,
) {
MetadatorTheme {
Navigator(
navController = navController,
- startDestination = startDestination
- ?: throw IllegalStateException("Start destination couldn't be determinate"),
- preferences = userPreferences
+ startDestination =
+ startDestination
+ ?: throw IllegalStateException(
+ "Start destination couldn't be determinate"
+ ),
+ preferences = userPreferences,
)
Toaster(
state = sonner,
richColors = true,
- darkTheme = userPreferences.value.darkThemePreference.isDarkTheme()
+ darkTheme = userPreferences.value.darkThemePreference.isDarkTheme(),
)
}
}
@@ -106,26 +97,4 @@ class MainActivity : ComponentActivity(), KoinComponent {
}
}
}
-
- override fun onStart() {
- super.onStart()
- startMediaPlayerService()
- }
-
- override fun onStop() {
- super.onStop()
- unbindService(serviceConnection)
- isMusicPlayerServiceStarted = false
- }
-
- private fun startMediaPlayerService() {
- val intent = Intent(this, MediaplayerService::class.java)
- if (!isMusicPlayerServiceStarted) {
- isMusicPlayerServiceStarted = true
- startService(intent)
- bindService(intent, serviceConnection, BIND_AUTO_CREATE)
- }
- }
-
- private var serviceConnection = MediaplayerServiceConnection(connectionHandler)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/Navigation.kt b/app/src/main/java/com/bobbyesp/metadator/Navigation.kt
index fef6245..7c778b6 100644
--- a/app/src/main/java/com/bobbyesp/metadator/Navigation.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/Navigation.kt
@@ -2,7 +2,6 @@ package com.bobbyesp.metadator
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -19,17 +18,13 @@ import com.bobbyesp.metadator.core.presentation.settingsRouting
import com.bobbyesp.metadator.core.util.cleanNavigate
import com.bobbyesp.metadator.core.util.navigateBack
import com.bobbyesp.metadator.mediaplayer.mediaplayerRouting
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel
import com.bobbyesp.metadator.mediastore.presentation.MediaStorePageViewModel
import com.bobbyesp.metadator.mediastore.presentation.pages.home.HomePage
import com.bobbyesp.metadator.onboarding.onboardingRouting
import com.bobbyesp.metadator.tageditor.tagEditorRouting
import com.bobbyesp.ui.motion.animatedComposable
-import com.bobbyesp.ui.util.recomposeHighlighter
import org.koin.androidx.compose.koinViewModel
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-
@Composable
fun Navigator(
navController: NavHostController,
@@ -37,14 +32,11 @@ fun Navigator(
preferences: State,
) {
val mediaStoreViewModel = koinViewModel()
- val mediaplayerViewModel = koinViewModel()
val (_, setOnboardingCompleted) = rememberPreferenceState(COMPLETED_ONBOARDING)
NavHost(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.background)
- .fillMaxSize(),
+ modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxSize(),
navController = navController,
startDestination = startDestination,
) {
@@ -53,32 +45,26 @@ fun Navigator(
onCompletedOnboarding = {
setOnboardingCompleted(true)
navController.cleanNavigate(Route.MetadatorNavigator)
- }
+ },
)
- navigation(
- startDestination = Route.MetadatorNavigator.Home,
- ) {
+ navigation(startDestination = Route.MetadatorNavigator.Home) {
animatedComposable {
- val songsState =
- mediaStoreViewModel.songs.collectAsStateWithLifecycle()
+ val songsState = mediaStoreViewModel.songs.collectAsStateWithLifecycle()
HomePage(
songs = songsState,
preferences = preferences,
- onEvent = mediaStoreViewModel::onEvent
+ onEvent = mediaStoreViewModel::onEvent,
)
}
}
mediaplayerRouting(
- mediaplayerViewModel = mediaplayerViewModel,
- onNavigateBack = {
- navController.navigateBack()
- }
+ // mediaplayerViewModel = mediaplayerViewModel,
+ onNavigateBack = { navController.navigateBack() }
)
tagEditorRouting { navController.navigateBack() }
settingsRouting { navController.navigateBack() }
}
}
-
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/data/local/DarkThemePreference.kt b/app/src/main/java/com/bobbyesp/metadator/core/data/local/DarkThemePreference.kt
index 71e2ae9..74715f2 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/data/local/DarkThemePreference.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/data/local/DarkThemePreference.kt
@@ -11,20 +11,19 @@ import com.bobbyesp.utilities.R
@Stable
data class DarkThemePreference(
val darkThemeValue: DarkThemeValue = DarkThemeValue.valueOf(DARK_THEME_VALUE.defaultValue),
- val isHighContrastModeEnabled: Boolean = HIGH_CONTRAST.defaultValue
+ val isHighContrastModeEnabled: Boolean = HIGH_CONTRAST.defaultValue,
) {
companion object {
enum class DarkThemeValue {
FOLLOW_SYSTEM,
ON,
- OFF
+ OFF,
}
}
@Composable
fun isDarkTheme(): Boolean {
- return if (darkThemeValue == DarkThemeValue.FOLLOW_SYSTEM)
- isSystemInDarkTheme()
+ return if (darkThemeValue == DarkThemeValue.FOLLOW_SYSTEM) isSystemInDarkTheme()
else darkThemeValue == DarkThemeValue.ON
}
@@ -36,4 +35,4 @@ data class DarkThemePreference(
else -> stringResource(R.string.off)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferences.kt b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferences.kt
index b84cfcd..86720e2 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferences.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferences.kt
@@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
+import com.bobbyesp.coreutilities.theming.isDynamicColoringSupported
import com.bobbyesp.metadator.core.data.local.DarkThemePreference
import com.bobbyesp.metadator.core.data.local.DarkThemePreference.Companion.DarkThemeValue
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.DARK_THEME_VALUE
@@ -16,9 +17,8 @@ import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.SONGS_L
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.SONG_CARD_SIZE
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.THEME_COLOR
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.USE_DYNAMIC_COLORING
-import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType
import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize
-import com.bobbyesp.metadator.core.presentation.theme.isDynamicColoringSupported
+import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType
import com.materialkolor.PaletteStyle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -30,22 +30,23 @@ import kotlinx.io.IOException
class AppPreferences(
private val dataStore: DataStore,
- scope: CoroutineScope //May be used in the future
+ scope: CoroutineScope, // May be used in the future
) : AppPreferencesController {
override val userPreferencesFlow: Flow
- get() = dataStore.data
- .catch { exception ->
- // dataStore.data throws an IOException when an error is encountered when reading data
- if (exception is IOException) {
- Log.e(TAG, "Error reading preferences.", exception)
- emit(emptyPreferences())
- } else {
- throw exception
+ get() =
+ dataStore.data
+ .catch { exception ->
+ // dataStore.data throws an IOException when an error is encountered when
+ // reading data
+ if (exception is IOException) {
+ Log.e(TAG, "Error reading preferences.", exception)
+ emit(emptyPreferences())
+ } else {
+ throw exception
+ }
}
- }.map { preferences ->
- mapUserPreferences(preferences)
- }
+ .map { preferences -> mapUserPreferences(preferences) }
override suspend fun getUserPreferences(): UserPreferences {
val preferences = dataStore.data.firstOrNull()
@@ -106,9 +107,7 @@ class AppPreferences(
saveSetting(PALETTE_STYLE, paletteStyle.name)
}
- private fun mapUserPreferences(
- preferences: Preferences
- ): UserPreferences {
+ private fun mapUserPreferences(preferences: Preferences): UserPreferences {
val desiredLayout: LayoutType =
LayoutType.valueOf(preferences[SONGS_LAYOUT.key] ?: SONGS_LAYOUT.defaultValue)
val reduceShadows: Boolean = preferences[REDUCE_SHADOWS.key] ?: REDUCE_SHADOWS.defaultValue
@@ -132,14 +131,15 @@ class AppPreferences(
darkThemePreference,
useDynamicColoring,
themeColor,
- paletteStyle
+ paletteStyle,
)
}
private fun mapDarkThemePreferences(preferences: Preferences): DarkThemePreference {
- val currentDarkThemeValue: DarkThemeValue = DarkThemeValue.valueOf(
- preferences[DARK_THEME_VALUE.key] ?: DARK_THEME_VALUE.defaultValue
- )
+ val currentDarkThemeValue: DarkThemeValue =
+ DarkThemeValue.valueOf(
+ preferences[DARK_THEME_VALUE.key] ?: DARK_THEME_VALUE.defaultValue
+ )
val highContrast: Boolean = preferences[HIGH_CONTRAST.key] ?: HIGH_CONTRAST.defaultValue
return DarkThemePreference(currentDarkThemeValue, highContrast)
}
@@ -153,21 +153,20 @@ class AppPreferences(
is String -> preferences[key.key] = value
is Boolean -> preferences[key.key] = value
is Int -> preferences[key.key] = value
- else -> throw IllegalArgumentException("Unsupported type: ${value!!::class.simpleName}")
+ else ->
+ throw IllegalArgumentException("Unsupported type: ${value!!::class.simpleName}")
}
}
}
override fun getSettingFlow(key: PreferencesKey, defaultValue: T?): Flow {
- return dataStore.data
- .map { preferences ->
- preferences[key.key] ?: defaultValue ?: key.defaultValue
- }
+ return dataStore.data.map { preferences ->
+ preferences[key.key] ?: defaultValue ?: key.defaultValue
+ }
}
override suspend fun getSetting(key: PreferencesKey, defaultValue: T?): T {
val preferences = dataStore.data.firstOrNull()
return preferences?.get(key.key) ?: defaultValue ?: key.defaultValue
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferencesController.kt b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferencesController.kt
index d87b6c7..62c3967 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferencesController.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/AppPreferencesController.kt
@@ -3,12 +3,16 @@ package com.bobbyesp.metadator.core.data.local.preferences
import kotlinx.coroutines.flow.Flow
interface AppPreferencesController {
- val TAG get() = "AppPreferencesController"
+ val TAG
+ get() = "AppPreferencesController"
val userPreferencesFlow: Flow
+
suspend fun getUserPreferences(): UserPreferences
suspend fun saveSetting(key: PreferencesKey, value: T)
+
fun getSettingFlow(key: PreferencesKey, defaultValue: T?): Flow
+
suspend fun getSetting(key: PreferencesKey, defaultValue: T?): T
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/PreferencesKey.kt b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/PreferencesKey.kt
index 275bd18..4df8765 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/PreferencesKey.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/PreferencesKey.kt
@@ -5,8 +5,8 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import com.bobbyesp.metadator.core.data.local.DarkThemePreference.Companion.DarkThemeValue
-import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType
import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize
+import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType
import com.bobbyesp.utilities.ui.DEFAULT_SEED_COLOR
import com.materialkolor.PaletteStyle
@@ -29,10 +29,11 @@ sealed class PreferencesKey(val key: Preferences.Key, val defaultValue: T)
PreferencesKey(stringPreferencesKey("song_card_size"), CompactCardSize.LARGE.name)
// --> Theming
- data object DARK_THEME_VALUE : PreferencesKey(
- stringPreferencesKey("dark_theme_value"),
- DarkThemeValue.FOLLOW_SYSTEM.name
- )
+ data object DARK_THEME_VALUE :
+ PreferencesKey(
+ stringPreferencesKey("dark_theme_value"),
+ DarkThemeValue.FOLLOW_SYSTEM.name,
+ )
data object HIGH_CONTRAST :
PreferencesKey(booleanPreferencesKey("high_contrast"), false)
@@ -45,4 +46,4 @@ sealed class PreferencesKey(val key: Preferences.Key, val defaultValue: T)
data object PALETTE_STYLE :
PreferencesKey(stringPreferencesKey("palette_style"), PaletteStyle.Vibrant.name)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/UserPreferences.kt b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/UserPreferences.kt
index 506f782..82d3d8b 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/UserPreferences.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/UserPreferences.kt
@@ -9,8 +9,8 @@ import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.SONGS_L
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.SONG_CARD_SIZE
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.THEME_COLOR
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.USE_DYNAMIC_COLORING
-import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType
import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize
+import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType
import com.materialkolor.PaletteStyle
@Stable
@@ -22,7 +22,7 @@ data class UserPreferences(
val darkThemePreference: DarkThemePreference,
val useDynamicColoring: Boolean,
val themeColor: Int,
- val paletteStyle: PaletteStyle
+ val paletteStyle: PaletteStyle,
) {
companion object {
fun emptyUserPreferences(): UserPreferences =
@@ -34,7 +34,7 @@ data class UserPreferences(
darkThemePreference = DarkThemePreference(),
useDynamicColoring = USE_DYNAMIC_COLORING.defaultValue,
themeColor = THEME_COLOR.defaultValue,
- paletteStyle = PaletteStyle.valueOf(PALETTE_STYLE.defaultValue)
+ paletteStyle = PaletteStyle.valueOf(PALETTE_STYLE.defaultValue),
)
}
}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/datastore/DataStorePreferences.kt b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/datastore/DataStorePreferences.kt
index d0c62b7..e0170ed 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/datastore/DataStorePreferences.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/data/local/preferences/datastore/DataStorePreferences.kt
@@ -19,34 +19,32 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
-val Context.dataStore: DataStore by preferencesDataStore(
- name = PREFERENCES_NAME,
- corruptionHandler = ReplaceFileCorruptionHandler(
- produceNewData = { emptyPreferences() }
- ),
- //migrations = emptyList(),
- scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-)
+val Context.dataStore: DataStore by
+ preferencesDataStore(
+ name = PREFERENCES_NAME,
+ corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
+ // migrations = emptyList(),
+ scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+ )
@Composable
fun rememberPreferenceState(
key: PreferencesKey,
- defaultValue: T = key.defaultValue
+ defaultValue: T = key.defaultValue,
): Pair, (T) -> Unit> {
val appPreferences = LocalAppPreferencesController.current
val coroutineScope = rememberCoroutineScope()
- val preferenceFlow =
- remember { appPreferences.getSettingFlow(key, defaultValue).distinctUntilChanged() }
+ val preferenceFlow = remember {
+ appPreferences.getSettingFlow(key, defaultValue).distinctUntilChanged()
+ }
val valueState = preferenceFlow.collectAsStateWithLifecycle(initialValue = defaultValue)
val updatePreference: (T) -> Unit = { newValue ->
if (valueState.value != newValue) {
- coroutineScope.launch(Dispatchers.IO) {
- appPreferences.saveSetting(key, newValue)
- }
+ coroutineScope.launch(Dispatchers.IO) { appPreferences.saveSetting(key, newValue) }
}
}
return valueState to updatePreference
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/di/CoresModule.kt b/app/src/main/java/com/bobbyesp/metadator/core/di/CoresModule.kt
index b563f27..e901101 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/di/CoresModule.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/di/CoresModule.kt
@@ -15,34 +15,25 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module
val appCoroutinesScope = module {
- single(
- qualifier = named("AppMainSupervisedScope")
- ) { CoroutineScope(SupervisorJob()) }
+ single(qualifier = named("AppMainSupervisedScope")) {
+ CoroutineScope(SupervisorJob())
+ }
}
val coreFunctionalitiesModule = module {
- single> {
- androidContext().dataStore
- }
+ single> { androidContext().dataStore }
single {
- AppPreferences(
- dataStore = get(),
- scope = get(qualifier = named("AppMainSupervisedScope"))
- )
+ AppPreferences(dataStore = get(), scope = get(qualifier = named("AppMainSupervisedScope")))
}
single {
val context = androidContext()
ImageLoader.Builder(context)
- .memoryCache {
- MemoryCache.Builder(context)
- .maxSizePercent(0.4)
- .build()
- }
+ .memoryCache { MemoryCache.Builder(context).maxSizePercent(0.4).build() }
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
- .maxSizeBytes(7 * 1024 * 1024)
+ .maxSizeBytes(64 * 1024 * 1024)
.build()
}
.respectCacheHeaders(false)
@@ -53,4 +44,4 @@ val coreFunctionalitiesModule = module {
.dispatcher(Dispatchers.IO)
.build()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/di/SystemManagersModule.kt b/app/src/main/java/com/bobbyesp/metadator/core/di/SystemManagersModule.kt
index e240f86..578c328 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/di/SystemManagersModule.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/di/SystemManagersModule.kt
@@ -9,6 +9,10 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val appSystemManagers = module {
- single { androidApplication().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager }
- single { androidContext().getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager }
-}
\ No newline at end of file
+ single {
+ androidApplication().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ }
+ single {
+ androidContext().getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ }
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/domain/model/ParcelableSong.kt b/app/src/main/java/com/bobbyesp/metadator/core/domain/model/ParcelableSong.kt
index d6cc238..dbfa3d4 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/domain/model/ParcelableSong.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/domain/model/ParcelableSong.kt
@@ -3,6 +3,8 @@ package com.bobbyesp.metadator.core.domain.model
import android.net.Uri
import android.os.Parcelable
import androidx.compose.runtime.Immutable
+import androidx.core.net.toUri
+import com.bobbyesp.mediaplayer.domain.model.MusicTrack
import com.bobbyesp.utilities.mediastore.model.UriSerializer
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@@ -15,5 +17,15 @@ data class ParcelableSong(
val mainArtist: String,
val localPath: String,
@Serializable(with = UriSerializer::class) val artworkPath: Uri? = null,
- val filename: String
-) : Parcelable
\ No newline at end of file
+ val filename: String,
+) : Parcelable {
+ fun MusicTrack.toParcelableSong(): ParcelableSong {
+ return ParcelableSong(
+ name = title,
+ mainArtist = artist ?: "",
+ localPath = path,
+ artworkPath = artworkUri?.toUri(),
+ filename = title,
+ )
+ }
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/ext/KClass.kt b/app/src/main/java/com/bobbyesp/metadator/core/ext/KClass.kt
index f7695db..49affc4 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/ext/KClass.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/ext/KClass.kt
@@ -2,4 +2,4 @@ package com.bobbyesp.metadator.core.ext
import kotlin.reflect.KClass
-fun KClass<*>.qualifiedName(): String = this.qualifiedName.toString()
\ No newline at end of file
+fun KClass<*>.qualifiedName(): String = this.qualifiedName.toString()
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/ext/List.kt b/app/src/main/java/com/bobbyesp/metadator/core/ext/List.kt
index d64b4d5..3b4493a 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/ext/List.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/ext/List.kt
@@ -3,21 +3,24 @@ package com.bobbyesp.metadator.core.ext
import com.adamratzman.spotify.models.SimpleArtist
fun List.formatArtists(useAmpersands: Boolean = false): String {
- val artistNames = when (firstOrNull()) {
- is String -> this as List
- is SimpleArtist -> (this as List).mapNotNull { it.name }
- else -> return ""
- }
+ val artistNames =
+ when (firstOrNull()) {
+ is String -> this as List
+ is SimpleArtist -> (this as List).mapNotNull { it.name }
+ else -> return ""
+ }
return if (useAmpersands) {
when (artistNames.size) {
0 -> ""
1 -> artistNames.first()
2 -> artistNames.joinToString(" & ")
- else -> artistNames.subList(0, artistNames.size - 1)
- .joinToString(", ") + " & " + artistNames.last()
+ else ->
+ artistNames.subList(0, artistNames.size - 1).joinToString(", ") +
+ " & " +
+ artistNames.last()
}
} else {
artistNames.joinToString(", ")
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/ext/MediaItem.kt b/app/src/main/java/com/bobbyesp/metadator/core/ext/MediaItem.kt
index bef2d51..159cecb 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/ext/MediaItem.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/ext/MediaItem.kt
@@ -4,8 +4,7 @@ import androidx.media3.common.MediaItem
import com.bobbyesp.utilities.mediastore.model.Song
fun MediaItem.toSong(): Song {
- val mediaMetadata =
- this.mediaMetadata
+ val mediaMetadata = this.mediaMetadata
return Song(
id = mediaId.hashCode().toLong(),
title = (mediaMetadata.displayTitle ?: "").toString(),
@@ -14,6 +13,6 @@ fun MediaItem.toSong(): Song {
artworkPath = mediaMetadata.artworkUri,
duration = 0.0,
path = this.localConfiguration?.uri.toString(),
- fileName = (mediaMetadata.title ?: "").toString()
+ fileName = (mediaMetadata.title ?: "").toString(),
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/ext/Player.kt b/app/src/main/java/com/bobbyesp/metadator/core/ext/Player.kt
index c578d22..7c52b25 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/ext/Player.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/ext/Player.kt
@@ -19,7 +19,10 @@ fun Player.getQueueWindows(): List {
var firstMediaItemIndex = currentMediaItemIndex
var lastMediaItemIndex = currentMediaItemIndex
val shuffleModeEnabled = shuffleModeEnabled
- while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET) && queue.size < queueSize) {
+ while (
+ (firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET) &&
+ queue.size < queueSize
+ ) {
if (lastMediaItemIndex != C.INDEX_UNSET) {
lastMediaItemIndex =
timeline.getNextWindowIndex(lastMediaItemIndex, REPEAT_MODE_OFF, shuffleModeEnabled)
@@ -28,11 +31,12 @@ fun Player.getQueueWindows(): List {
}
}
if (firstMediaItemIndex != C.INDEX_UNSET && queue.size < queueSize) {
- firstMediaItemIndex = timeline.getPreviousWindowIndex(
- firstMediaItemIndex,
- REPEAT_MODE_OFF,
- shuffleModeEnabled
- )
+ firstMediaItemIndex =
+ timeline.getPreviousWindowIndex(
+ firstMediaItemIndex,
+ REPEAT_MODE_OFF,
+ shuffleModeEnabled,
+ )
if (firstMediaItemIndex != C.INDEX_UNSET) {
queue.addFirst(timeline.getWindow(firstMediaItemIndex, Timeline.Window()))
}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/ext/ReleaseDate.kt b/app/src/main/java/com/bobbyesp/metadator/core/ext/ReleaseDate.kt
index 2b96f81..1b70cda 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/ext/ReleaseDate.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/ext/ReleaseDate.kt
@@ -20,4 +20,4 @@ fun ReleaseDate.format(precision: String?, separator: String = "/"): String {
"Unknown"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/ext/Song.kt b/app/src/main/java/com/bobbyesp/metadator/core/ext/Song.kt
index 067afc8..0b7d90d 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/ext/Song.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/ext/Song.kt
@@ -9,6 +9,6 @@ fun Song.toParcelableSong(): ParcelableSong {
mainArtist = this.artist,
localPath = this.path,
artworkPath = this.artworkPath,
- filename = this.fileName
+ filename = this.fileName,
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/ext/String.kt b/app/src/main/java/com/bobbyesp/metadator/core/ext/String.kt
index 6a36dee..381ef79 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/ext/String.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/ext/String.kt
@@ -56,4 +56,4 @@ object TagLib {
else -> Icons.Rounded.RunningWithErrors
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/SettingsRouting.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/SettingsRouting.kt
index 6336b45..1db36b4 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/SettingsRouting.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/SettingsRouting.kt
@@ -7,28 +7,16 @@ import com.bobbyesp.metadator.core.presentation.pages.settings.SettingsPage
import com.bobbyesp.metadator.core.presentation.pages.settings.modules.GeneralSettingsPage
import com.bobbyesp.ui.motion.animatedComposable
-fun NavGraphBuilder.settingsRouting(
- onNavigateBack: () -> Unit
-) {
- navigation(
- startDestination = Route.SettingsNavigator.Settings,
- ) {
+fun NavGraphBuilder.settingsRouting(onNavigateBack: () -> Unit) {
+ navigation(startDestination = Route.SettingsNavigator.Settings) {
animatedComposable {
- SettingsPage(
- onBackPressed = onNavigateBack
- )
+ SettingsPage(onBackPressed = onNavigateBack)
}
- animatedComposable {
- GeneralSettingsPage()
- }
-
- animatedComposable {
+ animatedComposable { GeneralSettingsPage() }
- }
+ animatedComposable {}
- animatedComposable {
-
- }
+ animatedComposable {}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/CompositionLocals.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/CompositionLocals.kt
index ced6fa8..ba6ee31 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/CompositionLocals.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/CompositionLocals.kt
@@ -10,7 +10,6 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import coil.ImageLoader
-import com.bobbyesp.mediaplayer.service.ConnectionHandler
import com.bobbyesp.metadator.core.data.local.DarkThemePreference
import com.bobbyesp.metadator.core.data.local.preferences.AppPreferences
import com.bobbyesp.metadator.core.data.local.preferences.UserPreferences.Companion.emptyUserPreferences
@@ -35,24 +34,24 @@ val LocalAppPreferencesController =
val LocalNavController =
compositionLocalOf { error("No nav controller provided") }
-val LocalWindowWidthState =
- staticCompositionLocalOf { WindowWidthSizeClass.Compact } //This value probably will never change, that's why it is static
+val LocalWindowWidthState = staticCompositionLocalOf {
+ WindowWidthSizeClass.Compact
+} // This value probably will never change, that's why it is static
val LocalSonner = compositionLocalOf { error("No sonner toaster state provided") }
-val LocalMediaplayerConnection =
- compositionLocalOf { error("No Media Player Service Connection handler has been provided") }
@Composable
fun AppLocalSettingsProvider(
windowWidthSize: WindowWidthSizeClass,
- playerConnectionHandler: ConnectionHandler,
sonner: ToasterState = rememberToasterState(),
appPreferences: AppPreferences,
imageLoader: ImageLoader,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
) {
val settingsFlow =
- appPreferences.userPreferencesFlow.collectAsStateWithLifecycle(initialValue = emptyUserPreferences())
+ appPreferences.userPreferencesFlow.collectAsStateWithLifecycle(
+ initialValue = emptyUserPreferences()
+ )
val seedColor = settingsFlow.value.themeColor
val darkTheme = settingsFlow.value.darkThemePreference
@@ -60,26 +59,31 @@ fun AppLocalSettingsProvider(
val config = LocalConfiguration.current
- val themeState = rememberDynamicMaterialThemeState(
- seedColor = Color(seedColor),
- isDark = darkTheme.isDarkTheme(),
- style = themeStyle,
- isAmoled = darkTheme.isHighContrastModeEnabled
- )
+ val themeState =
+ rememberDynamicMaterialThemeState(
+ seedColor = Color(seedColor),
+ isDark = darkTheme.isDarkTheme(),
+ style = themeStyle,
+ isAmoled = darkTheme.isHighContrastModeEnabled,
+ )
CompositionLocalProvider(
- LocalDarkTheme provides darkTheme, //Tells the app what dark theme to use
- //TODO: Modify to handle multiple colors (like based on images)
- LocalSeedColor provides seedColor, //Tells the app what color to use as seed for the palette
- LocalDynamicColoringSwitch provides settingsFlow.value.useDynamicColoring, //Tells the app if it should use dynamic colors or not (Android 12+ feature)
- LocalDynamicThemeState provides themeState, //Provides the theme state to the app
+ LocalDarkTheme provides darkTheme, // Tells the app what dark theme to use
+ // TODO: Modify to handle multiple colors (like based on images)
+ LocalSeedColor provides
+ seedColor, // Tells the app what color to use as seed for the palette
+ LocalDynamicColoringSwitch provides
+ settingsFlow.value
+ .useDynamicColoring, // Tells the app if it should use dynamic colors or not
+ // (Android
+ // 12+ feature)
+ LocalDynamicThemeState provides themeState, // Provides the theme state to the app
LocalAppPreferencesController provides appPreferences,
LocalWindowWidthState provides windowWidthSize,
LocalOrientation provides config.orientation,
LocalSonner provides sonner,
LocalCoilImageLoader provides imageLoader,
- LocalMediaplayerConnection provides playerConnectionHandler,
) {
- content() //The content of the app
+ content() // The content of the app
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/DestinationInfo.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/NavigatorInfo.kt
similarity index 65%
rename from app/src/main/java/com/bobbyesp/metadator/core/presentation/common/DestinationInfo.kt
rename to app/src/main/java/com/bobbyesp/metadator/core/presentation/common/NavigatorInfo.kt
index 0a0ffc7..88b803f 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/DestinationInfo.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/NavigatorInfo.kt
@@ -9,21 +9,12 @@ import androidx.compose.ui.graphics.vector.ImageVector
import com.bobbyesp.metadator.R
@Immutable
-enum class DestinationInfo(
- val icon: ImageVector,
- @StringRes val title: Int,
-) {
- HOME(
- icon = Icons.Rounded.Home,
- title = R.string.home
- ),
- MEDIAPLAYER(
- icon = Icons.Rounded.PlayArrow,
- title = R.string.mediaplayer
- );
+enum class NavigatorInfo(val icon: ImageVector, @StringRes val title: Int) {
+ HOME(icon = Icons.Rounded.Home, title = R.string.home),
+ MEDIAPLAYER(icon = Icons.Rounded.PlayArrow, title = R.string.mediaplayer);
companion object {
- fun fromRoute(route: Route): DestinationInfo? {
+ fun fromRoute(route: Route): NavigatorInfo? {
return when (route) {
is Route.MetadatorNavigator -> HOME
is Route.MediaplayerNavigator -> MEDIAPLAYER
@@ -31,4 +22,4 @@ enum class DestinationInfo(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/Route.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/Route.kt
index 8a9c855..efd9ad5 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/Route.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/common/Route.kt
@@ -8,11 +8,9 @@ sealed interface Route {
@Serializable
data object OnboardingNavigator : Route {
- @Serializable
- data object Welcome : Route
+ @Serializable data object Welcome : Route
- @Serializable
- data object Permissions : Route
+ @Serializable data object Permissions : Route
}
@Serializable
@@ -20,42 +18,33 @@ sealed interface Route {
@Serializable
data object Home : Route {
- @Serializable
- data object VisualSettings : Route
+ @Serializable data object VisualSettings : Route
}
}
@Serializable
data object MediaplayerNavigator : Route {
- @Serializable
- data object Mediaplayer : Route
+ @Serializable data object Mediaplayer : Route
}
@Serializable
data object UtilitiesNavigator : Route {
- @Serializable
- data class TagEditor(val selectedSong: ParcelableSong) : Route
+ @Serializable data class TagEditor(val selectedSong: ParcelableSong) : Route
}
@Serializable
data object SettingsNavigator : Route {
@Serializable
data object Settings : Route {
- @Serializable
- data object General : Route
+ @Serializable data object General : Route
- @Serializable
- data object Appearance : Route
+ @Serializable data object Appearance : Route
- @Serializable
- data object About : Route
+ @Serializable data object About : Route
}
}
}
-val mainNavigators = listOf(
- Route.MetadatorNavigator,
- Route.MediaplayerNavigator
-)
+val mainNavigators = listOf(Route.MetadatorNavigator, Route.MediaplayerNavigator)
-fun Any.qualifiedName(): String = this::class.qualifiedName.toString()
\ No newline at end of file
+fun Any.qualifiedName(): String = this::class.qualifiedName.toString()
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/AppDetails.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/AppDetails.kt
index cade7af..d2dc46e 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/AppDetails.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/AppDetails.kt
@@ -21,50 +21,47 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.core.util.getAppVersionName
import com.bobbyesp.metadator.core.presentation.theme.MetadatorLogoBackground
import com.bobbyesp.metadator.core.presentation.theme.MetadatorLogoForeground
+import com.bobbyesp.metadator.core.util.getAppVersionName
@Composable
-fun AppDetails(
- modifier: Modifier = Modifier,
- subtitle: String? = null
-) {
+fun AppDetails(modifier: Modifier = Modifier, subtitle: String? = null) {
val context = LocalContext.current
val isDarkMode = isSystemInDarkTheme()
- val animatedColor by animateColorAsState(
- targetValue = if(isDarkMode) MetadatorLogoForeground else MetadatorLogoBackground
- )
+ val animatedColor by
+ animateColorAsState(
+ targetValue = if (isDarkMode) MetadatorLogoForeground else MetadatorLogoBackground
+ )
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Image(
painter = painterResource(R.drawable.metadator_logo_foreground),
colorFilter = ColorFilter.tint(animatedColor),
contentDescription = null,
- modifier = Modifier
- .size(200.dp)
- .graphicsLayer {
+ modifier =
+ Modifier.size(200.dp).graphicsLayer {
scaleX = 2f
scaleY = 2f
- }
+ },
)
Text(
text = stringResource(R.string.app_name).uppercase(),
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.SemiBold,
- fontFamily = FontFamily.Monospace
+ fontFamily = FontFamily.Monospace,
)
Text(
text = subtitle ?: context.getAppVersionName(),
color = MaterialTheme.colorScheme.outline,
- fontFamily = FontFamily.Monospace
+ fontFamily = FontFamily.Monospace,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/image/AsyncImage.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/image/AsyncImage.kt
index 51a20c2..6b25c2a 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/image/AsyncImage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/image/AsyncImage.kt
@@ -39,11 +39,10 @@ fun AsyncImage(
placeholder: ImageVector? = null,
context: Context = LocalContext.current,
imageLoader: ImageLoader? = LocalCoilImageLoader.current,
- imageOptions: ImageOptions = ImageOptions(
- contentDescription = null, contentScale = ContentScale.Crop
- ),
+ imageOptions: ImageOptions =
+ ImageOptions(contentDescription = null, contentScale = ContentScale.Crop),
requestListener: (() -> ImageRequest.Listener)? = null,
- onSuccessData: (CoilImageState.Success) -> Unit = { _ -> }
+ onSuccessData: (CoilImageState.Success) -> Unit = { _ -> },
) {
val imageUrl by remember(imageModel) { mutableStateOf(imageModel) }
@@ -62,20 +61,21 @@ fun AsyncImage(
modifier = imageModifier.fillMaxSize(),
icon = placeholder ?: Icons.Rounded.MusicNote,
colorful = false,
- contentDescription = "Song cover placeholder"
+ contentDescription = "Song cover placeholder",
)
},
failure = { error ->
- val icon = if (error.reason is FileNotFoundException) {
- Icons.Rounded.MusicNote
- } else {
- Icons.Rounded.ErrorOutline
- }
+ val icon =
+ if (error.reason is FileNotFoundException) {
+ Icons.Rounded.MusicNote
+ } else {
+ Icons.Rounded.ErrorOutline
+ }
Placeholder(
modifier = imageModifier.fillMaxSize(),
icon = icon,
colorful = false,
- contentDescription = "Song cover failed to load"
+ contentDescription = "Song cover failed to load",
)
},
imageLoader = { imageLoader ?: ImageLoader(context) },
@@ -89,11 +89,15 @@ fun loadBitmapFromUrl(url: String): Bitmap? {
LaunchedEffect(url) {
val imageLoader = ImageLoader(context)
- val request = ImageRequest.Builder(context).data(url).target { drawable ->
- if (drawable is BitmapDrawable) {
- bitmap = drawable.bitmap
- }
- }.build()
+ val request =
+ ImageRequest.Builder(context)
+ .data(url)
+ .target { drawable ->
+ if (drawable is BitmapDrawable) {
+ bitmap = drawable.bitmap
+ }
+ }
+ .build()
val result = (imageLoader.execute(request) as SuccessResult).drawable
if (result is BitmapDrawable) {
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/text/ConditionedMarqueeText.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/text/ConditionedMarqueeText.kt
index f39ccc9..3f62d71 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/text/ConditionedMarqueeText.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/components/text/ConditionedMarqueeText.kt
@@ -42,7 +42,7 @@ fun ConditionedMarqueeText(
sideGradient: MarqueeTextGradientOptions = MarqueeTextGradientOptions(),
customEasing: Easing? = null,
animationDuration: Float = 4000f,
- delayBetweenAnimations: Long = 500L
+ delayBetweenAnimations: Long = 500L,
) {
val (useMarqueeText) = rememberPreferenceState(MARQUEE_TEXT_ENABLED)
@@ -68,7 +68,7 @@ fun ConditionedMarqueeText(
sideGradient,
customEasing,
animationDuration,
- delayBetweenAnimations
+ delayBetweenAnimations,
)
} else {
Text(
@@ -87,7 +87,7 @@ fun ConditionedMarqueeText(
overflow = overflow,
softWrap = softWrap,
onTextLayout = onTextLayout,
- style = style
+ style = style,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/SettingsPage.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/SettingsPage.kt
index 3ae70d6..70b84f8 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/SettingsPage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/SettingsPage.kt
@@ -40,50 +40,46 @@ import com.bobbyesp.ui.components.topbar.ColumnWithCollapsibleTopBar
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun SettingsPage(
- onBackPressed: () -> Unit
-) {
+fun SettingsPage(onBackPressed: () -> Unit) {
val navController = LocalNavController.current
var collapseFraction by remember { mutableFloatStateOf(0f) }
- val mainSettingsGroup: List = listOf(
- SettingsItem(
- title = stringResource(id = R.string.general),
- supportingText = stringResource(id = R.string.general_description),
- icon = Icons.Rounded.Settings,
- onClick = {
- navController.navigate(Route.SettingsNavigator.Settings.General)
- }),
- SettingsItem(
- title = stringResource(id = R.string.appearance),
- supportingText = stringResource(id = R.string.appearance_description),
- icon = Icons.Rounded.Brush,
- onClick = {
- navController.navigate(Route.SettingsNavigator.Settings.Appearance)
- }),
- )
+ val mainSettingsGroup: List =
+ listOf(
+ SettingsItem(
+ title = stringResource(id = R.string.general),
+ supportingText = stringResource(id = R.string.general_description),
+ icon = Icons.Rounded.Settings,
+ onClick = { navController.navigate(Route.SettingsNavigator.Settings.General) },
+ ),
+ SettingsItem(
+ title = stringResource(id = R.string.appearance),
+ supportingText = stringResource(id = R.string.appearance_description),
+ icon = Icons.Rounded.Brush,
+ onClick = { navController.navigate(Route.SettingsNavigator.Settings.Appearance) },
+ ),
+ )
- val infoSettingsGroup: List = listOf(
- SettingsItem(
- title = stringResource(id = R.string.about),
- supportingText = stringResource(id = R.string.about_description),
- icon = Icons.Rounded.Info,
- onClick = {
- navController.navigate(Route.SettingsNavigator.Settings.About)
- }
+ val infoSettingsGroup: List =
+ listOf(
+ SettingsItem(
+ title = stringResource(id = R.string.about),
+ supportingText = stringResource(id = R.string.about_description),
+ icon = Icons.Rounded.Info,
+ onClick = { navController.navigate(Route.SettingsNavigator.Settings.About) },
+ )
)
- )
ColumnWithCollapsibleTopBar(
topBarContent = {
IconButton(
onClick = onBackPressed,
- modifier = Modifier
- .align(Alignment.BottomStart)
- .padding(horizontal = 12.dp, vertical = 4.dp)
+ modifier =
+ Modifier.align(Alignment.BottomStart)
+ .padding(horizontal = 12.dp, vertical = 4.dp),
) {
Icon(
imageVector = Icons.Rounded.ArrowBackIosNew,
- contentDescription = stringResource(id = com.bobbyesp.ui.R.string.back)
+ contentDescription = stringResource(id = com.bobbyesp.ui.R.string.back),
)
}
@@ -92,42 +88,30 @@ fun SettingsPage(
style = MaterialTheme.typography.displaySmall,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium,
- modifier = Modifier
- .align(Alignment.Center)
- .padding(horizontal = 16.dp)
- .graphicsLayer {
+ modifier =
+ Modifier.align(Alignment.Center).padding(horizontal = 16.dp).graphicsLayer {
val scale = lerp(0.7f, 1f, collapseFraction)
scaleX = scale
scaleY = scale
- }
+ },
)
},
- collapseFraction = {
- collapseFraction = it
- },
+ collapseFraction = { collapseFraction = it },
contentPadding = PaddingValues(horizontal = 32.dp),
contentHorizontalAlignment = Alignment.CenterHorizontally,
contentVerticalArrangement = Arrangement.spacedBy(16.dp),
- modifier = Modifier
- .fillMaxSize()
- .safeDrawingPadding()
+ modifier = Modifier.fillMaxSize().safeDrawingPadding(),
) {
- SettingsGroup(
- modifier = Modifier, items = mainSettingsGroup
- )
+ SettingsGroup(modifier = Modifier, items = mainSettingsGroup)
- SettingsGroup(
- modifier = Modifier, items = infoSettingsGroup
- )
+ SettingsGroup(modifier = Modifier, items = infoSettingsGroup)
Text(
text = stringResource(id = R.string.made_with_love_by),
- modifier = Modifier
- .padding(horizontal = 12.dp)
- .fillMaxWidth(),
+ modifier = Modifier.padding(horizontal = 12.dp).fillMaxWidth(),
textAlign = TextAlign.Center,
fontFamily = FontFamily.Monospace,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/modules/GeneralSettingsPage.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/modules/GeneralSettingsPage.kt
index b6b4aaf..814adeb 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/modules/GeneralSettingsPage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/pages/settings/modules/GeneralSettingsPage.kt
@@ -28,37 +28,30 @@ fun GeneralSettingsPage() {
val (reduceShadows, updateReduceShadows) = rememberPreferenceState(REDUCE_SHADOWS)
val navController = LocalNavController.current
- val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
- state = rememberTopAppBarState(),
- canScroll = { true }
- )
+ val scrollBehavior =
+ TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+ state = rememberTopAppBarState(),
+ canScroll = { true },
+ )
Scaffold(
- modifier = Modifier
- .fillMaxSize()
- .nestedScroll(scrollBehavior.nestedScrollConnection),
+ modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
MediumTopAppBar(
title = {
Text(
text = stringResource(id = R.string.general),
- style = MaterialTheme.typography.titleLarge
+ style = MaterialTheme.typography.titleLarge,
)
},
scrollBehavior = scrollBehavior,
- navigationIcon = {
- BackButton {
- navController.popBackStack()
- }
- }
+ navigationIcon = { BackButton { navController.popBackStack() } },
)
- }
+ },
) { paddingValues ->
LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .nestedScroll(scrollBehavior.nestedScrollConnection),
- contentPadding = paddingValues
+ modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
+ contentPadding = paddingValues,
) {
item {
PreferenceSwitch(
@@ -66,9 +59,7 @@ fun GeneralSettingsPage() {
description = stringResource(R.string.marquee_text_description),
isChecked = useMarqueeText.value,
thumbContent = null,
- onClick = {
- updateMarqueeText(!useMarqueeText.value)
- }
+ onClick = { updateMarqueeText(!useMarqueeText.value) },
)
}
item {
@@ -77,11 +68,9 @@ fun GeneralSettingsPage() {
description = stringResource(R.string.reduce_shadows_description),
isChecked = reduceShadows.value,
thumbContent = null,
- onClick = {
- updateReduceShadows(!reduceShadows.value)
- }
+ onClick = { updateReduceShadows(!reduceShadows.value) },
)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Shapes.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Shapes.kt
index b1e6e1a..6a24f49 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Shapes.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Shapes.kt
@@ -4,10 +4,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
-val AppShapes = Shapes(
- extraSmall = RoundedCornerShape(2.dp),
- small = RoundedCornerShape(4.dp),
- medium = RoundedCornerShape(8.dp),
- large = RoundedCornerShape(16.dp),
- extraLarge = RoundedCornerShape(32.dp)
-)
\ No newline at end of file
+val AppShapes =
+ Shapes(
+ extraSmall = RoundedCornerShape(2.dp),
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(8.dp),
+ large = RoundedCornerShape(16.dp),
+ extraLarge = RoundedCornerShape(32.dp),
+ )
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Theme.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Theme.kt
index 3258cbc..01bf500 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Theme.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Theme.kt
@@ -1,6 +1,5 @@
package com.bobbyesp.metadator.core.presentation.theme
-import android.os.Build
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
@@ -11,44 +10,40 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextDirection
+import com.bobbyesp.coreutilities.theming.isDynamicColoringSupported
import com.bobbyesp.metadator.core.presentation.common.LocalDynamicColoringSwitch
import com.bobbyesp.metadator.core.presentation.common.LocalDynamicThemeState
import com.materialkolor.DynamicMaterialTheme
-fun isDynamicColoringSupported(): Boolean {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
-}
-
val MetadatorLogoForeground = Color(0xFFFFFFF0)
val MetadatorLogoBackground = Color(0xFF313638)
@Composable
-fun MetadatorTheme(
- content: @Composable () -> Unit
-) {
+fun MetadatorTheme(content: @Composable () -> Unit) {
val themeState = LocalDynamicThemeState.current
val dynamicColoring = LocalDynamicColoringSwitch.current
val context = LocalContext.current
val canUseDynamicColor = dynamicColoring && isDynamicColoringSupported()
- val dynamicColorScheme = if (canUseDynamicColor) {
- if (themeState.isDark) {
- dynamicDarkColorScheme(context).let {
- if (themeState.isAmoled) it.copy(
- surface = Color.Black,
- background = Color.Black
- ) else it
+ val dynamicColorScheme =
+ if (canUseDynamicColor) {
+ if (themeState.isDark) {
+ dynamicDarkColorScheme(context).let {
+ if (themeState.isAmoled)
+ it.copy(surface = Color.Black, background = Color.Black)
+ else it
+ }
+ } else {
+ dynamicLightColorScheme(context)
}
- } else {
- dynamicLightColorScheme(context)
- }
- } else null
+ } else null
ProvideTextStyle(
- value = LocalTextStyle.current.copy(
- lineBreak = LineBreak.Paragraph,
- textDirection = TextDirection.Content
- )
+ value =
+ LocalTextStyle.current.copy(
+ lineBreak = LineBreak.Paragraph,
+ textDirection = TextDirection.Content,
+ )
) {
if (dynamicColorScheme != null) {
MaterialTheme(colorScheme = dynamicColorScheme, shapes = AppShapes, content = content)
@@ -57,8 +52,8 @@ fun MetadatorTheme(
state = themeState,
animate = true,
shapes = AppShapes,
- content = content
+ content = content,
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Type.kt b/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Type.kt
index 7578170..0a415e6 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Type.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/presentation/theme/Type.kt
@@ -7,28 +7,30 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
-val Typography = Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
+val Typography =
+ Typography(
+ bodyLarge =
+ TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
)
- /* Other default text styles to override
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp
- ),
- labelSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp
- )
- */
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/util/Debugging.kt b/app/src/main/java/com/bobbyesp/metadator/core/util/Debugging.kt
index 2ddefe6..0f37589 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/util/Debugging.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/util/Debugging.kt
@@ -2,7 +2,7 @@ package com.bobbyesp.metadator.core.util
import com.bobbyesp.metadator.BuildConfig
-//execute the code inside if it is a debug release
+// execute the code inside if it is a debug release
fun executeIfDebugging(debugOnlyOperation: () -> Unit) {
if (BuildConfig.DEBUG) debugOnlyOperation()
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/util/Navigation.kt b/app/src/main/java/com/bobbyesp/metadator/core/util/Navigation.kt
index 1138d44..83c2846 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/util/Navigation.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/util/Navigation.kt
@@ -6,11 +6,11 @@ import androidx.navigation.NavHostController
/**
* Determines whether the navigation controller can navigate back.
*
- * This property checks if the current back stack entry's lifecycle state is RESUMED.
- * If the current entry is in the RESUMED state, it indicates that it's currently
- * visible and interacting with the user, and therefore it's safe to navigate back from it.
- * If the current entry is not in the RESUMED state (e.g., it's in CREATED, STARTED, or DESTROYED),
- * navigating back might lead to unexpected behavior or UI inconsistencies.
+ * This property checks if the current back stack entry's lifecycle state is RESUMED. If the current
+ * entry is in the RESUMED state, it indicates that it's currently visible and interacting with the
+ * user, and therefore it's safe to navigate back from it. If the current entry is not in the
+ * RESUMED state (e.g., it's in CREATED, STARTED, or DESTROYED), navigating back might lead to
+ * unexpected behavior or UI inconsistencies.
*
* @return `true` if the navigation controller can go back, `false` otherwise.
*/
@@ -20,10 +20,10 @@ val NavHostController.canGoBack: Boolean
/**
* Navigates back to the previous destination in the navigation stack, if possible.
*
- * This function checks if the navigation controller can go back using [NavHostController.canGoBack].
- * If it can, it pops the current destination off the stack using [NavHostController.popBackStack],
- * effectively navigating to the previous destination. If there's no previous destination to go back to,
- * this function does nothing.
+ * This function checks if the navigation controller can go back using
+ * [NavHostController.canGoBack]. If it can, it pops the current destination off the stack using
+ * [NavHostController.popBackStack], effectively navigating to the previous destination. If there's
+ * no previous destination to go back to, this function does nothing.
*
* @receiver The [NavHostController] instance that manages the navigation stack.
*/
@@ -34,18 +34,18 @@ fun NavHostController.navigateBack() {
}
/**
- * Extension function for NavHostController to navigate to a destination while cleaning up the back stack.
+ * Extension function for NavHostController to navigate to a destination while cleaning up the back
+ * stack.
*
* @param T The type of the destination, which must be a subclass of Any.
* @param destination The destination to navigate to.
*/
-fun NavHostController.cleanNavigate(destination: T) = navigate(destination) {
- // Pop up to the start destination of the graph, saving the state
- popUpTo(graph.startDestinationId) {
- saveState = true
+fun NavHostController.cleanNavigate(destination: T) =
+ navigate(destination) {
+ // Pop up to the start destination of the graph, saving the state
+ popUpTo(graph.startDestinationId) { saveState = true }
+ // Launch the destination as a single top instance
+ launchSingleTop = true
+ // Restore the state if possible
+ restoreState = true
}
- // Launch the destination as a single top instance
- launchSingleTop = true
- // Restore the state if possible
- restoreState = true
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/util/Permissions.kt b/app/src/main/java/com/bobbyesp/metadator/core/util/Permissions.kt
index e63fdd1..1b9d5cc 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/util/Permissions.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/util/Permissions.kt
@@ -5,17 +5,15 @@ import android.os.Build
fun getNeededStoragePermissions(): Array {
return when {
- Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- )
+ Build.VERSION.SDK_INT <= Build.VERSION_CODES.P ->
+ arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ )
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> arrayOf(
- Manifest.permission.READ_MEDIA_AUDIO
- )
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ->
+ arrayOf(Manifest.permission.READ_MEDIA_AUDIO)
- else -> arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE
- )
+ else -> arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/core/util/getAppVersionName.kt b/app/src/main/java/com/bobbyesp/metadator/core/util/getAppVersionName.kt
index e0b3b81..99ec30b 100644
--- a/app/src/main/java/com/bobbyesp/metadator/core/util/getAppVersionName.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/core/util/getAppVersionName.kt
@@ -7,8 +7,10 @@ import com.bobbyesp.metadator.R
fun Context.getAppVersionName(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)).versionName
+ packageManager
+ .getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
+ .versionName
} else {
packageManager.getPackageInfo(packageName, 0).versionName
} ?: this.getString(R.string.unknown)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/SpotifyServiceImpl.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/SpotifyServiceImpl.kt
index 69b1323..9f82c8b 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/SpotifyServiceImpl.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/SpotifyServiceImpl.kt
@@ -26,11 +26,12 @@ class SpotifyServiceImpl : SpotifyService, KoinComponent {
* Retrieves the Spotify API instance.
*
* This function returns a [SpotifyAppApi] instance, allowing interaction with the Spotify API.
- * It ensures that the API is built before returning it. If the API has not been built yet, it will call the [buildApi] function to initialize it.
+ * It ensures that the API is built before returning it. If the API has not been built yet, it
+ * will call the [buildApi] function to initialize it.
*
* @return A [SpotifyAppApi] instance, ready for use.
- * @throws IllegalStateException If the connection to the Spotify API was not established. This can occur due to network issues or server outages.
- *
+ * @throws IllegalStateException If the connection to the Spotify API was not established. This
+ * can occur due to network issues or server outages.
*/
override suspend fun getSpotifyApi(): SpotifyAppApi {
if (api == null) {
@@ -39,21 +40,19 @@ class SpotifyServiceImpl : SpotifyService, KoinComponent {
return api
?: throw IllegalStateException(
"The connection to the Spotify API was not established." +
- " This may be due to a network error or a servers outage."
+ " This may be due to a network error or a servers outage."
)
}
/**
* Retrieves the Spotify access token.
*
- * This function fetches the Spotify access token, ensuring it's available
- * before returning it. If the token is null, it attempts to build/refresh
- * the token via the `buildApi()` method.
+ * This function fetches the Spotify access token, ensuring it's available before returning it.
+ * If the token is null, it attempts to build/refresh the token via the `buildApi()` method.
*
* @return A [Token] object representing the Spotify access token.
- * @throws IllegalStateException if the Spotify token is still null after
- * attempting to build/refresh it. This indicates a failure in the token
- * acquisition process.
+ * @throws IllegalStateException if the Spotify token is still null after attempting to
+ * build/refresh it. This indicates a failure in the token acquisition process.
*/
override suspend fun getSpotifyToken(): Token {
if (token == null) {
@@ -63,14 +62,15 @@ class SpotifyServiceImpl : SpotifyService, KoinComponent {
}
/**
- * This method is responsible for building the Spotify API.
- * It first checks if the application is in debug mode, and if so, logs the client ID and secret.
- * Then, it attempts to build the Spotify API with the provided client ID and secret.
- * If the API is successfully built, it retrieves the token from the API and stores it.
+ * This method is responsible for building the Spotify API. It first checks if the application
+ * is in debug mode, and if so, logs the client ID and secret. Then, it attempts to build the
+ * Spotify API with the provided client ID and secret. If the API is successfully built, it
+ * retrieves the token from the API and stores it.
*
- * If an exception occurs during the building of the API, it logs the error.
- * If a BadRequestException occurs, it checks if the token should be refreshed and if the recursion depth is less than the maximum allowed.
- * If both conditions are met, it logs the information, clears the API, and attempts to build the API again, incrementing the recursion depth.
+ * If an exception occurs during the building of the API, it logs the error. If a
+ * BadRequestException occurs, it checks if the token should be refreshed and if the recursion
+ * depth is less than the maximum allowed. If both conditions are met, it logs the information,
+ * clears the API, and attempts to build the API again, incrementing the recursion depth.
*
* @throws Exception if there is an error building the API.
* @throws SpotifyException.BadRequestException if a bad request is made to the Spotify API.
@@ -80,15 +80,16 @@ class SpotifyServiceImpl : SpotifyService, KoinComponent {
executeIfDebugging {
Log.d(
"SpotifyApiRequests",
- "Building API with client ID: $clientId and client secret: $clientSecret"
+ "Building API with client ID: $clientId and client secret: $clientSecret",
)
}
- api = spotifyAppApi(clientId, clientSecret).build().apply {
- with(this.spotifyApiOptions) {
- automaticRefresh = true
- enableDebugMode = isDebug
+ api =
+ spotifyAppApi(clientId, clientSecret).build().apply {
+ with(this.spotifyApiOptions) {
+ automaticRefresh = true
+ enableDebugMode = isDebug
+ }
}
- }
token = api?.token
} catch (e: Exception) {
executeIfDebugging { Log.e("SpotifyApiRequests", "Error building API", e) }
@@ -98,12 +99,11 @@ class SpotifyServiceImpl : SpotifyService, KoinComponent {
if (it.shouldRefresh() && recursionDepth < MAX_RECURSION_DEPTH) {
Log.i(
"SpotifyApiRequests",
- "Token expired, refreshing token; recursion depth: $recursionDepth of $MAX_RECURSION_DEPTH"
+ "Token expired, refreshing token; recursion depth: $recursionDepth of $MAX_RECURSION_DEPTH",
)
clearApi()
buildApi()
recursionDepth++
- return@let
}
}
}
@@ -117,4 +117,4 @@ class SpotifyServiceImpl : SpotifyService, KoinComponent {
companion object {
private const val MAX_RECURSION_DEPTH = 3
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/repository/SearchRepositoryImpl.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/repository/SearchRepositoryImpl.kt
index 1277d10..2c195e2 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/repository/SearchRepositoryImpl.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/repository/SearchRepositoryImpl.kt
@@ -25,19 +25,21 @@ class SearchRepositoryImpl : SearchRepository, KoinComponent {
private val searchService by inject()
/**
- * Search for tracks on Spotify. The search does support pagination but this implementation
- * does not support it. For now, it will only return up to 50 results.
+ * Search for tracks on Spotify. The search does support pagination but this implementation does
+ * not support it. For now, it will only return up to 50 results.
*/
override suspend fun searchTracks(query: String): Result> {
try {
- val searchResult = searchService.search(
- query,
- searchTypes = arrayOf(SearchApi.SearchType.Track),
- filters = emptyList()
- )
+ val searchResult =
+ searchService.search(
+ query,
+ searchTypes = arrayOf(SearchApi.SearchType.Track),
+ filters = emptyList(),
+ )
- searchResult.tracks?.let { return Result.success(it.items) }
- ?: return Result.failure(NullPointerException("Search result is null"))
+ searchResult.tracks?.let {
+ return Result.success(it.items)
+ } ?: return Result.failure(NullPointerException("Search result is null"))
} catch (th: Throwable) {
return Result.failure(th)
}
@@ -70,4 +72,4 @@ class SearchRepositoryImpl : SearchRepository, KoinComponent {
override suspend fun searchPaginatedArtists(query: String): Pager {
TODO("Not yet implemented")
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/search/SpotifySearchServiceImpl.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/search/SpotifySearchServiceImpl.kt
index 056d324..db8c5e5 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/search/SpotifySearchServiceImpl.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/data/remote/search/SpotifySearchServiceImpl.kt
@@ -11,14 +11,13 @@ import com.bobbyesp.metadator.features.spotify.domain.services.SpotifyService
import com.bobbyesp.metadator.features.spotify.domain.services.search.SpotifySearchService
import org.koin.core.component.KoinComponent
-class SpotifySearchServiceImpl(
- private val spotifyService: SpotifyService
-) : SpotifySearchService, KoinComponent {
+class SpotifySearchServiceImpl(private val spotifyService: SpotifyService) :
+ SpotifySearchService, KoinComponent {
override suspend fun search(
query: String,
vararg searchTypes: SearchApi.SearchType,
- filters: List
+ filters: List,
): SpotifySearchResult {
val api = spotifyService.getSpotifyApi()
return api.search.search(query = query, searchTypes = searchTypes, filters = filters)
@@ -26,21 +25,12 @@ class SpotifySearchServiceImpl(
override suspend fun searchPaginatedTracks(
query: String,
- filters: List
+ filters: List,
): Pager {
val api = spotifyService.getSpotifyApi()
return Pager(
- config = PagingConfig(
- pageSize = 20,
- enablePlaceholders = false,
- initialLoadSize = 40,
- ),
- pagingSourceFactory = {
- TracksPagingSource(
- spotifyApi = api,
- query = query,
- )
- }
+ config = PagingConfig(pageSize = 20, enablePlaceholders = false, initialLoadSize = 40),
+ pagingSourceFactory = { TracksPagingSource(spotifyApi = api, query = query) },
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/di/SpotifyModule.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/di/SpotifyModule.kt
index 6e74e92..19e5a4e 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/di/SpotifyModule.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/di/SpotifyModule.kt
@@ -16,4 +16,4 @@ val spotifyMainModule = module {
val spotifyServicesModule = module {
single { SpotifySearchServiceImpl(get()) }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/pagination/TracksPagingSource.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/pagination/TracksPagingSource.kt
index 562b57b..2f3283f 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/pagination/TracksPagingSource.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/pagination/TracksPagingSource.kt
@@ -16,13 +16,14 @@ class TracksPagingSource(
val offset = params.key ?: 0
return try {
- val response = spotifyApi.search.searchTrack(
- query = query,
- limit = params.loadSize,
- offset = offset,
- market = null,
- filters = filters,
- )
+ val response =
+ spotifyApi.search.searchTrack(
+ query = query,
+ limit = params.loadSize,
+ offset = offset,
+ market = null,
+ filters = filters,
+ )
if (response.isNotEmpty()) {
val tracks = response.items
@@ -30,7 +31,7 @@ class TracksPagingSource(
LoadResult.Page(
data = tracks,
prevKey = if (offset > 0) offset - params.loadSize else null,
- nextKey = if (tracks.isNotEmpty()) offset + params.loadSize else null
+ nextKey = if (tracks.isNotEmpty()) offset + params.loadSize else null,
)
} else {
LoadResult.Error(IllegalStateException("No tracks found"))
@@ -43,4 +44,4 @@ class TracksPagingSource(
override fun getRefreshKey(state: PagingState): Int? {
return state.anchorPosition
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/repositories/SearchRepository.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/repositories/SearchRepository.kt
index 133b69e..5b1eb75 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/repositories/SearchRepository.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/repositories/SearchRepository.kt
@@ -8,11 +8,18 @@ import com.adamratzman.spotify.models.Track
interface SearchRepository {
suspend fun searchTracks(query: String): Result>
+
suspend fun searchAlbums(query: String): Result>
+
suspend fun searchPlaylists(query: String): Result>
+
suspend fun searchArtists(query: String): Result>
+
suspend fun searchPaginatedTracks(query: String): Pager
+
suspend fun searchPaginatedAlbums(query: String): Pager
+
suspend fun searchPaginatedPlaylists(query: String): Pager
+
suspend fun searchPaginatedArtists(query: String): Pager
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/SpotifyService.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/SpotifyService.kt
index 48653a2..259f67c 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/SpotifyService.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/SpotifyService.kt
@@ -5,5 +5,6 @@ import com.adamratzman.spotify.models.Token
interface SpotifyService {
suspend fun getSpotifyApi(): SpotifyAppApi
+
suspend fun getSpotifyToken(): Token
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/search/SpotifySearchService.kt b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/search/SpotifySearchService.kt
index d6eba6c..fd824cc 100644
--- a/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/search/SpotifySearchService.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/features/spotify/domain/services/search/SpotifySearchService.kt
@@ -10,11 +10,8 @@ interface SpotifySearchService {
suspend fun search(
query: String,
vararg searchTypes: SearchApi.SearchType,
- filters: List
+ filters: List,
): SpotifySearchResult
- suspend fun searchPaginatedTracks(
- query: String,
- filters: List,
- ): Pager
-}
\ No newline at end of file
+ suspend fun searchPaginatedTracks(query: String, filters: List): Pager
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/MediaplayerRouting.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/MediaplayerRouting.kt
index 0785f46..2af078b 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/MediaplayerRouting.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/MediaplayerRouting.kt
@@ -3,19 +3,15 @@ package com.bobbyesp.metadator.mediaplayer
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.navigation
import com.bobbyesp.metadator.core.presentation.common.Route
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerPage
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel
import com.bobbyesp.ui.motion.animatedComposable
fun NavGraphBuilder.mediaplayerRouting(
- mediaplayerViewModel: MediaplayerViewModel,
+ // mediaplayerViewModel: MediaplayerViewModel,
onNavigateBack: () -> Unit
) {
navigation(
- startDestination = Route.MediaplayerNavigator.Mediaplayer,
+ startDestination = Route.MediaplayerNavigator.Mediaplayer
) {
- animatedComposable {
- MediaplayerPage(mediaplayerViewModel)
- }
+ animatedComposable {}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/data/local/MediaplayerServiceConnection.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/data/local/MediaplayerServiceConnection.kt
deleted file mode 100644
index 7e8acbe..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/data/local/MediaplayerServiceConnection.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.data.local
-
-import android.content.ComponentName
-import android.content.ServiceConnection
-import android.os.IBinder
-import androidx.annotation.OptIn
-import androidx.media3.common.util.UnstableApi
-import com.bobbyesp.mediaplayer.service.ConnectionHandler
-import com.bobbyesp.mediaplayer.service.MediaplayerService
-import com.bobbyesp.utilities.Logging
-
-@OptIn(UnstableApi::class)
-class MediaplayerServiceConnection(
- private val connectionHandler: ConnectionHandler
-) : ServiceConnection {
- override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
- Logging.i(
- "The Music Service is connected. Updating the connection handler."
- )
- val binder = service as MediaplayerService.MusicBinder
- connectionHandler.connect(binder.service.mediaServiceHandler)
- }
-
- override fun onServiceDisconnected(name: ComponentName?) {
- Logging.i(
- "The Music Service has been disconnected. Detaching the connection handler."
- )
- connectionHandler.disconnect()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/di/MediaplayerViewModelsModule.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/di/MediaplayerViewModelsModule.kt
index 3ef3551..2c7af02 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/di/MediaplayerViewModelsModule.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/di/MediaplayerViewModelsModule.kt
@@ -1,9 +1,5 @@
package com.bobbyesp.metadator.mediaplayer.di
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel
-import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
-val mediaplayerViewModels = module {
- viewModel { MediaplayerViewModel(get(), get(), get(), get()) }
-}
\ No newline at end of file
+val mediaplayerViewModels = module {}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/buttons/PlayPauseAnimatedButton.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/buttons/PlayPauseAnimatedButton.kt
index d7d5bc8..cb6ea48 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/buttons/PlayPauseAnimatedButton.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/buttons/PlayPauseAnimatedButton.kt
@@ -28,48 +28,42 @@ import com.bobbyesp.metadator.R
fun PlayPauseAnimatedButton(
modifier: Modifier = Modifier,
isPlaying: Boolean,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
- val radius = if (isPlaying || isPressed.value) {
- 40.dp
- } else {
- 24.dp
- }
- val cornerRadius = animateDpAsState(targetValue = radius, label = "Animated button shape")
+ val cornerRadius =
+ animateDpAsState(
+ targetValue = if (isPlaying || isPressed.value) 40.dp else 24.dp,
+ label = "Animated button shape",
+ )
Surface(
tonalElevation = 10.dp,
- modifier = modifier
- .clip(RoundedCornerShape(cornerRadius.value))
+ modifier = modifier.clip(RoundedCornerShape(cornerRadius.value)),
) {
Box(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.primaryContainer)
- .size(72.dp)
- .clip(RoundedCornerShape(cornerRadius.value))
- .clickable(
- interactionSource = interactionSource,
- indication = ripple(bounded = false),
- ) { onClick() },
- contentAlignment = Alignment.Center
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.primaryContainer)
+ .size(72.dp)
+ .clip(RoundedCornerShape(cornerRadius.value))
+ .clickable(
+ interactionSource = interactionSource,
+ indication = ripple(bounded = false),
+ onClick = onClick,
+ ),
+ contentAlignment = Alignment.Center,
) {
- if (isPlaying) {
- Icon(
- imageVector = Icons.Rounded.Pause,
- contentDescription = stringResource(id = R.string.pause),
- tint = MaterialTheme.colorScheme.secondary,
- modifier = Modifier.size(32.dp)
- )
- } else {
- Icon(
- imageVector = Icons.Rounded.PlayArrow,
- contentDescription = stringResource(id = R.string.play),
- tint = MaterialTheme.colorScheme.secondary,
- modifier = Modifier.size(32.dp)
- )
- }
+ val icon = if (isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow
+ val contentDescription =
+ stringResource(id = if (isPlaying) R.string.pause else R.string.play)
+
+ Icon(
+ imageVector = icon,
+ contentDescription = contentDescription,
+ tint = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier.size(32.dp),
+ )
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/RepeatStateIcon.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/RepeatStateIcon.kt
index 9a57b41..3d15b39 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/RepeatStateIcon.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/RepeatStateIcon.kt
@@ -14,33 +14,18 @@ import androidx.media3.common.Player.REPEAT_MODE_ONE
import com.bobbyesp.mediaplayer.R
@Composable
-fun RepeatStateIcon(
- modifier: Modifier = Modifier,
- repeatMode: Int
-) {
- when (repeatMode) {
- REPEAT_MODE_OFF -> {
- Icon(
- imageVector = Icons.Rounded.Repeat,
- contentDescription = stringResource(id = R.string.repeat_mode_off),
- modifier = modifier.alpha(0.5f)
- )
+fun RepeatStateIcon(modifier: Modifier = Modifier, repeatMode: Int) {
+ val (icon, description, alpha) =
+ when (repeatMode) {
+ REPEAT_MODE_OFF -> Triple(Icons.Rounded.Repeat, R.string.repeat_mode_off, 0.5f)
+ REPEAT_MODE_ONE -> Triple(Icons.Rounded.RepeatOne, R.string.repeat_mode_one, 1f)
+ REPEAT_MODE_ALL -> Triple(Icons.Rounded.Repeat, R.string.repeat_mode_all, 1f)
+ else -> Triple(Icons.Rounded.Repeat, R.string.repeat_mode_off, 0.5f)
}
- REPEAT_MODE_ONE -> {
- Icon(
- imageVector = Icons.Rounded.RepeatOne,
- contentDescription = stringResource(id = R.string.repeat_mode_one),
- modifier = modifier
- )
- }
-
- REPEAT_MODE_ALL -> {
- Icon(
- imageVector = Icons.Rounded.Repeat,
- contentDescription = stringResource(id = R.string.repeat_mode_all),
- modifier = modifier
- )
- }
- }
-}
\ No newline at end of file
+ Icon(
+ imageVector = icon,
+ contentDescription = stringResource(id = description),
+ modifier = modifier.alpha(alpha),
+ )
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/ShuffleStateIcon.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/ShuffleStateIcon.kt
index 2ab416d..5697d5a 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/ShuffleStateIcon.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/components/others/ShuffleStateIcon.kt
@@ -10,25 +10,12 @@ import androidx.compose.ui.res.stringResource
import com.bobbyesp.mediaplayer.R
@Composable
-fun ShuffleStateIcon(
- modifier: Modifier = Modifier,
- isShuffleEnabled: Boolean
-) {
- when (isShuffleEnabled) {
- true -> {
- Icon(
- imageVector = Icons.Rounded.ShuffleOn,
- contentDescription = stringResource(id = R.string.action_shuffle_on),
- modifier = modifier
- )
- }
+fun ShuffleStateIcon(modifier: Modifier = Modifier, isShuffleEnabled: Boolean) {
+ val icon = if (isShuffleEnabled) Icons.Rounded.ShuffleOn else Icons.Rounded.Shuffle
+ val description =
+ stringResource(
+ id = if (isShuffleEnabled) R.string.action_shuffle_on else R.string.action_shuffle_off
+ )
- false -> {
- Icon(
- imageVector = Icons.Rounded.Shuffle,
- contentDescription = stringResource(id = R.string.action_shuffle_off),
- modifier = modifier
- )
- }
- }
-}
\ No newline at end of file
+ Icon(imageVector = icon, contentDescription = description, modifier = modifier)
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/MediaplayerPage.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/MediaplayerPage.kt
deleted file mode 100644
index cd054da..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/MediaplayerPage.kt
+++ /dev/null
@@ -1,203 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.scaleIn
-import androidx.compose.animation.scaleOut
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.add
-import androidx.compose.foundation.layout.displayCutoutPadding
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.KeyboardDoubleArrowUp
-import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.core.presentation.common.LocalNavController
-import com.bobbyesp.metadator.core.util.navigateBack
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.CollapsedPlayerHeight
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.MediaplayerSheet
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.PlayerAnimationSpec
-import com.bobbyesp.metadator.mediastore.presentation.components.card.songs.HorizontalSongCard
-import com.bobbyesp.ui.components.bottomsheet.draggable.rememberDraggableBottomSheetState
-import com.bobbyesp.ui.components.button.BackButton
-import com.bobbyesp.ui.util.isDeviceInLandscape
-import kotlinx.coroutines.launch
-import my.nanihadesuka.compose.LazyColumnScrollbar
-import my.nanihadesuka.compose.ScrollbarSelectionActionable
-import my.nanihadesuka.compose.ScrollbarSettings
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun MediaplayerPage(
- viewModel: MediaplayerViewModel,
-) {
- val mediaStoreLazyColumnState = rememberLazyListState()
- val isFirstItemVisible by remember { derivedStateOf { mediaStoreLazyColumnState.firstVisibleItemIndex == 0 } }
-
- val navController = LocalNavController.current
-
- val songs = viewModel.songsFlow.collectAsStateWithLifecycle(initialValue = emptyList()).value
-
- val scope = rememberCoroutineScope()
-
- val density = LocalDensity.current
- val windowsInsets = WindowInsets.systemBars
-
- BoxWithConstraints(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background)
- ) {
- val bottomInset = with(density) { windowsInsets.getBottom(density).toDp() }
- val mediaPlayerSheetState = rememberDraggableBottomSheetState(
- dismissedBound = 0.dp,
- collapsedBound = bottomInset + CollapsedPlayerHeight,
- expandedBound = this.maxHeight,
- animationSpec = PlayerAnimationSpec,
- )
-
- val targetBottom by remember {
- derivedStateOf {
- if (!mediaPlayerSheetState.isDismissed) {
- CollapsedPlayerHeight + bottomInset
- } else {
- bottomInset
- }
- }
- }
-
- val animatedBottom by animateDpAsState(
- targetValue = targetBottom, label = "Animated bottom insets for player sheet"
- )
-
- val playerAwareWindowInsets by remember {
- derivedStateOf {
- windowsInsets.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
- .add(WindowInsets(bottom = animatedBottom))
- }
- }
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- topBar = {
- CenterAlignedTopAppBar(navigationIcon = {
- BackButton {
- navController.navigateBack()
- }
- }, title = {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- text = stringResource(id = R.string.mediaplayer).uppercase(),
- fontWeight = FontWeight.SemiBold,
- fontFamily = FontFamily.Monospace,
- style = MaterialTheme.typography.titleLarge.copy(
- letterSpacing = 4.sp,
- ),
- )
- }
- })
- },
- floatingActionButton = {
- AnimatedVisibility(
- visible = !isFirstItemVisible,
- enter = fadeIn() + scaleIn(),
- exit = fadeOut() + scaleOut()
- ) {
- FloatingActionButton(onClick = {
- scope.launch {
- mediaStoreLazyColumnState.animateScrollToItem(0)
- }
- }) {
- Icon(
- imageVector = Icons.Rounded.KeyboardDoubleArrowUp,
- contentDescription = stringResource(
- id = R.string.scroll_to_top
- )
- )
- }
- }
- },
- contentWindowInsets = playerAwareWindowInsets,
- ) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .then(
- if(isDeviceInLandscape()) Modifier.displayCutoutPadding() else Modifier
- )
- .padding(it)
- ) {
- LazyColumnScrollbar(
- state = mediaStoreLazyColumnState,
- settings = ScrollbarSettings(
- thumbUnselectedColor = MaterialTheme.colorScheme.onSurfaceVariant,
- thumbSelectedColor = MaterialTheme.colorScheme.primary,
- selectionActionable = ScrollbarSelectionActionable.WhenVisible,
- ),
- ) {
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background),
- contentPadding = PaddingValues(horizontal = 8.dp),
- state = mediaStoreLazyColumnState,
- ) {
- items(
- count = songs.size,
- key = { index -> songs[index].id },
- contentType = { index -> songs[index].id.toString() }) { index ->
- val song = songs[index]
- HorizontalSongCard(
- song = song, modifier = Modifier.animateItem(
- fadeInSpec = null, fadeOutSpec = null
- ), onClick = {
- viewModel.playOrderedQueue(song)
-
- if (mediaPlayerSheetState.isDismissed) {
- mediaPlayerSheetState.collapseSoft()
- }
- })
- }
- }
- }
- }
- }
-
- MediaplayerSheet(
- state = mediaPlayerSheetState, viewModel = viewModel
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/MediaplayerViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/MediaplayerViewModel.kt
deleted file mode 100644
index 861e450..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/MediaplayerViewModel.kt
+++ /dev/null
@@ -1,247 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer
-
-import android.content.Context
-import android.net.Uri
-import androidx.annotation.OptIn
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import androidx.media3.common.C
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaMetadata
-import androidx.media3.common.Player.REPEAT_MODE_OFF
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.session.MediaLibraryService.MediaLibrarySession
-import com.bobbyesp.mediaplayer.service.ConnectionHandler
-import com.bobbyesp.mediaplayer.service.MediaServiceHandler
-import com.bobbyesp.mediaplayer.service.MediaState
-import com.bobbyesp.mediaplayer.service.PlayerEvent
-import com.bobbyesp.mediaplayer.service.queue.SongsQueue
-import com.bobbyesp.utilities.Time.formatDuration
-import com.bobbyesp.utilities.mediastore.MediaStoreReceiver.Advanced.getSongs
-import com.bobbyesp.utilities.mediastore.MediaStoreReceiver.Advanced.observeSongs
-import com.bobbyesp.utilities.mediastore.model.Song
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import java.io.File
-
-@OptIn(UnstableApi::class)
-class MediaplayerViewModel(
- private val applicationContext: Context,
- private val serviceHandler: MediaServiceHandler,
- private val mediaSession: MediaLibrarySession,
- val connectionHandler: ConnectionHandler
-) : ViewModel() {
- private val mutableMediaplayerPageState = MutableStateFlow(MediaplayerPageState())
- val pageViewState = mutableMediaplayerPageState.asStateFlow()
-
- val songsFlow = applicationContext.contentResolver.observeSongs()
-
- val songBeingPlayed = serviceHandler.currentMediaItem.asStateFlow()
-
- val isPlaying = serviceHandler.isPlaying
- val isShuffleEnabled = serviceHandler.shuffleModeEnabled
- val repeatMode = serviceHandler.repeatMode
-
- val canSkipNext = serviceHandler.canSkipNext
- val canSkipPrevious = serviceHandler.canSkipPrevious
-
- data class MediaplayerPageState(
- val uiState: PlayerState = PlayerState.Initial,
- )
-
- init {
- viewModelScope.launch(Dispatchers.IO) {
- serviceHandler.mediaState.collectLatest { mediaState ->
- when (mediaState) {
- is MediaState.Buffering -> {
- if (mediaState.progress != C.TIME_UNSET) {
- calculateProgressValues(mediaState.progress)
- }
- }
-
- is MediaState.Playing -> mutableMediaplayerPageState.update {
- (it.uiState as? PlayerState.Ready)?.let { readyState ->
- it.copy(
- uiState = readyState.copy(isPlaying = mediaState.isPlaying)
- )
- } ?: it
- }
-
- is MediaState.Idle -> mutableMediaplayerPageState.update { it.copy(uiState = PlayerState.Initial) }
- is MediaState.Progress -> {
- if (mediaState.progress != C.TIME_UNSET) {
- calculateProgressValues(mediaState.progress)
- }
- }
-
- is MediaState.Ready -> {
- mutableMediaplayerPageState.update {
- it.copy(
- uiState = PlayerState.Ready(
- duration = mediaState.duration,
- )
- )
- }
- }
- }
- }
- }
- }
-
- fun togglePlayPause() {
- viewModelScope.launch {
- serviceHandler.onPlayerEvent(PlayerEvent.PlayPause)
- }
- }
-
- fun toggleShuffle() {
- viewModelScope.launch {
- serviceHandler.onPlayerEvent(PlayerEvent.ToggleShuffle)
- }
- }
-
- fun toggleRepeat() {
- viewModelScope.launch {
- serviceHandler.onPlayerEvent(PlayerEvent.ToggleRepeat)
- }
- }
-
- fun playOrderedQueue(firstSong: Song) {
- playQueue(firstSong)
- viewModelScope.launch {
- serviceHandler.onPlayerEvent(PlayerEvent.PlayPause)
- }
- }
-
- fun playShuffledQueue(firstSong: Song) {
- playRandomQueue(firstSong)
- viewModelScope.launch {
- serviceHandler.onPlayerEvent(PlayerEvent.PlayPause)
- }
- }
-
- fun seekTo(progress: Float) {
- val duration = (pageViewState.value.uiState as? PlayerState.Ready)?.duration ?: 0L
- val seekPosition = (progress * duration).toLong()
- viewModelScope.launch {
- serviceHandler.seekTo(seekPosition)
- }
- }
-
- fun seekToPrevious() {
- viewModelScope.launch {
- serviceHandler.onPlayerEvent(PlayerEvent.Previous)
- }
- }
-
- fun seekToNext() {
- viewModelScope.launch {
- serviceHandler.onPlayerEvent(PlayerEvent.Next)
- }
- }
-
- private fun playRandomQueue(firstSong: Song) {
- viewModelScope.launch {
- val copiedList = applicationContext.contentResolver.getSongs().toMutableList()
-
- copiedList.shuffle()
-
- // Move the firstSong to the front of the list
- copiedList.remove(firstSong)
- copiedList.add(0, firstSong)
-
- loadQueueSongs(copiedList)
- }
- }
-
- private fun playQueue(firstSong: Song) {
- viewModelScope.launch {
- val copiedList = applicationContext.contentResolver.getSongs().toMutableList()
-
- copiedList.remove(firstSong)
- copiedList.add(0, firstSong)
-
- loadQueueSongs(copiedList)
- }
- }
-
- private fun loadQueueSongs(songs: List) {
- val mediaItems = songs.map { song ->
- MediaItem.Builder()
- .setCustomCacheKey(song.id.toString())
- .setUri(Uri.fromFile(File(song.path)))
- .setMediaMetadata(
- MediaMetadata.Builder()
- .setTitle(song.title)
- .setArtist(song.artist)
- .setAlbumTitle(song.album)
- .setArtworkUri(song.artworkPath)
- .build()
- ).build()
- }
-
- viewModelScope.launch {
- val queue = SongsQueue(title = null, items = mediaItems)
- serviceHandler.playQueue(queue)
- }
- }
-
- fun dismissPlayer() {
- mediaSession.player.stop()
- mediaSession.player.clearMediaItems()
- connectionHandler.disconnect()
- }
-
- private fun calculateProgressValues(currentProgress: Long) {
- if (currentProgress <= 0) {
- mutableMediaplayerPageState.update {
- (it.uiState as? PlayerState.Ready)?.let { readyState ->
- it.copy(
- uiState = readyState.copy(
- progress = 0f,
- progressString = "00:00",
- isPlaying = false
- )
- )
- } ?: it
- }
- return
- }
-
- (pageViewState.value.uiState as? PlayerState.Ready)?.let { readyState ->
- val progress = currentProgress.toFloat() / readyState.duration
- val progressString = formatDuration(currentProgress)
- mutableMediaplayerPageState.update {
- it.copy(
- uiState = readyState.copy(
- progress = progress,
- progressString = progressString
- )
- )
- }
- } ?: calculateProgressValues(0L)
- }
-
- override fun onCleared() {
- viewModelScope.launch {
- serviceHandler.killPlayer()
- }
- super.onCleared()
- }
-
- sealed interface PlayerState {
- data object Initial : PlayerState
- data class Ready(
- val progress: Float = 0f,
- val progressString: String = "00:00",
- val duration: Long = 0L,
- val isPlaying: Boolean = false,
- val isShuffleEnabled: Boolean = false,
- val repeatMode: Int = REPEAT_MODE_OFF
- ) : PlayerState
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerConstants.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerConstants.kt
deleted file mode 100644
index 1aacc92..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerConstants.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player
-
-import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.SizeTransform
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.EaseInOutSine
-import androidx.compose.animation.core.tween
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import com.bobbyesp.ui.motion.materialSharedAxisXIn
-import com.bobbyesp.ui.motion.materialSharedAxisXOut
-
-val CollapsedPlayerHeight = 84.dp
-val SeekToButtonSize = 48.dp
-val PlayerCommandsButtonSize = 48.dp
-
-val PlayerAnimationSpec: AnimationSpec = tween(
- durationMillis = 750,
- delayMillis = 0,
- easing = EaseInOutSine
-)
-
-val AnimatedTextContentTransformation = ContentTransform(
- materialSharedAxisXIn(initialOffsetX = { it / 10 }),
- materialSharedAxisXOut(targetOffsetX = { -it / 10 }),
- sizeTransform = SizeTransform(clip = false)
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheet.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheet.kt
deleted file mode 100644
index ba2cd23..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheet.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player
-
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.Modifier
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.bobbyesp.mediaplayer.service.ConnectionState
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views.MediaplayerCollapsedContent
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views.MediaplayerExpandedContent
-import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheet
-import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheetState
-import kotlinx.coroutines.launch
-
-@Composable
-fun MediaplayerSheet(
- modifier: Modifier = Modifier, state: DraggableBottomSheetState, viewModel: MediaplayerViewModel
-) {
- val playingSong =
- viewModel.songBeingPlayed.collectAsStateWithLifecycle().value?.mediaMetadata ?: return
- val connectionState =
- viewModel.connectionHandler.connectionState.collectAsStateWithLifecycle().value
-
- LaunchedEffect(connectionState, Unit) {
- if (connectionState is ConnectionState.Connected && state.isDismissed) {
- launch {
- state.collapseSoft()
- }
- }
- }
-
- DraggableBottomSheet(modifier = modifier, state = state, collapsedContent = {
- MediaplayerCollapsedContent(
- viewModel = viewModel, nowPlaying = playingSong
- )
- }, backgroundColor = MaterialTheme.colorScheme.surfaceContainerHigh, onDismiss = {
- viewModel.dismissPlayer()
- }) {
- MediaplayerExpandedContent(
- viewModel = viewModel,
- sheetState = state,
- )
- }
-}
-
-
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheetView.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheetView.kt
deleted file mode 100644
index b657877..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/MediaplayerSheetView.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player
-
-enum class MediaplayerSheetView {
- FULL_PLAYER,
- QUEUE
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/PlayerControls.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/PlayerControls.kt
deleted file mode 100644
index 6a3b0d8..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/PlayerControls.kt
+++ /dev/null
@@ -1,237 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.core.EaseInOutSine
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.core.rememberTransition
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.SkipNext
-import androidx.compose.material.icons.rounded.SkipPrevious
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Slider
-import androidx.compose.material3.SliderDefaults
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableLongStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.mediaplayer.presentation.components.buttons.PlayPauseAnimatedButton
-import com.bobbyesp.metadator.mediaplayer.presentation.components.others.RepeatStateIcon
-import com.bobbyesp.metadator.mediaplayer.presentation.components.others.ShuffleStateIcon
-import com.bobbyesp.metadator.core.presentation.components.text.ConditionedMarqueeText
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel
-import com.bobbyesp.ui.components.text.MarqueeTextGradientOptions
-import com.bobbyesp.utilities.Time
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun PlayerControls(
- modifier: Modifier = Modifier,
- viewModel: MediaplayerViewModel,
-) {
- val scope = rememberCoroutineScope()
-
- val viewState = viewModel.pageViewState.collectAsStateWithLifecycle().value
- val playerState = viewState.uiState
-
- val readyState = playerState as? MediaplayerViewModel.PlayerState.Ready
-
- val progress = readyState?.progress ?: 0f
-
- val playingSong = viewModel.songBeingPlayed.collectAsStateWithLifecycle().value?.mediaMetadata
-
- var sliderPosition by remember {
- mutableStateOf(null)
- }
-
- val duration by remember(readyState?.duration) {
- derivedStateOf {
- mutableLongStateOf(readyState?.duration ?: 0L)
- }
- }
-
- var temporalProgressString by remember {
- mutableStateOf(null)
- }
-
- val isPlaying = viewModel.isPlaying.collectAsStateWithLifecycle().value
- val isShuffleEnabled = viewModel.isShuffleEnabled.collectAsStateWithLifecycle().value
- val repeatMode = viewModel.repeatMode.collectAsStateWithLifecycle().value
-
- val transitionState = remember { MutableTransitionState(playingSong) }
-
- LaunchedEffect(playingSong) {
- transitionState.targetState = playingSong
- }
-
- val transition = rememberTransition(transitionState = transitionState)
-
-
- Column(
- modifier = modifier,
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- transition.AnimatedContent(
- modifier = Modifier
- .padding(horizontal = 24.dp)
- .align(Alignment.Start),
- transitionSpec = { AnimatedTextContentTransformation }) {
- Column {
- Text(
- text = it?.title.toString(),
- style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Medium)
- )
- ConditionedMarqueeText(
- text = it?.artist.toString(),
- style = MaterialTheme.typography.bodyLarge,
- customEasing = EaseInOutSine,
- sideGradient = MarqueeTextGradientOptions(
- color = MaterialTheme.colorScheme.surfaceContainer, left = false
- )
- )
- }
-
- }
- Column(
- modifier = Modifier.padding(horizontal = 18.dp)
- ) {
- val interactionSource = remember {
- MutableInteractionSource()
- }
-
- val songDuration by remember(readyState?.duration) {
- derivedStateOf {
- Time.formatDuration(readyState?.duration ?: 0L)
- }
- }
-
- val colors = SliderDefaults.colors()
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Slider(
- modifier = Modifier.height(20.dp),
- value = sliderPosition ?: progress,
- onValueChange = {
- sliderPosition = it
- temporalProgressString = Time.formatDuration((it * duration.longValue).toLong())
- },
- onValueChangeFinished = {
- viewModel.seekTo(sliderPosition ?: return@Slider)
- scope.launch {
- delay(350)
- sliderPosition = null
- temporalProgressString = null
- }
- },
- colors = colors,
- track = { sliderState ->
- SliderDefaults.Track(
- sliderState = sliderState,
- drawStopIndicator = null,
- thumbTrackGapSize = 4.dp,
- modifier = Modifier.height(8.dp)
- )
- },
- thumb = {
- SliderDefaults.Thumb(
- interactionSource = interactionSource,
- thumbSize = DpSize(width = 4.dp, height = 20.dp)
- )
- },
- interactionSource = interactionSource
- )
- Row(modifier = Modifier.padding(horizontal = 2.dp)) {
- Text(
- text = temporalProgressString ?: readyState?.progressString ?: "00:00",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Spacer(modifier = Modifier.weight(1f))
- Text(
- text = songDuration,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
-
- Spacer(modifier = Modifier.height(24.dp))
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
- verticalAlignment = Alignment.CenterVertically
- ) {
- IconButton(
- modifier = Modifier.size(PlayerCommandsButtonSize),
- onClick = viewModel::toggleShuffle
- ) {
- ShuffleStateIcon(
- modifier = Modifier,
- isShuffleEnabled = isShuffleEnabled
- )
- }
- IconButton(
- modifier = Modifier.size(SeekToButtonSize),
- onClick = { viewModel.seekToPrevious() }) {
- Icon(
- modifier = Modifier.fillMaxSize(),
- imageVector = Icons.Rounded.SkipPrevious,
- contentDescription = stringResource(id = R.string.seek_to_previous)
- )
- }
- PlayPauseAnimatedButton(isPlaying = isPlaying) {
- viewModel.togglePlayPause()
- }
- IconButton(
- modifier = Modifier.size(SeekToButtonSize),
- onClick = { viewModel.seekToNext() }) {
- Icon(
- modifier = Modifier.fillMaxSize(),
- imageVector = Icons.Rounded.SkipNext,
- contentDescription = stringResource(id = R.string.seek_to_previous)
- )
- }
-
- IconButton(
- modifier = Modifier.size(PlayerCommandsButtonSize),
- onClick = viewModel::toggleRepeat
- ) {
- RepeatStateIcon(
- modifier = Modifier,
- repeatMode = repeatMode
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/PlayerOptions.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/PlayerOptions.kt
deleted file mode 100644
index 7c77ad7..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/PlayerOptions.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.QueueMusic
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.bobbyesp.metadator.R
-
-@Composable
-fun PlayerOptions(
- modifier: Modifier = Modifier,
- onOpenQueue: () -> Unit = {}
-) {
- Box(
- modifier = modifier
- .clip(
- RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
- )
- .background(MaterialTheme.colorScheme.surfaceContainerHigh),
- contentAlignment = Alignment.Center
- ) {
- Row(
- modifier = Modifier
- .navigationBarsPadding()
- .padding(vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- IconButton(onClick = onOpenQueue) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.QueueMusic,
- contentDescription = stringResource(id = R.string.music_queue)
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MediaplayerCollapsedContent.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MediaplayerCollapsedContent.kt
deleted file mode 100644
index fca11cd..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MediaplayerCollapsedContent.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views
-
-import android.content.res.Configuration
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaMetadata
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.CollapsedPlayerHeight
-import com.bobbyesp.metadator.core.presentation.theme.MetadatorTheme
-
-@Composable
-fun MediaplayerCollapsedContent(
- nowPlaying: MediaMetadata,
- modifier: Modifier = Modifier,
- viewModel: MediaplayerViewModel,
-) {
- val viewState = viewModel.pageViewState.collectAsStateWithLifecycle().value
- val playerState = viewState.uiState
-
- val progress = (playerState as? MediaplayerViewModel.PlayerState.Ready)?.progress ?: 0f
- val isPlaying = viewModel.isPlaying.collectAsStateWithLifecycle().value
-
- Box(
- modifier = modifier
- .fillMaxWidth()
- .height(CollapsedPlayerHeight)
- .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)),
- contentAlignment = Alignment.Center
- ) {
- MiniplayerContent(
- modifier = Modifier
- .padding(horizontal = 12.dp)
- .padding(bottom = 6.dp),
- playingSong = nowPlaying,
- songProgress = progress,
- isPlaying = isPlaying,
- ) {
- viewModel.togglePlayPause()
- }
- }
-}
-
-@Preview
-@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
-@Composable
-private fun CollapsedContentPrev() {
- MetadatorTheme {
- val metadata = MediaItem.Builder().setUri("path").setMediaMetadata(
- MediaMetadata.Builder().setTitle("Bones").setArtist("Imagine Dragons")
- .setAlbumTitle("Mercury - Acts 1 & 2").setArtworkUri(null).build()
- ).build()
- MiniplayerContent(
- playingSong = metadata.mediaMetadata
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MediaplayerExpandedContent.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MediaplayerExpandedContent.kt
deleted file mode 100644
index 963592a..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MediaplayerExpandedContent.kt
+++ /dev/null
@@ -1,234 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views
-
-import android.content.res.Configuration
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ExperimentalSharedTransitionApi
-import androidx.compose.animation.SharedTransitionLayout
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.ArrowBackIosNew
-import androidx.compose.material.icons.rounded.MoreVert
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.core.presentation.components.image.AsyncImage
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.MediaplayerViewModel
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.MediaplayerSheetView
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.PlayerControls
-import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheetState
-import com.bobbyesp.ui.motion.MotionConstants
-import com.bobbyesp.ui.motion.tweenEnter
-import com.bobbyesp.ui.motion.tweenExit
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalSharedTransitionApi::class)
-@Composable
-fun MediaplayerExpandedContent(
- modifier: Modifier = Modifier,
- viewModel: MediaplayerViewModel,
- sheetState: DraggableBottomSheetState
-) {
- var view by remember {
- mutableStateOf(MediaplayerSheetView.FULL_PLAYER)
- }
- val scope = rememberCoroutineScope()
- val playingSong = viewModel.songBeingPlayed.collectAsStateWithLifecycle().value?.mediaMetadata
-
- val config = LocalConfiguration.current
-
- BackHandler {
- sheetState.collapseSoft()
- }
-
- SharedTransitionLayout(
- modifier = Modifier.fillMaxSize()
- ) {
- Surface(
- modifier = modifier
- .fillMaxSize()
- .padding(
- WindowInsets.systemBars
- .only(WindowInsetsSides.Horizontal)
- .asPaddingValues()
- ),
- color = MaterialTheme.colorScheme.surfaceContainer,
- ) {
- when (config.orientation) {
- Configuration.ORIENTATION_LANDSCAPE -> {
- Row(
- modifier = Modifier.fillMaxSize()
- ) {
- Box(
- modifier = Modifier
- .fillMaxHeight()
- .weight(1f),
- contentAlignment = Alignment.Center,
- ) {
- AsyncImage(
- imageModel = playingSong?.artworkUri,
- modifier = Modifier
- .fillMaxHeight(0.9f)
- .aspectRatio(1f)
- .clip(MaterialTheme.shapes.small)
- )
- Column(
- modifier = Modifier
- .align(Alignment.TopStart)
- .statusBarsPadding()
- .padding(start = 12.dp),
- ) {
- IconButton(onClick = {
- scope.launch {
- sheetState.collapseSoft()
- }
- }) {
- Icon(
- imageVector = Icons.Rounded.ArrowBackIosNew,
- contentDescription = stringResource(id = R.string.close),
- modifier = Modifier.rotate(-90f)
- )
- }
- IconButton(onClick = {
-
- }) {
- Icon(
- imageVector = Icons.Rounded.MoreVert,
- contentDescription = stringResource(
- id = R.string.more
- )
- )
- }
- }
- }
- Box(
- modifier = Modifier
- .fillMaxHeight()
- .weight(1f),
- contentAlignment = Alignment.Center,
- ) {
- PlayerControls(
- modifier = Modifier.fillMaxWidth(), viewModel = viewModel
- )
- }
- }
- }
-
- else -> {
- AnimatedContent(targetState = view, label = "", transitionSpec = {
- fadeIn(
- tweenEnter(delayMillis = MotionConstants.DURATION_EXIT_SHORT)
- ) togetherWith fadeOut(
- tweenExit(durationMillis = MotionConstants.DURATION_EXIT_SHORT)
- )
- }) {
- when (it) {
- MediaplayerSheetView.FULL_PLAYER -> {
- Column(
- modifier = Modifier.statusBarsPadding()
- ) {
- Row(
- modifier = Modifier
- .padding(horizontal = 12.dp)
- .padding(top = 12.dp, bottom = 12.dp)
- ) {
- IconButton(onClick = {
- scope.launch {
- sheetState.collapseSoft()
- }
- }) {
- Icon(
- imageVector = Icons.Rounded.ArrowBackIosNew,
- contentDescription = stringResource(id = R.string.close),
- modifier = Modifier.rotate(-90f)
- )
-
- }
- Spacer(modifier = Modifier.weight(1f))
- IconButton(onClick = {
-
- }) {
- Icon(
- imageVector = Icons.Rounded.MoreVert,
- contentDescription = stringResource(id = R.string.more)
- )
- }
- }
- Column(
- modifier = Modifier,
- verticalArrangement = Arrangement.spacedBy(24.dp)
- ) {
- AsyncImage(
- imageModel = playingSong?.artworkUri,
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(1f)
- .padding(horizontal = 24.dp, vertical = 16.dp)
- .clip(MaterialTheme.shapes.small)
- )
- PlayerControls(
- modifier = Modifier.fillMaxWidth(),
- viewModel = viewModel
- )
- }
- Spacer(modifier = Modifier.weight(1f))
-// PlayerOptions(
-// modifier = Modifier.fillMaxWidth(),
-// onOpenQueue = {
-// view = MediaplayerSheetView.QUEUE
-// }
-// )
- }
-
- }
-
- MediaplayerSheetView.QUEUE -> {
- PlayerQueue(
- imageModifier = Modifier,
- nowPlaying = playingSong,
- queue = emptyList(), onBackPressed = {
- view = MediaplayerSheetView.FULL_PLAYER
- }
- )
- }
- }
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MiniplayerContent.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MiniplayerContent.kt
deleted file mode 100644
index 621aded..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/MiniplayerContent.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.rememberTransition
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Pause
-import androidx.compose.material.icons.rounded.PlayArrow
-import androidx.compose.material3.Icon
-import androidx.compose.material3.LinearProgressIndicator
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ProgressIndicatorDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.media3.common.MediaMetadata
-import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.core.presentation.components.image.AsyncImage
-import com.bobbyesp.metadator.core.presentation.components.text.ConditionedMarqueeText
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.AnimatedTextContentTransformation
-import com.bobbyesp.ui.components.button.DynamicButton
-
-@Composable
-fun MiniplayerContent(
- modifier: Modifier = Modifier,
- playingSong: MediaMetadata,
- isPlaying: Boolean = false,
- songProgress: Float = 0f,
- onPlayPause: () -> Unit = {}
-) {
- val transitionState = remember { MutableTransitionState(playingSong) }
-
- LaunchedEffect(playingSong) {
- transitionState.targetState = playingSong
- }
-
- val transition = rememberTransition(transitionState = transitionState)
-
- val songCardArtworkUri = remember(transitionState.isIdle) {
- transitionState.currentState.artworkUri
- }
-
- val animatedSongProgress by animateFloatAsState(
- targetValue = songProgress,
- animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
- label = "Animated song progress"
- )
-
- Column(
- modifier = modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- AsyncImage(
- modifier = Modifier
- .size(52.dp)
- .clip(MaterialTheme.shapes.extraSmall),
- imageModel = songCardArtworkUri
- )
- Column(
- horizontalAlignment = Alignment.Start,
- modifier = Modifier
- .padding(vertical = 8.dp, horizontal = 6.dp)
- .weight(1f)
- ) {
- transition.AnimatedContent(transitionSpec = { AnimatedTextContentTransformation }) {
- Column {
- ConditionedMarqueeText(
- text = it.title.toString(),
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.Bold,
- fontSize = 16.sp
- )
-
- ConditionedMarqueeText(
- text = it.artist.toString(),
- style = MaterialTheme.typography.bodyMedium.copy(
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- ),
- fontSize = 12.sp
- )
- }
-
- }
- }
-
- DynamicButton(
- modifier = Modifier
- .size(42.dp)
- .padding(4.dp), icon = {
- Icon(
- imageVector = Icons.Rounded.Pause,
- contentDescription = stringResource(
- id = R.string.pause
- ),
- )
- }, icon2 = {
- Icon(
- imageVector = Icons.Rounded.PlayArrow,
- contentDescription = stringResource(
- id = R.string.play
- ),
- )
- }, isIcon1 = isPlaying
- ) {
- onPlayPause()
- }
- }
- LinearProgressIndicator(
- progress = {
- animatedSongProgress
- },
- modifier = Modifier.fillMaxWidth(),
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/PlayerQueue.kt b/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/PlayerQueue.kt
deleted file mode 100644
index 5ea96fe..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/mediaplayer/presentation/pages/mediaplayer/player/views/PlayerQueue.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.views
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.unit.dp
-import androidx.media3.common.MediaMetadata
-import com.bobbyesp.metadator.core.presentation.components.image.AsyncImage
-
-@Composable
-fun PlayerQueue(
- modifier: Modifier = Modifier,
- imageModifier: Modifier,
- nowPlaying: MediaMetadata?,
- queue: List,
- onPlay: (MediaMetadata) -> Unit = {},
- onBackPressed: () -> Unit = {}
-) {
- BackHandler {
- onBackPressed()
- }
-
- Column {
- Box(
- modifier = modifier
- .fillMaxWidth()
- .padding(24.dp)
- ) {
- AsyncImage(
- imageModel = nowPlaying?.artworkUri,
- modifier = imageModifier
- .clip(MaterialTheme.shapes.small)
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/di/MediaStoreViewModelsModule.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/di/MediaStoreViewModelsModule.kt
index 715aa69..3e24755 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/di/MediaStoreViewModelsModule.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/di/MediaStoreViewModelsModule.kt
@@ -4,6 +4,4 @@ import com.bobbyesp.metadator.mediastore.presentation.MediaStorePageViewModel
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
-val mediaStoreViewModelsModule = module {
- viewModel { MediaStorePageViewModel(get()) }
-}
\ No newline at end of file
+val mediaStoreViewModelsModule = module { viewModel { MediaStorePageViewModel(get()) } }
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/CompactCardSize.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/CompactCardSize.kt
index 0307a6a..1911b84 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/CompactCardSize.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/CompactCardSize.kt
@@ -18,11 +18,12 @@ enum class CompactCardSize(val value: Dp) {
CompactCardSize.entries.first { it.ordinal == this }
@Composable
- fun CompactCardSize.toShape(): CornerBasedShape = when (this) {
- SMALL -> MaterialTheme.shapes.small
- MEDIUM -> MaterialTheme.shapes.medium
- LARGE -> MaterialTheme.shapes.large
- EXTRA_LARGE -> MaterialTheme.shapes.extraLarge
- }
+ fun CompactCardSize.toShape(): CornerBasedShape =
+ when (this) {
+ SMALL -> MaterialTheme.shapes.small
+ MEDIUM -> MaterialTheme.shapes.medium
+ LARGE -> MaterialTheme.shapes.large
+ EXTRA_LARGE -> MaterialTheme.shapes.extraLarge
+ }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/LayoutType.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/LayoutType.kt
index 4f010cf..371e618 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/LayoutType.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/domain/enums/LayoutType.kt
@@ -12,4 +12,4 @@ enum class LayoutType(val icon: ImageVector) {
companion object {
fun Int.toListType(): LayoutType = LayoutType.entries.first { it.ordinal == this }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePage.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePage.kt
index fd7bce3..042fff5 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePage.kt
@@ -18,9 +18,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bobbyesp.metadator.R
+import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize
import com.bobbyesp.metadator.mediastore.domain.enums.LayoutType
import com.bobbyesp.metadator.mediastore.presentation.components.card.songs.HorizontalSongCard
-import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize
import com.bobbyesp.metadator.mediastore.presentation.components.card.songs.compact.CompactSongCard
import com.bobbyesp.metadator.mediastore.presentation.components.others.status.EmptyMediaStoreWarning
import com.bobbyesp.ui.common.pages.ErrorPage
@@ -41,7 +41,7 @@ fun MediaStorePage(
desiredLayout: LayoutType,
compactCardSize: CompactCardSize,
onReloadMediaStore: () -> Unit,
- onItemClicked: (Song) -> Unit
+ onItemClicked: (Song) -> Unit,
) {
val songsList = songs.value
@@ -49,60 +49,68 @@ fun MediaStorePage(
modifier = modifier.fillMaxSize(),
targetState = desiredLayout,
label = "List item transition",
- animationSpec = tween(200)
+ animationSpec = tween(200),
) { type ->
when (songsList) {
- is ResourceState.Loading -> LoadingPage(text = stringResource(R.string.loading_mediastore))
+ is ResourceState.Loading ->
+ LoadingPage(text = stringResource(R.string.loading_mediastore))
- is ResourceState.Error -> ErrorPage(
- modifier = Modifier.fillMaxSize(),
- throwable = Exception(songsList.message ?: stringResource(R.string.unknown))
- ) { onReloadMediaStore() }
+ is ResourceState.Error ->
+ ErrorPage(
+ modifier = Modifier.fillMaxSize(),
+ throwable = Exception(songsList.message ?: stringResource(R.string.unknown)),
+ ) {
+ onReloadMediaStore()
+ }
is ResourceState.Success -> {
- val dataSongsList = songsList.data ?: throw IllegalStateException(stringResource(R.string.data_null))
+ val dataSongsList =
+ songsList.data
+ ?: throw IllegalStateException(stringResource(R.string.data_null))
if (dataSongsList.isEmpty()) {
- EmptyMediaStoreWarning(
- modifier = Modifier.fillMaxSize()
- )
+ EmptyMediaStoreWarning(modifier = Modifier.fillMaxSize())
} else {
when (type) {
LayoutType.Grid -> {
LazyVerticalGridScrollbar(
- state = lazyGridState, settings = ScrollbarSettings(
- thumbUnselectedColor = MaterialTheme.colorScheme.onSurfaceVariant,
- thumbSelectedColor = MaterialTheme.colorScheme.primary,
- selectionActionable = ScrollbarSelectionActionable.WhenVisible,
- )
+ state = lazyGridState,
+ settings =
+ ScrollbarSettings(
+ thumbUnselectedColor =
+ MaterialTheme.colorScheme.onSurfaceVariant,
+ thumbSelectedColor = MaterialTheme.colorScheme.primary,
+ selectionActionable =
+ ScrollbarSelectionActionable.WhenVisible,
+ ),
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(125.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
contentPadding = PaddingValues(8.dp),
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background),
- state = lazyGridState
+ modifier =
+ Modifier.fillMaxSize()
+ .background(MaterialTheme.colorScheme.background),
+ state = lazyGridState,
) {
items(
count = dataSongsList.size,
key = { index -> dataSongsList[index].id },
- contentType = { _ -> "songItem" }) { index ->
+ contentType = { _ -> "songItem" },
+ ) { index ->
val song = dataSongsList[index]
CompactSongCard(
- modifier = Modifier
- .animateItem(
- fadeInSpec = null, fadeOutSpec = null
+ modifier =
+ Modifier.animateItem(
+ fadeInSpec = null,
+ fadeOutSpec = null,
),
size = compactCardSize,
name = song.title,
artists = song.artist,
artworkUri = song.artworkPath,
- onClick = {
- onItemClicked(song)
- }
+ onClick = { onItemClicked(song) },
)
}
}
@@ -111,31 +119,37 @@ fun MediaStorePage(
LayoutType.List -> {
LazyColumnScrollbar(
- state = lazyListState, settings = ScrollbarSettings(
- thumbUnselectedColor = MaterialTheme.colorScheme.onSurfaceVariant,
- thumbSelectedColor = MaterialTheme.colorScheme.primary,
- selectionActionable = ScrollbarSelectionActionable.WhenVisible,
- )
+ state = lazyListState,
+ settings =
+ ScrollbarSettings(
+ thumbUnselectedColor =
+ MaterialTheme.colorScheme.onSurfaceVariant,
+ thumbSelectedColor = MaterialTheme.colorScheme.primary,
+ selectionActionable =
+ ScrollbarSelectionActionable.WhenVisible,
+ ),
) {
LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background),
+ modifier =
+ Modifier.fillMaxSize()
+ .background(MaterialTheme.colorScheme.background),
state = lazyListState,
) {
items(
count = dataSongsList.size,
key = { index -> dataSongsList[index].id },
- contentType = { _ -> "songHorizontalItem" }) { index ->
+ contentType = { _ -> "songHorizontalItem" },
+ ) { index ->
val song = dataSongsList[index]
HorizontalSongCard(
song = song,
- modifier = Modifier.animateItem(
- fadeInSpec = null, fadeOutSpec = null
- ),
- onClick = {
- onItemClicked(song)
- })
+ modifier =
+ Modifier.animateItem(
+ fadeInSpec = null,
+ fadeOutSpec = null,
+ ),
+ onClick = { onItemClicked(song) },
+ )
}
}
}
@@ -145,4 +159,4 @@ fun MediaStorePage(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePageViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePageViewModel.kt
index b1135c6..c9eac83 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePageViewModel.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/MediaStorePageViewModel.kt
@@ -1,27 +1,29 @@
package com.bobbyesp.metadator.mediastore.presentation
-import android.content.Context
+import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.bobbyesp.utilities.mediastore.MediaStoreReceiver.Advanced.observeSongs
+import com.bobbyesp.mediaplayer.domain.model.MusicTrack
+import com.bobbyesp.mediaplayer.domain.repository.MusicLibraryRepository
import com.bobbyesp.utilities.mediastore.model.Song
import com.bobbyesp.utilities.states.ResourceState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-class MediaStorePageViewModel(
- context: Context
-) : ViewModel() {
+class MediaStorePageViewModel(musicLibraryRepository: MusicLibraryRepository) : ViewModel() {
private val _songs: MutableStateFlow>> =
MutableStateFlow(ResourceState.Loading())
val songs = _songs.asStateFlow()
private val mediaStoreSongsFlow =
- context.contentResolver.observeSongs()
+ musicLibraryRepository.observeMusicLibrary(null, null).map { musicTracks ->
+ musicTracks.map { it.toSong() }
+ }
private fun songsCollection() {
viewModelScope.launch(Dispatchers.IO) {
@@ -45,9 +47,23 @@ class MediaStorePageViewModel(
}
companion object {
+ fun MusicTrack.toSong(): Song {
+ return Song(
+ id = id,
+ title = title,
+ artist = artist ?: "",
+ album = album ?: "",
+ artworkPath = (artworkUri ?: "").toUri(),
+ duration = duration?.toDouble() ?: 0.0,
+ path = path,
+ fileName = this.path.substringAfterLast("/"),
+ )
+ }
+
interface Events {
data object StartObservingMediaStore : Events
+
data object ReloadMediaStore : Events
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/HorizontalSongCard.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/HorizontalSongCard.kt
index 151e944..92bb16a 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/HorizontalSongCard.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/HorizontalSongCard.kt
@@ -25,43 +25,31 @@ import com.bobbyesp.utilities.Time
import com.bobbyesp.utilities.mediastore.model.Song
@Composable
-fun HorizontalSongCard(
- modifier: Modifier = Modifier,
- song: Song,
- onClick: () -> Unit
-) {
- Surface(
- modifier = modifier,
- onClick = onClick
- ) {
+fun HorizontalSongCard(modifier: Modifier = Modifier, song: Song, onClick: () -> Unit) {
+ Surface(modifier = modifier, onClick = onClick) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(4.dp)
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
- AsyncImage(
- modifier = Modifier
- .size(64.dp)
- .padding(4.dp),
- imageModel = song.artworkPath
- )
+ AsyncImage(modifier = Modifier.size(64.dp).padding(4.dp), imageModel = song.artworkPath)
Column(
- horizontalAlignment = Alignment.Start, modifier = Modifier
- .padding(vertical = 8.dp, horizontal = 6.dp)
- .weight(1f)
+ horizontalAlignment = Alignment.Start,
+ modifier = Modifier.padding(vertical = 8.dp, horizontal = 6.dp).weight(1f),
) {
ConditionedMarqueeText(
text = song.title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
- fontSize = 15.sp
+ fontSize = 15.sp,
)
ConditionedMarqueeText(
text = song.artist,
- style = MaterialTheme.typography.bodyMedium.copy(
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- ),
- fontSize = 12.sp
+ style =
+ MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ ),
+ fontSize = 12.sp,
)
}
@@ -69,19 +57,18 @@ fun HorizontalSongCard(
text = Time.formatDuration(song.duration.toLong()),
style = MaterialTheme.typography.bodySmall,
fontSize = 12.sp,
- modifier = Modifier
- .padding(8.dp)
- .background(
- MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.1f),
- MaterialTheme.shapes.small
- )
- .padding(6.dp)
+ modifier =
+ Modifier.padding(8.dp)
+ .background(
+ MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.1f),
+ MaterialTheme.shapes.small,
+ )
+ .padding(6.dp),
)
}
}
}
-
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@@ -89,15 +76,18 @@ private fun HorizontalSongCardPreview() {
MetadatorTheme {
HorizontalSongCard(
modifier = Modifier.fillMaxWidth(),
- song = Song(
- id = 1,
- title = "Bones",
- artist = "Imagine Dragons",
- album = "Mercury - Acts 1 & 2",
- artworkPath = null,
- duration = 100.0,
- path = "path",
- fileName = "Bones"
- ), onClick = {})
+ song =
+ Song(
+ id = 1,
+ title = "Bones",
+ artist = "Imagine Dragons",
+ album = "Mercury - Acts 1 & 2",
+ artworkPath = null,
+ duration = 100.0,
+ path = "path",
+ fileName = "Bones",
+ ),
+ onClick = {},
+ )
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/VerticalSongCard.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/VerticalSongCard.kt
index d418879..e79b6e7 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/VerticalSongCard.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/VerticalSongCard.kt
@@ -21,60 +21,51 @@ import com.bobbyesp.metadator.core.presentation.theme.MetadatorTheme
import com.bobbyesp.utilities.mediastore.model.Song
@Composable
-fun VerticalSongCard(
- modifier: Modifier = Modifier,
- song: Song,
- onClick: () -> Unit
-) {
- Surface(
- modifier = modifier
- .clip(MaterialTheme.shapes.small),
- onClick = onClick
- ) {
+fun VerticalSongCard(modifier: Modifier = Modifier, song: Song, onClick: () -> Unit) {
+ Surface(modifier = modifier.clip(MaterialTheme.shapes.small), onClick = onClick) {
Column {
AsyncImage(
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(1f),
- imageModel = song.artworkPath
+ modifier = Modifier.fillMaxWidth().aspectRatio(1f),
+ imageModel = song.artworkPath,
)
- Column(
- horizontalAlignment = Alignment.Start, modifier = Modifier.padding(8.dp)
- ) {
+ Column(horizontalAlignment = Alignment.Start, modifier = Modifier.padding(8.dp)) {
ConditionedMarqueeText(
text = song.title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
- fontSize = 15.sp
+ fontSize = 15.sp,
)
ConditionedMarqueeText(
text = song.artist,
- style = MaterialTheme.typography.bodyMedium.copy(
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- ),
- fontSize = 12.sp
+ style =
+ MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ ),
+ fontSize = 12.sp,
)
}
}
}
}
-
@Preview
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun LocalSongCardPreview() {
MetadatorTheme {
VerticalSongCard(
- song = Song(
- id = 1,
- title = "Bones",
- artist = "Imagine Dragons",
- album = "Mercury - Acts 1 & 2",
- artworkPath = null,
- duration = 100.0,
- path = "path",
- fileName = "Bones"
- ), onClick = {})
+ song =
+ Song(
+ id = 1,
+ title = "Bones",
+ artist = "Imagine Dragons",
+ album = "Mercury - Acts 1 & 2",
+ artworkPath = null,
+ duration = 100.0,
+ path = "path",
+ fileName = "Bones",
+ ),
+ onClick = {},
+ )
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/compact/CompactSongCard.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/compact/CompactSongCard.kt
index 20e953b..bbbd269 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/compact/CompactSongCard.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/compact/CompactSongCard.kt
@@ -25,10 +25,10 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.bobbyesp.metadator.core.data.local.preferences.PreferencesKey.REDUCE_SHADOWS
import com.bobbyesp.metadator.core.data.local.preferences.datastore.rememberPreferenceState
-import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize.Companion.toShape
import com.bobbyesp.metadator.core.presentation.components.image.AsyncImage
import com.bobbyesp.metadator.core.presentation.components.text.ConditionedMarqueeText
import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize
+import com.bobbyesp.metadator.mediastore.domain.enums.CompactCardSize.Companion.toShape
@Composable
fun CompactSongCard(
@@ -38,22 +38,14 @@ fun CompactSongCard(
artworkUri: Uri? = null,
listIndex: Int? = null,
size: CompactCardSize = CompactCardSize.LARGE,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
val (reduceShadows, _) = rememberPreferenceState(REDUCE_SHADOWS)
val cardSize by remember(size) { mutableStateOf(size.value) }
val formalizedShape = size.toShape()
- Box(
- modifier = modifier
- .clip(formalizedShape)
- .size(cardSize)
- .clickable(onClick = onClick)
- ) {
- AsyncImage(
- modifier = Modifier.fillMaxSize(),
- imageModel = artworkUri,
- )
+ Box(modifier = modifier.clip(formalizedShape).size(cardSize).clickable(onClick = onClick)) {
+ AsyncImage(modifier = Modifier.fillMaxSize(), imageModel = artworkUri)
listIndex?.let {
Text(
@@ -61,31 +53,31 @@ fun CompactSongCard(
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(alpha = 0.8f),
fontWeight = FontWeight.Bold,
- modifier = Modifier
- .padding(8.dp)
- .align(Alignment.TopEnd)
+ modifier = Modifier.padding(8.dp).align(Alignment.TopEnd),
)
}
Column(
- modifier = Modifier
- .background(
- brush = Brush.verticalGradient(
- colors = listOf(Color.Transparent, MaterialTheme.colorScheme.scrim),
- startY = 0f, endY = 500f
- ),
- alpha = if (reduceShadows.value) 0f else 0.6f
- )
- .fillMaxSize()
- .padding(horizontal = 8.dp, vertical = 6.dp),
+ modifier =
+ Modifier.background(
+ brush =
+ Brush.verticalGradient(
+ colors = listOf(Color.Transparent, MaterialTheme.colorScheme.scrim),
+ startY = 0f,
+ endY = 500f,
+ ),
+ alpha = if (reduceShadows.value) 0f else 0.6f,
+ )
+ .fillMaxSize()
+ .padding(horizontal = 8.dp, vertical = 6.dp),
verticalArrangement = Arrangement.Bottom,
- horizontalAlignment = Alignment.Start
+ horizontalAlignment = Alignment.Start,
) {
ConditionedMarqueeText(
text = name,
style = MaterialTheme.typography.titleSmall,
color = Color.White,
overflow = TextOverflow.Ellipsis,
- maxLines = 1
+ maxLines = 1,
)
if (artists.isNotEmpty()) {
@@ -94,9 +86,9 @@ fun CompactSongCard(
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(alpha = 0.6f),
overflow = TextOverflow.Ellipsis,
- maxLines = 1
+ maxLines = 1,
)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/spotify/SpotifyHorizontalSongCard.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/spotify/SpotifyHorizontalSongCard.kt
index 3b53640..60539a5 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/spotify/SpotifyHorizontalSongCard.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/card/songs/spotify/SpotifyHorizontalSongCard.kt
@@ -39,51 +39,43 @@ fun SpotifyHorizontalSongCard(
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
) {
- val albumArtPath by remember(track) {
- mutableStateOf(track.album.images?.getOrNull(0)?.url)
- }
+ val albumArtPath by remember(track) { mutableStateOf(track.album.images?.getOrNull(0)?.url) }
Surface(
- modifier = modifier
- .fillMaxWidth()
- .clip(MaterialTheme.shapes.extraSmall)
- .combinedClickable(
- onClick = onClick,
- onLongClick = onLongClick
- ),
- color = surfaceColor
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.extraSmall)
+ .combinedClickable(onClick = onClick, onLongClick = onLongClick),
+ color = surfaceColor,
) {
Row(
modifier = innerModifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
) {
if (listIndex != null) {
Text(
text = "${listIndex + 1}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
- modifier = Modifier
- .padding(8.dp)
- .padding(end = 4.dp)
+ modifier = Modifier.padding(8.dp).padding(end = 4.dp),
)
}
Box(contentAlignment = Alignment.CenterStart) {
AsyncImage(
modifier = imageModifier.size(64.dp),
imageModel = albumArtPath,
- shape = MaterialTheme.shapes.extraSmall
+ shape = MaterialTheme.shapes.extraSmall,
)
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
+ horizontalArrangement = Arrangement.Center,
) {
Column(
horizontalAlignment = Alignment.Start,
- modifier = Modifier
- .padding(8.dp)
- .weight(1f)
+ modifier = Modifier.padding(8.dp).weight(1f),
) {
ConditionedMarqueeText(
text = track.name,
@@ -92,12 +84,13 @@ fun SpotifyHorizontalSongCard(
)
ConditionedMarqueeText(
text = track.artists.formatArtists(),
- style = MaterialTheme.typography.bodyMedium.copy(
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- )
+ style =
+ MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ ),
)
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/others/status/EmptyMediaStoreWarning.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/others/status/EmptyMediaStoreWarning.kt
index 7707217..7d42d81 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/others/status/EmptyMediaStoreWarning.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/components/others/status/EmptyMediaStoreWarning.kt
@@ -27,35 +27,28 @@ import com.bobbyesp.metadator.R
import com.bobbyesp.metadator.core.presentation.theme.MetadatorTheme
@Composable
-fun EmptyMediaStoreWarning(
- modifier: Modifier = Modifier
-) {
- Box(
- modifier = modifier,
- contentAlignment = Alignment.Center
- ) {
+fun EmptyMediaStoreWarning(modifier: Modifier = Modifier) {
+ Box(modifier = modifier, contentAlignment = Alignment.Center) {
Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
- horizontalAlignment = Alignment.CenterHorizontally
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier.size(72.dp),
imageVector = Icons.TwoTone.LibraryMusic,
- contentDescription = stringResource(id = R.string.empty_media_store)
+ contentDescription = stringResource(id = R.string.empty_media_store),
)
HorizontalDivider(modifier = Modifier.fillMaxWidth(0.7f))
Text(
text = stringResource(id = R.string.empty_media_store),
style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.SemiBold
+ fontWeight = FontWeight.SemiBold,
)
Text(
text = stringResource(id = R.string.empty_media_store_desc),
style = MaterialTheme.typography.bodyMedium,
- textAlign = TextAlign.Center
+ textAlign = TextAlign.Center,
)
}
}
@@ -67,9 +60,7 @@ fun EmptyMediaStoreWarning(
private fun EmptyMediaStorePrev() {
MetadatorTheme {
EmptyMediaStoreWarning(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background)
+ modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/pages/home/HomePage.kt b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/pages/home/HomePage.kt
index a4b7ebf..eda3ebe 100644
--- a/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/pages/home/HomePage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/mediastore/presentation/pages/home/HomePage.kt
@@ -63,10 +63,10 @@ import com.bobbyesp.ui.components.dropdown.DropdownItemContainer
import com.bobbyesp.ui.components.text.AutoResizableText
import com.bobbyesp.utilities.mediastore.model.Song
import com.bobbyesp.utilities.states.ResourceState
-import com.bobbyesp.utilities.ui.permission.PermissionNotGrantedDialog
-import com.bobbyesp.utilities.ui.permission.PermissionRequestHandler
-import com.bobbyesp.utilities.ui.permission.PermissionType.Companion.toPermissionType
-import com.bobbyesp.utilities.ui.rememberForeverLazyGridState
+import com.bobbyesp.utilities.ui.layouts.lazygrid.rememberForeverLazyGridState
+import com.bobbyesp.utilities.ui.permissions.PermissionNotGrantedDialog
+import com.bobbyesp.utilities.ui.permissions.PermissionRequestHandler
+import com.bobbyesp.utilities.ui.permissions.PermissionType.Companion.toPermissionType
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -80,14 +80,16 @@ fun HomePage(
modifier: Modifier = Modifier,
songs: State>>,
preferences: State,
- onEvent: (MediaStorePageViewModel.Companion.Events) -> Unit = {}
+ onEvent: (MediaStorePageViewModel.Companion.Events) -> Unit = {},
) {
val context = LocalActivity.current
- val readAudioFiles = when {
- Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU -> Manifest.permission.READ_EXTERNAL_STORAGE
+ val readAudioFiles =
+ when {
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ->
+ Manifest.permission.READ_EXTERNAL_STORAGE
- else -> Manifest.permission.READ_MEDIA_AUDIO
- }
+ else -> Manifest.permission.READ_MEDIA_AUDIO
+ }
val storagePermissionState = rememberPermissionState(permission = readAudioFiles)
@@ -100,9 +102,7 @@ fun HomePage(
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
- var moreOptionsVisible by remember {
- mutableStateOf(false)
- }
+ var moreOptionsVisible by remember { mutableStateOf(false) }
val mediaStoreLazyGridState = rememberForeverLazyGridState(key = "lazyGrid")
val mediaStoreLazyColumnState = rememberLazyListState()
@@ -113,14 +113,10 @@ fun HomePage(
val songCardSize = preferences.value.songCardSize
val gridIsFirstItemVisible by remember {
- derivedStateOf {
- mediaStoreLazyGridState.firstVisibleItemIndex == 0
- }
+ derivedStateOf { mediaStoreLazyGridState.firstVisibleItemIndex == 0 }
}
val listIsFirstItemVisible by remember {
- derivedStateOf {
- mediaStoreLazyColumnState.firstVisibleItemIndex == 0
- }
+ derivedStateOf { mediaStoreLazyColumnState.firstVisibleItemIndex == 0 }
}
Scaffold(
@@ -128,95 +124,76 @@ fun HomePage(
topBar = {
TopAppBar(
title = {
- Column(
- horizontalAlignment = Alignment.Start,
- ) {
+ Column(horizontalAlignment = Alignment.Start) {
Text(
text = stringResource(id = R.string.app_name).uppercase(),
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily.Monospace,
- style = MaterialTheme.typography.titleLarge.copy(
- letterSpacing = 4.sp,
- ),
+ style = MaterialTheme.typography.titleLarge.copy(letterSpacing = 4.sp),
)
AutoResizableText(
text = stringResource(id = R.string.app_desc).uppercase(),
fontWeight = FontWeight.Normal,
fontFamily = FontFamily.Monospace,
- style = MaterialTheme.typography.bodySmall.copy(
- letterSpacing = 2.sp,
- ),
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
+ style = MaterialTheme.typography.bodySmall.copy(letterSpacing = 2.sp),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
}
- }, actions = {
+ },
+ actions = {
IconButton(
onClick = {
- scope.launch {
- moreOptionsVisible = false
- }
+ scope.launch { moreOptionsVisible = false }
navController.navigate(Route.MediaplayerNavigator)
}
) {
Icon(
imageVector = Icons.Rounded.PlayArrow,
- contentDescription = stringResource(
- id = R.string.mediaplayer
- )
+ contentDescription = stringResource(id = R.string.mediaplayer),
)
}
IconButton(
- onClick = { navController.navigate(Route.SettingsNavigator.Settings) }) {
+ onClick = { navController.navigate(Route.SettingsNavigator.Settings) }
+ ) {
Icon(
imageVector = Icons.Rounded.Settings,
- contentDescription = stringResource(
- id = R.string.settings
- )
+ contentDescription = stringResource(id = R.string.settings),
)
}
- IconButton(
- onClick = {
- moreOptionsVisible = !moreOptionsVisible
- }) {
+ IconButton(onClick = { moreOptionsVisible = !moreOptionsVisible }) {
Icon(
imageVector = Icons.Rounded.MoreVert,
- contentDescription = stringResource(
- id = R.string.open_more_options
- )
+ contentDescription = stringResource(id = R.string.open_more_options),
)
}
AnimatedDropdownMenu(
- expanded = moreOptionsVisible, onDismissRequest = {
- moreOptionsVisible = false
- }) {
+ expanded = moreOptionsVisible,
+ onDismissRequest = { moreOptionsVisible = false },
+ ) {
DropdownMenuContent(
desiredLayout = configuredLayout,
- onLayoutChanged = {
- setConfiguredLayout(it.name)
- }
+ onLayoutChanged = { setConfiguredLayout(it.name) },
)
}
-
- })
- }, floatingActionButton = {
+ },
+ )
+ },
+ floatingActionButton = {
when (configuredLayout) {
LayoutType.Grid -> {
AnimatedVisibility(
visible = !gridIsFirstItemVisible,
enter = fadeIn() + scaleIn(),
- exit = fadeOut() + scaleOut()
+ exit = fadeOut() + scaleOut(),
) {
FloatingActionButton(
onClick = {
- scope.launch {
- mediaStoreLazyGridState.animateScrollToItem(0)
- }
- }) {
+ scope.launch { mediaStoreLazyGridState.animateScrollToItem(0) }
+ }
+ ) {
Icon(
imageVector = Icons.Rounded.KeyboardDoubleArrowUp,
- contentDescription = stringResource(
- id = R.string.scroll_to_top
- )
+ contentDescription = stringResource(id = R.string.scroll_to_top),
)
}
}
@@ -226,36 +203,31 @@ fun HomePage(
AnimatedVisibility(
visible = !listIsFirstItemVisible,
enter = fadeIn() + scaleIn(),
- exit = fadeOut() + scaleOut()
+ exit = fadeOut() + scaleOut(),
) {
- FloatingActionButton(onClick = {
- scope.launch {
- mediaStoreLazyColumnState.animateScrollToItem(0)
+ FloatingActionButton(
+ onClick = {
+ scope.launch { mediaStoreLazyColumnState.animateScrollToItem(0) }
}
- }) {
+ ) {
Icon(
imageVector = Icons.Rounded.KeyboardDoubleArrowUp,
- contentDescription = stringResource(
- id = R.string.scroll_to_top
- )
+ contentDescription = stringResource(id = R.string.scroll_to_top),
)
}
}
}
}
- }) { paddingValues ->
+ },
+ ) { paddingValues ->
PermissionRequestHandler(
permissionState = storagePermissionState,
deniedContent = { shouldShowRationale ->
PermissionNotGrantedDialog(
neededPermissions = persistentListOf(readAudioFiles.toPermissionType()),
- onGrantRequest = {
- storagePermissionState.launchPermissionRequest()
- },
- onDismissRequest = {
- context?.finish()
- },
- shouldShowRationale = shouldShowRationale
+ onGrantRequest = { storagePermissionState.launchPermissionRequest() },
+ onDismissRequest = { context?.finish() },
+ shouldShowRationale = shouldShowRationale,
)
},
content = {
@@ -273,28 +245,27 @@ fun HomePage(
navController.navigate(
Route.UtilitiesNavigator.TagEditor(song.toParcelableSong())
)
- })
- })
+ },
+ )
+ },
+ )
}
}
@Composable
-private fun DropdownMenuContent(
- desiredLayout: LayoutType,
- onLayoutChanged: (LayoutType) -> Unit,
-) {
+private fun DropdownMenuContent(desiredLayout: LayoutType, onLayoutChanged: (LayoutType) -> Unit) {
val availableLayoutType = LayoutType.entries.toImmutableList()
Column(
modifier = Modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
- horizontalAlignment = Alignment.CenterHorizontally
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(id = R.string.layout_type),
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
- style = MaterialTheme.typography.labelMedium
+ style = MaterialTheme.typography.labelMedium,
)
DropdownItemContainer(
modifier = Modifier,
@@ -303,22 +274,21 @@ private fun DropdownMenuContent(
availableLayoutType.forEachIndexed { index, listType ->
SegmentedButton(
selected = desiredLayout.ordinal == listType.ordinal,
- onClick = {
- onLayoutChanged(listType)
- },
- shape = SegmentedButtonDefaults.itemShape(
- index = index, count = availableLayoutType.size
- ),
+ onClick = { onLayoutChanged(listType) },
+ shape =
+ SegmentedButtonDefaults.itemShape(
+ index = index,
+ count = availableLayoutType.size,
+ ),
) {
Icon(
imageVector = listType.icon,
- contentDescription = stringResource(id = R.string.list_type)
+ contentDescription = stringResource(id = R.string.list_type),
)
}
}
}
- }
+ },
)
}
}
-
diff --git a/app/src/main/java/com/bobbyesp/metadator/onboarding/OnboardingRouting.kt b/app/src/main/java/com/bobbyesp/metadator/onboarding/OnboardingRouting.kt
index 134bcfd..e925557 100644
--- a/app/src/main/java/com/bobbyesp/metadator/onboarding/OnboardingRouting.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/onboarding/OnboardingRouting.kt
@@ -10,33 +10,30 @@ import com.bobbyesp.metadator.core.util.getNeededStoragePermissions
import com.bobbyesp.metadator.onboarding.presentation.pages.OnboardingPermissionsPage
import com.bobbyesp.metadator.onboarding.presentation.pages.OnboardingWelcomePage
import com.bobbyesp.ui.motion.animatedComposable
-import com.bobbyesp.utilities.ui.permission.PermissionType.Companion.toPermissionType
+import com.bobbyesp.utilities.ui.permissions.PermissionType.Companion.toPermissionType
fun NavGraphBuilder.onboardingRouting(
onNavigate: (Route) -> Unit,
- onCompletedOnboarding: () -> Unit
+ onCompletedOnboarding: () -> Unit,
) {
navigation(
- startDestination = Route.OnboardingNavigator.Welcome::class,
+ startDestination = Route.OnboardingNavigator.Welcome::class
) {
animatedComposable {
OnboardingWelcomePage(
- onGetStarted = {
- onNavigate(Route.OnboardingNavigator.Permissions)
- }
+ onGetStarted = { onNavigate(Route.OnboardingNavigator.Permissions) }
)
}
animatedComposable {
-
- val neededPermissions by remember { mutableStateOf(getNeededStoragePermissions().map { it.toPermissionType() }) }
+ val neededPermissions by remember {
+ mutableStateOf(getNeededStoragePermissions().map { it.toPermissionType() })
+ }
OnboardingPermissionsPage(
neededPermissions = neededPermissions,
- onNextClick = {
- onCompletedOnboarding()
- }
+ onNextClick = { onCompletedOnboarding() },
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/onboarding/domain/PermissionItem.kt b/app/src/main/java/com/bobbyesp/metadator/onboarding/domain/PermissionItem.kt
index fed097b..a8a04ef 100644
--- a/app/src/main/java/com/bobbyesp/metadator/onboarding/domain/PermissionItem.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/onboarding/domain/PermissionItem.kt
@@ -2,12 +2,12 @@ package com.bobbyesp.metadator.onboarding.domain
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.vector.ImageVector
-import com.bobbyesp.utilities.ui.permission.PermissionType
+import com.bobbyesp.utilities.ui.permissions.PermissionType
@Stable
data class PermissionItem(
val permission: PermissionType,
val icon: ImageVector,
val isGranted: Boolean,
- val onClick: () -> Unit
-)
\ No newline at end of file
+ val onClick: () -> Unit,
+)
diff --git a/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/components/OnboardingScreenHeader.kt b/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/components/OnboardingScreenHeader.kt
index d697019..9ea52a5 100644
--- a/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/components/OnboardingScreenHeader.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/components/OnboardingScreenHeader.kt
@@ -29,33 +29,33 @@ fun OnboardingScreenHeader(
modifier: Modifier = Modifier,
title: String,
description: String? = null,
- icon: ImageVector
+ icon: ImageVector,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondary,
- modifier = Modifier
- .size(150.dp)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.secondary)
- .padding(32.dp)
+ modifier =
+ Modifier.size(150.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.secondary)
+ .padding(32.dp),
)
Column(
modifier = Modifier,
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = title.uppercase(),
style = MaterialTheme.typography.headlineLarge,
fontFamily = FontFamily.Monospace,
- letterSpacing = 5.sp
+ letterSpacing = 5.sp,
)
description?.let {
Text(
@@ -63,12 +63,11 @@ fun OnboardingScreenHeader(
style = MaterialTheme.typography.bodyMediumEmphasized,
fontFamily = FontFamily.Monospace,
letterSpacing = 1.sp,
- textAlign = TextAlign.Center
+ textAlign = TextAlign.Center,
)
}
}
}
-
}
@Preview
@@ -76,14 +75,13 @@ fun OnboardingScreenHeader(
private fun Preview() {
MaterialTheme {
OnboardingScreenHeader(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.background)
- .padding(16.dp),
+ modifier = Modifier.background(MaterialTheme.colorScheme.background).padding(16.dp),
title = "Permissions",
- description = "This is a very large text just to see how this reacts." + " " +
+ description =
+ "This is a very large text just to see how this reacts." +
+ " " +
"Permissions are needed for the app to work properly",
- icon = Icons.Rounded.Security
+ icon = Icons.Rounded.Security,
)
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingPermissionsPage.kt b/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingPermissionsPage.kt
index d031895..56337be 100644
--- a/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingPermissionsPage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingPermissionsPage.kt
@@ -42,13 +42,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.mediaplayer.presentation.pages.mediaplayer.player.AnimatedTextContentTransformation
import com.bobbyesp.metadator.onboarding.domain.PermissionItem
import com.bobbyesp.metadator.onboarding.presentation.components.OnboardingScreenHeader
import com.bobbyesp.ui.components.others.AdditionalInformation
+import com.bobbyesp.ui.util.AnimatedTextContentTransformation
import com.bobbyesp.ui.util.isDeviceInLandscape
-import com.bobbyesp.utilities.ui.permission.PermissionType
-import com.bobbyesp.utilities.ui.permission.PermissionType.Companion.toPermissionType
+import com.bobbyesp.utilities.ui.permissions.PermissionType
+import com.bobbyesp.utilities.ui.permissions.PermissionType.Companion.toPermissionType
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -60,32 +60,28 @@ fun OnboardingPermissionsPage(
onNextClick: () -> Unit = {},
) {
- val shouldShowRationale = neededPermissions.any {
- !rememberPermissionState(it.permission).status.isGranted
- }
+ val shouldShowRationale =
+ neededPermissions.any { !rememberPermissionState(it.permission).status.isGranted }
- val allPermissionsGranted = neededPermissions.all {
- rememberPermissionState(it.permission).status.isGranted
- }
+ val allPermissionsGranted =
+ neededPermissions.all { rememberPermissionState(it.permission).status.isGranted }
- val permissions = neededPermissions.map {
- val storagePermissionState = rememberPermissionState(it.permission)
-
- PermissionItem(
- permission = it.permission.toPermissionType(),
- icon = it.toPermissionIcon(),
- isGranted = storagePermissionState.status.isGranted,
- onClick = { storagePermissionState.launchPermissionRequest() }
- )
- }
+ val permissions =
+ neededPermissions.map {
+ val storagePermissionState = rememberPermissionState(it.permission)
+ PermissionItem(
+ permission = it.permission.toPermissionType(),
+ icon = it.toPermissionIcon(),
+ isGranted = storagePermissionState.status.isGranted,
+ onClick = { storagePermissionState.launchPermissionRequest() },
+ )
+ }
Scaffold(
modifier = Modifier.fillMaxSize(),
floatingActionButton = {
- val transitionState = remember {
- MutableTransitionState(allPermissionsGranted)
- }
+ val transitionState = remember { MutableTransitionState(allPermissionsGranted) }
LaunchedEffect(allPermissionsGranted) {
transitionState.targetState = allPermissionsGranted
@@ -93,90 +89,81 @@ fun OnboardingPermissionsPage(
val transition = rememberTransition(transitionState = transitionState)
-
Button(
- modifier = Modifier
- .padding(16.dp),
+ modifier = Modifier.padding(16.dp),
onClick = {
- //if they are all granted, go to the next page, otherwise request the permissions
+ // if they are all granted, go to the next page, otherwise request the
+ // permissions
if (allPermissionsGranted) {
onNextClick()
} else {
- permissions.fastForEach {
- it.onClick()
- }
+ permissions.fastForEach { it.onClick() }
}
},
) {
- transition.AnimatedContent(
- transitionSpec = { AnimatedTextContentTransformation }
- ) {
+ transition.AnimatedContent(transitionSpec = { AnimatedTextContentTransformation }) {
Row(
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(4.dp)
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
val textId =
if (it) R.string.finish else com.bobbyesp.utilities.R.string.grant
val icon =
- if (it) Icons.AutoMirrored.Rounded.KeyboardArrowRight else Icons.Rounded.Check
+ if (it) Icons.AutoMirrored.Rounded.KeyboardArrowRight
+ else Icons.Rounded.Check
Text(text = stringResource(id = textId))
Icon(
modifier = Modifier,
imageVector = icon,
- contentDescription = stringResource(id = textId)
+ contentDescription = stringResource(id = textId),
)
}
}
}
- }
+ },
) { paddingValues ->
if (isDeviceInLandscape()) {
val scrollState = rememberScrollState()
Row(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues),
+ modifier = Modifier.fillMaxSize().padding(paddingValues),
horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
) {
OnboardingScreenHeader(
modifier = Modifier.padding(16.dp).weight(0.4f),
title = stringResource(R.string.permissions),
description = stringResource(R.string.permissions_description),
- icon = Icons.Rounded.Security
+ icon = Icons.Rounded.Security,
)
PermissionsScreenContent(
modifier = Modifier.weight(0.6f),
permissions = permissions,
shouldShowRationale = shouldShowRationale,
- scrollState = scrollState
+ scrollState = scrollState,
)
-
}
} else {
Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- .padding(horizontal = 16.dp),
+ modifier =
+ Modifier.fillMaxSize().padding(paddingValues).padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ verticalArrangement = Arrangement.spacedBy(16.dp),
) {
OnboardingScreenHeader(
modifier = Modifier.padding(16.dp),
title = stringResource(R.string.permissions),
description = stringResource(R.string.permissions_description),
- icon = Icons.Rounded.Security
+ icon = Icons.Rounded.Security,
)
PermissionsScreenContent(
modifier = Modifier,
permissions = permissions,
shouldShowRationale = shouldShowRationale,
- scrollState = rememberScrollState()
+ scrollState = rememberScrollState(),
)
}
}
@@ -188,41 +175,33 @@ fun PermissionsScreenContent(
modifier: Modifier = Modifier,
permissions: List,
shouldShowRationale: Boolean,
- scrollState: ScrollState
+ scrollState: ScrollState,
) {
- Column(
- modifier = modifier
- .padding(horizontal = 16.dp)
- .verticalScroll(scrollState)
- ) {
+ Column(modifier = modifier.padding(horizontal = 16.dp).verticalScroll(scrollState)) {
Column(
modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ verticalArrangement = Arrangement.spacedBy(4.dp),
) {
PermissionItemsGroup(
modifier = Modifier.padding(vertical = 16.dp),
- permissionItems = permissions
+ permissionItems = permissions,
)
}
- AnimatedContent(
- targetState = shouldShowRationale,
- modifier = Modifier
- ) { visible ->
+ AnimatedContent(targetState = shouldShowRationale, modifier = Modifier) { visible ->
if (visible) {
Column(
modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ verticalArrangement = Arrangement.spacedBy(16.dp),
) {
HorizontalDivider()
AdditionalInformation(
modifier = Modifier,
text = stringResource(R.string.permissions_additional_information),
- fontFamily = FontFamily.Monospace
+ fontFamily = FontFamily.Monospace,
)
}
-
}
}
}
@@ -231,63 +210,59 @@ fun PermissionsScreenContent(
@Preview
@Composable
private fun OnboardingPermissionsPagePreview() {
- val neededPermissions = listOf(
- PermissionType.READ_EXTERNAL_STORAGE,
- PermissionType.READ_MEDIA_AUDIO
- )
- MaterialTheme {
- OnboardingPermissionsPage(
- neededPermissions = neededPermissions
- )
- }
+ val neededPermissions =
+ listOf(PermissionType.READ_EXTERNAL_STORAGE, PermissionType.READ_MEDIA_AUDIO)
+ MaterialTheme { OnboardingPermissionsPage(neededPermissions = neededPermissions) }
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun PermissionItemButton(
- permissionItem: PermissionItem,
- modifier: Modifier = Modifier,
-) {
- val animatedBackgroundColor by animateColorAsState(
- targetValue = if (!permissionItem.isGranted) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary
- )
+private fun PermissionItemButton(permissionItem: PermissionItem, modifier: Modifier = Modifier) {
+ val animatedBackgroundColor by
+ animateColorAsState(
+ targetValue =
+ if (!permissionItem.isGranted) MaterialTheme.colorScheme.tertiary
+ else MaterialTheme.colorScheme.primary
+ )
- val animatedTextColor by animateColorAsState(
- targetValue = if (!permissionItem.isGranted) MaterialTheme.colorScheme.onTertiary else MaterialTheme.colorScheme.onPrimary
- )
+ val animatedTextColor by
+ animateColorAsState(
+ targetValue =
+ if (!permissionItem.isGranted) MaterialTheme.colorScheme.onTertiary
+ else MaterialTheme.colorScheme.onPrimary
+ )
Row(
- modifier = modifier
- .background(color = MaterialTheme.colorScheme.surfaceContainer)
- .combinedClickable(
- onClick = permissionItem.onClick
- )
- .padding(horizontal = 24.dp, vertical = 16.dp),
+ modifier =
+ modifier
+ .background(color = MaterialTheme.colorScheme.surfaceContainer)
+ .combinedClickable(onClick = permissionItem.onClick)
+ .padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
- modifier = Modifier
- .size(48.dp)
- .clip(CircleShape)
- .background(animatedBackgroundColor)
- .padding(8.dp),
+ modifier =
+ Modifier.size(48.dp)
+ .clip(CircleShape)
+ .background(animatedBackgroundColor)
+ .padding(8.dp),
imageVector = permissionItem.icon,
tint = animatedTextColor,
- contentDescription = null
+ contentDescription = null,
)
Column {
Text(
text = permissionItem.permission.toPermissionString(),
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = permissionItem.permission.toPermissionDescription(),
style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -298,41 +273,38 @@ private fun PermissionItemsGroup(
permissionItems: List,
modifier: Modifier = Modifier,
) {
- Column(
- modifier = modifier,
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
+ Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
permissionItems.fastForEachIndexed { index, item ->
PermissionItemButton(
permissionItem = item,
- modifier = Modifier
- .fillMaxWidth()
- .clip(
- when {
- permissionItems.size == 1 -> {
- MaterialTheme.shapes.extraLarge
- }
+ modifier =
+ Modifier.fillMaxWidth()
+ .clip(
+ when {
+ permissionItems.size == 1 -> {
+ MaterialTheme.shapes.extraLarge
+ }
- index == 0 -> {
- MaterialTheme.shapes.extraLarge.copy(
- bottomStart = MaterialTheme.shapes.medium.bottomStart,
- bottomEnd = MaterialTheme.shapes.medium.bottomEnd
- )
- }
+ index == 0 -> {
+ MaterialTheme.shapes.extraLarge.copy(
+ bottomStart = MaterialTheme.shapes.medium.bottomStart,
+ bottomEnd = MaterialTheme.shapes.medium.bottomEnd,
+ )
+ }
- index == permissionItems.lastIndex -> {
- MaterialTheme.shapes.extraLarge.copy(
- topStart = MaterialTheme.shapes.medium.topStart,
- topEnd = MaterialTheme.shapes.medium.topEnd
- )
- }
+ index == permissionItems.lastIndex -> {
+ MaterialTheme.shapes.extraLarge.copy(
+ topStart = MaterialTheme.shapes.medium.topStart,
+ topEnd = MaterialTheme.shapes.medium.topEnd,
+ )
+ }
- else -> {
- MaterialTheme.shapes.medium
+ else -> {
+ MaterialTheme.shapes.medium
+ }
}
- }
- )
+ ),
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingWelcomePage.kt b/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingWelcomePage.kt
index ce1564c..b09236c 100644
--- a/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingWelcomePage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/onboarding/presentation/pages/OnboardingWelcomePage.kt
@@ -24,35 +24,22 @@ import com.bobbyesp.metadator.R
import com.bobbyesp.metadator.core.presentation.components.AppDetails
@Composable
-fun OnboardingWelcomePage(
- onGetStarted: () -> Unit,
- subtitle: String? = null
-) {
+fun OnboardingWelcomePage(onGetStarted: () -> Unit, subtitle: String? = null) {
Scaffold(
modifier = Modifier.fillMaxSize(),
floatingActionButton = {
- Button(
- modifier = Modifier
- .padding(16.dp),
- onClick = onGetStarted,
- ) {
+ Button(modifier = Modifier.padding(16.dp), onClick = onGetStarted) {
Text(text = stringResource(id = R.string.get_started))
Icon(
modifier = Modifier,
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
- contentDescription = stringResource(id = R.string.get_started)
+ contentDescription = stringResource(id = R.string.get_started),
)
}
- }
+ },
) { paddingValues ->
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues),
- ) {
- AppDetails(
- modifier = Modifier.align(Alignment.Center), subtitle = subtitle
- )
+ Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
+ AppDetails(modifier = Modifier.align(Alignment.Center), subtitle = subtitle)
}
}
}
@@ -64,9 +51,6 @@ private fun OnboardingPreview() {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
) {
- OnboardingWelcomePage(
- subtitle = "1.0.0-beta09",
- onGetStarted = {}
- )
+ OnboardingWelcomePage(subtitle = "1.0.0-beta09", onGetStarted = {})
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/TagEditorRouting.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/TagEditorRouting.kt
index 34d7578..4a2cc3d 100644
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/TagEditorRouting.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/TagEditorRouting.kt
@@ -13,36 +13,39 @@ import com.bobbyesp.metadator.core.domain.model.ParcelableSong
import com.bobbyesp.metadator.core.presentation.common.Route
import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.MetadataEditorPage
import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.MetadataEditorViewModel
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.MetadataBottomSheetViewModel
import com.bobbyesp.ui.motion.slideInVerticallyComposable
import com.bobbyesp.utilities.navigation.parcelableType
+import kotlin.reflect.typeOf
import kotlinx.coroutines.flow.collectLatest
import org.koin.androidx.compose.koinViewModel
-import kotlin.reflect.typeOf
-fun NavGraphBuilder.tagEditorRouting(
- onNavigateBack: () -> Unit
-) {
+fun NavGraphBuilder.tagEditorRouting(onNavigateBack: () -> Unit) {
navigation(
- startDestination = Route.UtilitiesNavigator.TagEditor::class,
+ startDestination = Route.UtilitiesNavigator.TagEditor::class
) {
slideInVerticallyComposable(
- typeMap = mapOf(typeOf() to parcelableType()),
+ typeMap = mapOf(typeOf() to parcelableType())
) {
+ val viewModel = koinViewModel()
+
val song = it.toRoute()
- val viewModel = koinViewModel()
- val bsViewModel = koinViewModel()
+ LaunchedEffect(song) {
+ viewModel.onEvent(
+ MetadataEditorViewModel.Event.LoadMetadata(song.selectedSong.localPath)
+ )
+ }
val state = viewModel.state.collectAsStateWithLifecycle()
- val bsState = bsViewModel.viewStateFlow.collectAsStateWithLifecycle()
val securityErrorHandler =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
- viewModel.savePropertyMap(
- audioPath = song.selectedSong.localPath
+ viewModel.onEvent(
+ MetadataEditorViewModel.Event.SaveProperties(
+ song.selectedSong.localPath
+ )
)
onNavigateBack()
}
@@ -52,9 +55,7 @@ fun NavGraphBuilder.tagEditorRouting(
viewModel.eventFlow.collectLatest { event ->
when (event) {
is MetadataEditorViewModel.UiEvent.RequestPermission -> {
- val intent =
- IntentSenderRequest.Builder(event.intent)
- .build()
+ val intent = IntentSenderRequest.Builder(event.intent).build()
securityErrorHandler.launch(intent)
}
@@ -65,30 +66,11 @@ fun NavGraphBuilder.tagEditorRouting(
}
}
- LaunchedEffect(true) {
- bsViewModel.outerEventsFlow.collectLatest { event ->
- when (event) {
- is MetadataBottomSheetViewModel.OuterEvent.SaveMetadata -> {
- event.modifiedFields.forEach { field ->
- viewModel.onEvent(
- MetadataEditorViewModel.Event.UpdateProperty(
- field.key,
- field.value
- )
- )
- }
- }
- }
- }
- }
-
MetadataEditorPage(
- state = state,
- bsViewState = bsState,
+ pageState = state.value,
receivedAudio = song.selectedSong,
- onBsEvent = bsViewModel::onEvent,
- onEvent = viewModel::onEvent
+ onEvent = viewModel::onEvent,
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/data/local/UriToPictureConverter.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/data/local/UriToPictureConverter.kt
new file mode 100644
index 0000000..be41cee
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/data/local/UriToPictureConverter.kt
@@ -0,0 +1,74 @@
+package com.bobbyesp.metadator.tageditor.data.local
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import com.bobbyesp.metadator.tageditor.model.PictureType
+import com.kyant.taglib.Picture
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class UriToPictureConverter(private val context: Context) {
+ /**
+ * Converts a list of URIs into Picture objects.
+ *
+ * @param uris List of URIs to be converted.
+ * @param pictureType Type of image according to ID3v2 specification.
+ * @param descriptionFormat Format string for custom descriptions (use %d for index).
+ * @return List of Picture objects.
+ */
+ suspend fun convert(
+ uris: List,
+ pictureType: PictureType = PictureType.FRONT_COVER,
+ descriptionFormat: String = "Audio image %d - Metadator",
+ ): List =
+ withContext(Dispatchers.IO) {
+ uris.mapIndexedNotNull { index, uri ->
+ try {
+ val byteArray =
+ context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
+ ?: run {
+ Log.e(TAG, "Failed to read image data: $uri")
+ return@mapIndexedNotNull null
+ }
+
+ val mimeType =
+ context.contentResolver.getType(uri)
+ ?: run {
+ Log.e(TAG, "Could not determine MIME type: $uri")
+ return@mapIndexedNotNull null
+ }
+
+ Picture(
+ data = byteArray,
+ mimeType = mimeType,
+ description = descriptionFormat.format(index + 1),
+ pictureType = pictureType.id3Type,
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Error processing image: $uri", e)
+ null
+ }
+ }
+ }
+
+ /**
+ * Converts a single URI into a Picture object.
+ *
+ * @param uri The URI to be converted.
+ * @param pictureType Image type as per ID3v2 specification.
+ * @param description Description of the image.
+ * @return A Picture object, or null if the conversion fails.
+ */
+ suspend fun convertSingle(
+ uri: Uri,
+ pictureType: PictureType = PictureType.FRONT_COVER,
+ description: String = "Audio image - Metadator",
+ ): Picture? {
+ return convert(listOf(uri), pictureType, description).firstOrNull()
+ }
+
+ companion object {
+ private const val TAG = "UriToPictureConverter"
+ }
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/data/local/repository/AudioMetadataRepositoryImpl.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/data/local/repository/AudioMetadataRepositoryImpl.kt
new file mode 100644
index 0000000..92c8d63
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/data/local/repository/AudioMetadataRepositoryImpl.kt
@@ -0,0 +1,63 @@
+package com.bobbyesp.metadator.tageditor.data.local.repository
+
+import com.bobbyesp.metadator.tageditor.model.repository.AudioMetadataRepository
+import com.bobbyesp.utilities.mediastore.model.FileDescriptorMode
+import com.bobbyesp.utilities.mediastore.model.repository.MediaStoreUseCase
+import com.kyant.taglib.AudioProperties
+import com.kyant.taglib.AudioPropertiesReadStyle
+import com.kyant.taglib.Metadata
+import com.kyant.taglib.Picture
+import com.kyant.taglib.PropertyMap
+import com.kyant.taglib.TagLib
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class AudioMetadataRepositoryImpl(private val mediaStoreUseCase: MediaStoreUseCase) :
+ AudioMetadataRepository {
+
+ /** Retrieves a detached file descriptor from the given path and mode. */
+ private fun getDetachedFileDescriptor(path: String, mode: FileDescriptorMode): Int? {
+ return mediaStoreUseCase.getFileDescriptorFromPath(path, mode)?.use { fd ->
+ fd.dup()?.detachFd()
+ }
+ }
+
+ override suspend fun getMetadata(path: String): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ getDetachedFileDescriptor(path, FileDescriptorMode.READ)?.let {
+ TagLib.getMetadata(it)
+ } ?: throw IllegalStateException("Failed to retrieve metadata")
+ }
+ }
+
+ override suspend fun getAudioProperties(
+ path: String,
+ style: AudioPropertiesReadStyle,
+ ): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ getDetachedFileDescriptor(path, FileDescriptorMode.READ)?.let {
+ TagLib.getAudioProperties(it, style)
+ } ?: throw IllegalStateException("Failed to retrieve audio properties")
+ }
+ }
+
+ override suspend fun writePropertyMap(path: String, propertyMap: PropertyMap): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ getDetachedFileDescriptor(path, FileDescriptorMode.WRITE)?.let {
+ TagLib.savePropertyMap(it, propertyMap)
+ } ?: throw IllegalStateException("Failed to write property map")
+ }
+ }
+
+ override suspend fun writePictures(path: String, pictures: List): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ getDetachedFileDescriptor(path, FileDescriptorMode.WRITE)?.let {
+ TagLib.savePictures(it, pictures.toTypedArray())
+ } ?: throw IllegalStateException("Failed to write pictures")
+ }
+ }
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorModule.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorModule.kt
new file mode 100644
index 0000000..4fad71d
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorModule.kt
@@ -0,0 +1,17 @@
+package com.bobbyesp.metadator.tageditor.di
+
+import com.bobbyesp.metadator.tageditor.data.local.UriToPictureConverter
+import com.bobbyesp.metadator.tageditor.data.local.repository.AudioMetadataRepositoryImpl
+import com.bobbyesp.metadator.tageditor.model.repository.AudioMetadataRepository
+import com.bobbyesp.utilities.mediastore.data.local.repository.MediaStoreUseCaseImpl
+import com.bobbyesp.utilities.mediastore.model.repository.MediaStoreUseCase
+import org.koin.android.ext.koin.androidContext
+import org.koin.dsl.module
+
+val tagEditorModule = module {
+ single { MediaStoreUseCaseImpl(context = androidContext()) }
+
+ single { UriToPictureConverter(androidContext()) }
+
+ single { AudioMetadataRepositoryImpl(get()) }
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorViewModelsModule.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorViewModelsModule.kt
index f200832..91f1a74 100644
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorViewModelsModule.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/di/TagEditorViewModelsModule.kt
@@ -1,21 +1,9 @@
package com.bobbyesp.metadator.tageditor.di
import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.MetadataEditorViewModel
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.MetadataBottomSheetViewModel
-import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val tagEditorViewModelsModule = module {
- viewModel {
- MetadataEditorViewModel(
- context = androidContext(),
- stateHandle = get()
- )
- }
- viewModel {
- MetadataBottomSheetViewModel(
- searchService = get()
- )
- }
-}
\ No newline at end of file
+ viewModel { MetadataEditorViewModel(get(), get(), get()) }
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/domain/AudioEditableMetadata.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/domain/AudioEditableMetadata.kt
new file mode 100644
index 0000000..32fa1f8
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/domain/AudioEditableMetadata.kt
@@ -0,0 +1,44 @@
+package com.bobbyesp.metadator.tageditor.domain
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class AudioEditableMetadata(
+ val title: String = "",
+ val artist: String = "",
+ val album: String = "",
+ val trackNumber: Int = 0,
+ val discNumber: Int = 0,
+ val date: String = "",
+ val genre: String = "",
+ val comment: String = "",
+ val lyrics: String = "",
+) {
+ companion object {
+ fun fromMap(map: Map): AudioEditableMetadata {
+ return AudioEditableMetadata(
+ title = map["TITLE"] ?: "",
+ artist = map["ARTIST"] ?: "",
+ album = map["ALBUM"] ?: "",
+ trackNumber = map["TRACKNUMBER"]?.toIntOrNull() ?: 0,
+ discNumber = map["DISCNUMBER"]?.toIntOrNull() ?: 0,
+ date = map["DATE"] ?: "",
+ genre = map["GENRE"] ?: "",
+ comment = map["COMMENT"] ?: "",
+ lyrics = map["LYRICS"] ?: ""
+ )
+ }
+ }
+
+ fun toMap(): Map = mapOf(
+ "TITLE" to title,
+ "ARTIST" to artist,
+ "ALBUM" to album,
+ "TRACKNUMBER" to trackNumber.toString(),
+ "DISCNUMBER" to discNumber.toString(),
+ "DATE" to date,
+ "GENRE" to genre,
+ "COMMENT" to comment,
+ "LYRICS" to lyrics,
+ )
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/model/PictureType.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/model/PictureType.kt
new file mode 100644
index 0000000..3bab36e
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/model/PictureType.kt
@@ -0,0 +1,12 @@
+package com.bobbyesp.metadator.tageditor.model
+
+enum class PictureType(val id3Type: String) {
+ OTHER("Other"),
+ FRONT_COVER("Front Cover"),
+ BACK_COVER("Back Cover"),
+ ARTIST("Artist"),
+ BAND("Band"),
+ COMPOSER("Composer"),
+ DURING_RECORDING("During Recording"),
+ ILLUSTRATION("Illustration"),
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/model/repository/AudioMetadataRepository.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/model/repository/AudioMetadataRepository.kt
new file mode 100644
index 0000000..226a04c
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/model/repository/AudioMetadataRepository.kt
@@ -0,0 +1,20 @@
+package com.bobbyesp.metadator.tageditor.model.repository
+
+import com.kyant.taglib.AudioProperties
+import com.kyant.taglib.AudioPropertiesReadStyle
+import com.kyant.taglib.Metadata
+import com.kyant.taglib.Picture
+import com.kyant.taglib.PropertyMap
+
+interface AudioMetadataRepository {
+ suspend fun getMetadata(path: String): Result
+
+ suspend fun getAudioProperties(
+ path: String,
+ style: AudioPropertiesReadStyle,
+ ): Result
+
+ suspend fun writePropertyMap(path: String, propertyMap: PropertyMap): Result
+
+ suspend fun writePictures(path: String, pictures: List): Result
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/PreConfiguredOutlinedTextField.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/components/textfield/MetadataOutlinedTextField.kt
similarity index 50%
rename from app/ui/src/main/java/com/bobbyesp/ui/components/text/PreConfiguredOutlinedTextField.kt
rename to app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/components/textfield/MetadataOutlinedTextField.kt
index bacc6f3..3bb8f88 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/PreConfiguredOutlinedTextField.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/components/textfield/MetadataOutlinedTextField.kt
@@ -1,4 +1,4 @@
-package com.bobbyesp.ui.components.text
+package com.bobbyesp.metadator.tageditor.presentation.components.textfield
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -9,23 +9,34 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Undo
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
import com.bobbyesp.ui.R
-import com.bobbyesp.ui.util.rememberVolatileSaveable
+import com.materialkolor.DynamicMaterialTheme
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun PreConfiguredOutlinedTextField(
+fun MetadataOutlinedTextField(
modifier: Modifier = Modifier,
value: String?,
label: String = "",
+ isModified: Boolean = false,
enabled: Boolean = true,
readOnly: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions(),
@@ -33,17 +44,19 @@ fun PreConfiguredOutlinedTextField(
singleLine: Boolean = false,
maxLines: Int = 2,
minLines: Int = 1,
- returnModifiedValue: (String) -> Unit = {}
+ onValueChange: (String) -> Unit = {},
) {
- val (text, setText) = rememberVolatileSaveable(value ?: "")
- val originalValue = remember { value ?: "" }
+ val originalValue by remember { mutableStateOf(value ?: "") }
+ var text by rememberSaveable { mutableStateOf(originalValue) }
+
+ val fieldModified = text != originalValue || isModified
OutlinedTextField(
modifier = modifier,
value = text,
onValueChange = { newValue ->
- setText(newValue)
- returnModifiedValue(newValue)
+ text = newValue
+ onValueChange(newValue)
},
label = { Text(text = label, maxLines = 1, overflow = TextOverflow.Ellipsis) },
enabled = enabled,
@@ -53,22 +66,42 @@ fun PreConfiguredOutlinedTextField(
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
+ shape = MaterialTheme.shapes.large,
+ colors =
+ OutlinedTextFieldDefaults.colors(
+ focusedBorderColor =
+ if (fieldModified) MaterialTheme.colorScheme.primary
+ else MaterialTheme.colorScheme.outline,
+ unfocusedBorderColor =
+ if (fieldModified) MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
+ else MaterialTheme.colorScheme.outline,
+ ),
trailingIcon = {
AnimatedVisibility(
- visible = text != originalValue,
+ visible = fieldModified,
enter = fadeIn() + slideInHorizontally(),
- exit = fadeOut() + slideOutHorizontally()
+ exit = fadeOut() + slideOutHorizontally(),
) {
- IconButton(onClick = {
- setText(originalValue)
- returnModifiedValue(originalValue)
- }) {
+ IconButton(
+ onClick = {
+ text = originalValue
+ onValueChange(originalValue)
+ }
+ ) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.Undo,
- contentDescription = stringResource(id = R.string.undo)
+ contentDescription = stringResource(id = R.string.undo),
)
}
}
- }
+ },
)
-}
\ No newline at end of file
+}
+
+@Preview
+@Composable
+private fun PreConfiguredOutlineTextFieldPreview() {
+ DynamicMaterialTheme(seedColor = Color(0xFF4565FF)) {
+ MetadataOutlinedTextField(value = "Hello, World!", label = "Label")
+ }
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MediaStoreInfoDialog.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MediaStoreInfoDialog.kt
deleted file mode 100644
index 6e6c68b..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MediaStoreInfoDialog.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import com.bobbyesp.metadator.R
-
-@Composable
-fun MediaStoreInfoDialog(
- modifier: Modifier = Modifier,
- onDismissRequest: () -> Unit,
-) {
- AlertDialog(
- modifier = modifier,
- onDismissRequest = onDismissRequest,
- text = {
- Column {
- }
- },
- confirmButton = {},
- dismissButton = {
- TextButton(
- onClick = onDismissRequest
- ) {
- Text(stringResource(id = R.string.dismiss))
- }
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorPage.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorPage.kt
index 58590f2..3f456a4 100644
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorPage.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorPage.kt
@@ -9,74 +9,62 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.Crossfade
-import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Downloading
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Lyrics
-import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.rememberBottomSheetScaffoldState
-import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bobbyesp.metadator.R
+import com.bobbyesp.metadator.core.domain.model.ParcelableSong
import com.bobbyesp.metadator.core.presentation.common.LocalNavController
-import com.bobbyesp.metadator.core.presentation.common.LocalOrientation
import com.bobbyesp.metadator.core.presentation.common.LocalSonner
-import com.bobbyesp.metadator.core.domain.model.ParcelableSong
import com.bobbyesp.metadator.core.presentation.components.image.AsyncImage
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.MetadataBottomSheetViewModel
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.SpMetadataBottomSheetContent
+import com.bobbyesp.metadator.tageditor.presentation.components.textfield.MetadataOutlinedTextField
+import com.bobbyesp.metadator.tageditor.presentation.state.MetadataEditorUiState
+import com.bobbyesp.metadator.tageditor.presentation.state.FieldState
import com.bobbyesp.ui.common.pages.ErrorPage
import com.bobbyesp.ui.common.pages.LoadingPage
import com.bobbyesp.ui.components.button.CloseButton
import com.bobbyesp.ui.components.others.MetadataTag
import com.bobbyesp.ui.components.text.LargeCategoryTitle
-import com.bobbyesp.ui.components.text.PreConfiguredOutlinedTextField
import com.bobbyesp.utilities.ext.fromMillisToMinutes
import com.bobbyesp.utilities.ext.isNeitherNullNorBlank
import com.bobbyesp.utilities.states.ResourceState
@@ -89,101 +77,90 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MetadataEditorPage(
- state: State,
- bsViewState: State,
+ pageState: MetadataEditorViewModel.PageViewState,
receivedAudio: ParcelableSong,
- onBsEvent: (MetadataBottomSheetViewModel.Event) -> Unit,
- onEvent: (MetadataEditorViewModel.Event) -> Unit
+ onEvent: (MetadataEditorViewModel.Event) -> Unit,
) {
val navController = LocalNavController.current
val sonner = LocalSonner.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
- val pageState = state.value
LaunchedEffect(receivedAudio) {
onEvent(MetadataEditorViewModel.Event.LoadMetadata(receivedAudio.localPath))
}
var newArtworkAddress by rememberSaveable { mutableStateOf(null) }
+ var showInstallSongSyncDialog by remember { mutableStateOf(false) }
- val singleImagePickerLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.PickVisualMedia(),
- onResult = { uri -> newArtworkAddress = uri }
- )
-
- val scaffoldState = rememberBottomSheetScaffoldState(
- bottomSheetState = rememberStandardBottomSheetState(
- initialValue = SheetValue.Hidden,
- skipHiddenState = false
+ val singleImagePickerLauncher =
+ rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia(),
+ onResult = { uri -> newArtworkAddress = uri },
)
- )
- var showInstallSongSyncDialog by remember {
- mutableStateOf(false)
- }
-
- val lyricsActivityLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.StartActivityForResult()
- ) { result ->
- when (result.resultCode) {
- Activity.RESULT_OK -> {
- val receivedLyrics = result.data?.getStringExtra("lyrics")
- if (receivedLyrics.isNeitherNullNorBlank()) {
- pageState.mutablePropertiesMap["LYRICS"] = receivedLyrics!!
+ val lyricsActivityLauncher =
+ rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ when (result.resultCode) {
+ Activity.RESULT_OK -> {
+ val receivedLyrics = result.data?.getStringExtra("lyrics")
+ if (receivedLyrics.isNeitherNullNorBlank()) {
+ onEvent(
+ MetadataEditorViewModel.Event.UpdateProperty("LYRICS", receivedLyrics!!)
+ )
+ scope.launch {
+ sonner.show(
+ message = context.getString(R.string.lyrics_received),
+ type = ToastType.Success,
+ )
+ }
+ } else {
+ scope.launch {
+ sonner.show(
+ message = context.getString(R.string.empty_lyrics_received),
+ type = ToastType.Error,
+ )
+ }
+ }
+ }
+ Activity.RESULT_CANCELED ->
scope.launch {
sonner.show(
- message = context.getString(R.string.lyrics_received),
- type = ToastType.Success
+ message = context.getString(R.string.lyrics_retrieve_cancelled),
+ type = ToastType.Info,
)
}
- } else {
+ else ->
scope.launch {
sonner.show(
- message = context.getString(R.string.empty_lyrics_received),
- type = ToastType.Error
+ message = context.getString(R.string.something_unexpected_occurred),
+ type = ToastType.Error,
)
}
- }
- }
-
- Activity.RESULT_CANCELED -> {
- scope.launch {
- sonner.show(
- message = context.getString(R.string.lyrics_retrieve_cancelled),
- type = ToastType.Info
- )
- }
- }
-
- else -> {
- scope.launch {
- sonner.show(
- message = context.getString(R.string.something_unexpected_occurred),
- type = ToastType.Error
- )
- }
}
}
- }
- val lyricsRetrieveIntent = Intent("android.intent.action.SEND").apply {
- putExtra("songName", pageState.mutablePropertiesMap["TITLE"])
- putExtra("artistName", pageState.mutablePropertiesMap["ARTIST"])
- type = "text/plain"
- setPackage("pl.lambada.songsync")
- }
+ val lyricsRetrieveIntent =
+ Intent("android.intent.action.SEND").apply {
+// putExtra("songName", pageState.uiState.fields["TITLE"])
+// putExtra("artistName", pageState.properties["ARTIST"])
+ type = "text/plain"
+ setPackage("pl.lambada.songsync")
+ }
fun launchLyricsRetrieveIntent() {
try {
lyricsActivityLauncher.launch(lyricsRetrieveIntent)
} catch (e: Exception) {
- when (e) {
- is ActivityNotFoundException -> showInstallSongSyncDialog = true
- else -> scope.launch {
+ if (e is ActivityNotFoundException) {
+ showInstallSongSyncDialog = true
+ } else {
+ scope.launch {
sonner.show(
message = context.getString(R.string.something_unexpected_occurred),
- type = ToastType.Error
+ type = ToastType.Error,
)
}
}
@@ -191,434 +168,298 @@ fun MetadataEditorPage(
}
if (showInstallSongSyncDialog) {
- SongSyncNeededDialog(
- onDismissRequest = { showInstallSongSyncDialog = false }
- )
+ SongSyncNeededDialog(onDismissRequest = { showInstallSongSyncDialog = false })
}
- BottomSheetScaffold(
+ Scaffold(
topBar = {
- TopAppBar(
- title = {
- Column(modifier = Modifier.fillMaxWidth()) {
- Text(
- text = stringResource(id = R.string.viewing_metadata),
- style = MaterialTheme.typography.bodyLarge,
- fontSize = 20.sp,
- fontWeight = FontWeight.Bold
+ EditorTopBar(
+ onClose = { navController.popBackStack() },
+ onSave = {
+ onEvent(
+ MetadataEditorViewModel.Event.SaveAll(
+ receivedAudio.localPath,
+ listOf(newArtworkAddress ?: receivedAudio.artworkPath ?: Uri.EMPTY),
)
- }
+ )
},
- navigationIcon = { CloseButton { navController.popBackStack() } },
- actions = {
- IconButton(
- onClick = {
- if (scaffoldState.bottomSheetState.isVisible) {
- onBsEvent(MetadataBottomSheetViewModel.Event.Search(receivedAudio.name + " " + receivedAudio.mainArtist))
- } else {
- scope.launch { scaffoldState.bottomSheetState.partialExpand() }
- }
- }
- ) {
- Icon(
- imageVector = Icons.Rounded.Downloading,
- contentDescription = stringResource(id = R.string.retrieve_song_info)
- )
- }
- TextButton(
- onClick = {
- onEvent(
- MetadataEditorViewModel.Event.SaveAll(
- receivedAudio.localPath,
- listOf(
- newArtworkAddress ?: receivedAudio.artworkPath ?: Uri.EMPTY
- )
- )
- )
- }
- ) {
- Text(text = stringResource(id = R.string.save))
- }
+ )
+ }
+ ) { innerPadding ->
+ Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
+ EditorContent(
+ pageState = pageState,
+ artworkUri = newArtworkAddress ?: receivedAudio.artworkPath,
+ onEditArtwork = {
+ singleImagePickerLauncher.launch(
+ PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+ )
},
- scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+ onRetrieveLyrics = { launchLyricsRetrieveIntent() },
+ onEvent = onEvent,
+ )
+ }
+ }
+}
+
+/** Composable para la TopAppBar del editor */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun EditorTopBar(onClose: () -> Unit, onSave: () -> Unit) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(id = R.string.viewing_metadata),
+ style = MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp),
)
},
- modifier = Modifier.fillMaxSize(),
- scaffoldState = scaffoldState,
- sheetPeekHeight = 148.dp,
- sheetShadowElevation = 8.dp,
- sheetContent = {
- SpMetadataBottomSheetContent(
- name = receivedAudio.name,
- artist = receivedAudio.mainArtist,
- sheetState = scaffoldState.bottomSheetState,
- bsViewState = bsViewState,
- onEvent = onBsEvent,
- ) {
- scope.launch { scaffoldState.bottomSheetState.hide() }
- }
+ navigationIcon = { CloseButton(onClick = onClose) },
+ actions = {
+ TextButton(onClick = onSave) { Text(text = stringResource(id = R.string.save)) }
},
- ) { innerPadding ->
- val animatedBottomPadding by animateDpAsState(
- targetValue = if (scaffoldState.bottomSheetState.isVisible) innerPadding.calculateBottomPadding() + 6.dp else 0.dp,
- label = "animatedBottomPadding"
- )
+ scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
+ )
+}
- Crossfade(
- targetState = pageState.pageState,
- animationSpec = tween(175),
- label = "Fade between pages (ID3MetadataEditorPage)",
- modifier = Modifier
- .fillMaxSize()
- .navigationBarsPadding()
- ) { state ->
- when (state) {
- is ScreenState.Error -> ErrorPage(
- modifier = Modifier.fillMaxSize(),
- throwable = state.exception
- ) {
- onEvent(MetadataEditorViewModel.Event.LoadMetadata(receivedAudio.localPath))
+@Composable
+private fun EditorContent(
+ pageState: MetadataEditorViewModel.PageViewState,
+ artworkUri: Uri?,
+ onEditArtwork: () -> Unit,
+ onRetrieveLyrics: () -> Unit,
+ onEvent: (MetadataEditorViewModel.Event) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ val configuration = LocalConfiguration.current
+
+ Crossfade(targetState = pageState.pageState, animationSpec = tween(175)) { state ->
+ when (state) {
+ is ScreenState.Error ->
+ ErrorPage(modifier = Modifier.fillMaxSize(), throwable = state.exception) {
+ // Acción de reintento, según convenga
}
-
- ScreenState.Loading -> LoadingPage(
+ ScreenState.Loading ->
+ LoadingPage(
modifier = Modifier.fillMaxSize(),
- text = stringResource(id = R.string.loading_metadata)
+ text = stringResource(id = R.string.loading_metadata),
)
+ is ScreenState.Success -> {
+ if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
+ PortraitContent(
+ pageState = pageState,
+ artworkUri = artworkUri,
+ onEditArtwork = onEditArtwork,
+ onRetrieveLyrics = onRetrieveLyrics,
+ scrollState = scrollState,
+ onEvent = onEvent,
+ )
+ } else {
+ LandscapeContent(
+ pageState = pageState,
+ artworkUri = artworkUri,
+ onEditArtwork = onEditArtwork,
+ onRetrieveLyrics = onRetrieveLyrics,
+ scrollState = scrollState,
+ onEvent = onEvent,
+ )
+ }
+ }
+ }
+ }
+}
- is ScreenState.Success -> {
- val scrollState = rememberScrollState()
- val orientation = LocalOrientation.current
- val artworkUri = newArtworkAddress ?: receivedAudio.artworkPath
-
- when (orientation) {
- Configuration.ORIENTATION_PORTRAIT -> {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(scrollState)
- .padding(horizontal = 16.dp)
- ) {
- Box(
- modifier = Modifier
- .size(250.dp)
- .padding(8.dp)
- .aspectRatio(1f)
- .align(Alignment.CenterHorizontally),
- ) {
- AsyncImage(
- modifier = Modifier
- .fillMaxSize()
- .clip(MaterialTheme.shapes.small)
- .align(Alignment.Center),
- imageModel = artworkUri,
- )
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp),
- contentAlignment = Alignment.BottomEnd
- ) {
- IconButton(
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = Color.Black.copy(alpha = 0.5f)
- ),
- onClick = {
- singleImagePickerLauncher.launch(
- PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
- )
- }
- ) {
- Icon(
- imageVector = Icons.Rounded.Edit,
- tint = Color.White.harmonize(Color.Black.copy(alpha = 0.5f)),
- contentDescription = stringResource(id = R.string.edit_image)
- )
- }
- }
- }
-
- if (pageState.audioProperties is ResourceState.Success && pageState.audioProperties.data != null) {
- AudioProperties(
- modifier = Modifier,
- audioProperties = pageState.audioProperties.data!!
- )
- }
-
- if (pageState.metadata is ResourceState.Success) {
- SongProperties(
- mutablePropertiesMap = pageState.mutablePropertiesMap,
- retrieveLyrics = { launchLyricsRetrieveIntent() }
- )
- Spacer(modifier = Modifier.height(animatedBottomPadding))
- }
- }
- }
+/** Contenido en orientación vertical */
+@Composable
+private fun PortraitContent(
+ pageState: MetadataEditorViewModel.PageViewState,
+ artworkUri: Uri?,
+ onEditArtwork: () -> Unit,
+ onRetrieveLyrics: () -> Unit,
+ scrollState: ScrollState,
+ onEvent: (MetadataEditorViewModel.Event) -> Unit,
+) {
+ Column(
+ modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(horizontal = 16.dp)
+ ) {
+ ArtworkSection(
+ artworkUri = artworkUri,
+ onEditArtwork = onEditArtwork,
+ modifier = Modifier.size(250.dp).padding(8.dp).align(Alignment.CenterHorizontally),
+ )
+ if (
+ pageState.audioProperties is ResourceState.Success &&
+ pageState.audioProperties.data != null
+ ) {
+ AudioPropertiesSection(audioProperties = pageState.audioProperties.data!!)
+ }
+ if (pageState.metadata is ResourceState.Success) {
+ // Dynamic fields based on UI state
+ DynamicFieldsSection(
+ uiState = pageState.uiState,
+ onUpdateProperty = { key, value ->
+ onEvent(MetadataEditorViewModel.Event.UpdateProperty(key, value))
+ }
+ )
+ }
+ }
+}
- Configuration.ORIENTATION_LANDSCAPE -> {
- Row(
- modifier = Modifier.fillMaxSize().displayCutoutPadding(),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Box(
- modifier = Modifier
- .padding(8.dp)
- .aspectRatio(1f)
- .align(Alignment.CenterVertically),
- ) {
- AsyncImage(
- modifier = Modifier
- .fillMaxSize()
- .clip(MaterialTheme.shapes.small)
- .align(Alignment.Center),
- imageModel = artworkUri,
- )
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp),
- contentAlignment = Alignment.BottomEnd
- ) {
- IconButton(
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = Color.Black.copy(alpha = 0.5f)
- ),
- onClick = {
- singleImagePickerLauncher.launch(
- PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
- )
- }
- ) {
- Icon(
- imageVector = Icons.Rounded.Edit,
- tint = Color.White.harmonize(Color.Black.copy(alpha = 0.5f)),
- contentDescription = stringResource(id = R.string.edit_image)
- )
- }
- }
- }
- Column(
- modifier = Modifier
- .weight(0.5f)
- .verticalScroll(scrollState)
- ) {
- if (pageState.audioProperties is ResourceState.Success && pageState.audioProperties.data != null) {
- AudioProperties(
- modifier = Modifier,
- audioProperties = pageState.audioProperties.data!!
- )
- }
- if (pageState.metadata is ResourceState.Success) {
- SongProperties(
- mutablePropertiesMap = pageState.mutablePropertiesMap,
- retrieveLyrics = { launchLyricsRetrieveIntent() }
- )
- }
- }
- }
- }
+/** Contenido en orientación horizontal */
+@Composable
+private fun LandscapeContent(
+ pageState: MetadataEditorViewModel.PageViewState,
+ artworkUri: Uri?,
+ onEditArtwork: () -> Unit,
+ onRetrieveLyrics: () -> Unit,
+ scrollState: ScrollState,
+ onEvent: (MetadataEditorViewModel.Event) -> Unit,
+) {
+ Row(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ ArtworkSection(
+ artworkUri = artworkUri,
+ onEditArtwork = onEditArtwork,
+ modifier = Modifier.padding(8.dp).aspectRatio(1f).align(Alignment.CenterVertically),
+ )
+ Column(modifier = Modifier.weight(0.5f).verticalScroll(scrollState)) {
+ if (
+ pageState.audioProperties is ResourceState.Success &&
+ pageState.audioProperties.data != null
+ ) {
+ AudioPropertiesSection(audioProperties = pageState.audioProperties.data!!)
+ }
+ if (pageState.metadata is ResourceState.Success) {
+ // Dynamic fields based on UI state
+ DynamicFieldsSection(
+ uiState = pageState.uiState,
+ onUpdateProperty = { key, value ->
+ onEvent(MetadataEditorViewModel.Event.UpdateProperty(key, value))
}
- }
+ )
+ }
+ }
+ }
+}
+
+/** Sección para mostrar y editar la imagen de portada */
+@Composable
+private fun ArtworkSection(
+ artworkUri: Uri?,
+ onEditArtwork: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier = modifier) {
+ AsyncImage(
+ modifier = Modifier.fillMaxSize().clip(MaterialTheme.shapes.small),
+ imageModel = artworkUri,
+ )
+ Box(
+ modifier = Modifier.fillMaxSize().padding(8.dp),
+ contentAlignment = Alignment.BottomEnd,
+ ) {
+ IconButton(
+ colors =
+ IconButtonDefaults.iconButtonColors(
+ containerColor = Color.Black.copy(alpha = 0.5f)
+ ),
+ onClick = onEditArtwork,
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Edit,
+ tint = Color.White.harmonize(Color.Black.copy(alpha = 0.5f)),
+ contentDescription = stringResource(id = R.string.edit_image),
+ )
}
}
}
}
@Composable
-private fun AudioProperties(modifier: Modifier = Modifier, audioProperties: AudioProperties) {
+private fun AudioPropertiesSection(audioProperties: AudioProperties) {
LargeCategoryTitle(
modifier = Modifier.padding(vertical = 6.dp),
- text = stringResource(id = R.string.audio_details)
+ text = stringResource(id = R.string.audio_details),
)
-
Column(
- modifier = modifier.fillMaxWidth(),
+ modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
- horizontalAlignment = Alignment.CenterHorizontally
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 8.dp),
- ) {
+ Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) {
MetadataTag(
modifier = Modifier.weight(0.5f),
typeOfMetadata = stringResource(id = R.string.bitrate),
- metadata = audioProperties.bitrate.toString() + " kbps"
+ metadata = "${audioProperties.bitrate} kbps",
)
MetadataTag(
modifier = Modifier.weight(0.5f),
typeOfMetadata = stringResource(id = R.string.sample_rate),
- metadata = audioProperties.sampleRate.toString() + " Hz"
+ metadata = "${audioProperties.sampleRate} Hz",
)
}
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 8.dp),
- ) {
+ Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) {
MetadataTag(
modifier = Modifier.weight(0.5f),
typeOfMetadata = stringResource(id = R.string.channels),
- metadata = audioProperties.channels.toString()
+ metadata = audioProperties.channels.toString(),
)
MetadataTag(
modifier = Modifier.weight(0.5f),
typeOfMetadata = stringResource(id = R.string.duration),
- metadata = audioProperties.length.fromMillisToMinutes()
+ metadata = audioProperties.length.fromMillisToMinutes(),
)
}
}
}
+/** Encabezado de sección */
@Composable
-private fun SongProperties(
- mutablePropertiesMap: SnapshotStateMap,
- retrieveLyrics: () -> Unit
-) {
- LargeCategoryTitle(
- modifier = Modifier.padding(vertical = 6.dp),
- text = stringResource(id = R.string.general_tags)
- )
-
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["TITLE"],
- label = stringResource(id = R.string.title),
- modifier = Modifier.fillMaxWidth()
- ) { title -> mutablePropertiesMap["TITLE"] = title }
-
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["ARTIST"],
- label = stringResource(id = R.string.artist),
- modifier = Modifier.fillMaxWidth()
- ) { artists -> mutablePropertiesMap["ARTIST"] = artists }
-
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["ALBUM"],
- label = stringResource(id = R.string.album),
- modifier = Modifier.fillMaxWidth()
- ) { album -> mutablePropertiesMap["ALBUM"] = album }
-
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["ALBUMARTIST"],
- label = stringResource(id = R.string.album_artist),
- modifier = Modifier.fillMaxWidth()
- ) { albumArtist -> mutablePropertiesMap["ALBUMARTIST"] = albumArtist }
-
- Column(modifier = Modifier.fillMaxWidth()) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["TRACKNUMBER"],
- label = stringResource(id = R.string.track_number),
- modifier = Modifier.weight(0.5f)
- ) { trackNumber -> mutablePropertiesMap["TRACKNUMBER"] = trackNumber }
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["DISCNUMBER"],
- label = stringResource(id = R.string.disc_number),
- modifier = Modifier.weight(0.5f)
- ) { discNumber -> mutablePropertiesMap["DISCNUMBER"] = discNumber }
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["DATE"],
- label = stringResource(id = R.string.date),
- modifier = Modifier.weight(0.5f)
- ) { date -> mutablePropertiesMap["DATE"] = date }
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["GENRE"],
- label = stringResource(id = R.string.genre),
- modifier = Modifier.weight(0.5f)
- ) { genre -> mutablePropertiesMap["GENRE"] = genre }
- }
- }
+private fun SectionHeader(title: String) {
+ LargeCategoryTitle(modifier = Modifier.padding(vertical = 12.dp), text = title)
+}
- LargeCategoryTitle(
- modifier = Modifier.padding(vertical = 6.dp),
- text = stringResource(id = R.string.credits)
+/** Campo editable de metadata */
+@Composable
+private fun MetadataField(
+ key: String,
+ properties: Map,
+ modifiedKeys: Set,
+ label: String,
+ onUpdateProperty: (String, String) -> Unit,
+ modifier: Modifier = Modifier,
+ maxLines: Int = 1,
+) {
+ val value = properties[key] ?: ""
+ val isModified = key in modifiedKeys
+
+ MetadataOutlinedTextField(
+ value = value,
+ label = label,
+ isModified = isModified,
+ modifier = modifier,
+ maxLines = maxLines,
+ onValueChange = { onUpdateProperty(key, it) },
)
+}
- Column(modifier = Modifier.fillMaxWidth()) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["COMPOSER"],
- label = stringResource(id = R.string.composer),
- modifier = Modifier.weight(0.5f)
- ) { composer -> mutablePropertiesMap["COMPOSER"] = composer }
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["LYRICIST"],
- label = stringResource(id = R.string.lyricist),
- modifier = Modifier.weight(0.5f)
- ) { lyricist -> mutablePropertiesMap["LYRICIST"] = lyricist }
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["CONDUCTOR"],
- label = stringResource(id = R.string.conductor),
- modifier = Modifier.weight(0.5f)
- ) { conductor -> mutablePropertiesMap["CONDUCTOR"] = conductor }
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["REMIXER"],
- label = stringResource(id = R.string.remixer),
- modifier = Modifier.weight(0.5f)
- ) { remixer -> mutablePropertiesMap["REMIXER"] = remixer }
- }
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["PERFORMER"],
- label = stringResource(id = R.string.performer),
- modifier = Modifier.fillMaxWidth()
- ) { performer -> mutablePropertiesMap["PERFORMER"] = performer }
-
- LargeCategoryTitle(
- modifier = Modifier.padding(vertical = 6.dp),
- text = stringResource(id = R.string.others)
- )
-
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["COMMENT"],
- label = stringResource(id = R.string.comment),
+// Dynamic rendering of fields from MetadataEditorUiState
+@Composable
+private fun DynamicFieldsSection(
+ uiState: MetadataEditorUiState,
+ onUpdateProperty: (String, String) -> Unit,
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ uiState.fields.forEach { field: FieldState<*> ->
+ MetadataOutlinedTextField(
+ value = field.current.toString(),
+ label = stringResource(id = field.labelRes),
+ isModified = field.isModified,
modifier = Modifier.fillMaxWidth(),
- maxLines = 3
- ) { comment -> mutablePropertiesMap["COMMENT"] = comment }
- Column(modifier = Modifier.fillMaxWidth()) {
- TextButton(
- modifier = Modifier.align(Alignment.End),
- onClick = retrieveLyrics
- ) {
- Row(
- modifier = Modifier,
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = Icons.Rounded.Lyrics,
- contentDescription = stringResource(id = R.string.retrieve_lyrics)
- )
-
- Text(text = stringResource(id = R.string.retrieve_lyrics))
- }
- }
- PreConfiguredOutlinedTextField(
- value = mutablePropertiesMap["LYRICS"],
- label = stringResource(id = R.string.lyrics),
- modifier = Modifier.fillMaxWidth(),
- maxLines = 20
- ) { lyrics -> mutablePropertiesMap["LYRICS"] = lyrics }
- }
+ maxLines = if (field is com.bobbyesp.metadator.tageditor.presentation.state.StringFieldState && field.key == "COMMENT") 3 else 1,
+ onValueChange = { onUpdateProperty(field.key, it) }
+ )
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorViewModel.kt
index f2745d1..52d7e7e 100644
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorViewModel.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/MetadataEditorViewModel.kt
@@ -2,31 +2,26 @@ package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor
import android.app.PendingIntent
import android.app.RecoverableSecurityException
-import android.content.Context
import android.net.Uri
import android.os.Build
-import android.os.ParcelFileDescriptor
import android.util.Log
-import androidx.compose.runtime.mutableStateMapOf
-import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.bobbyesp.metadator.core.util.executeIfDebugging
+import com.bobbyesp.metadator.tageditor.data.local.UriToPictureConverter
+import com.bobbyesp.metadator.tageditor.domain.AudioEditableMetadata
+import com.bobbyesp.metadator.tageditor.model.repository.AudioMetadataRepository
+import com.bobbyesp.metadator.tageditor.presentation.state.MetadataEditorUiState
import com.bobbyesp.utilities.ext.toModifiableMap
import com.bobbyesp.utilities.mediastore.AudioFileMetadata.Companion.toAudioFileMetadata
import com.bobbyesp.utilities.mediastore.AudioFileMetadata.Companion.toPropertyMap
-import com.bobbyesp.utilities.mediastore.MediaStoreReceiver
import com.bobbyesp.utilities.states.ResourceState
import com.bobbyesp.utilities.states.ScreenState
import com.kyant.taglib.AudioProperties
import com.kyant.taglib.AudioPropertiesReadStyle
import com.kyant.taglib.Metadata
-import com.kyant.taglib.Picture
-import com.kyant.taglib.PropertyMap
-import com.kyant.taglib.TagLib
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -35,25 +30,14 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlin.coroutines.CoroutineContext
class MetadataEditorViewModel(
- private val context: Context,
- private val stateHandle: SavedStateHandle
+ private val repository: AudioMetadataRepository,
+ private val uriConverter: UriToPictureConverter,
+ private val stateHandle: SavedStateHandle,
) : ViewModel() {
- private val mutableState = MutableStateFlow(PageViewState())
- val state = mutableState.onStart {
- stateHandle.get("path")?.let {
- onEvent(Event.LoadMetadata(it))
- }
- }.stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(5000),
- PageViewState()
- )
- private val _eventFlow = MutableSharedFlow()
+ private val _eventFlow = MutableSharedFlow(extraBufferCapacity = 1)
val eventFlow = _eventFlow.asSharedFlow()
private var latestLoadedSongPath: String? = null
@@ -62,236 +46,115 @@ class MetadataEditorViewModel(
val metadata: ResourceState = ResourceState.Loading(),
val audioProperties: ResourceState = ResourceState.Loading(),
val pageState: ScreenState = ScreenState.Loading,
- val mutablePropertiesMap: SnapshotStateMap = mutableStateMapOf()
+ val uiState: MetadataEditorUiState = MetadataEditorUiState(),
)
+ private val _state = MutableStateFlow(PageViewState())
+ val state =
+ _state
+ .onStart { stateHandle.get("path")?.let { onEvent(Event.LoadMetadata(it)) } }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PageViewState())
+
override fun onCleared() {
super.onCleared()
updateState(ScreenState.Loading)
- mutableState.update {
- it.copy(
- metadata = ResourceState.Loading(),
- audioProperties = ResourceState.Loading()
- )
+ _state.update {
+ it.copy(metadata = ResourceState.Loading(), audioProperties = ResourceState.Loading())
}
}
- private suspend fun loadTrackMetadata(path: String, scope: CoroutineScope) {
- updateState(ScreenState.Loading)
- mutableState.value.mutablePropertiesMap.clear()
-
+ private suspend fun loadTrackMetadata(path: String) {
+ _state.update { it.copy(pageState = ScreenState.Loading) }
try {
stateHandle["path"] = path
- MediaStoreReceiver.getFileDescriptorFromPath(context, path, mode = "r")?.use { songFd ->
- val metadataDeferred = scope.async {
- loadAudioMetadata(
- songFd = songFd,
- coroutineContext = scope.coroutineContext
- )
- }
-
- val audioPropertiesDeferred = scope.async {
- loadAudioProperties(
- songFd = songFd,
- readStyle = AudioPropertiesReadStyle.Average,
- coroutineContext = scope.coroutineContext
- )
- }
-
- val metadata = metadataDeferred.await()
- val audioProperties = audioPropertiesDeferred.await()
-
- updateAudioInformation(metadata, audioProperties)
- }
- updateState(ScreenState.Success(null))
- mutableState.value.metadata.data?.propertyMap?.toModifiableMap()?.forEach {
- mutableState.value.mutablePropertiesMap[it.key] = it.value ?: ""
- }
- } catch (error: Exception) {
- Log.e(
- "MetadataEditorVM", "Error while trying to load the audio file: ${error.message}"
- )
- when (error) {
- is NullAudioFileDescriptorException -> {
- if (error.isAudioProperties) {
- mutableState.update {
- it.copy(
- audioProperties = ResourceState.Error(
- errorMessage = error.message ?: error.stackTraceToString()
- )
- )
- }
- } else {
- mutableState.update {
- it.copy(
- metadata = ResourceState.Error(
- errorMessage = error.message ?: error.stackTraceToString()
- )
- )
- }
- }
- }
-
- else -> updateState(ScreenState.Error(error))
- }
- }
- }
-
- private suspend fun loadAudioMetadata(
- songFd: ParcelFileDescriptor,
- coroutineContext: CoroutineContext = Dispatchers.IO
- ): Metadata? {
- val fd = songFd.dup()?.detachFd() ?: throw NullAudioFileDescriptorException(
- isAudioProperties = false
- )
-
- return withContext(coroutineContext) {
- TagLib.getMetadata(fd = fd)
- }
- }
-
-
- private suspend fun loadAudioProperties(
- songFd: ParcelFileDescriptor,
- readStyle: AudioPropertiesReadStyle = AudioPropertiesReadStyle.Average,
- coroutineContext: CoroutineContext = Dispatchers.IO
- ): AudioProperties? {
- val fd = songFd.dup()?.detachFd() ?: throw NullAudioFileDescriptorException(
- isAudioProperties = true
- )
-
- return withContext(coroutineContext) {
- TagLib.getAudioProperties(
- fd = fd, readStyle = readStyle
- )
- }
- }
-
- private fun updateMapProperty(key: String, value: String) {
- mutableState.value.mutablePropertiesMap[key] = value
- }
-
- fun savePropertyMap(
- context: Context = this.context,
- newPropertiesMap: PropertyMap = mutableState.value.mutablePropertiesMap.toAudioFileMetadata()
- .toPropertyMap(),
- audioPath: String,
- intentPassthrough: (PendingIntent) -> Unit = {}
- ): Boolean {
- return try {
- val fd = MediaStoreReceiver.getFileDescriptorFromPath(context, audioPath, mode = "w")
- ?: throw NullAudioFileDescriptorException(isAudioProperties = false)
-
- fd.dup().detachFd().let {
- TagLib.savePropertyMap(
- it, propertyMap = newPropertiesMap
+ val meta = repository.getMetadata(path).getOrThrow()
+ val audioProps =
+ repository.getAudioProperties(path, AudioPropertiesReadStyle.Average).getOrThrow()
+
+ // Converts PropertyMap → AudioFileMetadata → AudioEditableMetadata
+ val flatMap =
+ meta.propertyMap.toModifiableMap().mapValues { it.value ?: "" }
+ val fileMeta = flatMap.toAudioFileMetadata()
+ val editable =
+ AudioEditableMetadata(
+ title = fileMeta.title.orEmpty(),
+ artist = fileMeta.artist.orEmpty(),
+ album = fileMeta.album.orEmpty(),
+ trackNumber = fileMeta.trackNumber?.toIntOrNull() ?: 0,
+ discNumber = fileMeta.discNumber?.toIntOrNull() ?: 0,
+ date = fileMeta.date.orEmpty(),
+ genre = fileMeta.genre.orEmpty(),
+ comment = fileMeta.comment.orEmpty(),
+ lyrics = fileMeta.lyrics.orEmpty(),
)
- }
- true
- } catch (securityException: SecurityException) {
- handleSecurityException(securityException, intentPassthrough)
- false
- } catch (e: Exception) {
- mutableState.update {
+ _state.update {
it.copy(
- pageState = ScreenState.Error(e)
+ metadata = ResourceState.Success(meta),
+ audioProperties = ResourceState.Success(audioProps),
+ pageState = ScreenState.Success(null),
+ uiState = MetadataEditorUiState().apply { loadFrom(editable) },
)
}
- false
+ } catch (e: Exception) {
+ Log.e("MetadataEditorVM", "Error loading: ${e.message}")
+ _state.update { it.copy(pageState = ScreenState.Error(e)) }
}
}
- private fun savePictures(
- context: Context = this.context,
- imagesUri: List = emptyList(),
+ private suspend fun savePropertyMap(
audioPath: String,
- intentPassthrough: (PendingIntent) -> Unit
- ): Boolean {
- return try {
- val fd = MediaStoreReceiver.getFileDescriptorFromPath(context, audioPath, mode = "w")
- ?: throw NullAudioFileDescriptorException(isAudioProperties = false)
-
- val mutablePicturesList = mutableListOf()
- fd.dup().detachFd().let {
- imagesUri.forEachIndexed { index, imageUri ->
- val byteArray = context.contentResolver.openInputStream(imageUri)?.readBytes()
- ?: return@forEachIndexed
- val mimeType =
- context.contentResolver.getType(imageUri) ?: return@forEachIndexed
- val picture = Picture(
- data = byteArray,
- mimeType = mimeType,
- description = "Audio image $index - Metadator",
- pictureType = "Front cover"
- )
-
- mutablePicturesList.add(picture)
+ intentPassthrough: (PendingIntent) -> Unit = {},
+ ) {
+ try {
+ // Convert the actual properties to a PropertyMap
+ val flatMap = _state.value.uiState.fields.associate { it.key to it.current.toString() }
+ val propertyMap = flatMap.toAudioFileMetadata().toPropertyMap()
+
+ repository.writePropertyMap(audioPath, propertyMap)
+ .onSuccess {
+ // Clear modified flags after saving properties
+ _state.update { it.copy(uiState = it.uiState.clearModified()) }
+ }
+ .onFailure { error ->
+ if (error is SecurityException) {
+ handleSecurityException(error, intentPassthrough)
+ } else {
+ _state.update { it.copy(pageState = ScreenState.Error(error)) }
+ }
}
-
- TagLib.savePictures(
- it, pictures = mutablePicturesList.toTypedArray()
- )
- }
- true
- } catch (securityException: SecurityException) {
- handleSecurityException(securityException, intentPassthrough)
- false
} catch (e: Exception) {
- mutableState.update {
- it.copy(
- pageState = ScreenState.Error(e)
- )
- }
- false
+ _state.update { it.copy(pageState = ScreenState.Error(e)) }
}
}
- private fun savePictures(
- context: Context = this.context,
- imagesUri: List = emptyList(),
- fileDescriptorId: Int = -1
- ): Boolean {
-
- val mutablePicturesList = mutableListOf()
-
- imagesUri.forEachIndexed { index, uri ->
- val byteArray = context.contentResolver.openInputStream(uri)?.readBytes()
- ?: throw IllegalStateException("Image byte array is null")
- val mimeType = context.contentResolver.getType(uri)
- ?: throw IllegalStateException("Image mime type is null")
-
- val picture = Picture(
- data = byteArray,
- mimeType = mimeType,
- description = "Audio image $index - Metadator",
- pictureType = "Front cover"
- )
-
- mutablePicturesList.add(picture)
- }
-
- runCatching {
- TagLib.savePictures(
- fileDescriptorId, pictures = mutablePicturesList.toTypedArray()
- )
- }.onFailure {
- return false
- }.onSuccess {
- return true
+ private suspend fun savePictures(
+ imagesUri: List,
+ audioPath: String,
+ intentPassthrough: (PendingIntent) -> Unit,
+ ) {
+ try {
+ val pictures = uriConverter.convert(imagesUri)
+ repository.writePictures(audioPath, pictures).onFailure { error ->
+ if (error is SecurityException) {
+ handleSecurityException(error, intentPassthrough)
+ } else {
+ _state.update { it.copy(pageState = ScreenState.Error(error)) }
+ }
+ }
+ } catch (e: Exception) {
+ _state.update { it.copy(pageState = ScreenState.Error(e)) }
}
-
- return true
}
private fun handleSecurityException(
- securityException: SecurityException, intentPassthrough: (PendingIntent) -> Unit
+ securityException: SecurityException,
+ intentPassthrough: (PendingIntent) -> Unit,
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
- securityException as? RecoverableSecurityException ?: throw RuntimeException(
- securityException.message, securityException
- )
+ securityException as? RecoverableSecurityException
+ ?: throw RuntimeException(securityException.message, securityException)
intentPassthrough(recoverableSecurityException.userAction.actionIntent)
} else {
@@ -299,28 +162,12 @@ class MetadataEditorViewModel(
}
}
-
- private fun updateAudioInformation(metadata: Metadata?, audioProperties: AudioProperties?) {
- mutableState.update {
- it.copy(
- metadata = ResourceState.Success(metadata),
- audioProperties = ResourceState.Success(audioProperties)
- )
- }
- }
-
private fun updateState(state: ScreenState) {
- mutableState.update {
- it.copy(
- pageState = state
- )
- }
+ _state.update { it.copy(pageState = state) }
}
private fun emitUiEvent(event: UiEvent) {
- viewModelScope.launch {
- _eventFlow.emit(event)
- }
+ viewModelScope.launch { _eventFlow.emit(event) }
}
fun onEvent(event: Event) {
@@ -328,94 +175,113 @@ class MetadataEditorViewModel(
is Event.LoadMetadata -> {
viewModelScope.launch {
if (latestLoadedSongPath != event.path) {
- loadTrackMetadata(event.path, this)
+ loadTrackMetadata(event.path)
latestLoadedSongPath = event.path
}
}
}
is Event.SaveProperties -> {
- val succeeded = savePropertyMap(
- audioPath = event.path,
- intentPassthrough = {
- emitUiEvent(UiEvent.RequestPermission(it))
- }
- )
+ viewModelScope.launch(Dispatchers.IO) {
+ val succeeded =
+ try {
+ savePropertyMap(
+ audioPath = event.path,
+ intentPassthrough = { emitUiEvent(UiEvent.RequestPermission(it)) },
+ )
+ true
+ } catch (e: Exception) {
+ executeIfDebugging {
+ Log.e(
+ "MetadataEditorVM",
+ "Error while trying to save the properties: ${e.message}",
+ )
+ }
+ emitUiEvent(UiEvent.SaveFailed)
+ false
+ }
- if (succeeded) {
- emitUiEvent(UiEvent.SaveSuccess(properties = true))
- } else {
- emitUiEvent(UiEvent.SaveFailed)
+ if (succeeded) {
+ emitUiEvent(UiEvent.SaveSuccess(properties = true))
+ } else {
+ emitUiEvent(UiEvent.SaveFailed)
+ }
}
}
is Event.SavePictures -> {
- val succeeded = savePictures(
- imagesUri = event.imagesUri,
- audioPath = event.path,
- intentPassthrough = {
- emitUiEvent(UiEvent.RequestPermission(it))
- })
-
- if (succeeded) {
- emitUiEvent(UiEvent.SaveSuccess(pictures = true))
- } else {
- emitUiEvent(UiEvent.SaveFailed)
- }
- }
-
- is Event.SaveAll -> {
- val propertiesSaved = savePropertyMap(
- audioPath = event.path,
- intentPassthrough = {
- emitUiEvent(UiEvent.RequestPermission(it))
- }
- )
+ viewModelScope.launch(Dispatchers.IO) {
+ val succeeded =
+ try {
+ savePictures(
+ audioPath = event.path,
+ imagesUri = event.imagesUri,
+ intentPassthrough = { emitUiEvent(UiEvent.RequestPermission(it)) },
+ )
+ true
+ } catch (e: Exception) {
+ executeIfDebugging {
+ Log.e(
+ "MetadataEditorVM",
+ "Error while trying to save the pictures: ${e.message}",
+ )
+ }
+ emitUiEvent(UiEvent.SaveFailed)
+ false
+ }
- val succeededPictures = savePictures(
- audioPath = event.path,
- imagesUri = event.imagesUri,
- intentPassthrough = {
- emitUiEvent(UiEvent.RequestPermission(it))
+ if (succeeded) {
+ emitUiEvent(UiEvent.SaveSuccess(pictures = true))
+ } else {
+ emitUiEvent(UiEvent.SaveFailed)
}
- )
-
- if (propertiesSaved || succeededPictures) {
- emitUiEvent(
- UiEvent.SaveSuccess(
- pictures = succeededPictures, properties = propertiesSaved
- )
- )
- } else {
- emitUiEvent(UiEvent.SaveFailed)
}
}
is Event.UpdateProperty -> {
- updateMapProperty(event.key, event.value)
+ // Update a single field in the UI state
+ _state.update { it.copy(uiState = it.uiState.updateField(event.key, event.value)) }
+ }
+
+ is Event.SaveAll -> {
+ viewModelScope.launch(Dispatchers.IO) {
+ val flatMap = state.value.uiState.fields.associate { it.key to it.current.toString() }
+ val propertyMap = flatMap.toAudioFileMetadata().toPropertyMap()
+ try {
+ repository.writePropertyMap(event.path, propertyMap)
+ val pictures = uriConverter.convert(event.imagesUri)
+ repository.writePictures(event.path, pictures)
+ // Clear all modified fields after full save
+ _state.update { it.copy(uiState = it.uiState.clearModified()) }
+ emitUiEvent(UiEvent.SaveSuccess(pictures = true, properties = true))
+ } catch (e: SecurityException) {
+ handleSecurityException(e) { emitUiEvent(UiEvent.RequestPermission(it)) }
+ } catch (e: Exception) {
+ emitUiEvent(UiEvent.SaveFailed)
+ }
+ }
}
}
}
-
interface Event {
data class LoadMetadata(val path: String) : Event
+
data class SaveAll(val path: String, val imagesUri: List) : Event
+
data class SaveProperties(val path: String) : Event
+
data class SavePictures(val path: String, val imagesUri: List) : Event
+
data class UpdateProperty(val key: String, val value: String) : Event
}
interface UiEvent {
data class RequestPermission(val intent: PendingIntent) : UiEvent
+
data class SaveSuccess(val pictures: Boolean? = null, val properties: Boolean? = null) :
UiEvent
data object SaveFailed : UiEvent
}
-
- companion object {
- class NullAudioFileDescriptorException(val isAudioProperties: Boolean) :
- Exception("Audio file descriptor is null")
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/SongSyncNeededDialog.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/SongSyncNeededDialog.kt
index 808619f..1831912 100644
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/SongSyncNeededDialog.kt
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/SongSyncNeededDialog.kt
@@ -18,29 +18,26 @@ import com.bobbyesp.utilities.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun SongSyncNeededDialog(
- modifier: Modifier = Modifier,
- onDismissRequest: () -> Unit,
-) {
+fun SongSyncNeededDialog(modifier: Modifier = Modifier, onDismissRequest: () -> Unit) {
val uriLauncher = LocalUriHandler.current
AlertDialog(
icon = {
- Icon(
- imageVector = Icons.Filled.Lyrics,
- contentDescription = "SongSync app needed"
- )
- }, modifier = modifier, onDismissRequest = onDismissRequest,
- title = {
- Text(text = stringResource(id = R.string.song_sync_needed))
- }, text = {
+ Icon(imageVector = Icons.Filled.Lyrics, contentDescription = "SongSync app icon")
+ },
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = stringResource(id = R.string.song_sync_needed)) },
+ text = {
Text(
- text = buildAnnotatedString {
- append(stringResource(id = R.string.song_sync_needed_desc))
- append(" \n")
- append(stringResource(id = R.string.song_sync_not_installed))
- },
+ text =
+ buildAnnotatedString {
+ append(stringResource(id = R.string.song_sync_needed_desc))
+ append(" \n")
+ append(stringResource(id = R.string.song_sync_not_installed))
+ }
)
- }, confirmButton = {
+ },
+ confirmButton = {
TextButton(
onClick = {
uriLauncher.openUri("https://github.com/Lambada10/SongSync/releases/latest")
@@ -48,18 +45,15 @@ fun SongSyncNeededDialog(
) {
Text(stringResource(id = R.string.download))
}
- }, dismissButton = {
- TextButton(
- onClick = onDismissRequest
- ) {
- Text(stringResource(id = R.string.dismiss))
- }
- }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) { Text(stringResource(id = R.string.dismiss)) }
+ },
)
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun SongSyncNeededDialogPreview() {
- SongSyncNeededDialog(onDismissRequest = { })
-}
\ No newline at end of file
+ SongSyncNeededDialog(onDismissRequest = {})
+}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/MetadataBottomSheetViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/MetadataBottomSheetViewModel.kt
deleted file mode 100644
index e4e015a..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/MetadataBottomSheetViewModel.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import androidx.paging.PagingData
-import androidx.paging.cachedIn
-import com.adamratzman.spotify.models.Track
-import com.bobbyesp.metadator.features.spotify.domain.services.search.SpotifySearchService
-import com.bobbyesp.utilities.states.ResourceState
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import org.koin.core.component.KoinComponent
-
-class MetadataBottomSheetViewModel(
- private val searchService: SpotifySearchService
-) : KoinComponent, ViewModel() {
- private val mutableViewStateFlow = MutableStateFlow(ViewState())
- val viewStateFlow = mutableViewStateFlow.asStateFlow()
-
- private val _outerEventsFlow = MutableSharedFlow()
- val outerEventsFlow = _outerEventsFlow.asSharedFlow()
-
- data class ViewState(
- val stage: BottomSheetStage = BottomSheetStage.SEARCH,
- val searchedTracks: ResourceState>> = ResourceState.Loading(null),
- val selectedTrack: Track? = null,
- val lastQuery: String = "",
- )
-
- private fun searchTracks(query: String) {
- updateQuery(query)
- viewModelScope.launch(Dispatchers.IO) {
- getTracksPaginatedData(query)
- }
- }
-
- private suspend fun getTracksPaginatedData(query: String) {
- try {
- val tracksPager = searchService.searchPaginatedTracks(
- query = query,
- filters = emptyList()
- ).flow.cachedIn(viewModelScope)
-
- mutableViewStateFlow.update {
- it.copy(
- searchedTracks = ResourceState.Success(tracksPager)
- )
- }
- } catch (th: Throwable) {
- mutableViewStateFlow.update {
- it.copy(
- searchedTracks = ResourceState.Error(
- errorMessage = th.message ?: th.stackTrace.toString()
- )
- )
- }
- }
- }
-
- private fun chooseTrack(track: Track?) {
- mutableViewStateFlow.update {
- it.copy(
- selectedTrack = track
- )
- }
- updateStage(BottomSheetStage.TRACK_DETAILS)
- }
-
- private fun updateStage(stage: BottomSheetStage) {
- mutableViewStateFlow.update {
- it.copy(
- stage = stage
- )
- }
- }
-
- private fun saveMetadata(modifiedFields: Map) {
- viewModelScope.launch {
- _outerEventsFlow.emit(OuterEvent.SaveMetadata(modifiedFields))
- }
- }
-
- private fun updateQuery(query: String) {
- mutableViewStateFlow.update {
- it.copy(
- lastQuery = query
- )
- }
- }
-
- fun onEvent(event: Event) {
- when (event) {
- is Event.Search -> {
- searchTracks(event.query)
- }
-
- is Event.ChangeState -> {
- updateStage(event.state)
- }
-
- is Event.SelectTrack -> {
- chooseTrack(event.track)
- if (event.track == null) updateStage(BottomSheetStage.SEARCH)
- }
-
- is Event.UpdateMetadataFields -> {
- saveMetadata(event.properties)
- }
- }
- }
-
- interface OuterEvent {
- data class SaveMetadata(val modifiedFields: Map) : OuterEvent
- }
-
- interface Event {
- data class Search(val query: String) : Event
- data class ChangeState(val state: BottomSheetStage) : Event
- data class SelectTrack(val track: Track?) : Event
- data class UpdateMetadataFields(val properties: Map) : Event
- }
-
- companion object {
- enum class BottomSheetStage {
- SEARCH,
- TRACK_DETAILS
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/SpMetadataBottomSheetContent.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/SpMetadataBottomSheetContent.kt
deleted file mode 100644
index 9ea4e2d..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/SpMetadataBottomSheetContent.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.SheetState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.ui.Modifier
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.MetadataBottomSheetViewModel.Companion.BottomSheetStage
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.stages.NoSongInformationProvided
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.stages.SpMetadataBsDetails
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.stages.SpMetadataBsSearch
-import com.bobbyesp.ui.motion.MotionConstants.DURATION_EXIT_SHORT
-import com.bobbyesp.ui.motion.tweenEnter
-import com.bobbyesp.ui.motion.tweenExit
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun SpMetadataBottomSheetContent(
- name: String,
- artist: String,
- sheetState: SheetState,
- bsViewState: State,
- onEvent: (MetadataBottomSheetViewModel.Event) -> Unit,
- onCloseSheet: () -> Unit
-) {
- val lazyListState = rememberLazyListState()
-
- fun search(query: String) {
- onEvent(MetadataBottomSheetViewModel.Event.ChangeState(BottomSheetStage.SEARCH))
- onEvent(MetadataBottomSheetViewModel.Event.Search(query))
- }
-
- if (name.isEmpty() && artist.isEmpty()) {
- NoSongInformationProvided { providedName, providedArtist ->
- val query = "$providedName $providedArtist"
- search(query)
- }
- }
-
- LaunchedEffect(sheetState.isVisible, name, artist) {
- val query = "$name $artist"
- if (sheetState.isVisible && bsViewState.value.lastQuery != query) {
- search(query)
- }
- }
-
- AnimatedContent(
- targetState = bsViewState.value.stage,
- label = "Transition between bs states",
- transitionSpec = {
- fadeIn(
- tweenEnter(delayMillis = DURATION_EXIT_SHORT)
- ) togetherWith fadeOut(
- tweenExit(durationMillis = DURATION_EXIT_SHORT)
- )
- }) { actualStage ->
- when (actualStage) {
- BottomSheetStage.SEARCH -> {
- SpMetadataBsSearch(
- name = name,
- artist = artist,
- listState = lazyListState,
- pageViewState = bsViewState,
- onChooseTrack = { track ->
- onEvent(MetadataBottomSheetViewModel.Event.SelectTrack(track))
- }
- )
- }
-
- BottomSheetStage.TRACK_DETAILS -> {
- SpMetadataBsDetails(
- modifier = Modifier.fillMaxSize(),
- onEvent = onEvent,
- pageViewState = bsViewState,
- onCloseSheet = onCloseSheet
- )
- }
- }
- }
-}
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/NoSongInformationProvided.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/NoSongInformationProvided.kt
deleted file mode 100644
index 810bf10..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/NoSongInformationProvided.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.stages
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.MoodBad
-import androidx.compose.material3.Button
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-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.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import com.bobbyesp.metadator.R
-import com.bobbyesp.ui.components.text.PreConfiguredOutlinedTextField
-
-@Composable
-fun NoSongInformationProvided(
- onRetrySearch: (String, String) -> Unit
-) {
- val (name, setName) = remember { mutableStateOf("") }
- val (artist, setArtist) = remember { mutableStateOf("") }
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
- ) {
- Icon(
- imageVector = Icons.Rounded.MoodBad,
- contentDescription = stringResource(id = R.string.no_song_information_provided),
- modifier = Modifier.size(48.dp),
- tint = MaterialTheme.colorScheme.onSurface
- )
- Column(
- modifier = Modifier.fillMaxWidth(0.9f),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = stringResource(id = R.string.no_song_information_provided),
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.SemiBold
- )
- Row(
- modifier = Modifier.fillMaxWidth(),
- ) {
- PreConfiguredOutlinedTextField(
- value = name,
- label = stringResource(id = R.string.title),
- modifier = Modifier.weight(0.5f)
- ) { title ->
- setName(title)
- }
- Spacer(modifier = Modifier.width(8.dp))
- PreConfiguredOutlinedTextField(
- value = artist,
- label = stringResource(id = R.string.artist),
- modifier = Modifier.weight(0.5f)
- ) { artist ->
- setArtist(artist)
- }
- }
- }
- Button(
- modifier = Modifier.fillMaxWidth(0.7f),
- onClick = { onRetrySearch(name, artist) }
- ) {
- Text(
- text = stringResource(id = R.string.retry_search),
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.SemiBold
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/SpMetadataBsDetails.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/SpMetadataBsDetails.kt
deleted file mode 100644
index 819a337..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/SpMetadataBsDetails.kt
+++ /dev/null
@@ -1,306 +0,0 @@
-package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.stages
-
-import android.content.Context
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.rememberLazyGridState
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.adamratzman.spotify.models.Track
-import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.core.ext.TagLib.toImageVector
-import com.bobbyesp.metadator.core.ext.TagLib.toLocalizedName
-import com.bobbyesp.metadator.core.ext.format
-import com.bobbyesp.metadator.core.ext.formatArtists
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.MetadataBottomSheetViewModel
-import com.bobbyesp.metadator.core.presentation.components.image.AsyncImage
-import com.bobbyesp.metadator.core.presentation.components.text.ConditionedMarqueeText
-import com.bobbyesp.ui.components.button.BackButton
-import com.bobbyesp.ui.components.others.SelectableSurface
-import com.bobbyesp.ui.util.rememberVolatileSaveable
-
-@Composable
-fun SpMetadataBsDetails(
- modifier: Modifier = Modifier,
- pageViewState: State,
- onEvent: (MetadataBottomSheetViewModel.Event) -> Unit,
- onCloseSheet: () -> Unit,
-) {
- BackHandler {
- onEvent(MetadataBottomSheetViewModel.Event.SelectTrack(null))
- }
-
- val chosenMetadata = rememberSaveable(key = "chosenMetadata") {
- mutableMapOf()
- }
-
- val lazyGirdState = rememberLazyGridState()
-
- Column(
- modifier = modifier
- .padding(horizontal = 8.dp)
- .fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
- verticalAlignment = Alignment.CenterVertically
- ) {
- BackButton(
- onClick = {
- onEvent(MetadataBottomSheetViewModel.Event.SelectTrack(null))
- }
- )
- Text(
- text = stringResource(id = R.string.spotify_metadata),
- style = MaterialTheme.typography.titleSmall.copy(
- fontSize = 20.sp,
- fontWeight = FontWeight.Bold
- ),
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier
- )
- Spacer(modifier = Modifier.weight(1f))
- TextButton(
- onClick = {
- onEvent(MetadataBottomSheetViewModel.Event.UpdateMetadataFields(chosenMetadata.toMap()))
- onCloseSheet()
- }
- ) {
- Text(text = stringResource(id = R.string.save))
- }
- }
- pageViewState.value.selectedTrack?.let { track ->
- val context = LocalContext.current
- val metadataMap = createMetadataMap(context, track)
-
- TrackInfo(
- modifier = Modifier.padding(vertical = 6.dp, horizontal = 8.dp),
- track = track
- )
-
- LazyVerticalGrid(
- state = lazyGirdState,
- columns = GridCells.Adaptive(200.dp),
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(8.dp)
- ) {
- val metadataList = metadataMap.toList()
- val isTheListOdd = metadataList.size % 2 != 0
-
- items(metadataList.size) { index ->
- // Skip the last item if the size of metadataMap is odd
- if (isTheListOdd && index == metadataList.size - 1) {
- return@items
- }
-
- val (field, retrievedValue) = metadataList[index]
- SelectableMetadataField(
- modifier = Modifier
- .padding(4.dp)
- .heightIn(min = 100.dp),
- title = field,
- value = retrievedValue,
- onSelectMetadata = { title, value -> chosenMetadata[title] = value },
- onDeleteMetadata = { title -> chosenMetadata.remove(title) }
- )
- }
-
- if (isTheListOdd) {
- val lastElement = metadataList.last()
- val (field, retrievedValue) = lastElement
- item(span = { GridItemSpan(maxLineSpan) }) {
- SelectableMetadataField(
- modifier = Modifier
- .padding(4.dp)
- .heightIn(min = 100.dp),
- title = field,
- value = retrievedValue,
- onSelectMetadata = { title, value -> chosenMetadata[title] = value },
- onDeleteMetadata = { title -> chosenMetadata.remove(title) }
- )
- }
- }
- }
- }
- }
-}
-
-@Composable
-fun createMetadataMap(context: Context, track: Track) = rememberSaveable {
- mutableMapOf(
- "TITLE" to track.name,
- "ARTIST" to track.artists.formatArtists(),
- "ALBUM" to track.album.name,
- "ALBUMARTIST" to track.album.artists.formatArtists(),
- "TRACKNUMBER" to track.trackNumber.toString(),
- "DISCNUMBER" to track.discNumber.toString(),
- "DATE" to (track.album.releaseDate?.format(track.album.releaseDatePrecisionString)
- ?: context.getString(R.string.unknown)),
- )
-}
-
-@Composable
-private fun TrackInfo(
- modifier: Modifier = Modifier,
- track: Track,
-) {
- val albumArtPath = track.album.images?.getOrNull(0)?.url
-
- Row(
- modifier = modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- AsyncImage(
- modifier = Modifier.size(64.dp),
- imageModel = albumArtPath,
- shape = MaterialTheme.shapes.extraSmall
- )
- Row(
- modifier = Modifier.weight(1f),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- Column(
- horizontalAlignment = Alignment.Start,
- modifier = Modifier
- .padding(8.dp)
- .weight(1f)
- ) {
- ConditionedMarqueeText(
- text = track.name,
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.Bold,
- )
- Text(
- text = track.artists.formatArtists(),
- style = MaterialTheme.typography.bodyMedium.copy(
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- ),
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
- }
-}
-
-@Composable
-private fun SelectableMetadataField(
- modifier: Modifier = Modifier,
- title: String,
- value: String,
- useIcon: Boolean = true,
- onSelectMetadata: (title: String, value: String) -> Unit,
- onDeleteMetadata: (title: String) -> Unit
-) {
- var isSelected by rememberVolatileSaveable(initialValue = false)
- SelectableSurface(
- modifier = modifier,
- isSelected = isSelected,
- tonalElevation = 2.dp,
- borderStroke = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
- onSelected = {
- isSelected = !isSelected
- if (isSelected) onSelectMetadata(title, value) else onDeleteMetadata(title)
- },
- shape = MaterialTheme.shapes.medium,
- ) {
- Box {
- Column(
- modifier = Modifier
- .padding(8.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
- horizontalAlignment = Alignment.Start
- ) {
- Row(
- modifier = Modifier,
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start)
- ) {
- if (useIcon) {
- Icon(imageVector = title.toImageVector(), contentDescription = null)
- }
- Text(
- text = title.toLocalizedName(),
- style = MaterialTheme.typography.bodyMedium.copy(
- fontWeight = FontWeight.Bold
- )
- )
- }
- Text(
- text = value,
- style = MaterialTheme.typography.bodyMedium,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
- }
-
- RadioButton(
- selected = isSelected,
- onClick = {
- isSelected = !isSelected
- if (isSelected) onSelectMetadata(title, value) else onDeleteMetadata(title)
- },
- modifier = Modifier.align(Alignment.BottomEnd)
- )
- }
- }
-}
-
-@Preview
-@Composable
-fun SelectableMetadataFieldNoIconPreview() {
- SelectableMetadataField(
- modifier = Modifier.size(200.dp),
- title = "TITLE",
- value = "Title",
- onSelectMetadata = { _, _ -> },
- onDeleteMetadata = { },
- useIcon = false
- )
-}
-
-@Preview
-@Composable
-fun SelectableMetadataFieldWithIconPreview() {
- SelectableMetadataField(
- modifier = Modifier.size(200.dp),
- title = "TITLE",
- value = "Title",
- onSelectMetadata = { _, _ -> },
- onDeleteMetadata = { },
- useIcon = true
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/SpMetadataBsSearch.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/SpMetadataBsSearch.kt
deleted file mode 100644
index d2a7688..0000000
--- a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/pages/tageditor/spotify/stages/SpMetadataBsSearch.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-package com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.stages
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.paging.compose.collectAsLazyPagingItems
-import androidx.paging.compose.itemContentType
-import androidx.paging.compose.itemKey
-import com.adamratzman.spotify.models.Track
-import com.bobbyesp.metadator.R
-import com.bobbyesp.metadator.mediastore.presentation.components.card.songs.spotify.SpotifyHorizontalSongCard
-import com.bobbyesp.metadator.tageditor.presentation.pages.tageditor.spotify.MetadataBottomSheetViewModel
-import com.bobbyesp.ui.components.button.VerticalButtonWithIconAndText
-import com.bobbyesp.ui.components.state.LoadingState
-import com.bobbyesp.utilities.states.ResourceState
-import com.bobbyesp.utilities.ui.pagingStateHandler
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun SpMetadataBsSearch(
- name: String,
- artist: String,
- listState: LazyListState = rememberLazyListState(),
- pageViewState: State,
- onChooseTrack: (Track) -> Unit
-) {
- val paginatedTracksState = pageViewState.value.searchedTracks
- val paginatedTracks = paginatedTracksState.data?.collectAsLazyPagingItems()
-
- LazyColumn(
- state = listState,
- modifier = Modifier.fillMaxSize()
- ) {
- if (name.isNotEmpty() && artist.isNotEmpty()) {
- item {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .background(
- brush = Brush.verticalGradient(
- colors = listOf(
- MaterialTheme.colorScheme.surfaceContainerLow, Color.Transparent
- ),
- startY = 50f
- )
- )
- .padding(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) { // TODO: Be able to change the query
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(4.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- Column(
- modifier = Modifier.weight(1f),
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- Text(
- text = stringResource(R.string.showing_results_for).uppercase(),
- style = MaterialTheme.typography.labelLarge.copy(
- color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
- letterSpacing = 2.sp
- )
- )
- Text(
- text = name, style = MaterialTheme.typography.headlineSmall
- )
- Text(
- text = buildAnnotatedString {
- append(stringResource(R.string.by))
- append(" ")
- withStyle(MaterialTheme.typography.titleMedium.toSpanStyle()) {
- append(artist)
- }
- },
- )
- }
- Row(
- modifier = Modifier
- .fillMaxHeight()
- .weight(1f),
- horizontalArrangement = Arrangement.SpaceEvenly,
- verticalAlignment = Alignment.CenterVertically
- ) {
- VerticalButtonWithIconAndText(
- icon = Icons.Rounded.Edit,
- text = stringResource(R.string.edit_query),
- modifier = Modifier.weight(1f),
- onClick = { /* TODO: Edit query */ },
- enabled = true,
- shape = RoundedCornerShape(8.dp)
- )
- }
- }
- }
- }
- }
-
- when (paginatedTracksState) {
- is ResourceState.Loading -> {
- item {
- LoadingState(stringResource(id = R.string.retrieving_spotify_token))
- }
- }
-
- is ResourceState.Success -> {
- items(
- count = paginatedTracks!!.itemCount,
- key = paginatedTracks.itemKey(),
- contentType = paginatedTracks.itemContentType()
- ) { index ->
- val item = paginatedTracks[index] ?: return@items
- SpotifyHorizontalSongCard(
- innerModifier = Modifier.padding(8.dp),
- surfaceColor = Color.Transparent,
- track = item,
- onClick = {
- onChooseTrack(item)
- }
- )
- }
-
- pagingStateHandler(paginatedTracks, itemCount = 1) {
- LoadingState(stringResource(id = R.string.loading))
- }
- }
-
- is ResourceState.Error -> {
- item {
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp),
- border = BorderStroke(
- 1.dp,
- MaterialTheme.colorScheme.error.copy(alpha = 0.5f)
- ),
- contentColor = MaterialTheme.colorScheme.error,
- shape = MaterialTheme.shapes.medium
- ) {
- Column(
- modifier = Modifier.padding(4.dp),
- verticalArrangement = Arrangement.spacedBy(
- 8.dp,
- Alignment.CenterVertically
- ),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- modifier = Modifier,
- text = paginatedTracksState.message
- ?: stringResource(id = com.bobbyesp.ui.R.string.unknown_error_title),
- style = MaterialTheme.typography.bodyMedium,
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Normal
- )
- }
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/FieldState.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/FieldState.kt
new file mode 100644
index 0000000..8e24c57
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/FieldState.kt
@@ -0,0 +1,21 @@
+package com.bobbyesp.metadator.tageditor.presentation.state
+
+sealed class FieldState(
+ val key: String,
+ val labelRes: Int,
+ val original: T,
+ var current: T
+) {
+ val isModified: Boolean
+ get() = original != current
+
+ abstract val errorMessageRes: Int?
+
+ private var _error: Int? = null
+ val errorMessage: Int?
+ get() = _error
+
+ fun validate(validator: (T) -> Int?): FieldState = apply {
+ _error = validator(current)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/IntFieldState.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/IntFieldState.kt
new file mode 100644
index 0000000..54b224d
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/IntFieldState.kt
@@ -0,0 +1,9 @@
+package com.bobbyesp.metadator.tageditor.presentation.state
+
+class IntFieldState(
+ key: String,
+ labelRes: Int,
+ original: Int
+) : FieldState(key, labelRes, original, original) {
+ override val errorMessageRes: Int? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/MetadataEditorUiState.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/MetadataEditorUiState.kt
new file mode 100644
index 0000000..b6d2225
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/MetadataEditorUiState.kt
@@ -0,0 +1,52 @@
+package com.bobbyesp.metadator.tageditor.presentation.state
+
+import com.bobbyesp.metadator.R
+import com.bobbyesp.metadator.tageditor.domain.AudioEditableMetadata
+
+data class MetadataEditorUiState(
+ val fields: List> = emptyList()
+) {
+
+ fun loadFrom(editable: AudioEditableMetadata): MetadataEditorUiState =
+ copy(
+ fields = listOf(
+ StringFieldState("TITLE", R.string.title, editable.title),
+ StringFieldState("ARTIST", R.string.artist, editable.artist),
+ StringFieldState("ALBUM", R.string.album, editable.album),
+ IntFieldState("TRACKNUMBER", R.string.track_number, editable.trackNumber),
+ IntFieldState("DISCNUMBER", R.string.disc_number, editable.discNumber),
+ StringFieldState("DATE", R.string.date, editable.date),
+ StringFieldState("GENRE", R.string.genre, editable.genre),
+ StringFieldState("COMMENT", R.string.comment, editable.comment),
+ StringFieldState("LYRICS", R.string.lyrics, editable.lyrics),
+ )
+ )
+
+ fun toDomain(): AudioEditableMetadata {
+ val map = fields.associate { it.key to it.current.toString() }
+ return AudioEditableMetadata.fromMap(map)
+ }
+
+ val modifiedKeys: Set
+ get() = fields.filter { it.isModified }.map { it.key }.toSet()
+
+ /** Update a single field's current value based on its key */
+ fun updateField(key: String, value: String): MetadataEditorUiState =
+ copy(fields = fields.map { field ->
+ if (field.key == key) {
+ when (field) {
+ is StringFieldState -> StringFieldState(field.key, field.labelRes, field.current).apply { current = value }
+ is IntFieldState -> IntFieldState(field.key, field.labelRes, field.original).apply { current = value.toIntOrNull() ?: original }
+ }
+ } else field
+ })
+
+ /** Reset the original values to the current ones, clearing modification state */
+ fun clearModified(): MetadataEditorUiState =
+ copy(fields = fields.map { field ->
+ when (field) {
+ is StringFieldState -> StringFieldState(field.key, field.labelRes, field.current)
+ is IntFieldState -> IntFieldState(field.key, field.labelRes, field.current)
+ }
+ })
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/StringFieldState.kt b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/StringFieldState.kt
new file mode 100644
index 0000000..e4e06e6
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/metadator/tageditor/presentation/state/StringFieldState.kt
@@ -0,0 +1,9 @@
+package com.bobbyesp.metadator.tageditor.presentation.state
+
+class StringFieldState(
+ key: String,
+ labelRes: Int,
+ original: String
+) : FieldState(key, labelRes, original, original) {
+ override val errorMessageRes: Int? = null
+}
\ No newline at end of file
diff --git a/app/src/playstore/kotlin/FirebaseSetup.kt b/app/src/playstore/kotlin/FirebaseSetup.kt
index cae1b43..5781f05 100644
--- a/app/src/playstore/kotlin/FirebaseSetup.kt
+++ b/app/src/playstore/kotlin/FirebaseSetup.kt
@@ -10,9 +10,9 @@ fun App.initializeFirebase() {
/**
* Extension function for MainActivity to enable Crashlytics collection.
*
- * This function sets the Crashlytics collection to be enabled, allowing Firebase Crashlytics
- * to collect crash reports for the application.
+ * This function sets the Crashlytics collection to be enabled, allowing Firebase Crashlytics to
+ * collect crash reports for the application.
*/
fun setCrashlyticsCollection() {
Firebase.crashlytics.setCrashlyticsCollectionEnabled(true)
-}
\ No newline at end of file
+}
diff --git a/app/src/test/java/com/bobbyesp/metadator/ExampleUnitTest.kt b/app/src/test/java/com/bobbyesp/metadator/ExampleUnitTest.kt
index 2bb4768..cb6e609 100644
--- a/app/src/test/java/com/bobbyesp/metadator/ExampleUnitTest.kt
+++ b/app/src/test/java/com/bobbyesp/metadator/ExampleUnitTest.kt
@@ -13,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts
index 8ce3205..22e47fe 100644
--- a/app/ui/build.gradle.kts
+++ b/app/ui/build.gradle.kts
@@ -2,18 +2,17 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.android.kotlin)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.ktfmt.gradle)
}
android {
namespace = "com.bobbyesp.ui"
- compileSdk = 35
+ compileSdk = 36
defaultConfig {
minSdk = 24
- vectorDrawables {
- useSupportLibrary = true
- }
+ vectorDrawables { useSupportLibrary = true }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
@@ -24,44 +23,39 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
- packaging {
- resources.excludes.add("META-INF/*.kotlin_module")
- }
+ packaging { resources.excludes.add("META-INF/*.kotlin_module") }
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
- kotlinOptions {
- jvmTarget = "21"
- }
- buildFeatures {
- compose = true
- }
- composeCompiler {
- reportsDestination = layout.buildDirectory.dir("compose_compiler")
- }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
- }
+ buildFeatures { compose = true }
+ composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") }
+ packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
}
-dependencies {
+ktfmt {
+ // Google style - 2 space indentation & automatically adds/removes trailing commas
+ // googleStyle()
+
+ // KotlinLang style - 4 space indentation - From
+ // https://kotlinlang.org/docs/coding-conventions.html
+ kotlinLangStyle()
+}
+dependencies {
implementation(libs.core.ktx)
implementation(libs.bundles.compose)
implementation(libs.compose.tooling.preview)
implementation(libs.materialKolor)
implementation(libs.scrollbar)
- //Compose testing and tooling libraries
+ // Compose testing and tooling libraries
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.compose.test.junit4)
debugImplementation(libs.compose.tooling)
debugImplementation(libs.compose.test.manifest)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/androidTest/java/com/bobbyesp/ui/ExampleInstrumentedTest.kt b/app/ui/src/androidTest/java/com/bobbyesp/ui/ExampleInstrumentedTest.kt
index 4e44cea..0729e04 100644
--- a/app/ui/src/androidTest/java/com/bobbyesp/ui/ExampleInstrumentedTest.kt
+++ b/app/ui/src/androidTest/java/com/bobbyesp/ui/ExampleInstrumentedTest.kt
@@ -19,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.bobbyesp.ui.test", appContext.packageName)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/ErrorPage.kt b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/ErrorPage.kt
index 5253fdb..a589dfd 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/ErrorPage.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/ErrorPage.kt
@@ -66,52 +66,46 @@ import com.bobbyesp.ui.motion.MotionConstants.DURATION_EXIT_SHORT
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
-fun ErrorPage(
- modifier: Modifier = Modifier, throwable: Throwable, onRetry: () -> Unit
-) {
+fun ErrorPage(modifier: Modifier = Modifier, throwable: Throwable, onRetry: () -> Unit) {
var showFullscreenError by remember { mutableStateOf(false) }
- SharedTransitionLayout(
- modifier = modifier.background(MaterialTheme.colorScheme.background),
- ) {
+ SharedTransitionLayout(modifier = modifier.background(MaterialTheme.colorScheme.background)) {
AnimatedContent(
transitionSpec = {
fadeIn(
tween(
durationMillis = DURATION_ENTER,
delayMillis = DURATION_EXIT_SHORT,
- easing = EmphasizedDecelerateEasing
- )
- ) togetherWith fadeOut(
- tween(
- durationMillis = DURATION_EXIT_SHORT, easing = EmphasizedAccelerateEasing
+ easing = EmphasizedDecelerateEasing,
)
- ) using SizeTransform { _, _ ->
- tween(durationMillis = DURATION, easing = EmphasizedEasing)
- }
- }, targetState = showFullscreenError, label = "Error Page animated content transition"
+ ) togetherWith
+ fadeOut(
+ tween(
+ durationMillis = DURATION_EXIT_SHORT,
+ easing = EmphasizedAccelerateEasing,
+ )
+ ) using
+ SizeTransform { _, _ ->
+ tween(durationMillis = DURATION, easing = EmphasizedEasing)
+ }
+ },
+ targetState = showFullscreenError,
+ label = "Error Page animated content transition",
) { wantsFullscreen ->
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
if (wantsFullscreen) {
ExpandedErrorPage(
modifier = modifier,
throwable = throwable,
animatedVisibilityScope = this@AnimatedContent,
- onMinimize = {
- showFullscreenError = false
- }
+ onMinimize = { showFullscreenError = false },
)
} else {
MinimizedErrorPage(
modifier = modifier,
throwable = throwable,
animatedVisibilityScope = this@AnimatedContent,
- onCardClicked = {
- showFullscreenError = true
- },
- onRetry = onRetry
+ onCardClicked = { showFullscreenError = true },
+ onRetry = onRetry,
)
}
}
@@ -129,57 +123,58 @@ private fun SharedTransitionScope.MinimizedErrorPage(
onRetry: () -> Unit,
) {
Column(
- modifier = modifier
- .padding(8.dp),
+ modifier = modifier.padding(8.dp),
verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier.size(48.dp),
imageVector = Icons.Rounded.WarningAmber,
contentDescription = stringResource(id = R.string.error),
- tint = MaterialTheme.colorScheme.error
+ tint = MaterialTheme.colorScheme.error,
)
Text(
text = stringResource(id = R.string.unknown_error_title),
- style = MaterialTheme.typography.titleLarge.copy(
- color = MaterialTheme.colorScheme.onBackground
- ),
+ style =
+ MaterialTheme.typography.titleLarge.copy(
+ color = MaterialTheme.colorScheme.onBackground
+ ),
fontWeight = FontWeight.SemiBold,
)
PrimaryStacktraceCard(
- modifier = Modifier
- .sharedBounds(
- boundsTransform = DefaultBoundsTransform,
- enter = fadeIn(
- tween(
- durationMillis = DURATION_ENTER,
- delayMillis = DURATION_EXIT_SHORT,
- easing = EmphasizedDecelerateEasing
- )
- ),
- exit = fadeOut(
- tween(
- durationMillis = DURATION_EXIT_SHORT,
- easing = EmphasizedAccelerateEasing
- )
- ),
- sharedContentState = rememberSharedContentState(key = "stacktraceCardBounds"),
- animatedVisibilityScope = animatedVisibilityScope,
- placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
- )
- .padding(horizontal = 12.dp, vertical = 8.dp)
- .fillMaxWidth(),
- errorType = throwable::class.simpleName
- ?: stringResource(id = R.string.unknown_error_title),
- methodFailed = throwable.localizedMessage
- ?: stringResource(id = R.string.unknown_error_title),
+ modifier =
+ Modifier.sharedBounds(
+ boundsTransform = DefaultBoundsTransform,
+ enter =
+ fadeIn(
+ tween(
+ durationMillis = DURATION_ENTER,
+ delayMillis = DURATION_EXIT_SHORT,
+ easing = EmphasizedDecelerateEasing,
+ )
+ ),
+ exit =
+ fadeOut(
+ tween(
+ durationMillis = DURATION_EXIT_SHORT,
+ easing = EmphasizedAccelerateEasing,
+ )
+ ),
+ sharedContentState =
+ rememberSharedContentState(key = "stacktraceCardBounds"),
+ animatedVisibilityScope = animatedVisibilityScope,
+ placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
+ )
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ .fillMaxWidth(),
+ errorType =
+ throwable::class.simpleName ?: stringResource(id = R.string.unknown_error_title),
+ methodFailed =
+ throwable.localizedMessage ?: stringResource(id = R.string.unknown_error_title),
line = throwable.stackTrace.firstOrNull()?.lineNumber ?: 0,
- onClick = onCardClicked
+ onClick = onCardClicked,
)
- Button(onClick = onRetry) {
- Text(text = stringResource(id = R.string.retry))
- }
+ Button(onClick = onRetry) { Text(text = stringResource(id = R.string.retry)) }
}
}
@@ -191,86 +186,72 @@ private fun SharedTransitionScope.ExpandedErrorPage(
throwable: Throwable,
onMinimize: () -> Unit,
) {
- BackHandler {
- onMinimize()
- }
+ BackHandler { onMinimize() }
Column(
- modifier = modifier
- .sharedBounds(
- boundsTransform = DefaultBoundsTransform,
- enter = fadeIn(
- tween(
- durationMillis = DURATION_ENTER,
- delayMillis = DURATION_EXIT_SHORT,
- easing = EmphasizedDecelerateEasing
- )
- ),
- exit = fadeOut(
- tween(
- durationMillis = DURATION_EXIT_SHORT, easing = EmphasizedAccelerateEasing
- )
- ),
- sharedContentState = rememberSharedContentState(key = "stacktraceCardBounds"),
- animatedVisibilityScope = animatedVisibilityScope,
- placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
- )
- .fillMaxSize(),
+ modifier =
+ modifier
+ .sharedBounds(
+ boundsTransform = DefaultBoundsTransform,
+ enter =
+ fadeIn(
+ tween(
+ durationMillis = DURATION_ENTER,
+ delayMillis = DURATION_EXIT_SHORT,
+ easing = EmphasizedDecelerateEasing,
+ )
+ ),
+ exit =
+ fadeOut(
+ tween(
+ durationMillis = DURATION_EXIT_SHORT,
+ easing = EmphasizedAccelerateEasing,
+ )
+ ),
+ sharedContentState = rememberSharedContentState(key = "stacktraceCardBounds"),
+ animatedVisibilityScope = animatedVisibilityScope,
+ placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
+ )
+ .fillMaxSize()
) {
Row(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.surfaceContainer)
- .systemBarsPadding()
- .fillMaxWidth()
- .padding(4.dp),
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.surfaceContainer)
+ .systemBarsPadding()
+ .fillMaxWidth()
+ .padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
) {
- CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
- BackButton(
- onClick = {
- onMinimize()
- }
- )
+ CompositionLocalProvider(
+ LocalContentColor provides MaterialTheme.colorScheme.onSurface
+ ) {
+ BackButton(onClick = { onMinimize() })
Text(
modifier = Modifier,
text = stringResource(id = R.string.unknown_error_title),
style = MaterialTheme.typography.titleMedium,
- overflow = TextOverflow.Ellipsis
+ overflow = TextOverflow.Ellipsis,
)
}
}
- StackTraceViewer(
- modifier = Modifier
- .fillMaxWidth(),
- throwable = throwable
- )
+ StackTraceViewer(modifier = Modifier.fillMaxWidth(), throwable = throwable)
}
}
@Composable
-fun StackTraceViewer(
- modifier: Modifier = Modifier,
- throwable: Throwable
-) {
+fun StackTraceViewer(modifier: Modifier = Modifier, throwable: Throwable) {
Column(
- modifier = modifier
- .verticalScroll(rememberScrollState())
- .fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ modifier = modifier.verticalScroll(rememberScrollState()).fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
) {
throwable.stackTrace.forEachIndexed { index, element ->
- Row(
- modifier = Modifier.fillMaxWidth()
- ) {
+ Row(modifier = Modifier.fillMaxWidth()) {
Text(
text = "${index + 1}",
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier
- .width(32.dp)
- .padding(6.dp)
- .alpha(0.72f)
+ modifier = Modifier.width(32.dp).padding(6.dp).alpha(0.72f),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
@@ -278,7 +259,7 @@ fun StackTraceViewer(
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- overflow = TextOverflow.Clip
+ overflow = TextOverflow.Clip,
)
}
HorizontalDivider()
@@ -286,64 +267,60 @@ fun StackTraceViewer(
}
}
-
@Composable
private fun PrimaryStacktraceCard(
modifier: Modifier = Modifier,
errorType: String,
methodFailed: String,
line: Int,
- onClick: () -> Unit = {}
+ onClick: () -> Unit = {},
) {
Surface(
modifier = modifier,
onClick = onClick,
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surface,
- tonalElevation = 8.dp
+ tonalElevation = 8.dp,
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp),
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
imageVector = Icons.TwoTone.BugReport,
contentDescription = stringResource(id = R.string.error),
- modifier = Modifier
- .size(48.dp)
- .clip(MaterialTheme.shapes.small)
- .background(MaterialTheme.colorScheme.primaryContainer)
- .padding(4.dp),
- tint = MaterialTheme.colorScheme.primary
+ modifier =
+ Modifier.size(48.dp)
+ .clip(MaterialTheme.shapes.small)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .padding(4.dp),
+ tint = MaterialTheme.colorScheme.primary,
)
- Column(
- modifier = Modifier.fillMaxWidth(),
- ) {
+ Column(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier,
text = errorType.uppercase(),
- style = MaterialTheme.typography.bodyLarge.copy(
- color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f),
- letterSpacing = 1.sp
- ),
+ style =
+ MaterialTheme.typography.bodyLarge.copy(
+ color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f),
+ letterSpacing = 1.sp,
+ ),
fontWeight = FontWeight.SemiBold,
)
Text(
text = methodFailed,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
- overflow = TextOverflow.Ellipsis
-
+ overflow = TextOverflow.Ellipsis,
)
+
Text(
text = stringResource(R.string.line, line),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Normal,
fontFamily = FontFamily.Monospace,
- overflow = TextOverflow.Ellipsis
+ overflow = TextOverflow.Ellipsis,
)
}
}
@@ -357,7 +334,8 @@ private fun ErrorPagePrev() {
ErrorPage(
modifier = Modifier.background(MaterialTheme.colorScheme.background),
throwable = Exception("An error occurred"),
- onRetry = {})
+ onRetry = {},
+ )
}
}
@@ -368,7 +346,8 @@ private fun ErrorPagePrevWhite() {
ErrorPage(
modifier = Modifier.background(MaterialTheme.colorScheme.background),
throwable = Exception("An error occurred"),
- onRetry = {})
+ onRetry = {},
+ )
}
}
@@ -376,9 +355,7 @@ private fun ErrorPagePrevWhite() {
@Composable
private fun PrimaryStacktraceCardPrev() {
MaterialTheme {
- PrimaryStacktraceCard(
- errorType = "Error", methodFailed = "Method failed", line = 1
- )
+ PrimaryStacktraceCard(errorType = "Error", methodFailed = "Method failed", line = 1)
}
}
@@ -386,8 +363,6 @@ private fun PrimaryStacktraceCardPrev() {
@Composable
private fun PrimaryStacktraceCardPrevDark() {
MaterialTheme(colorScheme = darkColorScheme()) {
- PrimaryStacktraceCard(
- errorType = "Error", methodFailed = "Method failed", line = 1
- )
+ PrimaryStacktraceCard(errorType = "Error", methodFailed = "Method failed", line = 1)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/IdlePage.kt b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/IdlePage.kt
index aa4184f..85e6746 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/IdlePage.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/IdlePage.kt
@@ -9,10 +9,7 @@ import androidx.compose.ui.Modifier
@Composable
fun IdlePage() {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = "Idle Page")
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt
index 110b819..d62e362 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/common/pages/LoadingPage.kt
@@ -16,29 +16,22 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
-fun LoadingPage(
- modifier: Modifier = Modifier,
- text: String
-) {
+fun LoadingPage(modifier: Modifier = Modifier, text: String) {
Box(
- modifier = modifier
- .fillMaxSize()
- .safeDrawingPadding(),
- contentAlignment = Alignment.Center
+ modifier = modifier.fillMaxSize().safeDrawingPadding(),
+ contentAlignment = Alignment.Center,
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
- horizontalAlignment = Alignment.CenterHorizontally
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.SemiBold
- )
- LinearProgressIndicator(
- modifier = Modifier.fillMaxWidth(0.7f)
+ fontWeight = FontWeight.SemiBold,
)
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth(0.7f))
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/animatable/rememberAnimatable.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/animatable/rememberAnimatable.kt
index eef89c3..ab04c2e 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/animatable/rememberAnimatable.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/animatable/rememberAnimatable.kt
@@ -10,11 +10,9 @@ import com.bobbyesp.ui.util.AnimatableSaver
@Composable
fun rememberAnimatable(
initialValue: Float,
- visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
+ visibilityThreshold: Float = Spring.DefaultDisplacementThreshold,
): Animatable {
- return rememberSaveable(
- saver = AnimatableSaver
- ) {
+ return rememberSaveable(saver = AnimatableSaver) {
Animatable(initialValue, visibilityThreshold)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt
index 68183a4..21f2de3 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt
@@ -38,10 +38,7 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
-/**
- * Bottom Sheet
- * Improved version from [ViMusic](https://github.com/vfsfitvnm/ViMusic)
- */
+/** Bottom Sheet Improved version from [ViMusic](https://github.com/vfsfitvnm/ViMusic) */
@Composable
fun DraggableBottomSheet(
state: DraggableBottomSheetState,
@@ -52,40 +49,39 @@ fun DraggableBottomSheet(
content: @Composable BoxScope.() -> Unit,
) {
Box(
- modifier = modifier
- .fillMaxSize()
- .offset {
- val y = (state.expandedBound - state.value)
- .roundToPx()
- .coerceAtLeast(0)
- IntOffset(x = 0, y = y)
- }
- .pointerInput(state) {
- val velocityTracker = VelocityTracker()
+ modifier =
+ modifier
+ .fillMaxSize()
+ .offset {
+ val y = (state.expandedBound - state.value).roundToPx().coerceAtLeast(0)
+ IntOffset(x = 0, y = y)
+ }
+ .pointerInput(state) {
+ val velocityTracker = VelocityTracker()
- detectVerticalDragGestures(
- onVerticalDrag = { change, dragAmount ->
- velocityTracker.addPointerInputChange(change)
- state.dispatchRawDelta(dragAmount)
- },
- onDragCancel = {
- velocityTracker.resetTracking()
- state.snapTo(state.collapsedBound)
- },
- onDragEnd = {
- val velocity = -velocityTracker.calculateVelocity().y
- velocityTracker.resetTracking()
- state.performFling(velocity, onDismiss)
- }
- )
- }
- .clip(
- RoundedCornerShape(
- topStart = if (!state.isExpanded) 8.dp else 0.dp,
- topEnd = if (!state.isExpanded) 8.dp else 0.dp
+ detectVerticalDragGestures(
+ onVerticalDrag = { change, dragAmount ->
+ velocityTracker.addPointerInputChange(change)
+ state.dispatchRawDelta(dragAmount)
+ },
+ onDragCancel = {
+ velocityTracker.resetTracking()
+ state.snapTo(state.collapsedBound)
+ },
+ onDragEnd = {
+ val velocity = -velocityTracker.calculateVelocity().y
+ velocityTracker.resetTracking()
+ state.performFling(velocity, onDismiss)
+ },
+ )
+ }
+ .clip(
+ RoundedCornerShape(
+ topStart = if (!state.isExpanded) 8.dp else 0.dp,
+ topEnd = if (!state.isExpanded) 8.dp else 0.dp,
+ )
)
- )
- .background(backgroundColor)
+ .background(backgroundColor)
) {
if (!state.isCollapsed && !state.isDismissed) {
BackHandler(onBack = state::collapseSoft)
@@ -93,29 +89,26 @@ fun DraggableBottomSheet(
if (!state.isCollapsed) {
BoxWithConstraints(
- modifier = Modifier
- .fillMaxSize()
- .graphicsLayer {
+ modifier =
+ Modifier.fillMaxSize().graphicsLayer {
alpha = ((state.progress - 0.25f) * 4).coerceIn(0f, 1f)
},
- content = content
+ content = content,
)
}
if (!state.isExpanded && (onDismiss == null || !state.isDismissed)) {
Box(
- modifier = Modifier
- .graphicsLayer {
- alpha = 1f - (state.progress * 4).coerceAtMost(1f)
- }
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = null,
- onClick = state::expandSoft
- )
- .fillMaxWidth()
- .height(state.collapsedBound),
- content = collapsedContent
+ modifier =
+ Modifier.graphicsLayer { alpha = 1f - (state.progress * 4).coerceAtMost(1f) }
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = state::expandSoft,
+ )
+ .fillMaxWidth()
+ .height(state.collapsedBound),
+ content = collapsedContent,
)
}
}
@@ -127,40 +120,37 @@ fun rememberDraggableBottomSheetState(
expandedBound: Dp,
collapsedBound: Dp = dismissedBound,
initialAnchor: DraggableBottomSheetAnchor = DraggableBottomSheetAnchor.DISMISSED,
- animationSpec: AnimationSpec
+ animationSpec: AnimationSpec,
): DraggableBottomSheetState {
val density = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
- var previousAnchor by rememberSaveable(key = "previousAnchorDraggableBs") {
- mutableStateOf(initialAnchor)
- }
- val animatable = remember {
- Animatable(0.dp, Dp.VectorConverter)
- }
+ var previousAnchor by
+ rememberSaveable(key = "previousAnchorDraggableBs") { mutableStateOf(initialAnchor) }
+ val animatable = remember { Animatable(0.dp, Dp.VectorConverter) }
return remember(dismissedBound, expandedBound, collapsedBound, coroutineScope) {
- val initialValue = when (previousAnchor) {
- DraggableBottomSheetAnchor.EXPANDED -> expandedBound
- DraggableBottomSheetAnchor.COLLAPSED -> collapsedBound
- DraggableBottomSheetAnchor.DISMISSED -> dismissedBound
- }
+ val initialValue =
+ when (previousAnchor) {
+ DraggableBottomSheetAnchor.EXPANDED -> expandedBound
+ DraggableBottomSheetAnchor.COLLAPSED -> collapsedBound
+ DraggableBottomSheetAnchor.DISMISSED -> dismissedBound
+ }
animatable.updateBounds(dismissedBound.coerceAtMost(expandedBound), expandedBound)
- coroutineScope.launch {
- animatable.animateTo(initialValue, animationSpec)
- }
+ coroutineScope.launch { animatable.animateTo(initialValue, animationSpec) }
DraggableBottomSheetState(
- draggableState = DraggableState { delta ->
- coroutineScope.launch {
- animatable.snapTo(animatable.value - with(density) { delta.toDp() })
- }
- },
+ draggableState =
+ DraggableState { delta ->
+ coroutineScope.launch {
+ animatable.snapTo(animatable.value - with(density) { delta.toDp() })
+ }
+ },
onAnchorChanged = { previousAnchor = it },
coroutineScope = coroutineScope,
animatable = animatable,
- collapsedBound = collapsedBound
+ collapsedBound = collapsedBound,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetAnchor.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetAnchor.kt
index f0599fa..5eac3fa 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetAnchor.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetAnchor.kt
@@ -3,5 +3,5 @@ package com.bobbyesp.ui.components.bottomsheet.draggable
enum class DraggableBottomSheetAnchor {
DISMISSED,
COLLAPSED,
- EXPANDED
-}
\ No newline at end of file
+ EXPANDED,
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetState.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetState.kt
index 261f660..60f7349 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetState.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheetState.kt
@@ -34,34 +34,26 @@ class DraggableBottomSheetState(
val value by animatable.asState()
- val isDismissed by derivedStateOf {
- value == animatable.lowerBound!!
- }
+ val isDismissed by derivedStateOf { value == animatable.lowerBound!! }
- val isCollapsed by derivedStateOf {
- value == collapsedBound
- }
+ val isCollapsed by derivedStateOf { value == collapsedBound }
- val isExpanded by derivedStateOf {
- value == animatable.upperBound
- }
+ val isExpanded by derivedStateOf { value == animatable.upperBound }
val progress by derivedStateOf {
- 1f - (animatable.upperBound!! - animatable.value) / (animatable.upperBound!! - collapsedBound)
+ 1f -
+ (animatable.upperBound!! - animatable.value) /
+ (animatable.upperBound!! - collapsedBound)
}
fun collapse(animationSpec: AnimationSpec) {
onAnchorChanged(DraggableBottomSheetAnchor.COLLAPSED)
- coroutineScope.launch {
- animatable.animateTo(collapsedBound, animationSpec)
- }
+ coroutineScope.launch { animatable.animateTo(collapsedBound, animationSpec) }
}
fun expand(animationSpec: AnimationSpec) {
onAnchorChanged(DraggableBottomSheetAnchor.EXPANDED)
- coroutineScope.launch {
- animatable.animateTo(animatable.upperBound!!, animationSpec)
- }
+ coroutineScope.launch { animatable.animateTo(animatable.upperBound!!, animationSpec) }
}
private fun collapse() {
@@ -82,15 +74,11 @@ class DraggableBottomSheetState(
fun dismiss() {
onAnchorChanged(DraggableBottomSheetAnchor.DISMISSED)
- coroutineScope.launch {
- animatable.animateTo(animatable.lowerBound!!)
- }
+ coroutineScope.launch { animatable.animateTo(animatable.lowerBound!!) }
}
fun snapTo(value: Dp) {
- coroutineScope.launch {
- animatable.snapTo(value)
- }
+ coroutineScope.launch { animatable.snapTo(value) }
}
fun performFling(velocity: Float, onDismiss: (() -> Unit)?) {
@@ -127,53 +115,61 @@ class DraggableBottomSheetState(
}
val preUpPostDownNestedScrollConnection
- get() = object : NestedScrollConnection {
- var isTopReached = false
+ get() =
+ object : NestedScrollConnection {
+ var isTopReached = false
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- if (isExpanded && available.y < 0) {
- isTopReached = false
- }
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ if (isExpanded && available.y < 0) {
+ isTopReached = false
+ }
- return if (isTopReached && available.y < 0 && source == NestedScrollSource.Companion.UserInput) {
- dispatchRawDelta(available.y)
- available
- } else {
- Offset.Companion.Zero
+ return if (
+ isTopReached &&
+ available.y < 0 &&
+ source == NestedScrollSource.Companion.UserInput
+ ) {
+ dispatchRawDelta(available.y)
+ available
+ } else {
+ Offset.Companion.Zero
+ }
}
- }
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource,
- ): Offset {
- if (!isTopReached) {
- isTopReached = consumed.y == 0f && available.y > 0
- }
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ if (!isTopReached) {
+ isTopReached = consumed.y == 0f && available.y > 0
+ }
- return if (isTopReached && source == NestedScrollSource.Companion.UserInput) {
- dispatchRawDelta(available.y)
- available
- } else {
- Offset.Companion.Zero
+ return if (isTopReached && source == NestedScrollSource.Companion.UserInput) {
+ dispatchRawDelta(available.y)
+ available
+ } else {
+ Offset.Companion.Zero
+ }
}
- }
- override suspend fun onPreFling(available: Velocity): Velocity {
- return if (isTopReached) {
- val velocity = -available.y
- performFling(velocity, null)
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ return if (isTopReached) {
+ val velocity = -available.y
+ performFling(velocity, null)
- available
- } else {
- Velocity.Companion.Zero
+ available
+ } else {
+ Velocity.Companion.Zero
+ }
}
- }
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- isTopReached = false
- return Velocity.Companion.Zero
+ override suspend fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ ): Velocity {
+ isTopReached = false
+ return Velocity.Companion.Zero
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/button/BackButton.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/button/BackButton.kt
index ac2d788..d297246 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/button/BackButton.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/button/BackButton.kt
@@ -24,10 +24,7 @@ fun BackButton(onClick: () -> Unit) {
@Composable
fun CloseButton(onClick: () -> Unit) {
IconButton(modifier = Modifier, onClick = onClick) {
- Icon(
- imageVector = Icons.Default.Close,
- contentDescription = stringResource(R.string.back),
- )
+ Icon(imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.back))
}
}
@@ -37,7 +34,7 @@ fun DynamicButton(
icon: @Composable () -> Unit,
icon2: @Composable () -> Unit,
isIcon1: Boolean,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
FilledTonalIconButton(modifier = modifier, onClick = onClick) {
if (isIcon1) {
@@ -46,4 +43,4 @@ fun DynamicButton(
icon2()
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/button/FilledButtons.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/button/FilledButtons.kt
index 35adec1..710c7ba 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/button/FilledButtons.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/button/FilledButtons.kt
@@ -19,23 +19,20 @@ fun FilledButtonWithIcon(
icon: ImageVector,
enabled: Boolean = true,
text: String,
- contentDescription: String? = null
+ contentDescription: String? = null,
) {
Button(
modifier = modifier,
onClick = onClick,
enabled = enabled,
- contentPadding = ButtonDefaults.ButtonWithIconContentPadding
+ contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
modifier = Modifier.size(18.dp),
imageVector = icon,
- contentDescription = contentDescription
- )
- Text(
- modifier = Modifier.padding(start = 6.dp),
- text = text
+ contentDescription = contentDescription,
)
+ Text(modifier = Modifier.padding(start = 6.dp), text = text)
}
}
@@ -45,21 +42,18 @@ fun FilledTonalButtonWithIcon(
onClick: () -> Unit,
icon: ImageVector,
text: String,
- contentDescription: String? = null
+ contentDescription: String? = null,
) {
FilledTonalButton(
modifier = modifier,
onClick = onClick,
- contentPadding = ButtonDefaults.ButtonWithIconContentPadding
+ contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
modifier = Modifier.size(18.dp),
imageVector = icon,
- contentDescription = contentDescription
- )
- Text(
- modifier = Modifier.padding(start = 8.dp),
- text = text
+ contentDescription = contentDescription,
)
+ Text(modifier = Modifier.padding(start = 8.dp), text = text)
}
}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/button/OutlinedButtons.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/button/OutlinedButtons.kt
index 67a9c0f..2f99b56 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/button/OutlinedButtons.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/button/OutlinedButtons.kt
@@ -20,22 +20,19 @@ fun OutlinedButtonWithIcon(
icon: ImageVector,
text: String,
contentColor: Color = MaterialTheme.colorScheme.primary,
- contentDescription: String? = null
+ contentDescription: String? = null,
) {
OutlinedButton(
modifier = modifier,
onClick = onClick,
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
- colors = ButtonDefaults.outlinedButtonColors(contentColor = contentColor)
+ colors = ButtonDefaults.outlinedButtonColors(contentColor = contentColor),
) {
Icon(
modifier = Modifier.size(18.dp),
imageVector = icon,
- contentDescription = contentDescription
- )
- Text(
- modifier = Modifier.padding(start = 8.dp),
- text = text
+ contentDescription = contentDescription,
)
+ Text(modifier = Modifier.padding(start = 8.dp), text = text)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/button/SquaredButton.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/button/SquaredButton.kt
index b5616ce..5a3986b 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/button/SquaredButton.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/button/SquaredButton.kt
@@ -37,44 +37,38 @@ fun VerticalButtonWithIconAndText(
border: Boolean = false,
backgroundColor: Color = MaterialTheme.colorScheme.secondaryContainer,
shape: CornerBasedShape = MaterialTheme.shapes.small,
- onClick: () -> Unit = {}
+ onClick: () -> Unit = {},
) {
- val animatedAlpha by animateFloatAsState(
- targetValue = if (enabled) 1f else 0.4f
- )
+ val animatedAlpha by animateFloatAsState(targetValue = if (enabled) 1f else 0.4f)
Surface(
- modifier = modifier
- .semantics { role = Role.Button }
- .alpha(animatedAlpha),
+ modifier = modifier.semantics { role = Role.Button }.alpha(animatedAlpha),
onClick = onClick,
enabled = enabled,
shape = shape,
border = if (border) BorderStroke(1.dp, MaterialTheme.colorScheme.outline) else null,
- color = backgroundColor
+ color = backgroundColor,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
- modifier = Modifier
- .padding(8.dp)
- .defaultMinSize(
- minWidth = ButtonDefaults.MinWidth,
- minHeight = ButtonDefaults.MinHeight
- )
- .fillMaxWidth()
+ modifier =
+ Modifier.padding(8.dp)
+ .defaultMinSize(
+ minWidth = ButtonDefaults.MinWidth,
+ minHeight = ButtonDefaults.MinHeight,
+ )
+ .fillMaxWidth(),
) {
Icon(
imageVector = icon,
contentDescription = null,
- tint = MaterialTheme.colorScheme.onSurface
+ tint = MaterialTheme.colorScheme.onSurface,
)
Text(
text = text,
- style = MaterialTheme.typography.bodySmall.copy(
- fontWeight = FontWeight.Medium,
- ),
+ style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
)
@@ -91,42 +85,35 @@ fun HorizontalButtonWithIconAndText(
border: Boolean = false,
backgroundColor: Color = MaterialTheme.colorScheme.secondaryContainer,
shape: CornerBasedShape = MaterialTheme.shapes.small,
- onClick: () -> Unit = {}
+ onClick: () -> Unit = {},
) {
- val animatedAlpha by animateFloatAsState(
- targetValue = if (enabled) 1f else 0.4f
- )
+ val animatedAlpha by animateFloatAsState(targetValue = if (enabled) 1f else 0.4f)
Surface(
- modifier = modifier
- .semantics { role = Role.Button }
- .alpha(animatedAlpha),
+ modifier = modifier.semantics { role = Role.Button }.alpha(animatedAlpha),
onClick = onClick,
enabled = enabled,
shape = shape,
border = if (border) BorderStroke(1.dp, MaterialTheme.colorScheme.outline) else null,
- color = backgroundColor
+ color = backgroundColor,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .padding(8.dp)
+ modifier = Modifier.padding(8.dp),
) {
Icon(
imageVector = icon,
contentDescription = null,
- tint = MaterialTheme.colorScheme.onSurface
+ tint = MaterialTheme.colorScheme.onSurface,
)
Text(
text = text,
- style = MaterialTheme.typography.bodySmall.copy(
- fontWeight = FontWeight.Medium,
- ),
+ style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/button/navigation/HorizontalFloatingAppBarItem.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/button/navigation/HorizontalFloatingAppBarItem.kt
index 335494e..4813732 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/button/navigation/HorizontalFloatingAppBarItem.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/button/navigation/HorizontalFloatingAppBarItem.kt
@@ -43,20 +43,20 @@ fun HorizontalFloatingAppBarItem(
badge: (@Composable () -> Unit)? = null,
shape: Shape = CircleShape,
colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
- val animatedSurfaceColor by animateColorAsState(
- targetValue = colors.containerColor(selected).value,
- label = "Animated Surface Color"
- )
+ val animatedSurfaceColor by
+ animateColorAsState(
+ targetValue = colors.containerColor(selected).value,
+ label = "Animated Surface Color",
+ )
Surface(
onClick = onClick,
- modifier = modifier
- .semantics { role = Role.Companion.Tab },
+ modifier = modifier.semantics { role = Role.Companion.Tab },
shape = shape,
color = animatedSurfaceColor,
- interactionSource = interactionSource
+ interactionSource = interactionSource,
) {
Row(
modifier = Modifier.Companion.padding(8.dp),
@@ -64,30 +64,21 @@ fun HorizontalFloatingAppBarItem(
) {
icon?.let {
Box(contentAlignment = Alignment.Companion.Center) {
- ProvideColor(colors.iconColor(selected).value) {
- icon()
- }
+ ProvideColor(colors.iconColor(selected).value) { icon() }
if (badge != null) {
Box(
- modifier = Modifier.Companion
- .align(Alignment.Companion.BottomEnd)
- .padding(bottom = 4.dp, end = 4.dp)
+ modifier =
+ Modifier.Companion.align(Alignment.Companion.BottomEnd)
+ .padding(bottom = 4.dp, end = 4.dp)
) {
- ProvideColor(colors.badgeColor(selected).value) {
- badge()
- }
+ ProvideColor(colors.badgeColor(selected).value) { badge() }
}
}
}
}
- AnimatedVisibility(
- visible = expanded && !selected,
- modifier = Modifier.Companion
- ) {
- ProvideColor(colors.textColor(selected).value) {
- label()
- }
+ AnimatedVisibility(visible = expanded && !selected, modifier = Modifier.Companion) {
+ ProvideColor(colors.textColor(selected).value) { label() }
}
}
}
@@ -103,17 +94,18 @@ private fun ProvideColor(color: Color, content: @Composable () -> Unit) {
private fun HorizontalFloatingAppBarItemPreview() {
var expanded by remember { mutableStateOf(true) }
var selected by remember { mutableStateOf(true) }
- HorizontalFloatingAppBarItem(label = {
- Text(
- modifier = Modifier.Companion.padding(horizontal = 12.dp),
- text = "Home",
- fontWeight = FontWeight.Companion.Bold
- )
- }, selected = selected, expanded = expanded, onClick = {
- selected = !selected
- }, icon = {
- Icon(
- imageVector = Icons.Rounded.Home, contentDescription = "Home"
- )
- }, badge = null)
-}
\ No newline at end of file
+ HorizontalFloatingAppBarItem(
+ label = {
+ Text(
+ modifier = Modifier.Companion.padding(horizontal = 12.dp),
+ text = "Home",
+ fontWeight = FontWeight.Companion.Bold,
+ )
+ },
+ selected = selected,
+ expanded = expanded,
+ onClick = { selected = !selected },
+ icon = { Icon(imageVector = Icons.Rounded.Home, contentDescription = "Home") },
+ badge = null,
+ )
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/card/ExpandableElevatedCard.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/card/ExpandableElevatedCard.kt
index a46b6be..518e674 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/card/ExpandableElevatedCard.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/card/ExpandableElevatedCard.kt
@@ -46,53 +46,43 @@ fun ExpandableElevatedCard(
ElevatedCard(
modifier = modifier,
onClick = { expanded = !expanded },
- shape = MaterialTheme.shapes.small
+ shape = MaterialTheme.shapes.small,
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp),
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.weight(0.1f),
imageVector = icon,
- contentDescription = "Device information"
+ contentDescription = "Device information",
)
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(6.dp)
- .weight(1f),
- ) {
+ Column(modifier = Modifier.fillMaxWidth().padding(6.dp).weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.Bold
+ fontWeight = FontWeight.Bold,
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.62f),
- fontWeight = FontWeight.Normal
+ fontWeight = FontWeight.Normal,
)
}
FilledTonalIconButton(
- modifier = Modifier
- .padding()
- .size(24.dp),
- onClick = { expanded = !expanded }) {
+ modifier = Modifier.padding().size(24.dp),
+ onClick = { expanded = !expanded },
+ ) {
Icon(
Icons.Outlined.ExpandLess,
null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
- modifier = Modifier.rotate(animatedDegree.value)
+ modifier = Modifier.rotate(animatedDegree.value),
)
}
}
- AnimatedVisibility(visible = expanded) {
- content()
- }
+ AnimatedVisibility(visible = expanded) { content() }
}
}
@@ -103,9 +93,7 @@ private fun ExpandableElevatedCardPreview() {
ExpandableElevatedCard(
title = "Title",
subtitle = "Subtitle",
- content = {
- Text(text = "Content")
- },
- icon = Icons.Outlined.PermDeviceInformation
+ content = { Text(text = "Content") },
+ icon = Icons.Outlined.PermDeviceInformation,
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/card/OnboardingCard.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/card/OnboardingCard.kt
index 5215779..da03d7d 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/card/OnboardingCard.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/card/OnboardingCard.kt
@@ -19,28 +19,24 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
-fun OnboardingCard(
- icon: @Composable () -> Unit,
- title: String,
- text: String
-) {
+fun OnboardingCard(icon: @Composable () -> Unit, title: String, text: String) {
Column(
- modifier = Modifier
- .fillMaxWidth()
- .clip(MaterialTheme.shapes.medium)
- .background(MaterialTheme.colorScheme.surfaceVariant)
- .padding(16.dp)
+ modifier =
+ Modifier.fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .padding(16.dp)
) {
Surface(
tonalElevation = 16.dp,
shape = MaterialTheme.shapes.medium,
- color = MaterialTheme.colorScheme.primary.copy(alpha = 0.35f)
- .compositeOver(MaterialTheme.colorScheme.surfaceVariant),
+ color =
+ MaterialTheme.colorScheme.primary
+ .copy(alpha = 0.35f)
+ .compositeOver(MaterialTheme.colorScheme.surfaceVariant),
contentColor = MaterialTheme.colorScheme.primary,
) {
- Box(Modifier.padding(12.dp)) {
- icon()
- }
+ Box(Modifier.padding(12.dp)) { icon() }
}
Spacer(modifier = Modifier.height(8.dp))
@@ -49,7 +45,7 @@ fun OnboardingCard(
text = title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(2.dp))
@@ -57,7 +53,7 @@ fun OnboardingCard(
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -68,18 +64,16 @@ private fun OnboardingCardPreview() {
OnboardingCard(
icon = {
Surface(
- modifier = Modifier
- .fillMaxWidth()
- .height(48.dp),
+ modifier = Modifier.fillMaxWidth().height(48.dp),
shape = MaterialTheme.shapes.medium,
- color = MaterialTheme.colorScheme.primary.copy(alpha = 0.35f)
- .compositeOver(MaterialTheme.colorScheme.surfaceVariant),
+ color =
+ MaterialTheme.colorScheme.primary
+ .copy(alpha = 0.35f)
+ .compositeOver(MaterialTheme.colorScheme.surfaceVariant),
contentColor = MaterialTheme.colorScheme.primary,
- ) {
-
- }
+ ) {}
},
title = "Title",
- text = "Text"
+ text = "Text",
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/card/UtilityCard.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/card/UtilityCard.kt
index 37998b3..afa41b1 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/card/UtilityCard.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/card/UtilityCard.kt
@@ -34,46 +34,47 @@ fun AppUtilityCard(
cardSize: Dp = 200.dp,
utilityName: String,
icon: ImageVector,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
val xOffset = cardSize / 2.3f
val yOffset = cardSize / 5
OutlinedCard(
- modifier = modifier
- .aspectRatio(1.0f)
- .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)),
+ modifier =
+ modifier
+ .aspectRatio(1.0f)
+ .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)),
onClick = onClick,
shape = MaterialTheme.shapes.small,
) {
- Box(
- modifier = Modifier
- ) {
+ Box(modifier = Modifier) {
Icon(
imageVector = icon,
contentDescription = "Utility Icon",
- modifier = Modifier
- .fillMaxSize(0.8f)
- .offset(xOffset, yOffset),
- tint = MaterialTheme.colorScheme.onSurface
+ modifier = Modifier.fillMaxSize(0.8f).offset(xOffset, yOffset),
+ tint = MaterialTheme.colorScheme.onSurface,
)
Box(
- modifier = Modifier
- .fillMaxSize()
- .background(
- brush = Brush.verticalGradient(
- colors = listOf(
- Color.Transparent,
- MaterialTheme.colorScheme.surface,
- ), startY = -200f
- )
- ), contentAlignment = Alignment.BottomEnd
+ modifier =
+ Modifier.fillMaxSize()
+ .background(
+ brush =
+ Brush.verticalGradient(
+ colors =
+ listOf(
+ Color.Transparent,
+ MaterialTheme.colorScheme.surface,
+ ),
+ startY = -200f,
+ )
+ ),
+ contentAlignment = Alignment.BottomEnd,
) {
MarqueeText(
modifier = Modifier.padding(12.dp),
text = utilityName,
fontWeight = FontWeight.Bold,
- fontSize = MaterialTheme.typography.headlineSmall.fontSize
+ fontSize = MaterialTheme.typography.headlineSmall.fontSize,
)
}
}
@@ -88,6 +89,6 @@ fun AppUtilityCardPreview() {
modifier = Modifier.size(200.dp),
onClick = {},
icon = Icons.AutoMirrored.Outlined.AirplaneTicket,
- utilityName = "Flights"
+ utilityName = "Flights",
)
}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/card/WarningCard.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/card/WarningCard.kt
index 657b6c1..e007528 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/card/WarningCard.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/card/WarningCard.kt
@@ -21,45 +21,34 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
-fun WarningCard(
- modifier: Modifier = Modifier,
- title: String,
- warningText: String,
-) {
+fun WarningCard(modifier: Modifier = Modifier, title: String, warningText: String) {
OutlinedCard(
modifier = modifier,
shape = MaterialTheme.shapes.small,
- colors = CardDefaults.outlinedCardColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer,
- )
+ colors =
+ CardDefaults.outlinedCardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer
+ ),
) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- ) {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.weight(0.175f),
imageVector = Icons.Default.WarningAmber,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
- contentDescription = "Warning icon"
+ contentDescription = "Warning icon",
)
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(6.dp)
- .weight(1f),
- ) {
+ Column(modifier = Modifier.fillMaxWidth().padding(6.dp).weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.Bold
+ fontWeight = FontWeight.Bold,
)
Text(
text = warningText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.62f),
fontWeight = FontWeight.Normal,
- textAlign = TextAlign.Justify
+ textAlign = TextAlign.Justify,
)
}
}
@@ -70,7 +59,5 @@ fun WarningCard(
@Preview
@Composable
fun WarningCardPreview() {
- WarningCard(
- title = "Warning", warningText = "This is a warning"
- )
-}
\ No newline at end of file
+ WarningCard(title = "Warning", warningText = "This is a warning")
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/chip/ButtonChips.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/chip/ButtonChips.kt
index de5ef34..6dead96 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/chip/ButtonChips.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/chip/ButtonChips.kt
@@ -39,14 +39,16 @@ fun ButtonChip(
colors = AssistChipDefaults.elevatedAssistChipColors(leadingIconContentColor = iconColor),
enabled = enabled,
leadingIcon = {
- if (icon != null) Icon(
- imageVector = icon, null, modifier = Modifier.size(AssistChipDefaults.IconSize)
- )
- }
+ if (icon != null)
+ Icon(
+ imageVector = icon,
+ null,
+ modifier = Modifier.size(AssistChipDefaults.IconSize),
+ )
+ },
)
}
-
@Composable
fun FlatButtonChip(
modifier: Modifier = Modifier,
@@ -54,24 +56,27 @@ fun FlatButtonChip(
label: String,
iconColor: Color = MaterialTheme.colorScheme.primary,
labelColor: Color = MaterialTheme.colorScheme.onSurface,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
AssistChip(
modifier = modifier.padding(horizontal = 4.dp),
- colors = AssistChipDefaults.assistChipColors(
- containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.78f),
- labelColor = labelColor,
- leadingIconContentColor = iconColor
- ),
+ colors =
+ AssistChipDefaults.assistChipColors(
+ containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.78f),
+ labelColor = labelColor,
+ leadingIconContentColor = iconColor,
+ ),
border = null,
onClick = onClick,
leadingIcon = {
Icon(
imageVector = icon,
- contentDescription = null, Modifier.size(AssistChipDefaults.IconSize)
+ contentDescription = null,
+ Modifier.size(AssistChipDefaults.IconSize),
)
},
- label = { Text(text = label) })
+ label = { Text(text = label) },
+ )
}
@Composable
@@ -81,18 +86,21 @@ fun OutlinedButtonChip(
label: String,
iconModifier: Modifier = Modifier,
tint: Color = MaterialTheme.colorScheme.primary,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
AssistChip(
- modifier = modifier, onClick = onClick, leadingIcon = {
+ modifier = modifier,
+ onClick = onClick,
+ leadingIcon = {
Icon(
imageVector = icon,
contentDescription = null,
modifier = iconModifier.size(AssistChipDefaults.IconSize),
- tint = tint
+ tint = tint,
)
- }, label = { Text(text = label) })
-
+ },
+ label = { Text(text = label) },
+ )
}
@Composable
@@ -102,28 +110,30 @@ fun OutlinedButtonChip(
label: String,
iconModifier: Modifier = Modifier,
tint: Color = MaterialTheme.colorScheme.primary,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
AssistChip(
- modifier = modifier, onClick = onClick,
+ modifier = modifier,
+ onClick = onClick,
leadingIcon = {
if (icon != null) {
Icon(
painter = painterResource(id = icon.hashCode()),
contentDescription = null,
modifier = iconModifier.size(AssistChipDefaults.IconSize),
- tint = tint
+ tint = tint,
)
} else {
Icon(
imageVector = Icons.Default.Public,
contentDescription = null,
modifier = iconModifier.size(AssistChipDefaults.IconSize),
- tint = tint
+ tint = tint,
)
}
- }, label = { Text(text = label) })
-
+ },
+ label = { Text(text = label) },
+ )
}
@Composable
@@ -134,32 +144,33 @@ fun OutlinedButtonChipWithIndex(
iconModifier: Modifier = Modifier,
tint: Color = MaterialTheme.colorScheme.primary,
index: Int?,
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
AssistChip(
- modifier = modifier, onClick = onClick,
+ modifier = modifier,
+ onClick = onClick,
leadingIcon = {
- Box(
- modifier = Modifier,
- ) {
+ Box(modifier = Modifier) {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = iconModifier
- .size(AssistChipDefaults.IconSize)
- .align(Alignment.Center),
- tint = tint
+ modifier =
+ iconModifier.size(AssistChipDefaults.IconSize).align(Alignment.Center),
+ tint = tint,
)
if (index != null) {
Text(
text = (index + 1).toString(),
- modifier = Modifier
- .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
- .align(Alignment.BottomEnd)
- .offset(x = 2.dp, y = 3.dp)
+ modifier =
+ Modifier.background(
+ MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
+ )
+ .align(Alignment.BottomEnd)
+ .offset(x = 2.dp, y = 3.dp),
)
}
}
- }, label = { Text(text = label) }
+ },
+ label = { Text(text = label) },
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/chip/SingleChoiceChip.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/chip/SingleChoiceChip.kt
index af73220..55cbdea 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/chip/SingleChoiceChip.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/chip/SingleChoiceChip.kt
@@ -21,25 +21,23 @@ fun SingleChoiceChip(
selected: Boolean,
onClick: () -> Unit,
label: String,
- leadingIcon: ImageVector = Icons.Outlined.Check
+ leadingIcon: ImageVector = Icons.Outlined.Check,
) {
FilterChip(
modifier = modifier.padding(horizontal = 4.dp),
selected = selected,
onClick = onClick,
- label = {
- Text(text = label)
- },
+ label = { Text(text = label) },
leadingIcon = {
Row {
AnimatedVisibility(visible = selected, modifier = Modifier) {
Icon(
imageVector = leadingIcon,
contentDescription = null,
- modifier = Modifier.size(FilterChipDefaults.IconSize)
+ modifier = Modifier.size(FilterChipDefaults.IconSize),
)
}
}
},
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/AnimatedDropdownMenu.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/AnimatedDropdownMenu.kt
index b41e785..121d46e 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/AnimatedDropdownMenu.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/AnimatedDropdownMenu.kt
@@ -16,12 +16,14 @@ import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
/**
- * Material Design dropdown menu.
+ * Material Design dropdown menu.
*
* Menus display a list of choices on a temporary surface. They appear when users interact with a
* button, action, or other control.
*
- * 
+ * 
*
* A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
* to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
@@ -29,12 +31,12 @@ import androidx.compose.ui.window.PopupProperties
* space in a layout, as the menu is displayed in a separate window, on top of other content.
*
* The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
- * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
- * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
- * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material specification
+ * for menus. Also note that the [content] is placed inside a scrollable [Column], so using a
+ * [LazyColumn] as the root layout inside [content] is unsupported.
*
- * [onDismissRequest] will be called when the menu should close - for example when there is a
- * tap outside the menu, or when the back key is pressed.
+ * [onDismissRequest] will be called when the menu should close - for example when there is a tap
+ * outside the menu, or when the back key is pressed.
*
* [DropdownMenu] changes its positioning depending on the available space, always trying to be
* fully visible. Depending on layout direction, first it will try to align its start to the start
@@ -45,13 +47,12 @@ import androidx.compose.ui.window.PopupProperties
* An [offset] can be provided to adjust the positioning of the menu for cases when the layout
* bounds of its parent do not coincide with its visual bounds.
*
- *
* @param expanded whether the menu is expanded or not
* @param onDismissRequest called when the user requests to dismiss the menu, such as by tapping
- * outside the menu's bounds
+ * outside the menu's bounds
* @param modifier [Modifier] to be applied to the menu's content
* @param offset [DpOffset] from the original position of the menu. The offset respects the
- * [LayoutDirection], so the offset's x position will be added in LTR and subtracted in RTL.
+ * [LayoutDirection], so the offset's x position will be added in LTR and subtracted in RTL.
* @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
* @param properties [PopupProperties] for further customization of this popup's behavior
* @param content the content of this dropdown menu, typically a [DropdownMenuItem]
@@ -64,30 +65,26 @@ fun AnimatedDropdownMenu(
offset: DpOffset = DpOffset(0.dp, 0.dp),
scrollState: ScrollState = rememberScrollState(),
properties: PopupProperties = PopupProperties(focusable = true),
- content: @Composable ColumnScope.() -> Unit
+ content: @Composable ColumnScope.() -> Unit,
) {
val expandedState = remember { MutableTransitionState(false) }
expandedState.targetState = expanded
if (expandedState.currentState || expandedState.targetState || !expandedState.isIdle) {
val density = LocalDensity.current
- val popupPositionProvider = remember(offset, density) {
- DropdownMenuPositionProvider(
- offset,
- density
- )
- }
+ val popupPositionProvider =
+ remember(offset, density) { DropdownMenuPositionProvider(offset, density) }
Popup(
onDismissRequest = onDismissRequest,
popupPositionProvider = popupPositionProvider,
- properties = properties
+ properties = properties,
) {
DropdownMenuContent(
expandedState = expandedState,
scrollState = scrollState,
modifier = modifier,
- content = content
+ content = content,
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownItemContainer.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownItemContainer.kt
index a17950f..b76042d 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownItemContainer.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownItemContainer.kt
@@ -14,20 +14,21 @@ import androidx.compose.ui.unit.dp
fun DropdownItemContainer(
modifier: Modifier = Modifier,
contentPadding: androidx.compose.ui.unit.Dp = 0.dp,
- content: @Composable RowScope.() -> Unit
+ content: @Composable RowScope.() -> Unit,
) {
Row(
- modifier = modifier
- .fillMaxWidth()
- // Preferred min and max width used during the intrinsic measurement.
- .sizeIn(
- minWidth = DropdownMenuItemDefaultMinWidth,
- maxWidth = DropdownMenuItemDefaultMaxWidth,
- minHeight = ListItemContainerHeight
- )
- .padding(contentPadding)
- .padding(horizontal = DropdownMenuItemHorizontalPadding),
- verticalAlignment = Alignment.CenterVertically
+ modifier =
+ modifier
+ .fillMaxWidth()
+ // Preferred min and max width used during the intrinsic measurement.
+ .sizeIn(
+ minWidth = DropdownMenuItemDefaultMinWidth,
+ maxWidth = DropdownMenuItemDefaultMaxWidth,
+ minHeight = ListItemContainerHeight,
+ )
+ .padding(contentPadding)
+ .padding(horizontal = DropdownMenuItemHorizontalPadding),
+ verticalAlignment = Alignment.CenterVertically,
) {
content()
}
@@ -36,4 +37,4 @@ fun DropdownItemContainer(
private val DropdownMenuItemHorizontalPadding = 12.dp
private val DropdownMenuItemDefaultMinWidth = 112.dp
private val DropdownMenuItemDefaultMaxWidth = 280.dp
-private val ListItemContainerHeight = 48.0.dp
\ No newline at end of file
+private val ListItemContainerHeight = 48.0.dp
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownMenuImplementation.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownMenuImplementation.kt
index 1b26808..e2925cb 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownMenuImplementation.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/DropdownMenuImplementation.kt
@@ -58,11 +58,7 @@ internal object MenuPosition {
*/
@Stable
fun interface Vertical {
- fun position(
- anchorBounds: IntRect,
- windowSize: IntSize,
- menuHeight: Int,
- ): Int
+ fun position(anchorBounds: IntRect, windowSize: IntSize, menuHeight: Int): Int
}
/**
@@ -88,11 +84,12 @@ internal object MenuPosition {
* The given [offset] is [LayoutDirection]-aware. It will be added to the resulting x position
* for [LayoutDirection.Ltr] and subtracted for [LayoutDirection.Rtl].
*/
- fun startToAnchorStart(offset: Int = 0): Horizontal = AnchorAlignmentOffsetPosition.Horizontal(
- menuAlignment = Alignment.Start,
- anchorAlignment = Alignment.Start,
- offset = offset,
- )
+ fun startToAnchorStart(offset: Int = 0): Horizontal =
+ AnchorAlignmentOffsetPosition.Horizontal(
+ menuAlignment = Alignment.Start,
+ anchorAlignment = Alignment.Start,
+ offset = offset,
+ )
/**
* Returns a [MenuPosition.Horizontal] which aligns the end of the menu to the end of the
@@ -101,11 +98,12 @@ internal object MenuPosition {
* The given [offset] is [LayoutDirection]-aware. It will be added to the resulting x position
* for [LayoutDirection.Ltr] and subtracted for [LayoutDirection.Rtl].
*/
- fun endToAnchorEnd(offset: Int = 0): Horizontal = AnchorAlignmentOffsetPosition.Horizontal(
- menuAlignment = Alignment.End,
- anchorAlignment = Alignment.End,
- offset = offset,
- )
+ fun endToAnchorEnd(offset: Int = 0): Horizontal =
+ AnchorAlignmentOffsetPosition.Horizontal(
+ menuAlignment = Alignment.End,
+ anchorAlignment = Alignment.End,
+ offset = offset,
+ )
/**
* Returns a [MenuPosition.Horizontal] which aligns the left of the menu to the left of the
@@ -114,10 +112,11 @@ internal object MenuPosition {
* The resulting x position will be coerced so that the menu remains within the area inside the
* given [margin] from the left and right edges of the window.
*/
- fun leftToWindowLeft(margin: Int = 0): Horizontal = WindowAlignmentMarginPosition.Horizontal(
- alignment = AbsoluteAlignment.Left,
- margin = margin,
- )
+ fun leftToWindowLeft(margin: Int = 0): Horizontal =
+ WindowAlignmentMarginPosition.Horizontal(
+ alignment = AbsoluteAlignment.Left,
+ margin = margin,
+ )
/**
* Returns a [MenuPosition.Horizontal] which aligns the right of the menu to the right of the
@@ -126,52 +125,53 @@ internal object MenuPosition {
* The resulting x position will be coerced so that the menu remains within the area inside the
* given [margin] from the left and right edges of the window.
*/
- fun rightToWindowRight(margin: Int = 0): Horizontal = WindowAlignmentMarginPosition.Horizontal(
- alignment = AbsoluteAlignment.Right,
- margin = margin,
- )
+ fun rightToWindowRight(margin: Int = 0): Horizontal =
+ WindowAlignmentMarginPosition.Horizontal(
+ alignment = AbsoluteAlignment.Right,
+ margin = margin,
+ )
/**
* Returns a [MenuPosition.Vertical] which aligns the top of the menu to the bottom of the
* anchor.
*/
- fun topToAnchorBottom(offset: Int = 0): Vertical = AnchorAlignmentOffsetPosition.Vertical(
- menuAlignment = Alignment.Top,
- anchorAlignment = Alignment.Bottom,
- offset = offset,
- )
+ fun topToAnchorBottom(offset: Int = 0): Vertical =
+ AnchorAlignmentOffsetPosition.Vertical(
+ menuAlignment = Alignment.Top,
+ anchorAlignment = Alignment.Bottom,
+ offset = offset,
+ )
/**
* Returns a [MenuPosition.Vertical] which aligns the bottom of the menu to the top of the
* anchor.
*/
- fun bottomToAnchorTop(offset: Int = 0): Vertical = AnchorAlignmentOffsetPosition.Vertical(
- menuAlignment = Alignment.Bottom,
- anchorAlignment = Alignment.Top,
- offset = offset,
- )
+ fun bottomToAnchorTop(offset: Int = 0): Vertical =
+ AnchorAlignmentOffsetPosition.Vertical(
+ menuAlignment = Alignment.Bottom,
+ anchorAlignment = Alignment.Top,
+ offset = offset,
+ )
/**
* Returns a [MenuPosition.Vertical] which aligns the center of the menu to the top of the
* anchor.
*/
- fun centerToAnchorTop(offset: Int = 0): Vertical = AnchorAlignmentOffsetPosition.Vertical(
- menuAlignment = Alignment.CenterVertically,
- anchorAlignment = Alignment.Top,
- offset = offset,
- )
+ fun centerToAnchorTop(offset: Int = 0): Vertical =
+ AnchorAlignmentOffsetPosition.Vertical(
+ menuAlignment = Alignment.CenterVertically,
+ anchorAlignment = Alignment.Top,
+ offset = offset,
+ )
/**
- * Returns a [MenuPosition.Vertical] which aligns the top of the menu to the top of the
- * window.
+ * Returns a [MenuPosition.Vertical] which aligns the top of the menu to the top of the window.
*
* The resulting y position will be coerced so that the menu remains within the area inside the
* given [margin] from the top and bottom edges of the window.
*/
- fun topToWindowTop(margin: Int = 0): Vertical = WindowAlignmentMarginPosition.Vertical(
- alignment = Alignment.Top,
- margin = margin,
- )
+ fun topToWindowTop(margin: Int = 0): Vertical =
+ WindowAlignmentMarginPosition.Vertical(alignment = Alignment.Top, margin = margin)
/**
* Returns a [MenuPosition.Vertical] which aligns the bottom of the menu to the bottom of the
@@ -180,10 +180,8 @@ internal object MenuPosition {
* The resulting y position will be coerced so that the menu remains within the area inside the
* given [margin] from the top and bottom edges of the window.
*/
- fun bottomToWindowBottom(margin: Int = 0): Vertical = WindowAlignmentMarginPosition.Vertical(
- alignment = Alignment.Bottom,
- margin = margin,
- )
+ fun bottomToWindowBottom(margin: Int = 0): Vertical =
+ WindowAlignmentMarginPosition.Vertical(alignment = Alignment.Bottom, margin = margin)
}
@Immutable
@@ -207,16 +205,14 @@ internal object AnchorAlignmentOffsetPosition {
menuWidth: Int,
layoutDirection: LayoutDirection,
): Int {
- val anchorAlignmentOffset = anchorAlignment.align(
- size = 0,
- space = anchorBounds.width,
- layoutDirection = layoutDirection,
- )
- val menuAlignmentOffset = -menuAlignment.align(
- size = 0,
- space = menuWidth,
- layoutDirection,
- )
+ val anchorAlignmentOffset =
+ anchorAlignment.align(
+ size = 0,
+ space = anchorBounds.width,
+ layoutDirection = layoutDirection,
+ )
+ val menuAlignmentOffset =
+ -menuAlignment.align(size = 0, space = menuWidth, layoutDirection)
val resolvedOffset = if (layoutDirection == LayoutDirection.Ltr) offset else -offset
return anchorBounds.left + anchorAlignmentOffset + menuAlignmentOffset + resolvedOffset
}
@@ -232,19 +228,9 @@ internal object AnchorAlignmentOffsetPosition {
private val anchorAlignment: Alignment.Vertical,
private val offset: Int,
) : MenuPosition.Vertical {
- override fun position(
- anchorBounds: IntRect,
- windowSize: IntSize,
- menuHeight: Int,
- ): Int {
- val anchorAlignmentOffset = anchorAlignment.align(
- size = 0,
- space = anchorBounds.height,
- )
- val menuAlignmentOffset = -menuAlignment.align(
- size = 0,
- space = menuHeight,
- )
+ override fun position(anchorBounds: IntRect, windowSize: IntSize, menuHeight: Int): Int {
+ val anchorAlignmentOffset = anchorAlignment.align(size = 0, space = anchorBounds.height)
+ val menuAlignmentOffset = -menuAlignment.align(size = 0, space = menuHeight)
return anchorBounds.top + anchorAlignmentOffset + menuAlignmentOffset + offset
}
}
@@ -253,18 +239,16 @@ internal object AnchorAlignmentOffsetPosition {
@Immutable
internal object WindowAlignmentMarginPosition {
/**
- * A [MenuPosition.Horizontal] which horizontally aligns the menu within the window according
- * to the given [alignment].
+ * A [MenuPosition.Horizontal] which horizontally aligns the menu within the window according to
+ * the given [alignment].
*
* The resulting x position will be coerced so that the menu remains within the area inside the
* given [margin] from the left and right edges of the window. If this is not possible, i.e.,
* the menu is too wide, then it is centered horizontally instead.
*/
@Immutable
- data class Horizontal(
- private val alignment: Alignment.Horizontal,
- private val margin: Int,
- ) : MenuPosition.Horizontal {
+ data class Horizontal(private val alignment: Alignment.Horizontal, private val margin: Int) :
+ MenuPosition.Horizontal {
override fun position(
anchorBounds: IntRect,
windowSize: IntSize,
@@ -278,57 +262,47 @@ internal object WindowAlignmentMarginPosition {
layoutDirection = layoutDirection,
)
}
- val x = alignment.align(
- size = menuWidth,
- space = windowSize.width,
- layoutDirection = layoutDirection,
- )
+ val x =
+ alignment.align(
+ size = menuWidth,
+ space = windowSize.width,
+ layoutDirection = layoutDirection,
+ )
return x.coerceIn(margin, windowSize.width - margin - menuWidth)
}
}
/**
- * A [MenuPosition.Vertical] which vertically aligns the menu within the window according to
- * the given [alignment].
+ * A [MenuPosition.Vertical] which vertically aligns the menu within the window according to the
+ * given [alignment].
*
* The resulting y position will be coerced so that the menu remains within the area inside the
* given [margin] from the top and bottom edges of the window. If this is not possible, i.e.,
* the menu is too tall, then it is centered vertically instead.
*/
@Immutable
- data class Vertical(
- private val alignment: Alignment.Vertical,
- private val margin: Int,
- ) : MenuPosition.Vertical {
- override fun position(
- anchorBounds: IntRect,
- windowSize: IntSize,
- menuHeight: Int,
- ): Int {
+ data class Vertical(private val alignment: Alignment.Vertical, private val margin: Int) :
+ MenuPosition.Vertical {
+ override fun position(anchorBounds: IntRect, windowSize: IntSize, menuHeight: Int): Int {
if (menuHeight >= windowSize.height - 2 * margin) {
return Alignment.CenterVertically.align(
size = menuHeight,
space = windowSize.height,
)
}
- val y = alignment.align(
- size = menuHeight,
- space = windowSize.height,
- )
+ val y = alignment.align(size = menuHeight, space = windowSize.height)
return y.coerceIn(margin, windowSize.height - margin - menuHeight)
}
}
}
-/**
- * Calculates the position of a Material [DropdownMenu].
- */
+/** Calculates the position of a Material [DropdownMenu]. */
@Immutable
internal data class DropdownMenuPositionProvider(
val contentOffset: DpOffset,
val density: Density,
val verticalMargin: Int = with(density) { MenuVerticalMargin.roundToPx() },
- val onPositionCalculated: (anchorBounds: IntRect, menuBounds: IntRect) -> Unit = { _, _ -> }
+ val onPositionCalculated: (anchorBounds: IntRect, menuBounds: IntRect) -> Unit = { _, _ -> },
) : PopupPositionProvider {
// Horizontal position
private val startToAnchorStart: Horizontal
@@ -363,55 +337,66 @@ internal data class DropdownMenuPositionProvider(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
- popupContentSize: IntSize
+ popupContentSize: IntSize,
): IntOffset {
- val xCandidates = listOf(
- startToAnchorStart, endToAnchorEnd, if (anchorBounds.center.x < windowSize.width / 2) {
- leftToWindowLeft
- } else {
- rightToWindowRight
- }
- ).fastMap {
- it.position(
- anchorBounds = anchorBounds,
- windowSize = windowSize,
- menuWidth = popupContentSize.width,
- layoutDirection = layoutDirection
- )
- }
- val x = xCandidates.fastFirstOrNull {
- it >= 0 && it + popupContentSize.width <= windowSize.width
- } ?: xCandidates.last()
+ val xCandidates =
+ listOf(
+ startToAnchorStart,
+ endToAnchorEnd,
+ if (anchorBounds.center.x < windowSize.width / 2) {
+ leftToWindowLeft
+ } else {
+ rightToWindowRight
+ },
+ )
+ .fastMap {
+ it.position(
+ anchorBounds = anchorBounds,
+ windowSize = windowSize,
+ menuWidth = popupContentSize.width,
+ layoutDirection = layoutDirection,
+ )
+ }
+ val x =
+ xCandidates.fastFirstOrNull {
+ it >= 0 && it + popupContentSize.width <= windowSize.width
+ } ?: xCandidates.last()
- val yCandidates = listOf(
- topToAnchorBottom,
- bottomToAnchorTop,
- centerToAnchorTop,
- if (anchorBounds.center.y < windowSize.height / 2) {
- topToWindowTop
- } else {
- bottomToWindowBottom
- }
- ).fastMap {
- it.position(
- anchorBounds = anchorBounds,
- windowSize = windowSize,
- menuHeight = popupContentSize.height
- )
- }
- val y = yCandidates.fastFirstOrNull {
- it >= verticalMargin && it + popupContentSize.height <= windowSize.height - verticalMargin
- } ?: yCandidates.last()
+ val yCandidates =
+ listOf(
+ topToAnchorBottom,
+ bottomToAnchorTop,
+ centerToAnchorTop,
+ if (anchorBounds.center.y < windowSize.height / 2) {
+ topToWindowTop
+ } else {
+ bottomToWindowBottom
+ },
+ )
+ .fastMap {
+ it.position(
+ anchorBounds = anchorBounds,
+ windowSize = windowSize,
+ menuHeight = popupContentSize.height,
+ )
+ }
+ val y =
+ yCandidates.fastFirstOrNull {
+ it >= verticalMargin &&
+ it + popupContentSize.height <= windowSize.height - verticalMargin
+ } ?: yCandidates.last()
val menuOffset = IntOffset(x, y)
- onPositionCalculated(/* anchorBounds = */anchorBounds,/* menuBounds = */
- IntRect(offset = menuOffset, size = popupContentSize)
+ onPositionCalculated(
+ /* anchorBounds = */ anchorBounds,
+ /* menuBounds = */ IntRect(offset = menuOffset, size = popupContentSize),
)
return menuOffset
}
}
-// The shadow disappears when the surface is fading out, delay the animation to make it less noticeable
+// The shadow disappears when the surface is fading out, delay the animation to make it less
+// noticeable
private const val FadeOutDuration = 80
@Composable
@@ -419,19 +404,22 @@ fun DropdownMenuContent(
expandedState: MutableTransitionState,
scrollState: ScrollState,
modifier: Modifier = Modifier,
- content: @Composable ColumnScope.() -> Unit
+ content: @Composable ColumnScope.() -> Unit,
) {
AnimatedVisibility(
visibleState = expandedState,
label = "Dropdown menu animation",
enter = EnterTransition.None,
- exit = fadeOut(
- animationSpec = tween(
- delayMillis = DURATION_EXIT - FadeOutDuration,
- durationMillis = FadeOutDuration,
- easing = LinearEasing
- )
- ), modifier = modifier
+ exit =
+ fadeOut(
+ animationSpec =
+ tween(
+ delayMillis = DURATION_EXIT - FadeOutDuration,
+ durationMillis = FadeOutDuration,
+ easing = LinearEasing,
+ )
+ ),
+ modifier = modifier,
) {
Surface(
modifier = Modifier,
@@ -441,51 +429,71 @@ fun DropdownMenuContent(
shadowElevation = ElevationTokens.Level1.dp,
) {
AnimatedVisibility(
- visibleState = expandedState, label = "", enter = fadeIn(
- animationSpec = tween(
- durationMillis = DURATION_ENTER, easing = EmphasizedDecelerate
- )
- ) + expandVertically(
- animationSpec = tween(
- durationMillis = DURATION_ENTER, easing = EmphasizedDecelerate
- ),
- expandFrom = Alignment.Top,
- ) + slideInVertically(
- animationSpec = tween(
- durationMillis = DURATION_ENTER, easing = EmphasizedDecelerate
- ),
- initialOffsetY = { -it / 10 },
- ), exit = fadeOut(
- animationSpec = tween(
- // Why ???
- durationMillis = DURATION_EXIT - 20,
- easing = EmphasizedAccelerate
- )
- ) + shrinkVertically(
- animationSpec = tween(
- durationMillis = DURATION_EXIT, easing = EmphasizedAccelerate
- ),
- shrinkTowards = Alignment.Top,
- ) + slideOutVertically(
- animationSpec = tween(
- durationMillis = DURATION_EXIT, easing = EmphasizedAccelerate
- ), targetOffsetY = { -it / 10 }), modifier = Modifier
+ visibleState = expandedState,
+ label = "",
+ enter =
+ fadeIn(
+ animationSpec =
+ tween(durationMillis = DURATION_ENTER, easing = EmphasizedDecelerate)
+ ) +
+ expandVertically(
+ animationSpec =
+ tween(
+ durationMillis = DURATION_ENTER,
+ easing = EmphasizedDecelerate,
+ ),
+ expandFrom = Alignment.Top,
+ ) +
+ slideInVertically(
+ animationSpec =
+ tween(
+ durationMillis = DURATION_ENTER,
+ easing = EmphasizedDecelerate,
+ ),
+ initialOffsetY = { -it / 10 },
+ ),
+ exit =
+ fadeOut(
+ animationSpec =
+ tween(
+ // Why ???
+ durationMillis = DURATION_EXIT - 20,
+ easing = EmphasizedAccelerate,
+ )
+ ) +
+ shrinkVertically(
+ animationSpec =
+ tween(
+ durationMillis = DURATION_EXIT,
+ easing = EmphasizedAccelerate,
+ ),
+ shrinkTowards = Alignment.Top,
+ ) +
+ slideOutVertically(
+ animationSpec =
+ tween(
+ durationMillis = DURATION_EXIT,
+ easing = EmphasizedAccelerate,
+ ),
+ targetOffsetY = { -it / 10 },
+ ),
+ modifier = Modifier,
) {
Column(
- modifier = Modifier
- .padding(
- vertical = DropdownMenuVerticalPadding,
- horizontal = DropdownMenuVerticalPadding / 2
- )
- .width(IntrinsicSize.Max)
- .verticalScroll(scrollState), content = content
+ modifier =
+ Modifier.padding(
+ vertical = DropdownMenuVerticalPadding,
+ horizontal = DropdownMenuVerticalPadding / 2,
+ )
+ .width(IntrinsicSize.Max)
+ .verticalScroll(scrollState),
+ content = content,
)
}
}
}
}
-
// Size defaults.
internal val MenuVerticalMargin = 48.dp
-internal val DropdownMenuVerticalPadding = 16.dp
\ No newline at end of file
+internal val DropdownMenuVerticalPadding = 16.dp
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/M3ElevationTokens.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/M3ElevationTokens.kt
index 7f54bb4..9c8c05a 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/M3ElevationTokens.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/dropdown/M3ElevationTokens.kt
@@ -12,4 +12,4 @@ internal object ElevationTokens {
const val Level3 = 6
const val Level4 = 8
const val Level5 = 12
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/image/ProfilePictureGenerator.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/image/ProfilePictureGenerator.kt
index c6efb14..ac4b044 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/image/ProfilePictureGenerator.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/image/ProfilePictureGenerator.kt
@@ -27,7 +27,7 @@ fun ProfilePicture(
name: String,
shape: CornerBasedShape = CircleShape,
surfaceColor: Color = MaterialTheme.colorScheme.primary,
- onClick: () -> Unit = {}
+ onClick: () -> Unit = {},
) {
val firstLetter = name.first().toString()
Surface(
@@ -58,4 +58,4 @@ private fun ProfilePicturePreview() {
modifier = Modifier,
size = 40,
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/others/AdditionalInformation.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/others/AdditionalInformation.kt
index 4e5d8a1..e12042d 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/others/AdditionalInformation.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/others/AdditionalInformation.kt
@@ -18,24 +18,15 @@ import androidx.compose.ui.unit.dp
import com.bobbyesp.ui.R
@Composable
-fun AdditionalInformation(
- modifier: Modifier = Modifier,
- text: AnnotatedString
-) {
- Column(
- modifier = modifier,
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
+fun AdditionalInformation(modifier: Modifier = Modifier, text: AnnotatedString) {
+ Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
imageVector = Icons.Rounded.Info,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
contentDescription = stringResource(id = R.string.additional_information),
)
- Text(
- text = text,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
+ Text(text = text, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
@@ -43,12 +34,9 @@ fun AdditionalInformation(
fun AdditionalInformation(
modifier: Modifier = Modifier,
text: String,
- fontFamily: FontFamily = FontFamily.Default
+ fontFamily: FontFamily = FontFamily.Default,
) {
- Column(
- modifier = modifier,
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
+ Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
modifier = Modifier.size(32.dp),
imageVector = Icons.Rounded.Info,
@@ -60,7 +48,7 @@ fun AdditionalInformation(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- fontFamily = fontFamily
+ fontFamily = fontFamily,
)
}
}
@@ -70,8 +58,9 @@ fun AdditionalInformation(
private fun Preview() {
MaterialTheme {
AdditionalInformation(
- text = "This is a preview text preview text preview text preview text preview text preview text " +
+ text =
+ "This is a preview text preview text preview text preview text preview text preview text " +
"preview text preview text"
)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/others/LoadingPlaceholder.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/others/LoadingPlaceholder.kt
index 61a85c9..b6a5200 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/others/LoadingPlaceholder.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/others/LoadingPlaceholder.kt
@@ -10,11 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
-fun LoadingPlaceholder(
- modifier: Modifier = Modifier,
- progress: Float? = null,
- colorful: Boolean,
-) {
+fun LoadingPlaceholder(modifier: Modifier = Modifier, progress: Float? = null, colorful: Boolean) {
val color =
if (colorful) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
@@ -22,27 +18,18 @@ fun LoadingPlaceholder(
if (colorful) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
val elevation = if (colorful) 0.dp else 8.dp
- Surface(
- tonalElevation = elevation,
- color = color,
- modifier = modifier
- ) {
+ Surface(tonalElevation = elevation, color = color, modifier = modifier) {
if (progress == null) {
CircularProgressIndicator(
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp),
+ modifier = Modifier.fillMaxSize().padding(8.dp),
color = onColor,
)
} else {
CircularProgressIndicator(
progress = { progress },
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp),
+ modifier = Modifier.fillMaxSize().padding(8.dp),
color = onColor,
)
}
-
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/others/MetadataTag.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/others/MetadataTag.kt
index afd6798..d17c360 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/others/MetadataTag.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/others/MetadataTag.kt
@@ -25,22 +25,23 @@ fun MetadataTag(
val clipboardManager = LocalClipboardManager.current
Column(
- modifier = modifier.clickable {
- clipboardManager.setText(AnnotatedString(metadata))
- Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
- }
+ modifier =
+ modifier.clickable {
+ clipboardManager.setText(AnnotatedString(metadata))
+ Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
+ }
) {
Text(
text = typeOfMetadata,
modifier = Modifier.alpha(alpha = 0.8f),
style = MaterialTheme.typography.labelSmall,
- textAlign = TextAlign.Start
+ textAlign = TextAlign.Start,
)
Text(
modifier = Modifier,
text = metadata,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 16.sp),
- textAlign = TextAlign.Start
+ textAlign = TextAlign.Start,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/others/Placeholder.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/others/Placeholder.kt
index 45c2d33..1be18dc 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/others/Placeholder.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/others/Placeholder.kt
@@ -26,22 +26,23 @@ fun Placeholder(
modifier: Modifier = Modifier,
icon: ImageVector?,
colorful: Boolean,
- contentDescription: String? = null
+ contentDescription: String? = null,
) {
Surface(
tonalElevation = if (colorful) 0.dp else 8.dp,
- color = if (colorful) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
- modifier = modifier
+ color =
+ if (colorful) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
+ modifier = modifier,
) {
icon?.let {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
- modifier = Modifier
- .fillMaxSize()
- .padding(8.dp),
+ modifier = Modifier.fillMaxSize().padding(8.dp),
imageVector = icon,
contentDescription = contentDescription,
- tint = if (colorful) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface,
+ tint =
+ if (colorful) MaterialTheme.colorScheme.onPrimary
+ else MaterialTheme.colorScheme.onSurface,
)
}
}
@@ -54,12 +55,10 @@ fun Placeholder(
private fun PlaceholderPreview() {
MaterialTheme {
Placeholder(
- modifier = Modifier
- .width(200.dp)
- .aspectRatio(1f),
+ modifier = Modifier.width(200.dp).aspectRatio(1f),
icon = Icons.Rounded.Lyrics,
colorful = true,
- contentDescription = "Song cover"
+ contentDescription = "Song cover",
)
}
}
@@ -70,12 +69,10 @@ private fun PlaceholderPreview() {
private fun PlaceholderPreviewNonColourful() {
MaterialTheme {
Placeholder(
- modifier = Modifier
- .width(200.dp)
- .aspectRatio(1f),
+ modifier = Modifier.width(200.dp).aspectRatio(1f),
icon = Icons.Default.Lyrics,
colorful = false,
- contentDescription = "Song cover"
+ contentDescription = "Song cover",
)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/others/SelectableSurface.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/others/SelectableSurface.kt
index 5f2478b..33e5329 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/others/SelectableSurface.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/others/SelectableSurface.kt
@@ -34,18 +34,17 @@ fun SelectableSurface(
shape: Shape = MaterialTheme.shapes.small,
isSelected: Boolean = false,
onSelected: () -> Unit = {},
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
) {
- val animatedColor by animateColorAsState(
- targetValue = selectColorHandler(isSelected), label = "Animated transition for color change"
- )
+ val animatedColor by
+ animateColorAsState(
+ targetValue = selectColorHandler(isSelected),
+ label = "Animated transition for color change",
+ )
Surface(
- modifier = modifier.selectable(
- selected = isSelected,
- onClick = onSelected,
- role = Role.Button
- ),
+ modifier =
+ modifier.selectable(selected = isSelected, onClick = onSelected, role = Role.Button),
color = animatedColor,
border = borderStroke,
shape = shape,
@@ -56,12 +55,9 @@ fun SelectableSurface(
}
@Composable
-private fun selectColorHandler(
- isSelected: Boolean
-): Color {
- return if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.harmonizeWithPrimary(
- MaterialTheme.colorScheme.onSecondary
- )
+private fun selectColorHandler(isSelected: Boolean): Color {
+ return if (isSelected) MaterialTheme.colorScheme.primaryContainer
+ else MaterialTheme.colorScheme.harmonizeWithPrimary(MaterialTheme.colorScheme.onSecondary)
}
@Preview
@@ -69,21 +65,19 @@ private fun selectColorHandler(
fun SelectableSurfacePreview() {
MaterialTheme {
var selected by remember { mutableStateOf(false) }
- SelectableSurface(
- isSelected = selected,
- onSelected = { selected = !selected }
- ) {
+ SelectableSurface(isSelected = selected, onSelected = { selected = !selected }) {
Column(
- modifier = Modifier
- .size(200.dp)
- .padding(12.dp),
+ modifier = Modifier.size(200.dp).padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
- horizontalAlignment = Alignment.Start
+ horizontalAlignment = Alignment.Start,
) {
Text(text = "SelectableSurface", fontWeight = FontWeight.Bold)
- Text(text = "This is just a test to see how this should work. Lol this is a very long text don't you think? I think so.")
+ Text(
+ text =
+ "This is just a test to see how this should work. Lol this is a very long text don't you think? I think so."
+ )
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/PreferencesItems.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/PreferencesItems.kt
index a247626..13c22fc 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/PreferencesItems.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/PreferencesItems.kt
@@ -67,7 +67,6 @@ private val PreferenceTitleVariant: TextStyle
private val PreferenceTitle
@Composable get() = MaterialTheme.typography.titleMedium
-
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PreferenceItem(
@@ -83,18 +82,17 @@ fun PreferenceItem(
onClick: () -> Unit = {},
) {
Surface(
- modifier = Modifier.combinedClickable(
- onClick = onClick,
- onClickLabel = onClickLabel,
- enabled = enabled,
- onLongClickLabel = onLongClickLabel,
- onLongClick = onLongClick
- )
+ modifier =
+ Modifier.combinedClickable(
+ onClick = onClick,
+ onClickLabel = onClickLabel,
+ enabled = enabled,
+ onLongClickLabel = onLongClickLabel,
+ onLongClick = onLongClick,
+ )
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal.dp, vertical.dp),
+ modifier = Modifier.fillMaxWidth().padding(horizontal.dp, vertical.dp),
verticalAlignment = Alignment.CenterVertically,
) {
leadingIcon?.invoke()
@@ -104,10 +102,8 @@ fun PreferenceItem(
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled)
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled),
)
}
@@ -115,39 +111,37 @@ fun PreferenceItem(
Icon(
painter = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled)
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled),
)
}
}
Column(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = if (icon == null && leadingIcon == null) 8.dp else 0.dp)
- .padding(end = 8.dp)
+ modifier =
+ Modifier.weight(1f)
+ .padding(
+ horizontal = if (icon == null && leadingIcon == null) 8.dp else 0.dp
+ )
+ .padding(end = 8.dp)
) {
PreferenceItemTitle(text = title, enabled = enabled)
- if (!description.isNullOrEmpty()) PreferenceItemDescription(
- text = description, enabled = enabled
- )
+ if (!description.isNullOrEmpty())
+ PreferenceItemDescription(text = description, enabled = enabled)
}
trailingIcon?.let {
VerticalDivider(
- modifier = Modifier
- .height(32.dp)
- .padding(horizontal = 8.dp)
- .align(Alignment.CenterVertically),
+ modifier =
+ Modifier.height(32.dp)
+ .padding(horizontal = 8.dp)
+ .align(Alignment.CenterVertically),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
- thickness = 1.dp
+ thickness = 1.dp,
)
trailingIcon.invoke()
}
}
}
-
}
@Composable
@@ -159,10 +153,14 @@ private fun PreferenceItemPreview() {
PreferenceSubtitle(text = "Preview")
PreferenceItem(title = "title", description = "description")
PreferenceItem(
- title = "title", description = "description", icon = Icons.Outlined.Update
+ title = "title",
+ description = "description",
+ icon = Icons.Outlined.Update,
)
PreferenceItemVariant(
- title = "title", description = "description", icon = Icons.Outlined.Update
+ title = "title",
+ description = "description",
+ icon = Icons.Outlined.Update,
)
}
}
@@ -183,35 +181,32 @@ fun PreferenceItemVariant(
onClick: () -> Unit = {},
) {
Surface(
- modifier = Modifier.combinedClickable(
- enabled = enabled,
- onClick = onClick,
- onClickLabel = onClickLabel,
- onLongClick = onLongClick,
- onLongClickLabel = onLongClickLabel
- )
+ modifier =
+ Modifier.combinedClickable(
+ enabled = enabled,
+ onClick = onClick,
+ onClickLabel = onClickLabel,
+ onLongClick = onLongClick,
+ onLongClickLabel = onLongClickLabel,
+ )
) {
Row(
- modifier = modifier
- .fillMaxWidth()
- .padding(12.dp, 16.dp),
+ modifier = modifier.fillMaxWidth().padding(12.dp, 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled)
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled),
)
}
Column(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = if (icon == null) 12.dp else 0.dp)
- .padding(end = 8.dp)
+ modifier =
+ Modifier.weight(1f)
+ .padding(horizontal = if (icon == null) 12.dp else 0.dp)
+ .padding(end = 8.dp)
) {
PreferenceItemTitle(text = title, enabled = enabled)
if (description != null) {
@@ -220,7 +215,6 @@ fun PreferenceItemVariant(
}
}
}
-
}
@Composable
@@ -229,38 +223,26 @@ fun PreferenceSingleChoiceItem(
text: String,
selected: Boolean,
contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 16.dp),
- onClick: () -> Unit
+ onClick: () -> Unit,
) {
- Surface(
- modifier = Modifier.selectable(
- selected = selected, onClick = onClick
- )
- ) {
+ Surface(modifier = Modifier.selectable(selected = selected, onClick = onClick)) {
Row(
- modifier = modifier
- .fillMaxWidth()
- .padding(contentPadding),
+ modifier = modifier.fillMaxWidth().padding(contentPadding),
verticalAlignment = Alignment.CenterVertically,
) {
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(start = 8.dp)
- ) {
+ Column(modifier = Modifier.weight(1f).padding(start = 8.dp)) {
Text(
text = text,
maxLines = 1,
style = PreferenceTitleVariant,
color = MaterialTheme.colorScheme.onSurface,
- overflow = TextOverflow.Ellipsis
+ overflow = TextOverflow.Ellipsis,
)
}
RadioButton(
selected = selected,
onClick = onClick,
- modifier = Modifier
- .padding()
- .clearAndSetSemantics { },
+ modifier = Modifier.padding().clearAndSetSemantics {},
)
}
}
@@ -274,7 +256,7 @@ internal fun PreferenceItemTitle(
style: TextStyle = PreferenceTitle,
enabled: Boolean,
color: Color = MaterialTheme.colorScheme.onBackground,
- overflow: TextOverflow = TextOverflow.Ellipsis
+ overflow: TextOverflow = TextOverflow.Ellipsis,
) {
Text(
modifier = modifier,
@@ -282,7 +264,7 @@ internal fun PreferenceItemTitle(
maxLines = maxLines,
style = style,
color = color.applyAlpha(enabled),
- overflow = overflow
+ overflow = overflow,
)
}
@@ -294,7 +276,7 @@ internal fun PreferenceItemDescription(
style: TextStyle = MaterialTheme.typography.bodyMedium,
enabled: Boolean,
color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
- overflow: TextOverflow = TextOverflow.Ellipsis
+ overflow: TextOverflow = TextOverflow.Ellipsis,
) {
Text(
modifier = modifier,
@@ -302,7 +284,7 @@ internal fun PreferenceItemDescription(
maxLines = maxLines,
style = style,
color = color.applyAlpha(enabled),
- overflow = overflow
+ overflow = overflow,
)
}
@@ -315,7 +297,7 @@ private fun PreferenceSwitchPreview() {
title = "PreferenceSwitch",
description = "Supporting text",
icon = Icons.Outlined.ToggleOn,
- isChecked = b
+ isChecked = b,
) {
b = !b
}
@@ -336,19 +318,20 @@ private fun PreferenceSwitchWithDividerPreview() {
fun rememberThumbContent(
isChecked: Boolean,
checkedIcon: ImageVector = Icons.Outlined.Check,
-): (@Composable () -> Unit)? = remember(isChecked, checkedIcon) {
- if (isChecked) {
- {
- Icon(
- imageVector = checkedIcon,
- contentDescription = null,
- modifier = Modifier.size(SwitchDefaults.IconSize),
- )
+): (@Composable () -> Unit)? =
+ remember(isChecked, checkedIcon) {
+ if (isChecked) {
+ {
+ Icon(
+ imageVector = checkedIcon,
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ }
+ } else {
+ null
}
- } else {
- null
}
-}
@Composable
fun PreferenceSwitchVariant(
@@ -362,40 +345,34 @@ fun PreferenceSwitchVariant(
) {
val interactionSource = remember { MutableInteractionSource() }
Surface(
- modifier = Modifier.toggleable(
- value = isChecked,
- enabled = enabled,
- onValueChange = { onClick() },
- indication = LocalIndication.current,
- interactionSource = interactionSource
- )
+ modifier =
+ Modifier.toggleable(
+ value = isChecked,
+ enabled = enabled,
+ onValueChange = { onClick() },
+ indication = LocalIndication.current,
+ interactionSource = interactionSource,
+ )
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal.dp, vertical.dp)
- .padding(start = if (icon == null) 12.dp else 0.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal.dp, vertical.dp)
+ .padding(start = if (icon == null) 12.dp else 0.dp),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled)
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled),
)
}
- Column(
- modifier = Modifier.weight(1f)
- ) {
- PreferenceItemTitle(
- text = title, enabled = enabled, style = PreferenceTitleVariant
- )
- if (!description.isNullOrEmpty()) PreferenceItemDescription(
- text = description, enabled = enabled
- )
+ Column(modifier = Modifier.weight(1f)) {
+ PreferenceItemTitle(text = title, enabled = enabled, style = PreferenceTitleVariant)
+ if (!description.isNullOrEmpty())
+ PreferenceItemDescription(text = description, enabled = enabled)
}
Switch(
checked = isChecked,
@@ -403,7 +380,7 @@ fun PreferenceSwitchVariant(
interactionSource = interactionSource,
modifier = Modifier.padding(start = 20.dp, end = 6.dp),
enabled = enabled,
- thumbContent = thumbContent
+ thumbContent = thumbContent,
)
}
}
@@ -422,40 +399,34 @@ fun PreferenceSwitch(
val interactionSource = remember { MutableInteractionSource() }
Surface(
- modifier = Modifier.toggleable(
- value = isChecked,
- enabled = enabled,
- onValueChange = { onClick() },
- indication = LocalIndication.current,
- interactionSource = interactionSource
- )
+ modifier =
+ Modifier.toggleable(
+ value = isChecked,
+ enabled = enabled,
+ onValueChange = { onClick() },
+ indication = LocalIndication.current,
+ interactionSource = interactionSource,
+ )
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal.dp, vertical.dp)
- .padding(start = if (icon == null) 12.dp else 0.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal.dp, vertical.dp)
+ .padding(start = if (icon == null) 12.dp else 0.dp),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled)
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled),
)
}
- Column(
- modifier = Modifier.weight(1f)
- ) {
- PreferenceItemTitle(
- text = title, enabled = enabled
- )
- if (!description.isNullOrEmpty()) PreferenceItemDescription(
- text = description, enabled = enabled
- )
+ Column(modifier = Modifier.weight(1f)) {
+ PreferenceItemTitle(text = title, enabled = enabled)
+ if (!description.isNullOrEmpty())
+ PreferenceItemDescription(text = description, enabled = enabled)
}
Switch(
checked = isChecked,
@@ -463,16 +434,13 @@ fun PreferenceSwitch(
interactionSource = interactionSource,
modifier = Modifier.padding(start = 20.dp, end = 6.dp),
enabled = enabled,
- colors = SwitchDefaults.colors(
- uncheckedBorderColor = Color.Transparent,
- ),
- thumbContent = thumbContent
+ colors = SwitchDefaults.colors(uncheckedBorderColor = Color.Transparent),
+ thumbContent = thumbContent,
)
}
}
}
-
@Composable
fun PreferenceSwitchWithDivider(
title: String,
@@ -483,62 +451,53 @@ fun PreferenceSwitchWithDivider(
isChecked: Boolean = true,
thumbContent: (@Composable () -> Unit)? = rememberThumbContent(isChecked = isChecked),
onClick: (() -> Unit) = {},
- onChecked: () -> Unit = {}
+ onChecked: () -> Unit = {},
) {
Surface(
- modifier = Modifier.clickable(
- enabled = enabled,
- onClick = onClick,
- onClickLabel = stringResource(id = R.string.open_settings)
- )
+ modifier =
+ Modifier.clickable(
+ enabled = enabled,
+ onClick = onClick,
+ onClickLabel = stringResource(id = R.string.open_settings),
+ )
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal.dp, vertical.dp)
- .height(IntrinsicSize.Min),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal.dp, vertical.dp)
+ .height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled)
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.applyAlpha(enabled),
)
}
- Column(
- modifier = Modifier.weight(1f)
- ) {
+ Column(modifier = Modifier.weight(1f)) {
PreferenceItemTitle(text = title, enabled = enabled)
- if (!description.isNullOrEmpty()) PreferenceItemDescription(
- text = description, enabled = enabled
- )
+ if (!description.isNullOrEmpty())
+ PreferenceItemDescription(text = description, enabled = enabled)
}
VerticalDivider(
- modifier = Modifier
- .height(32.dp)
- .padding(horizontal = 8.dp)
- .width(1f.dp)
- .align(Alignment.CenterVertically),
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
+ modifier =
+ Modifier.height(32.dp)
+ .padding(horizontal = 8.dp)
+ .width(1f.dp)
+ .align(Alignment.CenterVertically),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
)
Switch(
checked = isChecked,
onCheckedChange = { onChecked() },
- modifier = Modifier
- .padding(horizontal = 6.dp)
- .semantics {
- contentDescription = title
- },
+ modifier =
+ Modifier.padding(horizontal = 6.dp).semantics { contentDescription = title },
enabled = isSwitchEnabled,
- colors = SwitchDefaults.colors(
- uncheckedBorderColor = Color.Transparent,
- ),
- thumbContent = thumbContent
+ colors = SwitchDefaults.colors(uncheckedBorderColor = Color.Transparent),
+ thumbContent = thumbContent,
)
}
}
@@ -553,44 +512,47 @@ fun PreferencesCautionCard(
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 12.dp)
- .clip(MaterialTheme.shapes.extraLarge)
- .background(MaterialTheme.colorScheme.harmonizeWithPrimary(MaterialTheme.colorScheme.errorContainer))
- .clickable { onClick() }
- .padding(horizontal = 12.dp, vertical = 16.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 12.dp)
+ .clip(MaterialTheme.shapes.extraLarge)
+ .background(
+ MaterialTheme.colorScheme.harmonizeWithPrimary(
+ MaterialTheme.colorScheme.errorContainer
+ )
+ )
+ .clickable { onClick() }
+ .padding(horizontal = 12.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.harmonizeWithPrimary(MaterialTheme.colorScheme.error)
-
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint =
+ MaterialTheme.colorScheme.harmonizeWithPrimary(MaterialTheme.colorScheme.error),
)
}
Column(
- modifier = Modifier
- .weight(1f)
- .padding(start = if (icon == null) 12.dp else 0.dp, end = 12.dp)
+ modifier =
+ Modifier.weight(1f).padding(start = if (icon == null) 12.dp else 0.dp, end = 12.dp)
) {
with(MaterialTheme) {
Text(
text = title,
maxLines = 1,
style = PreferenceTitleVariant,
- color = colorScheme.harmonizeWithPrimary(colorScheme.onErrorContainer)
- )
- if (description != null) Text(
- text = description,
color = colorScheme.harmonizeWithPrimary(colorScheme.onErrorContainer),
- maxLines = 2, overflow = TextOverflow.Ellipsis,
- style = typography.bodyMedium,
)
+ if (description != null)
+ Text(
+ text = description,
+ color = colorScheme.harmonizeWithPrimary(colorScheme.onErrorContainer),
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = typography.bodyMedium,
+ )
}
}
}
@@ -606,40 +568,42 @@ fun PreferencesHintCard(
onClick: () -> Unit = {},
) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp)
- .clip(MaterialTheme.shapes.extraLarge)
- .background(containerColor)
- .clickable { onClick() }
- .padding(horizontal = 12.dp, vertical = 16.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ .clip(MaterialTheme.shapes.extraLarge)
+ .background(containerColor)
+ .clickable { onClick() }
+ .padding(horizontal = 12.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = contentColor
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = contentColor,
)
}
Column(
- modifier = Modifier
- .weight(1f)
- .padding(start = if (icon == null) 12.dp else 0.dp, end = 12.dp)
+ modifier =
+ Modifier.weight(1f).padding(start = if (icon == null) 12.dp else 0.dp, end = 12.dp)
) {
with(MaterialTheme) {
Text(
- text = title, maxLines = 1, style = PreferenceTitleVariant, color = contentColor
- )
- if (description != null) Text(
- text = description,
+ text = title,
+ maxLines = 1,
+ style = PreferenceTitleVariant,
color = contentColor,
- maxLines = 2, overflow = TextOverflow.Ellipsis,
- style = typography.bodyMedium,
)
+ if (description != null)
+ Text(
+ text = description,
+ color = contentColor,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = typography.bodyMedium,
+ )
}
}
}
@@ -655,7 +619,7 @@ private fun PreferenceSwitchWithContainerPreview() {
title = "Title ".repeat(2),
isChecked = isChecked,
onClick = { isChecked = !isChecked },
- icon = null
+ icon = null,
)
}
}
@@ -671,42 +635,37 @@ fun PreferenceSwitchWithContainer(
val interactionSource = remember { MutableInteractionSource() }
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp)
- .clip(MaterialTheme.shapes.extraLarge)
- .background(
- MaterialTheme.colorScheme.primaryContainer
- )
- .toggleable(
- value = isChecked,
- onValueChange = { onClick() },
- interactionSource = interactionSource,
- indication = LocalIndication.current
- )
- .padding(horizontal = 16.dp, vertical = 20.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ .clip(MaterialTheme.shapes.extraLarge)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .toggleable(
+ value = isChecked,
+ onValueChange = { onClick() },
+ interactionSource = interactionSource,
+ indication = LocalIndication.current,
+ )
+ .padding(horizontal = 16.dp, vertical = 20.dp),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier
- .padding(start = 8.dp, end = 16.dp)
- .size(24.dp),
- tint = MaterialTheme.colorScheme.onPrimaryContainer
+ modifier = Modifier.padding(start = 8.dp, end = 16.dp).size(24.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
Column(
- modifier = Modifier
- .weight(1f)
- .padding(start = if (icon == null) 12.dp else 0.dp, end = 12.dp)
+ modifier =
+ Modifier.weight(1f).padding(start = if (icon == null) 12.dp else 0.dp, end = 12.dp)
) {
Text(
text = title,
maxLines = 2,
style = PreferenceTitleVariant,
- color = MaterialTheme.colorScheme.onPrimaryContainer
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
Switch(
@@ -714,9 +673,7 @@ fun PreferenceSwitchWithContainer(
interactionSource = interactionSource,
onCheckedChange = null,
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
- colors = SwitchDefaults.colors(
- uncheckedBorderColor = Color.Transparent,
- ),
+ colors = SwitchDefaults.colors(uncheckedBorderColor = Color.Transparent),
thumbContent = thumbContent,
)
}
@@ -731,28 +688,23 @@ fun CreditItem(
) {
Surface(modifier = Modifier.clickable { onClick() }) {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 16.dp),
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 10.dp)
- ) {
+ Column(modifier = Modifier.weight(1f).padding(horizontal = 10.dp)) {
with(MaterialTheme) {
Text(
text = title,
maxLines = 1,
style = typography.titleMedium,
- color = colorScheme.onSurface.applyAlpha(enabled)
+ color = colorScheme.onSurface.applyAlpha(enabled),
)
license?.let {
Text(
text = it,
color = colorScheme.onSurfaceVariant.applyAlpha(enabled),
- maxLines = 2, overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
style = typography.bodyMedium,
)
}
@@ -765,11 +717,7 @@ fun CreditItem(
@Composable
fun PreferenceSubtitle(
modifier: Modifier = Modifier,
- contentPadding: PaddingValues = PaddingValues(
- start = 16.dp,
- top = 20.dp,
- bottom = 8.dp,
- ),
+ contentPadding: PaddingValues = PaddingValues(start = 16.dp, top = 20.dp, bottom = 8.dp),
text: String,
color: Color = MaterialTheme.colorScheme.primary,
) {
@@ -777,7 +725,7 @@ fun PreferenceSubtitle(
text = text,
modifier = modifier.padding(contentPadding),
color = color,
- style = MaterialTheme.typography.labelLarge
+ style = MaterialTheme.typography.labelLarge,
)
}
@@ -786,22 +734,19 @@ fun PreferenceInfo(
modifier: Modifier = Modifier,
text: String,
icon: ImageVector = Icons.Outlined.Info,
- applyPaddings: Boolean = true
+ applyPaddings: Boolean = true,
) {
Column(
- modifier = modifier
- .fillMaxWidth()
- .run {
- if (applyPaddings) padding(horizontal = 16.dp, vertical = 16.dp)
- else this
- }) {
- Icon(
- modifier = Modifier.padding(), imageVector = icon, contentDescription = null
- )
+ modifier =
+ modifier.fillMaxWidth().run {
+ if (applyPaddings) padding(horizontal = 16.dp, vertical = 16.dp) else this
+ }
+ ) {
+ Icon(modifier = Modifier.padding(), imageVector = icon, contentDescription = null)
Text(
modifier = Modifier.padding(top = 16.dp),
text = text,
- style = MaterialTheme.typography.bodyMedium
+ style = MaterialTheme.typography.bodyMedium,
)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOption.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOption.kt
index 12454a2..f111bf0 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOption.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOption.kt
@@ -1,7 +1,3 @@
-
package com.bobbyesp.ui.components.preferences
-sealed class SettingOption(
- val title: String,
- val onSelection: () -> Unit,
-)
+sealed class SettingOption(val title: String, val onSelection: () -> Unit)
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOptionsRow.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOptionsRow.kt
index b33d559..7267476 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOptionsRow.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingOptionsRow.kt
@@ -21,36 +21,28 @@ fun SettingOptionsRow(
title: String,
options: List,
modifier: Modifier = Modifier,
- optionContent: @Composable (T) -> Unit
+ optionContent: @Composable (T) -> Unit,
) {
Column(
- modifier = modifier
- .clip(ShapeDefaults.ExtraLarge)
- .background(color = MaterialTheme.colorScheme.surfaceContainer)
- .padding(vertical = 16.dp)
+ modifier =
+ modifier
+ .clip(ShapeDefaults.ExtraLarge)
+ .background(color = MaterialTheme.colorScheme.surfaceContainer)
+ .padding(vertical = 16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.padding(start = 20.dp)
+ modifier = Modifier.padding(start = 20.dp),
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow {
- item {
- Spacer(modifier = Modifier.width(8.dp))
- }
- items(
- items = options,
- key = { it.title }
- ) { option ->
- optionContent(option)
- }
- item {
- Spacer(modifier = Modifier.width(8.dp))
- }
+ item { Spacer(modifier = Modifier.width(8.dp)) }
+ items(items = options, key = { it.title }) { option -> optionContent(option) }
+ item { Spacer(modifier = Modifier.width(8.dp)) }
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOption.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOption.kt
index debac6f..caa0db8 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOption.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOption.kt
@@ -5,5 +5,5 @@ import androidx.compose.ui.graphics.vector.ImageVector
data class SettingSegmentOption(
val icon: ImageVector,
val contentDescription: String,
- val onClick: () -> Unit
-)
\ No newline at end of file
+ val onClick: () -> Unit,
+)
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOptions.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOptions.kt
index 0ca1857..fcec54c 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOptions.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSegmentOptions.kt
@@ -39,85 +39,77 @@ fun SettingSegmentOptions(
icon: ImageVector,
options: List,
selectedOptionIndex: Int,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
Row(
- modifier = modifier
- .clip(ShapeDefaults.Large),
- verticalAlignment = Alignment.CenterVertically
+ modifier = modifier.clip(ShapeDefaults.Large),
+ verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = icon,
tint = MaterialTheme.colorScheme.onSurface,
- contentDescription = null
+ contentDescription = null,
)
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 16.dp)
- ) {
+ Column(modifier = Modifier.weight(1f).padding(horizontal = 16.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = supportingText,
style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Box(
- modifier = modifier
- .width(IntrinsicSize.Min)
- .height(IntrinsicSize.Min)
- .clip(ShapeDefaults.ExtraLarge)
- .background(color = MaterialTheme.colorScheme.surfaceContainerHighest)
+ modifier =
+ modifier
+ .width(IntrinsicSize.Min)
+ .height(IntrinsicSize.Min)
+ .clip(ShapeDefaults.ExtraLarge)
+ .background(color = MaterialTheme.colorScheme.surfaceContainerHighest)
) {
- var midPoint by remember {
- mutableStateOf(0.dp)
- }
+ var midPoint by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
- val capsuleOffset by animateDpAsState(
- targetValue = if (selectedOptionIndex == 0) 0.dp else midPoint,
- label = "capsule-offset-animation"
- )
+ val capsuleOffset by
+ animateDpAsState(
+ targetValue = if (selectedOptionIndex == 0) 0.dp else midPoint,
+ label = "capsule-offset-animation",
+ )
Box(
- modifier = Modifier
- .fillMaxHeight()
- .fillMaxWidth(.5f)
- .offset(x = capsuleOffset)
- .clip(ShapeDefaults.ExtraLarge)
- .background(color = MaterialTheme.colorScheme.primary)
+ modifier =
+ Modifier.fillMaxHeight()
+ .fillMaxWidth(.5f)
+ .offset(x = capsuleOffset)
+ .clip(ShapeDefaults.ExtraLarge)
+ .background(color = MaterialTheme.colorScheme.primary)
)
Row(
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .onGloballyPositioned {
+ modifier =
+ Modifier.onGloballyPositioned {
midPoint = with(density) { (it.size.width / 2).toDp() }
- }
+ },
) {
options.fastForEachIndexed { index, option ->
Icon(
imageVector = option.icon,
contentDescription = option.contentDescription,
- tint = if (selectedOptionIndex == index) {
- MaterialTheme.colorScheme.onPrimary
- } else MaterialTheme.colorScheme.onSurface,
- modifier = Modifier
- .padding(8.dp)
- .clip(CircleShape)
- .clickable {
- option.onClick()
- }
+ tint =
+ if (selectedOptionIndex == index) {
+ MaterialTheme.colorScheme.onPrimary
+ } else MaterialTheme.colorScheme.onSurface,
+ modifier =
+ Modifier.padding(8.dp).clip(CircleShape).clickable { option.onClick() },
)
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSlider.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSlider.kt
index 1d308b2..b66435b 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSlider.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSlider.kt
@@ -1,4 +1,3 @@
-
package com.bobbyesp.ui.components.preferences
import androidx.annotation.IntRange
@@ -24,25 +23,20 @@ fun SettingSlider(
onValueChangeFinished: () -> Unit,
valueToShow: String? = null,
@IntRange steps: Int = 0,
- valueRange: ClosedFloatingPointRange = 0f..1f
+ valueRange: ClosedFloatingPointRange = 0f..1f,
) {
- Column(
- modifier = modifier
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
+ Column(modifier = modifier) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "${valueToShow ?: value.toInt()}",
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
)
}
@@ -54,7 +48,7 @@ fun SettingSlider(
onValueChangeFinished = onValueChangeFinished,
steps = steps,
valueRange = valueRange,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth(),
)
}
}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSwitch.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSwitch.kt
index f629825..53c291a 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSwitch.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingSwitch.kt
@@ -27,49 +27,42 @@ fun SettingSwitch(
icon: ImageVector,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
Row(
- modifier = modifier
- .clip(ShapeDefaults.Large)
- .clickable(
+ modifier =
+ modifier.clip(ShapeDefaults.Large).clickable(
interactionSource = remember { MutableInteractionSource() },
- indication = null
+ indication = null,
) {
onCheckedChange(!isChecked)
},
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = icon,
tint = MaterialTheme.colorScheme.onSurface,
- contentDescription = null
+ contentDescription = null,
)
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 16.dp)
- ) {
+ Column(modifier = Modifier.weight(1f).padding(horizontal = 16.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = supportingText,
style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(
checked = isChecked,
onCheckedChange = onCheckedChange,
- colors = SwitchDefaults.colors(
- uncheckedBorderColor = Color.Transparent,
- )
+ colors = SwitchDefaults.colors(uncheckedBorderColor = Color.Transparent),
)
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingsItem.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingsItem.kt
index 5cbf0da..b87de0d 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingsItem.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/preferences/SettingsItem.kt
@@ -31,98 +31,88 @@ data class SettingsItem(
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun SettingsItem(
- item: SettingsItem,
- modifier: Modifier = Modifier
-) {
+fun SettingsItem(item: SettingsItem, modifier: Modifier = Modifier) {
Row(
- modifier = modifier
- .background(color = MaterialTheme.colorScheme.surfaceContainer)
- .combinedClickable(
- onClick = item.onClick
- )
- .padding(horizontal = 24.dp, vertical = 16.dp), //maybe delete this padding
+ modifier =
+ modifier
+ .background(color = MaterialTheme.colorScheme.surfaceContainer)
+ .combinedClickable(onClick = item.onClick)
+ .padding(horizontal = 24.dp, vertical = 16.dp), // maybe delete this padding
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
imageVector = item.icon,
tint = MaterialTheme.colorScheme.onSurface,
- contentDescription = null
+ contentDescription = null,
)
Column {
Text(
text = item.title,
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = item.supportingText,
style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
-
@Composable
-fun SettingsGroup(
- items: List,
- modifier: Modifier = Modifier
-) {
- Column(
- modifier = modifier,
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
+fun SettingsGroup(items: List, modifier: Modifier = Modifier) {
+ Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
items.fastForEachIndexed { index, item ->
SettingsItem(
item = item,
- modifier = Modifier
- .fillMaxWidth()
- .clip(
- when {
- items.size == 1 -> {
- MaterialTheme.shapes.extraLarge
- }
+ modifier =
+ Modifier.fillMaxWidth()
+ .clip(
+ when {
+ items.size == 1 -> {
+ MaterialTheme.shapes.extraLarge
+ }
- index == 0 -> {
- MaterialTheme.shapes.extraLarge.copy(
- bottomStart = MaterialTheme.shapes.medium.bottomStart,
- bottomEnd = MaterialTheme.shapes.medium.bottomEnd
- )
- }
+ index == 0 -> {
+ MaterialTheme.shapes.extraLarge.copy(
+ bottomStart = MaterialTheme.shapes.medium.bottomStart,
+ bottomEnd = MaterialTheme.shapes.medium.bottomEnd,
+ )
+ }
- index == items.lastIndex -> {
- MaterialTheme.shapes.extraLarge.copy(
- topStart = MaterialTheme.shapes.medium.topStart,
- topEnd = MaterialTheme.shapes.medium.topEnd
- )
- }
+ index == items.lastIndex -> {
+ MaterialTheme.shapes.extraLarge.copy(
+ topStart = MaterialTheme.shapes.medium.topStart,
+ topEnd = MaterialTheme.shapes.medium.topEnd,
+ )
+ }
- else -> {
- MaterialTheme.shapes.medium
+ else -> {
+ MaterialTheme.shapes.medium
+ }
}
- }
- )
+ ),
)
}
}
}
-//create previews
+// create previews
@Preview
@Composable
fun SettingsItemPreview() {
SettingsItem(
- item = SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- )
+ item =
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ )
)
}
@@ -130,49 +120,50 @@ fun SettingsItemPreview() {
@Composable
fun SettingsGroupPreview() {
SettingsGroup(
- items = listOf(
- SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- ),
- SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- ),
- SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- ),
- SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- ),
- SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- ),
- SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- ),
- SettingsItem(
- title = "Title",
- supportingText = "Supporting Text",
- icon = Icons.Rounded.Settings,
- onClick = {}
- ),
- )
+ items =
+ listOf(
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ ),
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ ),
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ ),
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ ),
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ ),
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ ),
+ SettingsItem(
+ title = "Title",
+ supportingText = "Supporting Text",
+ icon = Icons.Rounded.Settings,
+ onClick = {},
+ ),
+ )
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullState.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullState.kt
index ffc4166..63a454c 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullState.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullState.kt
@@ -22,33 +22,24 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
-fun rememberPullState(
- config: PullStateConfig = PullStateConfig()
-): PullState {
+fun rememberPullState(config: PullStateConfig = PullStateConfig()): PullState {
val density = LocalDensity.current
val scope = rememberCoroutineScope()
val insetTop = WindowInsets.statusBars.getTop(density)
return remember(insetTop, config, density, scope) {
- PullState(
- insetTop,
- config,
- density,
- scope
- )
+ PullState(insetTop, config, density, scope)
}
}
-data class PullStateConfig(
- val heightRefreshing: Dp = 60.dp,
- val heightMax: Dp = 80.dp,
-) {
+data class PullStateConfig(val heightRefreshing: Dp = 60.dp, val heightMax: Dp = 80.dp) {
init {
require(heightMax >= heightRefreshing)
}
}
-class PullState internal constructor(
+class PullState
+internal constructor(
val maxInsetTop: Int,
val config: PullStateConfig,
private val density: Density,
@@ -58,16 +49,20 @@ class PullState internal constructor(
private val heightMax = with(density) { config.heightMax.toPx() }
private val _offsetY = Animatable(0f)
- val offsetY: Float get() = _offsetY.value
+ val offsetY: Float
+ get() = _offsetY.value
// 1f -> Refresh triggered on release
- val progressRefreshTrigger: Float get() = (offsetY / heightRefreshing).coerceIn(0f, 1f)
+ val progressRefreshTrigger: Float
+ get() = (offsetY / heightRefreshing).coerceIn(0f, 1f)
// 1f -> Max drag amount reached
- val progressHeightMax: Float get() = (offsetY / heightMax).coerceIn(0f, 1f)
+ val progressHeightMax: Float
+ get() = (offsetY / heightMax).coerceIn(0f, 1f)
// Use this for your content's top padding. Only relevant when app is drawing behind status bar
- val insetTop: Dp get() = with(density) { (maxInsetTop - maxInsetTop * progressRefreshTrigger).toDp() }
+ val insetTop: Dp
+ get() = with(density) { (maxInsetTop - maxInsetTop * progressRefreshTrigger).toDp() }
// User drag in progress
var isDragging by mutableStateOf(false)
@@ -101,93 +96,94 @@ class PullState internal constructor(
}
}
- val scrollConnection = object : NestedScrollConnection {
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- when {
- !isEnabled -> return Offset.Zero
- available.y > 0 && source == NestedScrollSource.UserInput -> {
- // 1. User is dragging
- // 2. Scrollable container reached the top (OR max drag reached and neither scroll container nor P2R are interested. Poor available Offset...)
- // 3. There is still drag available that the scrollable container did not consume
- // -> Start drag. Because next frame offsetY will be > 0f, onPreScroll will take over from here
- isDragging = true
- scope.launch {
- _offsetY.snapTo((offsetY + available.y).coerceIn(0f, heightMax))
+ val scrollConnection =
+ object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ when {
+ !isEnabled -> return Offset.Zero
+ available.y > 0 && source == NestedScrollSource.UserInput -> {
+ // 1. User is dragging
+ // 2. Scrollable container reached the top (OR max drag reached and neither
+ // scroll
+ // container nor P2R are interested. Poor available Offset...)
+ // 3. There is still drag available that the scrollable container did not
+ // consume
+ // -> Start drag. Because next frame offsetY will be > 0f, onPreScroll will
+ // take over
+ // from here
+ isDragging = true
+ scope.launch {
+ _offsetY.snapTo((offsetY + available.y).coerceIn(0f, heightMax))
+ }
}
}
- }
- return Offset.Zero
- }
-
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- when {
- !isEnabled -> return Offset.Zero
- offsetY > 0 && source == NestedScrollSource.UserInput -> {
- // Consumes the drag as long as the indicator is visible
- isDragging = true
- val newOffset = offsetY + available.y
-
- // Surplus drag amount is not consumed
- val remaining = when {
- newOffset > heightMax -> newOffset - heightMax
- newOffset < 0f -> newOffset
- else -> 0f
- }
+ return Offset.Zero
+ }
- scope.launch {
- _offsetY.snapTo(newOffset.coerceIn(0f, heightMax))
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ when {
+ !isEnabled -> return Offset.Zero
+ offsetY > 0 && source == NestedScrollSource.UserInput -> {
+ // Consumes the drag as long as the indicator is visible
+ isDragging = true
+ val newOffset = offsetY + available.y
+
+ // Surplus drag amount is not consumed
+ val remaining =
+ when {
+ newOffset > heightMax -> newOffset - heightMax
+ newOffset < 0f -> newOffset
+ else -> 0f
+ }
+
+ scope.launch { _offsetY.snapTo(newOffset.coerceIn(0f, heightMax)) }
+
+ return Offset(0f, (available.y - remaining))
}
-
- return Offset(0f, (available.y - remaining))
}
+
+ return Offset.Zero
}
- return Offset.Zero
- }
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ if (!isEnabled) return Velocity.Zero
- override suspend fun onPreFling(available: Velocity): Velocity {
- if (!isEnabled) return Velocity.Zero
+ isDragging = false
- isDragging = false
+ when {
+ // When refreshing and a drag stops, either settle to 0f or heightRefreshing,
+ isRefreshing -> {
+ val target =
+ when {
+ heightRefreshing - offsetY < heightRefreshing / 2 ->
+ heightRefreshing
+ else -> 0f
+ }
- when {
- // When refreshing and a drag stops, either settle to 0f or heightRefreshing,
- isRefreshing -> {
- val target = when {
- heightRefreshing - offsetY < heightRefreshing / 2 -> heightRefreshing
- else -> 0f
- }
+ scope.launch { settle(target) }
- scope.launch {
- settle(target)
+ // Consume the velocity as long as the indicator is visible
+ return if (offsetY == 0f) Velocity.Zero else available
}
- // Consume the velocity as long as the indicator is visible
- return if (offsetY == 0f) Velocity.Zero else available
- }
-
- // Trigger refresh
- offsetY >= heightRefreshing -> {
- isRefreshing = true
- scope.launch {
- settle(heightRefreshing)
+ // Trigger refresh
+ offsetY >= heightRefreshing -> {
+ isRefreshing = true
+ scope.launch { settle(heightRefreshing) }
}
- }
- // Drag cancelled, go back to 0f
- else -> {
- scope.launch {
- settle(0f)
+ // Drag cancelled, go back to 0f
+ else -> {
+ scope.launch { settle(0f) }
}
}
- }
- return Velocity.Zero
+ return Velocity.Zero
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshIndicator.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshIndicator.kt
index d69b048..dc89ea2 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshIndicator.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshIndicator.kt
@@ -29,14 +29,13 @@ import androidx.compose.ui.unit.dp
import com.bobbyesp.ui.R
@Composable
-fun Indicator(
- pullState: PullState
-) {
+fun Indicator(pullState: PullState) {
val hapticFeedback = LocalHapticFeedback.current
val scale = remember { Animatable(1f) }
- // Pop the indicator once shortly when reaching refresh trigger offset. Also trigger some haptic feedback
+ // Pop the indicator once shortly when reaching refresh trigger offset. Also trigger some haptic
+ // feedback
LaunchedEffect(pullState.progressRefreshTrigger >= 1f) {
if (pullState.progressRefreshTrigger >= 1f && !pullState.isRefreshing) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -46,51 +45,48 @@ fun Indicator(
}
Box(
- modifier = Modifier
- .statusBarsPadding()
- .heightIn(
- 24.dp,
- pullState.config.heightMax * pullState.progressHeightMax - pullState.insetTop
- )
- .fillMaxWidth(),
- contentAlignment = Alignment.Center
+ modifier =
+ Modifier.statusBarsPadding()
+ .heightIn(
+ 24.dp,
+ pullState.config.heightMax * pullState.progressHeightMax - pullState.insetTop,
+ )
+ .fillMaxWidth(),
+ contentAlignment = Alignment.Center,
) {
Row(
- modifier = Modifier
- .scale(scale.value),
- verticalAlignment = Alignment.CenterVertically
+ modifier = Modifier.scale(scale.value),
+ verticalAlignment = Alignment.CenterVertically,
) {
if (pullState.isRefreshing) {
- CircularProgressIndicator(
- modifier = Modifier
- .size(16.dp),
- strokeWidth = 2.dp,
- )
+ CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
} else if (pullState.isReloadFinished) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(R.string.successfully_refreshed),
- tint = MaterialTheme.colorScheme.primary
+ tint = MaterialTheme.colorScheme.primary,
)
} else {
CircularProgressIndicator(
progress = { pullState.progressRefreshTrigger },
- modifier = Modifier
- .size(16.dp),
+ modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
modifier = Modifier,
- text = when {
- pullState.isRefreshing -> stringResource(id = R.string.refreshing)
- pullState.isReloadFinished -> stringResource(id = R.string.successfully_refreshed)
- pullState.progressRefreshTrigger >= 1f -> stringResource(id = R.string.release_to_refresh)
- else -> stringResource(id = R.string.pull_to_refresh)
- },
+ text =
+ when {
+ pullState.isRefreshing -> stringResource(id = R.string.refreshing)
+ pullState.isReloadFinished ->
+ stringResource(id = R.string.successfully_refreshed)
+ pullState.progressRefreshTrigger >= 1f ->
+ stringResource(id = R.string.release_to_refresh)
+ else -> stringResource(id = R.string.pull_to_refresh)
+ },
style = MaterialTheme.typography.labelLarge,
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshLayout.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshLayout.kt
index b4d42db..600fdc7 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshLayout.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/pulltorefresh/PullToRefreshLayout.kt
@@ -2,6 +2,7 @@ package com.bobbyesp.ui.components.pulltorefresh
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration
+import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -21,9 +22,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
-/**
- * Made by Konstantin Klassen (https://twitter.com/Snokbert)
- */
+/** Made by Konstantin Klassen (https://twitter.com/Snokbert) */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PullToRefreshLayout(
@@ -32,44 +31,51 @@ fun PullToRefreshLayout(
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
- LocalOverscrollConfiguration provides null // Disable overscroll otherwise it consumes the drag before we get the chance
+ LocalOverscrollFactory provides
+ null // Disable overscroll otherwise it consumes the drag before we get the chance
) {
Box(
- modifier = modifier
- .background(MaterialTheme.colorScheme.primaryContainer)
- .background(
- brush = Brush.verticalGradient(
- colors = listOf(
- MaterialTheme.colorScheme.background,
- Color.Transparent
- ),
- startY = -10f,
- endY = pullState.progressRefreshTrigger * 120
+ modifier =
+ modifier
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .background(
+ brush =
+ Brush.verticalGradient(
+ colors =
+ listOf(MaterialTheme.colorScheme.background, Color.Transparent),
+ startY = -10f,
+ endY = pullState.progressRefreshTrigger * 120,
+ )
)
- )
- .nestedScroll(pullState.scrollConnection),
+ .nestedScroll(pullState.scrollConnection)
) {
Indicator(pullState = pullState)
Column {
- // This invisible spacer height + current top inset is always equals max top inset to keep scroll speed constant
- Spacer(modifier = Modifier.height(LocalDensity.current.run { pullState.maxInsetTop.toDp() } - pullState.insetTop))
+ // This invisible spacer height + current top inset is always equals max top inset
+ // to
+ // keep scroll speed constant
+ Spacer(
+ modifier =
+ Modifier.height(
+ LocalDensity.current.run { pullState.maxInsetTop.toDp() } -
+ pullState.insetTop
+ )
+ )
Surface(
- modifier = Modifier
- .offset {
- IntOffset(0, pullState.offsetY.toInt())
- },
+ modifier = Modifier.offset { IntOffset(0, pullState.offsetY.toInt()) },
color = Color.Transparent,
- shape = RoundedCornerShape(
- topStart = 16.dp * pullState.progressRefreshTrigger,
- topEnd = 16.dp * pullState.progressRefreshTrigger,
- bottomStart = 0.dp,
- bottomEnd = 0.dp
- )
+ shape =
+ RoundedCornerShape(
+ topStart = 16.dp * pullState.progressRefreshTrigger,
+ topEnd = 16.dp * pullState.progressRefreshTrigger,
+ bottomStart = 0.dp,
+ bottomEnd = 0.dp,
+ ),
) {
content()
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/state/LoadingState.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/state/LoadingState.kt
index 2e0646e..a5ebb85 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/state/LoadingState.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/state/LoadingState.kt
@@ -17,21 +17,16 @@ import androidx.compose.ui.unit.dp
fun LoadingState(text: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(
- 8.dp,
- Alignment.CenterVertically
- ),
- horizontalAlignment = Alignment.CenterHorizontally
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
- modifier = Modifier.fillMaxWidth()
- )
- LinearProgressIndicator(
- modifier = Modifier.fillMaxWidth(0.8f)
+ modifier = Modifier.fillMaxWidth(),
)
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth(0.8f))
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/tags/RoundedTag.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/tags/RoundedTag.kt
index 7970a00..7e094bd 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/tags/RoundedTag.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/tags/RoundedTag.kt
@@ -18,12 +18,12 @@ import androidx.compose.ui.unit.dp
fun RoundedTag(
modifier: Modifier = Modifier,
text: String,
- shape: Shape = MaterialTheme.shapes.medium
+ shape: Shape = MaterialTheme.shapes.medium,
) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.secondaryContainer,
- shape = shape
+ shape = shape,
) {
Row(
modifier = Modifier,
@@ -36,8 +36,8 @@ fun RoundedTag(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/AnimatedCounter.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/AnimatedCounter.kt
index a6ed105..657abfe 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/AnimatedCounter.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/AnimatedCounter.kt
@@ -20,37 +20,31 @@ import androidx.compose.ui.text.TextStyle
fun AnimatedCounter(
count: Int,
modifier: Modifier = Modifier,
- style: TextStyle = MaterialTheme.typography.bodySmall
+ style: TextStyle = MaterialTheme.typography.bodySmall,
) {
- var oldCount by remember {
- mutableIntStateOf(count)
- }
- SideEffect {
- oldCount = count
- }
+ var oldCount by remember { mutableIntStateOf(count) }
+ SideEffect { oldCount = count }
Row(modifier = modifier) {
val countString = count.toString()
val oldCountString = oldCount.toString()
countString.indices.forEach { i ->
val oldChar = oldCountString.getOrNull(i)
val newChar = countString[i]
- val char = if (oldChar == newChar) {
- oldCountString[i]
- } else {
- countString[i]
- }
+ val char =
+ if (oldChar == newChar) {
+ oldCountString[i]
+ } else {
+ countString[i]
+ }
AnimatedContent(
targetState = char,
transitionSpec = {
slideInVertically { it } togetherWith slideOutVertically { -it }
- }, label = "Animated Counter"
+ },
+ label = "Animated Counter",
) { character ->
- Text(
- text = character.toString(),
- style = style,
- softWrap = false
- )
+ Text(text = character.toString(), style = style, softWrap = false)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/AutoResizableText.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/AutoResizableText.kt
index b757b3c..0a10cc7 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/AutoResizableText.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/AutoResizableText.kt
@@ -20,7 +20,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isUnspecified
-//Gotten from the Philip Slacker's YouTube channel
+// Gotten from the Philip Slacker's YouTube channel
@Composable
fun AutoResizableText(
modifier: Modifier = Modifier,
@@ -29,12 +29,8 @@ fun AutoResizableText(
color: Color = textStyle.color,
maxLines: Int = 1,
) {
- var resizedTextStyle by remember {
- mutableStateOf(textStyle)
- }
- var shouldDraw by remember {
- mutableStateOf(false)
- }
+ var resizedTextStyle by remember { mutableStateOf(textStyle) }
+ var shouldDraw by remember { mutableStateOf(false) }
val defaultFontSize = MaterialTheme.typography.bodySmall.fontSize
@@ -42,31 +38,29 @@ fun AutoResizableText(
text = text,
color = color,
maxLines = maxLines,
- modifier = modifier.drawWithContent {
- if (shouldDraw) {
- drawContent()
- }
- },
+ modifier =
+ modifier.drawWithContent {
+ if (shouldDraw) {
+ drawContent()
+ }
+ },
softWrap = false,
style = resizedTextStyle,
onTextLayout = { result ->
if (result.didOverflowWidth) {
if (textStyle.fontSize.isUnspecified) {
- resizedTextStyle = resizedTextStyle.copy(
- fontSize = defaultFontSize
- )
+ resizedTextStyle = resizedTextStyle.copy(fontSize = defaultFontSize)
}
- resizedTextStyle = resizedTextStyle.copy(
- fontSize = resizedTextStyle.fontSize * 0.95
- )
+ resizedTextStyle =
+ resizedTextStyle.copy(fontSize = resizedTextStyle.fontSize * 0.95)
} else {
shouldDraw = true
}
- }
+ },
)
}
-//auto resizable text but with all the text parameters
+// auto resizable text but with all the text parameters
@Composable
fun AutoResizableText(
modifier: Modifier = Modifier,
@@ -83,12 +77,8 @@ fun AutoResizableText(
style: TextStyle = LocalTextStyle.current.plus(TextStyle()),
color: Color = style.color,
) {
- var resizedTextStyle by remember {
- mutableStateOf(style)
- }
- var shouldDraw by remember {
- mutableStateOf(false)
- }
+ var resizedTextStyle by remember { mutableStateOf(style) }
+ var shouldDraw by remember { mutableStateOf(false) }
val defaultFontSize = MaterialTheme.typography.bodySmall.fontSize
@@ -104,27 +94,24 @@ fun AutoResizableText(
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
-
- modifier = modifier.drawWithContent {
- if (shouldDraw) {
- drawContent()
- }
- },
+ modifier =
+ modifier.drawWithContent {
+ if (shouldDraw) {
+ drawContent()
+ }
+ },
softWrap = false,
style = resizedTextStyle,
onTextLayout = { result ->
if (result.didOverflowWidth) {
if (style.fontSize.isUnspecified) {
- resizedTextStyle = resizedTextStyle.copy(
- fontSize = defaultFontSize
- )
+ resizedTextStyle = resizedTextStyle.copy(fontSize = defaultFontSize)
}
- resizedTextStyle = resizedTextStyle.copy(
- fontSize = resizedTextStyle.fontSize * 0.95
- )
+ resizedTextStyle =
+ resizedTextStyle.copy(fontSize = resizedTextStyle.fontSize * 0.95)
} else {
shouldDraw = true
}
- }
+ },
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/CategoryTitles.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/CategoryTitles.kt
index 70822d9..3fc28b4 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/CategoryTitles.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/CategoryTitles.kt
@@ -18,12 +18,10 @@ fun ExtraLargeCategoryTitle(
) {
Text(
text = text,
- modifier = modifier
- .fillMaxWidth()
- .padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
+ modifier = modifier.fillMaxWidth().padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
color = color,
style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold
+ fontWeight = FontWeight.Bold,
)
}
@@ -35,11 +33,9 @@ fun LargeCategoryTitle(
) {
Text(
text = text,
- modifier = modifier
- .fillMaxWidth()
- .padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
+ modifier = modifier.fillMaxWidth().padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
color = color,
- style = MaterialTheme.typography.labelLarge
+ style = MaterialTheme.typography.labelLarge,
)
}
@@ -51,11 +47,9 @@ fun MediumCategoryTitle(
) {
Text(
text = text,
- modifier = modifier
- .fillMaxWidth()
- .padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
+ modifier = modifier.fillMaxWidth().padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
color = color,
- style = MaterialTheme.typography.labelMedium
+ style = MaterialTheme.typography.labelMedium,
)
}
@@ -67,10 +61,8 @@ fun SmallCategoryTitle(
) {
Text(
text = text,
- modifier = modifier
- .fillMaxWidth()
- .padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
+ modifier = modifier.fillMaxWidth().padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
color = color,
- style = MaterialTheme.typography.labelSmall
+ style = MaterialTheme.typography.labelSmall,
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/DotWithText.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/DotWithText.kt
index d5f577c..6340507 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/DotWithText.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/DotWithText.kt
@@ -19,15 +19,9 @@ import androidx.compose.ui.unit.dp
fun DotWithText(text: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
- modifier = Modifier
- .size(8.dp)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.primary)
- )
- Text(
- text = text,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(start = 8.dp)
+ modifier =
+ Modifier.size(8.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primary)
)
+ Text(text = text, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/ExpandableText.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/ExpandableText.kt
index abfdebc..0604fc8 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/ExpandableText.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/ExpandableText.kt
@@ -42,9 +42,9 @@ fun ExpandableText(
var canBeExpanded by remember { mutableStateOf(false) }
Text(
- modifier = if (canBeExpanded) modifier
- .clickable { expanded = !expanded }
- .animateContentSize() else modifier,
+ modifier =
+ if (canBeExpanded) modifier.clickable { expanded = !expanded }.animateContentSize()
+ else modifier,
text = text,
color = color,
fontSize = fontSize,
@@ -63,6 +63,6 @@ fun ExpandableText(
if (!canBeExpanded) {
canBeExpanded = it.hasVisualOverflow
}
- }
+ },
)
}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/MarqueeText.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/MarqueeText.kt
index 5090199..9cfa266 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/MarqueeText.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/MarqueeText.kt
@@ -41,10 +41,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
-fun SubtextOverline(
- text: String,
- modifier: Modifier = Modifier
-) {
+fun SubtextOverline(text: String, modifier: Modifier = Modifier) {
Text(
text,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
@@ -53,7 +50,7 @@ fun SubtextOverline(
lineHeight = 18.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
- modifier = modifier
+ modifier = modifier,
)
}
@@ -104,7 +101,7 @@ fun MarqueeText(
sideGradient: MarqueeTextGradientOptions = MarqueeTextGradientOptions(),
customEasing: Easing? = null,
animationDuration: Float = 4000f,
- delayBetweenAnimations: Long = 500L
+ delayBetweenAnimations: Long = 500L,
) {
// State to track text layout information
var textLayoutInfo by remember { mutableStateOf(null) }
@@ -113,26 +110,27 @@ fun MarqueeText(
var offset by remember(text) { mutableIntStateOf(0) }
// Composable to create the text with consistent styling
- val createText = @Composable { localModifier: Modifier ->
- Text(
- text = text,
- textAlign = textAlign,
- modifier = localModifier,
- color = color,
- fontSize = fontSize,
- fontStyle = fontStyle,
- fontWeight = fontWeight,
- fontFamily = fontFamily,
- letterSpacing = letterSpacing,
- textDecoration = textDecoration,
- lineHeight = lineHeight,
- overflow = overflow,
- softWrap = softWrap,
- maxLines = maxLines,
- onTextLayout = onTextLayout,
- style = style,
- )
- }
+ val createText =
+ @Composable { localModifier: Modifier ->
+ Text(
+ text = text,
+ textAlign = textAlign,
+ modifier = localModifier,
+ color = color,
+ fontSize = fontSize,
+ fontStyle = fontStyle,
+ fontWeight = fontWeight,
+ fontFamily = fontFamily,
+ letterSpacing = letterSpacing,
+ textDecoration = textDecoration,
+ lineHeight = lineHeight,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ onTextLayout = onTextLayout,
+ style = style,
+ )
+ }
LaunchedEffect(textLayoutInfo) {
val layoutInfo = textLayoutInfo ?: return@LaunchedEffect
@@ -146,30 +144,31 @@ fun MarqueeText(
animate(
initialValue = 0f,
targetValue = -layoutInfo.textWidth.toFloat(),
- animationSpec = infiniteRepeatable(
- animation = tween(
- durationMillis = duration,
- delayMillis = 1000,
- easing = customEasing ?: LinearEasing
+ animationSpec =
+ infiniteRepeatable(
+ animation =
+ tween(
+ durationMillis = duration,
+ delayMillis = 1000,
+ easing = customEasing ?: LinearEasing,
+ ),
+ repeatMode = RepeatMode.Restart,
),
- repeatMode = RepeatMode.Restart
- )
) { value, _ ->
offset = value.toInt()
}
}
// SubcomposeLayout for flexible text rendering
- SubcomposeLayout(
- modifier = modifier.clipToBounds()
- ) { constraints ->
+ SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints ->
// Allow infinite width for text measurement
val infiniteWidthConstraints = constraints.copy(maxWidth = Int.MAX_VALUE)
// Measure main text
- val mainText = subcompose(MarqueeLayers.MainText) {
- createText(textModifier)
- }.first().measure(infiniteWidthConstraints)
+ val mainText =
+ subcompose(MarqueeLayers.MainText) { createText(textModifier) }
+ .first()
+ .measure(infiniteWidthConstraints)
// Initialize placeholders
var gradient: Placeable? = null
@@ -183,76 +182,79 @@ fun MarqueeText(
} else {
// Calculate spacing and layout info
val spacing = constraints.maxWidth * 2 / 3
- textLayoutInfo = TextLayoutInfo(
- textWidth = mainText.width + spacing,
- containerWidth = constraints.maxWidth
- )
+ textLayoutInfo =
+ TextLayoutInfo(
+ textWidth = mainText.width + spacing,
+ containerWidth = constraints.maxWidth,
+ )
// Prepare secondary text placement
val secondTextOffset = mainText.width + offset + spacing
val secondTextSpace = constraints.maxWidth - secondTextOffset
if (secondTextSpace > 0) {
- secondPlaceableWithOffset = subcompose(MarqueeLayers.SecondaryText) {
- createText(textModifier)
- }.first().measure(infiniteWidthConstraints) to secondTextOffset
+ secondPlaceableWithOffset =
+ subcompose(MarqueeLayers.SecondaryText) { createText(textModifier) }
+ .first()
+ .measure(infiniteWidthConstraints) to secondTextOffset
}
// Create gradient edges
- gradient = subcompose(MarqueeLayers.EdgesGradient) {
- Row {
- if (sideGradient.left) {
- GradientEdge(
- startColor = sideGradient.color,
- endColor = Color.Transparent
- )
+ gradient =
+ subcompose(MarqueeLayers.EdgesGradient) {
+ Row {
+ if (sideGradient.left) {
+ GradientEdge(
+ startColor = sideGradient.color,
+ endColor = Color.Transparent,
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ if (sideGradient.right) {
+ GradientEdge(
+ startColor = Color.Transparent,
+ endColor = sideGradient.color,
+ )
+ }
+ }
}
- Spacer(modifier = Modifier.weight(1f))
- if (sideGradient.right) {
- GradientEdge(
- startColor = Color.Transparent,
- endColor = sideGradient.color
- )
- }
- }
- }.first().measure(constraints.copy(maxHeight = mainText.height))
+ .first()
+ .measure(constraints.copy(maxHeight = mainText.height))
}
// Final layout placement
layout(
- width = if (mainText.width > constraints.maxWidth) constraints.maxWidth else mainText.width,
- height = mainText.height
+ width =
+ if (mainText.width > constraints.maxWidth) constraints.maxWidth else mainText.width,
+ height = mainText.height,
) {
mainText.place(offset, 0)
- secondPlaceableWithOffset?.let {
- it.first.place(it.second, 0)
- }
+ secondPlaceableWithOffset?.let { it.first.place(it.second, 0) }
gradient?.place(0, 0)
}
}
}
@Composable
-private fun GradientEdge(
- startColor: Color, endColor: Color,
-) {
+private fun GradientEdge(startColor: Color, endColor: Color) {
Box(
- modifier = Modifier
- .width(10.dp)
- .fillMaxHeight()
- .background(
- brush = Brush.horizontalGradient(
- listOf(startColor, endColor)
- )
- )
+ modifier =
+ Modifier.width(10.dp)
+ .fillMaxHeight()
+ .background(brush = Brush.horizontalGradient(listOf(startColor, endColor)))
)
}
data class MarqueeTextGradientOptions(
val color: Color = Color.Transparent,
val right: Boolean = true,
- val left: Boolean = true
+ val left: Boolean = true,
)
-private enum class MarqueeLayers { MainText, SecondaryText, EdgesGradient }
-private data class TextLayoutInfo(val textWidth: Int, val containerWidth: Int)
\ No newline at end of file
+private enum class MarqueeLayers {
+ MainText,
+ SecondaryText,
+ EdgesGradient,
+}
+
+private data class TextLayoutInfo(val textWidth: Int, val containerWidth: Int)
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/TextSizedComponents.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/TextSizedComponents.kt
index 87a7945..1b4bac8 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/TextSizedComponents.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/TextSizedComponents.kt
@@ -31,7 +31,7 @@ fun MediumText(
maxLines = maxLines,
lineHeight = lineHeight,
overflow = TextOverflow.Ellipsis,
- modifier = modifier
+ modifier = modifier,
)
}
@@ -51,6 +51,6 @@ fun Subtext(
lineHeight = 18.sp,
maxLines = maxLines,
overflow = TextOverflow.Ellipsis,
- modifier = modifier
+ modifier = modifier,
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/text/bottomSheet/BottomSheetTexts.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/text/bottomSheet/BottomSheetTexts.kt
index 506d9e3..f2d84ac 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/text/bottomSheet/BottomSheetTexts.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/text/bottomSheet/BottomSheetTexts.kt
@@ -11,30 +11,24 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
@Composable
-fun BottomSheetHeader(
- modifier: Modifier = Modifier,
- text: String
-) {
+fun BottomSheetHeader(modifier: Modifier = Modifier, text: String) {
Text(
text,
fontSize = 24.sp,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium,
- modifier = modifier.fillMaxWidth()
+ modifier = modifier.fillMaxWidth(),
)
}
@Composable
-fun BottomSheetSubtitle(
- modifier: Modifier = Modifier,
- text: AnnotatedString
-) {
+fun BottomSheetSubtitle(modifier: Modifier = Modifier, text: AnnotatedString) {
Text(
text,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
textAlign = TextAlign.Center,
- modifier = modifier.fillMaxWidth()
+ modifier = modifier.fillMaxWidth(),
)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/topbar/ColumnWithCollapsibleTopBar.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/topbar/ColumnWithCollapsibleTopBar.kt
index c17b2a6..d964a53 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/components/topbar/ColumnWithCollapsibleTopBar.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/components/topbar/ColumnWithCollapsibleTopBar.kt
@@ -47,8 +47,8 @@ import kotlinx.coroutines.launch
import my.nanihadesuka.compose.LazyColumnScrollbar
import my.nanihadesuka.compose.ScrollbarSettings
-//Most of this components are from "Lotus" app
-//https://github.com/dn0ne/lotus/blob/master/app/src/main/java/com/dn0ne/player/app/presentation/components/topbar/ColumnWithCollapsibleTopBar.kt
+// Most of this components are from "Lotus" app
+// https://github.com/dn0ne/lotus/blob/master/app/src/main/java/com/dn0ne/player/app/presentation/components/topbar/ColumnWithCollapsibleTopBar.kt
@Composable
fun ColumnWithCollapsibleTopBar(
@@ -63,7 +63,7 @@ fun ColumnWithCollapsibleTopBar(
contentHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
contentVerticalArrangement: Arrangement.Vertical = Arrangement.Top,
modifier: Modifier = Modifier,
- content: @Composable ColumnScope.() -> Unit
+ content: @Composable ColumnScope.() -> Unit,
) {
val density = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
@@ -71,15 +71,18 @@ fun ColumnWithCollapsibleTopBar(
val isInLandscapeOrientation by rememberUpdatedState(newValue = isDeviceInLandscape())
val minHeightPx = with(density) { minTopBarHeight.toPx() }
- val maxHeightPx = with(density) {
- if (isInLandscapeOrientation) maxTopBarHeightLandscape.toPx() else maxTopBarHeight.toPx()
- }
+ val maxHeightPx =
+ with(density) {
+ if (isInLandscapeOrientation) maxTopBarHeightLandscape.toPx()
+ else maxTopBarHeight.toPx()
+ }
val topBarHeight = remember {
Animatable(
- initialValue = if (collapsedByDefault || isInLandscapeOrientation) {
- minHeightPx
- } else maxHeightPx
+ initialValue =
+ if (collapsedByDefault || isInLandscapeOrientation) {
+ minHeightPx
+ } else maxHeightPx
)
}
@@ -91,25 +94,18 @@ fun ColumnWithCollapsibleTopBar(
// Using derivedStateOf to avoid unnecessary recompositions
val collapseFractionState by remember {
- derivedStateOf {
- (topBarHeight.value - minHeightPx) / (maxHeightPx - minHeightPx)
- }
+ derivedStateOf { (topBarHeight.value - minHeightPx) / (maxHeightPx - minHeightPx) }
}
- LaunchedEffect(collapseFractionState) {
- collapseFraction(collapseFractionState)
- }
+ LaunchedEffect(collapseFractionState) { collapseFraction(collapseFractionState) }
val topBarScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val previousHeight = topBarHeight.value
- val newHeight = (previousHeight + available.y)
- .coerceIn(minHeightPx, maxHeightPx)
+ val newHeight = (previousHeight + available.y).coerceIn(minHeightPx, maxHeightPx)
- coroutineScope.launch {
- topBarHeight.snapTo(newHeight)
- }
+ coroutineScope.launch { topBarHeight.snapTo(newHeight) }
return Offset(0f, newHeight - previousHeight)
}
@@ -118,8 +114,9 @@ fun ColumnWithCollapsibleTopBar(
val threshold = (maxHeightPx - minHeightPx)
coroutineScope.launch {
topBarHeight.animateTo(
- targetValue = if (topBarHeight.value < threshold) minHeightPx else maxHeightPx,
- animationSpec = spring(stiffness = Spring.StiffnessLow)
+ targetValue =
+ if (topBarHeight.value < threshold) minHeightPx else maxHeightPx,
+ animationSpec = spring(stiffness = Spring.StiffnessLow),
)
}
@@ -128,23 +125,17 @@ fun ColumnWithCollapsibleTopBar(
}
}
- Box(
- modifier = modifier
- .nestedScroll(topBarScrollConnection)
- ) {
+ Box(modifier = modifier.nestedScroll(topBarScrollConnection)) {
Column {
- Spacer(
- modifier = Modifier
- .height(with(density) { topBarHeight.value.toDp() })
- )
+ Spacer(modifier = Modifier.height(with(density) { topBarHeight.value.toDp() }))
Column(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(contentScrollState)
- .padding(contentPadding),
+ modifier =
+ Modifier.fillMaxSize()
+ .verticalScroll(contentScrollState)
+ .padding(contentPadding),
horizontalAlignment = contentHorizontalAlignment,
- verticalArrangement = contentVerticalArrangement
+ verticalArrangement = contentVerticalArrangement,
) {
content()
Spacer(modifier = Modifier.height(200.dp))
@@ -152,9 +143,7 @@ fun ColumnWithCollapsibleTopBar(
}
Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(with(density) { topBarHeight.value.toDp() }),
+ modifier = Modifier.fillMaxWidth().height(with(density) { topBarHeight.value.toDp() })
) {
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface
@@ -179,7 +168,7 @@ fun LazyColumnWithCollapsibleTopBar(
contentVerticalArrangement: Arrangement.Vertical = Arrangement.Top,
enableScrollbar: Boolean = true,
modifier: Modifier = Modifier,
- content: LazyListScope.() -> Unit
+ content: LazyListScope.() -> Unit,
) {
val density = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
@@ -193,15 +182,15 @@ fun LazyColumnWithCollapsibleTopBar(
} else maxTopBarHeight.toPx()
}
}
- val topBarHeight = rememberAnimatable(
- initialValue = if (collapsedByDefault || isInLandscapeOrientation) {
- minTopBarHeight
- } else maxTopBarHeight
- )
+ val topBarHeight =
+ rememberAnimatable(
+ initialValue =
+ if (collapsedByDefault || isInLandscapeOrientation) {
+ minTopBarHeight
+ } else maxTopBarHeight
+ )
- LaunchedEffect(isInLandscapeOrientation) {
- topBarHeight.snapTo(maxTopBarHeight)
- }
+ LaunchedEffect(isInLandscapeOrientation) { topBarHeight.snapTo(maxTopBarHeight) }
LaunchedEffect(topBarHeight.value) {
collapseFraction(
@@ -211,29 +200,19 @@ fun LazyColumnWithCollapsibleTopBar(
val topBarScrollConnection = remember {
return@remember object : NestedScrollConnection {
- override fun onPreScroll(
- available: Offset,
- source: NestedScrollSource
- ): Offset {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val previousHeight = topBarHeight.value
- val newHeight = if (listState.firstVisibleItemIndex >= 0 && available.y < 0) {
- (previousHeight + available.y).coerceIn(
- minTopBarHeight,
- maxTopBarHeight
- )
- } else if (
- listState.firstVisibleItemIndex == 0 &&
- listState.layoutInfo.visibleItemsInfo.firstOrNull()?.offset == 0
- ) {
- (previousHeight + available.y).coerceIn(
- minTopBarHeight,
- maxTopBarHeight
- )
- } else previousHeight
+ val newHeight =
+ if (listState.firstVisibleItemIndex >= 0 && available.y < 0) {
+ (previousHeight + available.y).coerceIn(minTopBarHeight, maxTopBarHeight)
+ } else if (
+ listState.firstVisibleItemIndex == 0 &&
+ listState.layoutInfo.visibleItemsInfo.firstOrNull()?.offset == 0
+ ) {
+ (previousHeight + available.y).coerceIn(minTopBarHeight, maxTopBarHeight)
+ } else previousHeight
- coroutineScope.launch {
- topBarHeight.snapTo(newHeight)
- }
+ coroutineScope.launch { topBarHeight.snapTo(newHeight) }
return Offset(0f, newHeight - previousHeight)
}
@@ -241,8 +220,10 @@ fun LazyColumnWithCollapsibleTopBar(
coroutineScope.launch {
val threshold = (maxTopBarHeight - minTopBarHeight)
topBarHeight.animateTo(
- targetValue = if (topBarHeight.value < threshold) minTopBarHeight else maxTopBarHeight,
- animationSpec = spring(stiffness = Spring.StiffnessLow)
+ targetValue =
+ if (topBarHeight.value < threshold) minTopBarHeight
+ else maxTopBarHeight,
+ animationSpec = spring(stiffness = Spring.StiffnessLow),
)
}
@@ -251,45 +232,34 @@ fun LazyColumnWithCollapsibleTopBar(
}
}
- Box(
- modifier = modifier
- .nestedScroll(topBarScrollConnection)
- ) {
+ Box(modifier = modifier.nestedScroll(topBarScrollConnection)) {
Column {
- Spacer(
- modifier = Modifier
- .height(with(density) { topBarHeight.value.toDp() })
- )
+ Spacer(modifier = Modifier.height(with(density) { topBarHeight.value.toDp() }))
LazyColumnScrollbar(
state = listState,
- settings = ScrollbarSettings(
- enabled = enableScrollbar,
- thumbUnselectedColor = MaterialTheme.colorScheme.surfaceContainer,
- thumbSelectedColor = MaterialTheme.colorScheme.primaryContainer,
- )
+ settings =
+ ScrollbarSettings(
+ enabled = enableScrollbar,
+ thumbUnselectedColor = MaterialTheme.colorScheme.surfaceContainer,
+ thumbSelectedColor = MaterialTheme.colorScheme.primaryContainer,
+ ),
) {
LazyColumn(
state = listState,
- modifier = Modifier
- .fillMaxSize()
- .padding(contentPadding),
+ modifier = Modifier.fillMaxSize().padding(contentPadding),
horizontalAlignment = contentHorizontalAlignment,
- verticalArrangement = contentVerticalArrangement
+ verticalArrangement = contentVerticalArrangement,
) {
content()
- item {
- Spacer(modifier = Modifier.height(200.dp))
- }
+ item { Spacer(modifier = Modifier.height(200.dp)) }
}
}
}
Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(with(density) { topBarHeight.value.toDp() }),
+ modifier = Modifier.fillMaxWidth().height(with(density) { topBarHeight.value.toDp() })
) {
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface
@@ -298,4 +268,4 @@ fun LazyColumnWithCollapsibleTopBar(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/ext/Color.kt b/app/ui/src/main/java/com/bobbyesp/ui/ext/Color.kt
index 93a09ed..33391ec 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/ext/Color.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/ext/Color.kt
@@ -4,4 +4,4 @@ import androidx.compose.ui.graphics.Color
fun Color.applyAlpha(enabled: Boolean, alpha: Float = 0.62f): Color {
return if (enabled) this else this.copy(alpha = alpha)
-}
\ No newline at end of file
+}
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/ext/CornerBasedShape.kt b/app/ui/src/main/java/com/bobbyesp/ui/ext/CornerBasedShape.kt
index 562e4d3..d1f5bbb 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/ext/CornerBasedShape.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/ext/CornerBasedShape.kt
@@ -5,4 +5,5 @@ import androidx.compose.foundation.shape.CornerSize
import androidx.compose.ui.unit.dp
fun CornerBasedShape.top() = copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp))
-fun CornerBasedShape.bottom() = copy(topStart = CornerSize(0.dp), topEnd = CornerSize(0.dp))
\ No newline at end of file
+
+fun CornerBasedShape.bottom() = copy(topStart = CornerSize(0.dp), topEnd = CornerSize(0.dp))
diff --git a/app/ui/src/main/java/com/bobbyesp/ui/motion/AnimatedComposables.kt b/app/ui/src/main/java/com/bobbyesp/ui/motion/AnimatedComposables.kt
index 6079082..0526a94 100644
--- a/app/ui/src/main/java/com/bobbyesp/ui/motion/AnimatedComposables.kt
+++ b/app/ui/src/main/java/com/bobbyesp/ui/motion/AnimatedComposables.kt
@@ -67,107 +67,113 @@ inline fun NavGraphBuilder.slideInVerticallyComposable(
inline fun NavGraphBuilder.animatedComposablePredictiveBack(
deepLinks: List = emptyList(),
noinline content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit,
-) = composable(
- deepLinks = deepLinks,
- enterTransition = {
- materialSharedAxisXIn(initialOffsetX = { (it * 0.15f).toInt() })
- },
- exitTransition = {
- materialSharedAxisXOut(targetOffsetX = { -(it * InitialOffset).toInt() })
- },
- popEnterTransition = {
- materialSharedAxisXIn(initialOffsetX = { -(it * InitialOffset).toInt() }) + scaleIn(
- animationSpec = tween(durationMillis = 350, easing = EmphasizedDecelerate),
- initialScale = 0.9f,
- )
- },
- popExitTransition = {
- materialSharedAxisXOut(targetOffsetX = { (it * InitialOffset).toInt() }) + scaleOut(
- targetScale = 0.9f,
- animationSpec = tween(durationMillis = 350, easing = EmphasizedAccelerate),
- )
- },
- content = content,
-)
+) =
+ composable(
+ deepLinks = deepLinks,
+ enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * 0.15f).toInt() }) },
+ exitTransition = {
+ materialSharedAxisXOut(targetOffsetX = { -(it * InitialOffset).toInt() })
+ },
+ popEnterTransition = {
+ materialSharedAxisXIn(initialOffsetX = { -(it * InitialOffset).toInt() }) +
+ scaleIn(
+ animationSpec = tween(durationMillis = 350, easing = EmphasizedDecelerate),
+ initialScale = 0.9f,
+ )
+ },
+ popExitTransition = {
+ materialSharedAxisXOut(targetOffsetX = { (it * InitialOffset).toInt() }) +
+ scaleOut(
+ targetScale = 0.9f,
+ animationSpec = tween(durationMillis = 350, easing = EmphasizedAccelerate),
+ )
+ },
+ content = content,
+ )
inline fun NavGraphBuilder.animatedComposableLegacy(
deepLinks: List = emptyList(),
noinline content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit,
-) = composable(
- deepLinks = deepLinks,
- enterTransition = {
- materialSharedAxisXIn(initialOffsetX = { (it * InitialOffset).toInt() })
- },
- exitTransition = {
- materialSharedAxisXOut(targetOffsetX = { -(it * InitialOffset).toInt() })
- },
- popEnterTransition = {
- materialSharedAxisXIn(initialOffsetX = { -(it * InitialOffset).toInt() })
- },
- popExitTransition = {
- materialSharedAxisXOut(targetOffsetX = { (it * InitialOffset).toInt() })
- },
- content = content,
-)
+) =
+ composable(
+ deepLinks = deepLinks,
+ enterTransition = {
+ materialSharedAxisXIn(initialOffsetX = { (it * InitialOffset).toInt() })
+ },
+ exitTransition = {
+ materialSharedAxisXOut(targetOffsetX = { -(it * InitialOffset).toInt() })
+ },
+ popEnterTransition = {
+ materialSharedAxisXIn(initialOffsetX = { -(it * InitialOffset).toInt() })
+ },
+ popExitTransition = {
+ materialSharedAxisXOut(targetOffsetX = { (it * InitialOffset).toInt() })
+ },
+ content = content,
+ )
inline fun NavGraphBuilder.animatedComposableVariant(
deepLinks: List