mirror of
				https://github.com/emptyynes/EmptyPlayer.git
				synced 2025-10-31 06:39:01 +03:00 
			
		
		
		
	temporal abyss
This commit is contained in:
		
							parent
							
								
									6fcb1a7d39
								
							
						
					
					
						commit
						e5fb995c84
					
				
					 16 changed files with 399 additions and 94 deletions
				
			
		|  | @ -67,6 +67,8 @@ dependencies { | ||||||
|     implementation(libs.androidx.media3.session) |     implementation(libs.androidx.media3.session) | ||||||
|     implementation(libs.androidx.room.runtime) |     implementation(libs.androidx.room.runtime) | ||||||
|     implementation(libs.com.google.devtools.ksp.gradle.plugin) |     implementation(libs.com.google.devtools.ksp.gradle.plugin) | ||||||
|  |     implementation(libs.kotlinx.coroutines.android) | ||||||
|  |     implementation(libs.androidx.room.ktx) | ||||||
| 
 | 
 | ||||||
|     ksp(libs.androidx.room.compiler) |     ksp(libs.androidx.room.compiler) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| package usr.empty.player |  | ||||||
| 
 |  | ||||||
| import androidx.test.platform.app.InstrumentationRegistry |  | ||||||
| import androidx.test.ext.junit.runners.AndroidJUnit4 |  | ||||||
| 
 |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith |  | ||||||
| 
 |  | ||||||
| import org.junit.Assert.* |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Instrumented test, which will execute on an Android device. |  | ||||||
|  * |  | ||||||
|  * See [testing documentation](http://d.android.com/tools/testing). |  | ||||||
|  */ |  | ||||||
| @RunWith(AndroidJUnit4::class) |  | ||||||
| class ExampleInstrumentedTest { |  | ||||||
|     @Test |  | ||||||
|     fun useAppContext() { |  | ||||||
|         // Context of the app under test. |  | ||||||
|         val appContext = InstrumentationRegistry.getInstrumentation().targetContext |  | ||||||
|         assertEquals("usr.empty.player", appContext.packageName) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -16,6 +16,7 @@ | ||||||
|         android:label="@string/app_name" |         android:label="@string/app_name" | ||||||
|         android:roundIcon="@mipmap/ic_launcher_round" |         android:roundIcon="@mipmap/ic_launcher_round" | ||||||
|         android:supportsRtl="true" |         android:supportsRtl="true" | ||||||
|  |         android:name=".EmptyApplication" | ||||||
|         android:theme="@style/Theme.Player"> |         android:theme="@style/Theme.Player"> | ||||||
|         <activity |         <activity | ||||||
|             android:name=".MainActivity" |             android:name=".MainActivity" | ||||||
|  | @ -23,7 +24,6 @@ | ||||||
|             android:theme="@style/Theme.Player"> |             android:theme="@style/Theme.Player"> | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.intent.action.MAIN" /> |                 <action android:name="android.intent.action.MAIN" /> | ||||||
| 
 |  | ||||||
|                 <category android:name="android.intent.category.LAUNCHER" /> |                 <category android:name="android.intent.category.LAUNCHER" /> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </activity> |         </activity> | ||||||
|  |  | ||||||
							
								
								
									
										53
									
								
								app/src/main/java/usr/empty/player/EmptyApplication.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/src/main/java/usr/empty/player/EmptyApplication.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | ||||||
|  | package usr.empty.player | ||||||
|  | 
 | ||||||
|  | import android.app.Application | ||||||
|  | import androidx.room.Room | ||||||
|  | import usr.empty.player.database.AppDatabase | ||||||
|  | import usr.empty.player.items.AlbumDescriptor | ||||||
|  | import usr.empty.player.items.ArtistDescriptor | ||||||
|  | import usr.empty.player.items.NotaDescriptor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EmptyApplication : Application() { | ||||||
|  |     companion object { | ||||||
|  |         private var _appInstance: EmptyApplication? = null | ||||||
|  | 
 | ||||||
|  |         val appInstance: EmptyApplication | ||||||
|  |             get() = _appInstance!! | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     lateinit var database: AppDatabase | ||||||
|  | 
 | ||||||
|  |     val allTracks = ArrayList<NotaDescriptor>() | ||||||
|  | 
 | ||||||
|  |     val allAlbums = ArrayList<AlbumDescriptor>() | ||||||
|  | 
 | ||||||
|  |     val allArtists = HashMap<String, ArtistDescriptor>() | ||||||
|  | 
 | ||||||
|  |     override fun onCreate() { | ||||||
|  |         super.onCreate() | ||||||
|  |         database = Room.databaseBuilder( | ||||||
|  |             applicationContext, AppDatabase::class.java, "local" | ||||||
|  |         ).allowMainThreadQueries().build() | ||||||
|  |         database.artistDao().getAllArtists().forEach { allArtists[it.name ?: "unknown"] = ArtistDescriptor.fromArtist(it) } | ||||||
|  |         database.albumDao().getAllAlbums().forEach { album -> | ||||||
|  |             AlbumDescriptor.fromAlbum(album, database).let { | ||||||
|  |                 allAlbums.add(it) | ||||||
|  |                 allArtists[it.artistName]?.albumList?.add(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         database.trackDao().getAll().forEach { track -> | ||||||
|  |             NotaDescriptor.fromTrack(track, database).let { | ||||||
|  |                 allTracks.add(it) | ||||||
|  |                 allArtists[it.artist]?.trackList?.add(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         _appInstance = this | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun addNewTrack(nota: NotaDescriptor) { | ||||||
|  |         database.addNewTrack(nota) | ||||||
|  |         allArtists[nota.artist]?.trackList?.add(nota) | ||||||
|  |         allTracks.add(nota) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -12,36 +12,52 @@ import androidx.activity.compose.rememberLauncherForActivityResult | ||||||
| import androidx.activity.compose.setContent | import androidx.activity.compose.setContent | ||||||
| import androidx.activity.enableEdgeToEdge | import androidx.activity.enableEdgeToEdge | ||||||
| import androidx.activity.result.contract.ActivityResultContracts | import androidx.activity.result.contract.ActivityResultContracts | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.border | ||||||
| import androidx.compose.foundation.clickable | import androidx.compose.foundation.clickable | ||||||
| import androidx.compose.foundation.layout.Arrangement | import androidx.compose.foundation.layout.Arrangement | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
| import androidx.compose.foundation.layout.Column | import androidx.compose.foundation.layout.Column | ||||||
| import androidx.compose.foundation.layout.Row | import androidx.compose.foundation.layout.Row | ||||||
| import androidx.compose.foundation.layout.fillMaxSize | import androidx.compose.foundation.layout.fillMaxSize | ||||||
| import androidx.compose.foundation.layout.fillMaxWidth | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
|  | import androidx.compose.foundation.layout.height | ||||||
| import androidx.compose.foundation.layout.padding | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.layout.width | ||||||
| import androidx.compose.foundation.lazy.LazyColumn | import androidx.compose.foundation.lazy.LazyColumn | ||||||
| import androidx.compose.foundation.lazy.itemsIndexed | import androidx.compose.foundation.lazy.itemsIndexed | ||||||
|  | import androidx.compose.foundation.shape.RoundedCornerShape | ||||||
| import androidx.compose.material3.Button | import androidx.compose.material3.Button | ||||||
| import androidx.compose.material3.ButtonDefaults | import androidx.compose.material3.ButtonDefaults | ||||||
|  | import androidx.compose.material3.ExperimentalMaterial3Api | ||||||
| import androidx.compose.material3.MaterialTheme | import androidx.compose.material3.MaterialTheme | ||||||
| import androidx.compose.material3.Scaffold | import androidx.compose.material3.Scaffold | ||||||
|  | import androidx.compose.material3.Slider | ||||||
| import androidx.compose.material3.Text | import androidx.compose.material3.Text | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.runtime.mutableStateListOf | import androidx.compose.runtime.LaunchedEffect | ||||||
|  | import androidx.compose.runtime.MutableState | ||||||
|  | import androidx.compose.runtime.mutableFloatStateOf | ||||||
|  | import androidx.compose.runtime.mutableIntStateOf | ||||||
|  | import androidx.compose.runtime.mutableStateOf | ||||||
| import androidx.compose.runtime.remember | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.runtime.toMutableStateList | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.draw.clip | ||||||
|  | import androidx.compose.ui.graphics.Brush | ||||||
| import androidx.compose.ui.graphics.RectangleShape | import androidx.compose.ui.graphics.RectangleShape | ||||||
| import androidx.compose.ui.platform.LocalContext | import androidx.compose.ui.platform.LocalContext | ||||||
| import androidx.compose.ui.tooling.preview.Preview |  | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.compose.ui.unit.sp | import androidx.compose.ui.unit.sp | ||||||
| import androidx.media3.common.util.UnstableApi | import androidx.media3.common.util.UnstableApi | ||||||
| import androidx.room.Room | import kotlinx.coroutines.delay | ||||||
| import kotlinx.serialization.encodeToString | import kotlinx.serialization.encodeToString | ||||||
| import kotlinx.serialization.json.Json | import kotlinx.serialization.json.Json | ||||||
| import usr.empty.player.database.AppDatabase | import usr.empty.player.items.NotaDescriptor | ||||||
| import usr.empty.player.database.Track |  | ||||||
| import usr.empty.player.ui.theme.PlayerTheme | import usr.empty.player.ui.theme.PlayerTheme | ||||||
|  | import kotlin.math.max | ||||||
|  | import kotlin.math.min | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| inline fun <T> nullifyException(block: () -> T) = try { | inline fun <T> nullifyException(block: () -> T) = try { | ||||||
|  | @ -55,12 +71,6 @@ class MainActivity : ComponentActivity() { | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
| 
 | 
 | ||||||
|         //        Log.d("meow", "${filesDir.toPath().resolve("all_tracks.data")}") |  | ||||||
|         val db = Room.databaseBuilder( |  | ||||||
|             applicationContext, AppDatabase::class.java, "local" |  | ||||||
|         ).allowMainThreadQueries().build() |  | ||||||
|         Log.d("meow", "${db.trackDao().getAll()}") |  | ||||||
| 
 |  | ||||||
|         if (!Environment.isExternalStorageManager()) { |         if (!Environment.isExternalStorageManager()) { | ||||||
|             val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) |             val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) | ||||||
|             val uri = Uri.fromParts("package", packageName, null) |             val uri = Uri.fromParts("package", packageName, null) | ||||||
|  | @ -93,7 +103,12 @@ fun uriToPath(uri: Uri): String { | ||||||
| @Composable | @Composable | ||||||
| fun MainLayout(modifier: Modifier = Modifier) { | fun MainLayout(modifier: Modifier = Modifier) { | ||||||
|     val context = LocalContext.current |     val context = LocalContext.current | ||||||
|     val notas = remember { mutableStateListOf<NotaDescriptor>() } |     val queueId = remember { mutableIntStateOf(-1) } | ||||||
|  |     val notas = remember { EmptyApplication.appInstance.allTracks.toMutableStateList() } | ||||||
|  |     val isPlaying = | ||||||
|  |         remember { mutableStateOf(if (PlayerService.isServiceRunning) PlayerService.serviceInstance.player.isPlaying else false) } | ||||||
|  |     val currentNota = | ||||||
|  |         remember { mutableStateOf(if (PlayerService.isServiceRunning) PlayerService.serviceInstance.currentTrack?.descriptor else null) } | ||||||
| 
 | 
 | ||||||
|     val pickAudioLauncher = rememberLauncherForActivityResult( |     val pickAudioLauncher = rememberLauncherForActivityResult( | ||||||
|         ActivityResultContracts.GetContent() |         ActivityResultContracts.GetContent() | ||||||
|  | @ -101,26 +116,21 @@ fun MainLayout(modifier: Modifier = Modifier) { | ||||||
|         audioUri?.run { |         audioUri?.run { | ||||||
|             uriToPath(this).let { |             uriToPath(this).let { | ||||||
|                 MediaMetadataRetriever().apply { |                 MediaMetadataRetriever().apply { | ||||||
|                     setDataSource(it) //                    notas.add(NotaDescriptor(name = nullifyException { |                     setDataSource(it) | ||||||
|                     //                        extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) |                     val nd = NotaDescriptor(name = nullifyException { | ||||||
|                     //                    } ?: it.split('/').last().split('.').first(), artist = nullifyException { |  | ||||||
|                     //                        extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) |  | ||||||
|                     //                    } ?: "Unknown Artist", sourceType = NotaDescriptor.Source.LOCAL, source = it)) |  | ||||||
|                     val db = Room.databaseBuilder( |  | ||||||
|                         context.applicationContext, AppDatabase::class.java, "local" |  | ||||||
|                     ).allowMainThreadQueries().build() |  | ||||||
|                     db.trackDao().insertTrack(Track(title = nullifyException { |  | ||||||
|                         extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) |                         extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) | ||||||
|                     } ?: it.split('/').last().split('.').first(), |                     } ?: it.split('/').last().split('.').first(), artist = nullifyException { | ||||||
|                                                     artistId = 0, |                         extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) | ||||||
|                                                     albumId = null, |                     } ?: "Unknown Artist", sourceType = NotaDescriptor.Source.LOCAL, source = it) | ||||||
|                                                     sourceType = NotaDescriptor.Source.LOCAL, |                     notas.add(nd) | ||||||
|                                                     source = it)) |                     queueId.intValue-- | ||||||
|  |                     EmptyApplication.appInstance.addNewTrack(nd) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     Column(modifier.fillMaxSize()) { |     Column(modifier.fillMaxSize()) { | ||||||
|         Row( |         Row( | ||||||
|             horizontalArrangement = Arrangement.Absolute.SpaceEvenly, |             horizontalArrangement = Arrangement.Absolute.SpaceEvenly, | ||||||
|  | @ -133,7 +143,6 @@ fun MainLayout(modifier: Modifier = Modifier) { | ||||||
|             }) { |             }) { | ||||||
|                 Text("add track") |                 Text("add track") | ||||||
|             } |             } | ||||||
| //            VerticalDivider() |  | ||||||
|             Button(shape = RectangleShape, colors = ButtonDefaults.buttonColors( |             Button(shape = RectangleShape, colors = ButtonDefaults.buttonColors( | ||||||
|                 containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary |                 containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary | ||||||
|             ), modifier = Modifier, onClick = { |             ), modifier = Modifier, onClick = { | ||||||
|  | @ -155,19 +164,32 @@ fun MainLayout(modifier: Modifier = Modifier) { | ||||||
|                 Text("| play >") |                 Text("| play >") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         NotaList(notas) |         NotaList(notas, modifier = Modifier.weight(1f), queueId = queueId.intValue, playState = isPlaying, currentNota) | ||||||
|  |         if (isPlaying.value) Player( | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .padding(bottom = 20.dp) | ||||||
|  |                 .border(1.dp, color = MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(25)) | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @UnstableApi | @UnstableApi | ||||||
| @Composable | @Composable | ||||||
| fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId: Int = notas.hashCode()) { | fun NotaList( | ||||||
|  |     notas: List<NotaDescriptor>, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     queueId: Int = notas.hashCode(), | ||||||
|  |     playState: MutableState<Boolean>, | ||||||
|  |     currentNota: MutableState<NotaDescriptor?> | ||||||
|  | ) { | ||||||
|     val context = LocalContext.current |     val context = LocalContext.current | ||||||
|     LazyColumn( |     LazyColumn( | ||||||
|         modifier = modifier, |         modifier = modifier | ||||||
|     ) { |     ) { | ||||||
|         itemsIndexed(notas) { index, nota -> |         itemsIndexed(notas) { index, nota -> | ||||||
|             Column(modifier = Modifier |             val localModifier = | ||||||
|  |                 if (currentNota.value == nota) Modifier.background(MaterialTheme.colorScheme.secondaryContainer) else Modifier | ||||||
|  |             Column(modifier = localModifier | ||||||
|                 .fillParentMaxWidth() |                 .fillParentMaxWidth() | ||||||
|                 .clickable { |                 .clickable { | ||||||
|                     Log.d("meow", "queueId: $queueId") |                     Log.d("meow", "queueId: $queueId") | ||||||
|  | @ -177,6 +199,7 @@ fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId | ||||||
|                         putExtra("queueId", queueId) |                         putExtra("queueId", queueId) | ||||||
|                         putExtra("queue", Json.encodeToString(notas)) |                         putExtra("queue", Json.encodeToString(notas)) | ||||||
|                         putExtra("start", index) |                         putExtra("start", index) | ||||||
|  |                         playState.value = true | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|                 .padding(4.dp) |                 .padding(4.dp) | ||||||
|  | @ -192,14 +215,86 @@ fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | fun minMax(vMin: Float, x: Float, vMax: Float) = max(min(vMax, x), vMin) | ||||||
|  | 
 | ||||||
| @UnstableApi | @UnstableApi | ||||||
| @Preview | @OptIn(ExperimentalMaterial3Api::class) | ||||||
| @Composable | @Composable | ||||||
| fun Preview() { | fun Player(modifier: Modifier = Modifier) { | ||||||
|     NotaList( |     val sliderPosition = remember { mutableFloatStateOf(0.0f) } | ||||||
|  |     val sliderMovingAllow = remember { mutableStateOf(true) } | ||||||
|  |     val service = PlayerService.serviceInstance | ||||||
|  |     LaunchedEffect(Unit) { | ||||||
|  |         while (true) { | ||||||
|  |             if (service.player.isPlaying and sliderMovingAllow.value) { | ||||||
|  |                 sliderPosition.floatValue = minMax(0.0f, service.player.currentPosition.toFloat() / service.player.duration.toFloat(), 1.0f) | ||||||
|  |                 delay(service.player.duration / 2000) | ||||||
|  |                 Log.d("meow", "${service.player.duration / 2000}") | ||||||
|  |             } else delay(10) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     Row( | ||||||
|  |         modifier = Modifier | ||||||
|  |             .fillMaxWidth() | ||||||
|  |             .padding(1.dp) | ||||||
|  |             .then(modifier) | ||||||
|  |             .padding(horizontal = 16.dp, vertical = 8.dp) | ||||||
|  |     ) { //        Image( | ||||||
|  |         //            imageVector = Icons.Filled.PlayArrow, | ||||||
|  |         //            contentDescription = "play", | ||||||
|  |         //            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), | ||||||
|  |         //            modifier = Modifier | ||||||
|  |         //                .size(50.dp) | ||||||
|  |         //                .align(Alignment.CenterVertically) | ||||||
|  |         //        ) | ||||||
|  |         Column(modifier = Modifier.align(Alignment.CenterVertically)) { | ||||||
|  |             val nota = service.currentTrack?.descriptor ?: NotaDescriptor( | ||||||
|  |                 "unknown track", "unknown artist", NotaDescriptor.Source.LOCAL, "" | ||||||
|  |             ) | ||||||
|  |             Text(nota.name, style = MaterialTheme.typography.titleLarge) | ||||||
|  |             Text(nota.artist, style = MaterialTheme.typography.titleSmall) | ||||||
|  |             Slider(value = sliderPosition.floatValue, thumb = { | ||||||
|  |                 Box( | ||||||
|  |                     Modifier | ||||||
|  |                         .clip(RoundedCornerShape(2.dp)) | ||||||
|  |                         .width(3.dp) | ||||||
|  |                         .height(18.dp) | ||||||
|  |                         .background(MaterialTheme.colorScheme.tertiary) | ||||||
|  |                 ) | ||||||
|  |             }, track = { | ||||||
|  |                 Row(Modifier.clip(RoundedCornerShape(1.dp))) { | ||||||
|  |                     if (sliderPosition.floatValue != 0.0f) Box( | ||||||
|  |                         Modifier | ||||||
|  |                             .weight(sliderPosition.floatValue) | ||||||
|  |                             .height(2.dp) | ||||||
|  |                             .background( | ||||||
|  |                                 Brush.linearGradient( | ||||||
|                                     listOf( |                                     listOf( | ||||||
|             NotaDescriptor(name = "hello", artist = "there", sourceType = NotaDescriptor.Source.LOCAL, source = ""), |                                         MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.tertiary | ||||||
|             NotaDescriptor(name = "preview", artist = "me", sourceType = NotaDescriptor.Source.LOCAL, source = ""), |  | ||||||
|                                     ) |                                     ) | ||||||
|                                 ) |                                 ) | ||||||
|  |                             ) | ||||||
|  |                     ) | ||||||
|  |                     if (sliderPosition.floatValue != 1.0f) Box( | ||||||
|  |                         Modifier | ||||||
|  |                             .weight(1 - sliderPosition.floatValue) | ||||||
|  |                             .height(2.dp) | ||||||
|  |                             .background( | ||||||
|  |                                 Brush.linearGradient( | ||||||
|  |                                     listOf( | ||||||
|  |                                         MaterialTheme.colorScheme.tertiaryContainer, MaterialTheme.colorScheme.secondaryContainer | ||||||
|  |                                     ) | ||||||
|  |                                 ) | ||||||
|  |                             ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             }, onValueChange = { | ||||||
|  |                 sliderPosition.floatValue = it | ||||||
|  |                 sliderMovingAllow.value = false | ||||||
|  |             }, onValueChangeFinished = { | ||||||
|  |                 service.player.seekTo((service.player.duration.toFloat() * sliderPosition.floatValue).toLong()) | ||||||
|  |                 sliderMovingAllow.value = true | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -2,6 +2,7 @@ package usr.empty.player | ||||||
| 
 | 
 | ||||||
| import androidx.media3.common.MediaItem | import androidx.media3.common.MediaItem | ||||||
| import androidx.media3.exoplayer.ExoPlayer | import androidx.media3.exoplayer.ExoPlayer | ||||||
|  | import usr.empty.player.items.NotaDescriptor | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Nota(val descriptor: NotaDescriptor) { | class Nota(val descriptor: NotaDescriptor) { | ||||||
|  |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| package usr.empty.player |  | ||||||
| 
 |  | ||||||
| import kotlinx.serialization.Serializable |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @Serializable |  | ||||||
| data class NotaDescriptor( |  | ||||||
|     val name: String, |  | ||||||
|     val artist: String, |  | ||||||
|     val sourceType: Source, |  | ||||||
|     val source: String, |  | ||||||
| ) { |  | ||||||
|     enum class Source { |  | ||||||
|         LOCAL |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -18,21 +18,44 @@ import androidx.media3.session.MediaStyleNotificationHelper | ||||||
| import androidx.media3.ui.PlayerNotificationManager | import androidx.media3.ui.PlayerNotificationManager | ||||||
| import com.google.common.util.concurrent.Futures | import com.google.common.util.concurrent.Futures | ||||||
| import com.google.common.util.concurrent.ListenableFuture | import com.google.common.util.concurrent.ListenableFuture | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.ExperimentalCoroutinesApi | ||||||
|  | import kotlinx.coroutines.SupervisorJob | ||||||
|  | import kotlinx.coroutines.channels.Channel | ||||||
|  | import kotlinx.coroutines.delay | ||||||
|  | import kotlinx.coroutines.launch | ||||||
| import kotlinx.serialization.json.Json | import kotlinx.serialization.json.Json | ||||||
|  | import usr.empty.player.items.NotaDescriptor | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @UnstableApi | @UnstableApi | ||||||
| class PlayerService : MediaSessionService() { | class PlayerService : MediaSessionService() { | ||||||
|  |     companion object { | ||||||
|  |         private var _serviceInstance: PlayerService? = null | ||||||
|  | 
 | ||||||
|  |         val serviceInstance: PlayerService | ||||||
|  |             get() = _serviceInstance!! | ||||||
|  | 
 | ||||||
|  |         var isServiceRunning = false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private lateinit var mediaSession: MediaSession |     private lateinit var mediaSession: MediaSession | ||||||
|     lateinit var player: ExoPlayer |     lateinit var player: ExoPlayer | ||||||
|     private lateinit var playerNotificationManager: PlayerNotificationManager |     private lateinit var playerNotificationManager: PlayerNotificationManager | ||||||
|     private val notificationId = 1004 |     private val notificationId = 1004 | ||||||
|     private var virtualQueue = NotaQueue() |     private var virtualQueue = NotaQueue() | ||||||
|     private var queueId = -1 |     val currentTrack | ||||||
|  |         get() = virtualQueue.current | ||||||
|  |     private var queueId = 0 | ||||||
|  |     var control = Channel<Boolean>() | ||||||
|  |     var data = Channel<Pair<Boolean, Float>>() | ||||||
| 
 | 
 | ||||||
|  |     @OptIn(ExperimentalCoroutinesApi::class) | ||||||
|     @SuppressLint("ForegroundServiceType") |     @SuppressLint("ForegroundServiceType") | ||||||
|     override fun onCreate() { |     override fun onCreate() { | ||||||
|         super.onCreate() |         super.onCreate() | ||||||
|  |         _serviceInstance = this | ||||||
|  |         isServiceRunning = true | ||||||
|         player = ExoPlayer.Builder(this).build() |         player = ExoPlayer.Builder(this).build() | ||||||
|         player.addListener(object : Player.Listener { |         player.addListener(object : Player.Listener { | ||||||
|             override fun onIsPlayingChanged(isPlaying: Boolean) { |             override fun onIsPlayingChanged(isPlaying: Boolean) { | ||||||
|  | @ -49,13 +72,13 @@ class PlayerService : MediaSessionService() { | ||||||
| 
 | 
 | ||||||
|                 override fun createCurrentContentIntent(player: Player) = null |                 override fun createCurrentContentIntent(player: Player) = null | ||||||
| 
 | 
 | ||||||
|                 override fun getCurrentContentText(player: Player) = "context?" |                 override fun getCurrentContentText(player: Player) = "content?" | ||||||
| 
 | 
 | ||||||
|                 override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback) = null |                 override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback) = null | ||||||
|             }) |             }) | ||||||
|             .build() |             .build() | ||||||
|             .apply { |             .apply { | ||||||
|                 setPriority(NotificationCompat.PRIORITY_DEFAULT) |                 setPriority(NotificationCompat.PRIORITY_MAX) | ||||||
|                 setVisibility(NotificationCompat.VISIBILITY_PUBLIC) |                 setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||||
|                 setPlayer(player) |                 setPlayer(player) | ||||||
|                 setMediaSessionToken(mediaSession.platformToken) |                 setMediaSessionToken(mediaSession.platformToken) | ||||||
|  | @ -66,6 +89,16 @@ class PlayerService : MediaSessionService() { | ||||||
| 
 | 
 | ||||||
|         val nBuilder = NotificationCompat.Builder(this, "emptyynes").setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)) |         val nBuilder = NotificationCompat.Builder(this, "emptyynes").setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)) | ||||||
|         startForeground(notificationId, nBuilder.build(), FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) |         startForeground(notificationId, nBuilder.build(), FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) | ||||||
|  | 
 | ||||||
|  |         CoroutineScope(SupervisorJob()).launch { //            data. | ||||||
|  |             while (true) { | ||||||
|  |                 while (control.isEmpty) { | ||||||
|  |                     data.send(player.isPlaying to (player.currentPosition.toFloat() / player.duration.toFloat())) | ||||||
|  |                     delay(if (player.isPlaying) player.duration / 2000 else 33) | ||||||
|  |                 } | ||||||
|  |                 if (!control.receive()) break | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { |     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								app/src/main/java/usr/empty/player/database/Album.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/src/main/java/usr/empty/player/database/Album.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | package usr.empty.player.database | ||||||
|  | 
 | ||||||
|  | import androidx.room.ColumnInfo | ||||||
|  | import androidx.room.Dao | ||||||
|  | import androidx.room.Entity | ||||||
|  | import androidx.room.Insert | ||||||
|  | import androidx.room.OnConflictStrategy.Companion.IGNORE | ||||||
|  | import androidx.room.PrimaryKey | ||||||
|  | import androidx.room.Query | ||||||
|  | import java.util.UUID | ||||||
|  | 
 | ||||||
|  | @Entity | ||||||
|  | data class Album( | ||||||
|  |     @ColumnInfo val name: String, @ColumnInfo val artistId: UUID | ||||||
|  | ) { | ||||||
|  |     @PrimaryKey | ||||||
|  |     var uuid: UUID = UUID.nameUUIDFromBytes("$name$artistId".toByteArray()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Dao | ||||||
|  | interface AlbumDao { | ||||||
|  |     @Insert(onConflict = IGNORE) | ||||||
|  |     fun insertAlbum(album: Album) | ||||||
|  | 
 | ||||||
|  |     @Query("SELECT * FROM album") | ||||||
|  |     fun getAllAlbums(): List<Album> | ||||||
|  | 
 | ||||||
|  |     @Query("SELECT * FROM album WHERE :uuid = uuid") | ||||||
|  |     fun getAlbumByUUID(uuid: UUID): Album? | ||||||
|  | } | ||||||
|  | @ -2,10 +2,20 @@ package usr.empty.player.database | ||||||
| 
 | 
 | ||||||
| import androidx.room.Database | import androidx.room.Database | ||||||
| import androidx.room.RoomDatabase | import androidx.room.RoomDatabase | ||||||
|  | import usr.empty.player.items.NotaDescriptor | ||||||
| 
 | 
 | ||||||
| @Database( | @Database( | ||||||
|     entities = [Track::class], version = 1 |     entities = [Track::class, Artist::class, Album::class], version = 1 | ||||||
| ) | ) | ||||||
| abstract class AppDatabase : RoomDatabase() { | abstract class AppDatabase : RoomDatabase() { | ||||||
|     abstract fun trackDao(): TrackDao |     abstract fun trackDao(): TrackDao | ||||||
|  |     abstract fun artistDao(): ArtistDao | ||||||
|  |     abstract fun albumDao(): AlbumDao | ||||||
|  | 
 | ||||||
|  |     fun addNewTrack(nota: NotaDescriptor) { | ||||||
|  |         Artist(Artist.ArtistType.UNKNOWN, nota.artist).let { artist -> | ||||||
|  |             artistDao().insertArtist(artist) | ||||||
|  |             trackDao().insertTrack(Track(nota.name, artist.uuid, null, nota.sourceType, nota.source)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
							
								
								
									
										35
									
								
								app/src/main/java/usr/empty/player/database/Artist.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/src/main/java/usr/empty/player/database/Artist.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | package usr.empty.player.database | ||||||
|  | 
 | ||||||
|  | import androidx.room.ColumnInfo | ||||||
|  | import androidx.room.Dao | ||||||
|  | import androidx.room.Entity | ||||||
|  | import androidx.room.Insert | ||||||
|  | import androidx.room.OnConflictStrategy.Companion.IGNORE | ||||||
|  | import androidx.room.PrimaryKey | ||||||
|  | import androidx.room.Query | ||||||
|  | import java.util.UUID | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @Entity | ||||||
|  | data class Artist( | ||||||
|  |     @ColumnInfo val type: ArtistType, @ColumnInfo val name: String? | ||||||
|  | ) { | ||||||
|  |     enum class ArtistType { | ||||||
|  |         UNKNOWN, SINGLE, DUET, FEAT, BAND, CIRCLE | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @PrimaryKey | ||||||
|  |     var uuid: UUID = if (name == null) UUID.randomUUID() else UUID.nameUUIDFromBytes(name.toByteArray()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Dao | ||||||
|  | interface ArtistDao { | ||||||
|  |     @Insert(onConflict = IGNORE) | ||||||
|  |     fun insertArtist(artist: Artist) | ||||||
|  | 
 | ||||||
|  |     @Query("SELECT * FROM artist") | ||||||
|  |     fun getAllArtists(): List<Artist> | ||||||
|  | 
 | ||||||
|  |     @Query("SELECT * FROM artist WHERE :uuid = uuid") | ||||||
|  |     fun getArtistByUUID(uuid: UUID): Artist? | ||||||
|  | } | ||||||
|  | @ -4,38 +4,39 @@ import androidx.room.ColumnInfo | ||||||
| import androidx.room.Dao | import androidx.room.Dao | ||||||
| import androidx.room.Entity | import androidx.room.Entity | ||||||
| import androidx.room.Insert | import androidx.room.Insert | ||||||
|  | import androidx.room.OnConflictStrategy.Companion.REPLACE | ||||||
| import androidx.room.PrimaryKey | import androidx.room.PrimaryKey | ||||||
| import androidx.room.Query | import androidx.room.Query | ||||||
| import usr.empty.player.NotaDescriptor | import usr.empty.player.items.NotaDescriptor | ||||||
| import java.util.UUID | import java.util.UUID | ||||||
| 
 | 
 | ||||||
| // a.k.a. Nota | // a.k.a. Nota | ||||||
| @Entity | @Entity | ||||||
| data class Track( | data class Track( | ||||||
|     @ColumnInfo val title: String, |     @ColumnInfo val title: String, | ||||||
|     @ColumnInfo val artistId: Int, |     @ColumnInfo val artistId: UUID, | ||||||
|     @ColumnInfo val albumId: Int?, |     @ColumnInfo val albumId: UUID?, | ||||||
|     @ColumnInfo val sourceType: NotaDescriptor.Source, |     @ColumnInfo val sourceType: NotaDescriptor.Source, | ||||||
|     @ColumnInfo val source: String, |     @ColumnInfo val source: String, | ||||||
| ) { | ) { | ||||||
|     @PrimaryKey(autoGenerate = true) |     @PrimaryKey | ||||||
|     var uid: Int = 0 |  | ||||||
| 
 |  | ||||||
|     @ColumnInfo |  | ||||||
|     var uuid: UUID = UUID.nameUUIDFromBytes("${title}${artistId}".toByteArray()) |     var uuid: UUID = UUID.nameUUIDFromBytes("${title}${artistId}".toByteArray()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @Dao | @Dao | ||||||
| interface TrackDao { | interface TrackDao { | ||||||
|     @Insert |     @Insert(onConflict = REPLACE) | ||||||
|     fun insertTrack(track: Track) |     fun insertTrack(track: Track) | ||||||
| 
 | 
 | ||||||
|     @Query("SELECT * FROM track") |     @Query("SELECT * FROM track") | ||||||
|     fun getAll(): List<Track> |     fun getAll(): List<Track> | ||||||
| 
 | 
 | ||||||
|     @Query("SELECT * FROM track WHERE :artistId = artistId") |     @Query("SELECT * FROM track WHERE :artistId = artistId") | ||||||
|     fun getByArtistId(artistId: Int): List<Track> |     fun getByArtistId(artistId: UUID): List<Track> | ||||||
| 
 | 
 | ||||||
|     @Query("SELECT * FROM track WHERE :albumId = albumId") |     @Query("SELECT * FROM track WHERE :albumId = albumId") | ||||||
|     fun getByAlbumId(albumId: Int): List<Track> |     fun getByAlbumId(albumId: UUID): List<Track> | ||||||
|  | 
 | ||||||
|  |     @Query("DELETE FROM track WHERE uuid = :uuid") | ||||||
|  |     fun deleteByUUID(uuid: UUID) | ||||||
| } | } | ||||||
							
								
								
									
										19
									
								
								app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | package usr.empty.player.items | ||||||
|  | 
 | ||||||
|  | import usr.empty.player.database.Album | ||||||
|  | import usr.empty.player.database.AppDatabase | ||||||
|  | 
 | ||||||
|  | class AlbumDescriptor( | ||||||
|  |     val name: String, val artistName: String?, @Suppress("unused") val trackList: List<NotaDescriptor> | ||||||
|  | ) { | ||||||
|  |     companion object { | ||||||
|  |         fun fromAlbum(album: Album, database: AppDatabase): AlbumDescriptor { | ||||||
|  |             return AlbumDescriptor( | ||||||
|  |                 album.name, | ||||||
|  |                 database.artistDao().getArtistByUUID(album.artistId)?.name, | ||||||
|  |                 database.trackDao().getByAlbumId(album.uuid).map { | ||||||
|  |                     NotaDescriptor.fromTrack(it, database) | ||||||
|  |                 }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | package usr.empty.player.items | ||||||
|  | 
 | ||||||
|  | import usr.empty.player.database.Artist | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ArtistDescriptor( | ||||||
|  |     val name: String | ||||||
|  | ) { | ||||||
|  |     val albumList = ArrayList<AlbumDescriptor>() | ||||||
|  |     val trackList = ArrayList<NotaDescriptor>() | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         fun fromArtist(artist: Artist): ArtistDescriptor { | ||||||
|  |             return ArtistDescriptor(artist.name ?: "unknown") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								app/src/main/java/usr/empty/player/items/NotaDescriptor.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/src/main/java/usr/empty/player/items/NotaDescriptor.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | package usr.empty.player.items | ||||||
|  | 
 | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import usr.empty.player.database.AppDatabase | ||||||
|  | import usr.empty.player.database.Track | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @Serializable | ||||||
|  | data class NotaDescriptor( | ||||||
|  |     val name: String, | ||||||
|  |     val artist: String, | ||||||
|  |     val sourceType: Source, | ||||||
|  |     val source: String, | ||||||
|  | ) { | ||||||
|  |     companion object { | ||||||
|  |         fun fromTrack(track: Track, database: AppDatabase): NotaDescriptor { | ||||||
|  |             return NotaDescriptor( | ||||||
|  |                 track.title, database.artistDao().getArtistByUUID(track.artistId)?.name ?: "unknown", track.sourceType, track.source | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     enum class Source { | ||||||
|  |         LOCAL | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun equals(other: Any?): Boolean { | ||||||
|  |         if (other == null) return false | ||||||
|  |         if (other !is NotaDescriptor) return false | ||||||
|  |         if (this.hashCode() != other.hashCode()) return false | ||||||
|  |         if (this.sourceType != other.sourceType) return false | ||||||
|  |         if (this.source != other.source) return false | ||||||
|  |         if (this.artist != other.artist) return false | ||||||
|  |         if (this.name != other.name) return false | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun hashCode(): Int { | ||||||
|  |         var result = name.hashCode() | ||||||
|  |         result = 31 * result + artist.hashCode() | ||||||
|  |         result = 31 * result + sourceType.hashCode() | ||||||
|  |         result = 31 * result + source.hashCode() | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -2,12 +2,14 @@ | ||||||
| agp = "8.6.1" | agp = "8.6.1" | ||||||
| kotlin = "2.0.0" | kotlin = "2.0.0" | ||||||
| coreKtx = "1.13.1" | coreKtx = "1.13.1" | ||||||
|  | kotlinxCoroutinesAndroid = "1.7.3" | ||||||
| kotlinxSerializationJson = "1.7.3" | kotlinxSerializationJson = "1.7.3" | ||||||
| lifecycleRuntimeKtx = "2.8.6" | lifecycleRuntimeKtx = "2.8.6" | ||||||
| activityCompose = "1.9.2" | activityCompose = "1.9.2" | ||||||
| composeBom = "2024.09.03" | composeBom = "2024.09.03" | ||||||
| media3Exoplayer = "1.4.1" | media3Exoplayer = "1.4.1" | ||||||
| roomRuntime = "2.6.1" | roomRuntime = "2.6.1" | ||||||
|  | composeAudiowaveformVersion = "1.1.2" | ||||||
| 
 | 
 | ||||||
| [libraries] | [libraries] | ||||||
| androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } | ||||||
|  | @ -19,6 +21,7 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc | ||||||
| androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } | ||||||
| androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } | ||||||
| androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } | ||||||
|  | androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } | ||||||
| androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } | androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } | ||||||
| androidx-ui = { group = "androidx.compose.ui", name = "ui" } | androidx-ui = { group = "androidx.compose.ui", name = "ui" } | ||||||
| androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } | ||||||
|  | @ -26,6 +29,7 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } | ||||||
| androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } | ||||||
| androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.3" } | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.3" } | ||||||
| androidx-material3 = { group = "androidx.compose.material3", name = "material3" } | androidx-material3 = { group = "androidx.compose.material3", name = "material3" } | ||||||
|  | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } | ||||||
| kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } | ||||||
| com-google-devtools-ksp-gradle-plugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version = "2.0.20-1.0.25" } | com-google-devtools-ksp-gradle-plugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version = "2.0.20-1.0.25" } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue