temporal abyss

master
Isaac Iwasaki 2 months ago
parent 6fcb1a7d39
commit e5fb995c84

@ -67,6 +67,8 @@ dependencies {
implementation(libs.androidx.media3.session)
implementation(libs.androidx.room.runtime)
implementation(libs.com.google.devtools.ksp.gradle.plugin)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.room.ktx)
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:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".EmptyApplication"
android:theme="@style/Theme.Player">
<activity
android:name=".MainActivity"
@ -23,7 +24,6 @@
android:theme="@style/Theme.Player">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

@ -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.enableEdgeToEdge
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.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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
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.toMutableStateList
import androidx.compose.ui.Alignment
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.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.util.UnstableApi
import androidx.room.Room
import kotlinx.coroutines.delay
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import usr.empty.player.database.AppDatabase
import usr.empty.player.database.Track
import usr.empty.player.items.NotaDescriptor
import usr.empty.player.ui.theme.PlayerTheme
import kotlin.math.max
import kotlin.math.min
inline fun <T> nullifyException(block: () -> T) = try {
@ -55,12 +71,6 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
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()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", packageName, null)
@ -93,7 +103,12 @@ fun uriToPath(uri: Uri): String {
@Composable
fun MainLayout(modifier: Modifier = Modifier) {
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(
ActivityResultContracts.GetContent()
@ -101,26 +116,21 @@ fun MainLayout(modifier: Modifier = Modifier) {
audioUri?.run {
uriToPath(this).let {
MediaMetadataRetriever().apply {
setDataSource(it) // notas.add(NotaDescriptor(name = nullifyException {
// extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
// } ?: 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 {
setDataSource(it)
val nd = NotaDescriptor(name = nullifyException {
extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
} ?: it.split('/').last().split('.').first(),
artistId = 0,
albumId = null,
sourceType = NotaDescriptor.Source.LOCAL,
source = it))
} ?: it.split('/').last().split('.').first(), artist = nullifyException {
extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
} ?: "Unknown Artist", sourceType = NotaDescriptor.Source.LOCAL, source = it)
notas.add(nd)
queueId.intValue--
EmptyApplication.appInstance.addNewTrack(nd)
}
}
}
}
Column(modifier.fillMaxSize()) {
Row(
horizontalArrangement = Arrangement.Absolute.SpaceEvenly,
@ -133,7 +143,6 @@ fun MainLayout(modifier: Modifier = Modifier) {
}) {
Text("add track")
}
// VerticalDivider()
Button(shape = RectangleShape, colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary
), modifier = Modifier, onClick = {
@ -155,19 +164,32 @@ fun MainLayout(modifier: Modifier = Modifier) {
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
@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
LazyColumn(
modifier = modifier,
modifier = modifier
) {
itemsIndexed(notas) { index, nota ->
Column(modifier = Modifier
val localModifier =
if (currentNota.value == nota) Modifier.background(MaterialTheme.colorScheme.secondaryContainer) else Modifier
Column(modifier = localModifier
.fillParentMaxWidth()
.clickable {
Log.d("meow", "queueId: $queueId")
@ -177,6 +199,7 @@ fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId
putExtra("queueId", queueId)
putExtra("queue", Json.encodeToString(notas))
putExtra("start", index)
playState.value = true
})
}
.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
@Preview
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Preview() {
NotaList(
fun Player(modifier: Modifier = Modifier) {
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(
NotaDescriptor(name = "hello", artist = "there", sourceType = NotaDescriptor.Source.LOCAL, source = ""),
NotaDescriptor(name = "preview", artist = "me", sourceType = NotaDescriptor.Source.LOCAL, source = ""),
MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.tertiary
)
)
)
)
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.exoplayer.ExoPlayer
import usr.empty.player.items.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 com.google.common.util.concurrent.Futures
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 usr.empty.player.items.NotaDescriptor
@UnstableApi
class PlayerService : MediaSessionService() {
companion object {
private var _serviceInstance: PlayerService? = null
val serviceInstance: PlayerService
get() = _serviceInstance!!
var isServiceRunning = false
}
private lateinit var mediaSession: MediaSession
lateinit var player: ExoPlayer
private lateinit var playerNotificationManager: PlayerNotificationManager
private val notificationId = 1004
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")
override fun onCreate() {
super.onCreate()
_serviceInstance = this
isServiceRunning = true
player = ExoPlayer.Builder(this).build()
player.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
@ -49,13 +72,13 @@ class PlayerService : MediaSessionService() {
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
})
.build()
.apply {
setPriority(NotificationCompat.PRIORITY_DEFAULT)
setPriority(NotificationCompat.PRIORITY_MAX)
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setPlayer(player)
setMediaSessionToken(mediaSession.platformToken)
@ -66,6 +89,16 @@ class PlayerService : MediaSessionService() {
val nBuilder = NotificationCompat.Builder(this, "emptyynes").setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession))
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 {

@ -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.RoomDatabase
import usr.empty.player.items.NotaDescriptor
@Database(
entities = [Track::class], version = 1
entities = [Track::class, Artist::class, Album::class], version = 1
)
abstract class AppDatabase : RoomDatabase() {
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))
}
}
}

@ -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.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.REPLACE
import androidx.room.PrimaryKey
import androidx.room.Query
import usr.empty.player.NotaDescriptor
import usr.empty.player.items.NotaDescriptor
import java.util.UUID
// a.k.a. Nota
@Entity
data class Track(
@ColumnInfo val title: String,
@ColumnInfo val artistId: Int,
@ColumnInfo val albumId: Int?,
@ColumnInfo val artistId: UUID,
@ColumnInfo val albumId: UUID?,
@ColumnInfo val sourceType: NotaDescriptor.Source,
@ColumnInfo val source: String,
) {
@PrimaryKey(autoGenerate = true)
var uid: Int = 0
@ColumnInfo
@PrimaryKey
var uuid: UUID = UUID.nameUUIDFromBytes("${title}${artistId}".toByteArray())
}
@Dao
interface TrackDao {
@Insert
@Insert(onConflict = REPLACE)
fun insertTrack(track: Track)
@Query("SELECT * FROM track")
fun getAll(): List<Track>
@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")
fun getByAlbumId(albumId: Int): List<Track>
fun getByAlbumId(albumId: UUID): List<Track>
@Query("DELETE FROM track WHERE uuid = :uuid")
fun deleteByUUID(uuid: UUID)
}

@ -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)
})
}
}
}

@ -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")
}
}
}

@ -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"
kotlin = "2.0.0"
coreKtx = "1.13.1"
kotlinxCoroutinesAndroid = "1.7.3"
kotlinxSerializationJson = "1.7.3"
lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2"
composeBom = "2024.09.03"
media3Exoplayer = "1.4.1"
roomRuntime = "2.6.1"
composeAudiowaveformVersion = "1.1.2"
[libraries]
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-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
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-ui = { group = "androidx.compose.ui", name = "ui" }
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-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.3" }
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" }
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…
Cancel
Save