From e5fb995c847c77d12acfb3ee83db98cfa9294147 Mon Sep 17 00:00:00 2001 From: Isaac Iwasaki Date: Sat, 12 Oct 2024 19:17:32 +0700 Subject: [PATCH] temporal abyss --- app/build.gradle.kts | 2 + .../empty/player/ExampleInstrumentedTest.kt | 24 --- app/src/main/AndroidManifest.xml | 2 +- .../java/usr/empty/player/EmptyApplication.kt | 53 ++++++ .../java/usr/empty/player/MainActivity.kt | 173 ++++++++++++++---- app/src/main/java/usr/empty/player/Nota.kt | 1 + .../java/usr/empty/player/NotaDescriptor.kt | 16 -- .../java/usr/empty/player/PlayerService.kt | 39 +++- .../java/usr/empty/player/database/Album.kt | 30 +++ .../usr/empty/player/database/AppDatabase.kt | 12 +- .../java/usr/empty/player/database/Artist.kt | 35 ++++ .../java/usr/empty/player/database/Track.kt | 21 ++- .../usr/empty/player/items/AlbumDescriptor.kt | 19 ++ .../empty/player/items/ArtistDescriptor.kt | 17 ++ .../usr/empty/player/items/NotaDescriptor.kt | 45 +++++ gradle/libs.versions.toml | 4 + 16 files changed, 399 insertions(+), 94 deletions(-) delete mode 100644 app/src/androidTest/java/usr/empty/player/ExampleInstrumentedTest.kt create mode 100644 app/src/main/java/usr/empty/player/EmptyApplication.kt delete mode 100644 app/src/main/java/usr/empty/player/NotaDescriptor.kt create mode 100644 app/src/main/java/usr/empty/player/database/Album.kt create mode 100644 app/src/main/java/usr/empty/player/database/Artist.kt create mode 100644 app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt create mode 100644 app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt create mode 100644 app/src/main/java/usr/empty/player/items/NotaDescriptor.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 134fd83..57f2fd5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/androidTest/java/usr/empty/player/ExampleInstrumentedTest.kt b/app/src/androidTest/java/usr/empty/player/ExampleInstrumentedTest.kt deleted file mode 100644 index ad64afd..0000000 --- a/app/src/androidTest/java/usr/empty/player/ExampleInstrumentedTest.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0cf4f82..eed58ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> - diff --git a/app/src/main/java/usr/empty/player/EmptyApplication.kt b/app/src/main/java/usr/empty/player/EmptyApplication.kt new file mode 100644 index 0000000..1a7e4b2 --- /dev/null +++ b/app/src/main/java/usr/empty/player/EmptyApplication.kt @@ -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() + + val allAlbums = ArrayList() + + val allArtists = HashMap() + + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/MainActivity.kt b/app/src/main/java/usr/empty/player/MainActivity.kt index a43e8c3..198558a 100644 --- a/app/src/main/java/usr/empty/player/MainActivity.kt +++ b/app/src/main/java/usr/empty/player/MainActivity.kt @@ -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 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() } + 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, modifier: Modifier = Modifier, queueId: Int = notas.hashCode()) { +fun NotaList( + notas: List, + modifier: Modifier = Modifier, + queueId: Int = notas.hashCode(), + playState: MutableState, + currentNota: MutableState +) { 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, 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, 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( - listOf( - NotaDescriptor(name = "hello", artist = "there", sourceType = NotaDescriptor.Source.LOCAL, source = ""), - NotaDescriptor(name = "preview", artist = "me", sourceType = NotaDescriptor.Source.LOCAL, source = ""), - ) - ) +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( + 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 + }) + } + } } \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/Nota.kt b/app/src/main/java/usr/empty/player/Nota.kt index bc6e84e..0c8e1ba 100644 --- a/app/src/main/java/usr/empty/player/Nota.kt +++ b/app/src/main/java/usr/empty/player/Nota.kt @@ -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) { diff --git a/app/src/main/java/usr/empty/player/NotaDescriptor.kt b/app/src/main/java/usr/empty/player/NotaDescriptor.kt deleted file mode 100644 index 7f1ad11..0000000 --- a/app/src/main/java/usr/empty/player/NotaDescriptor.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/PlayerService.kt b/app/src/main/java/usr/empty/player/PlayerService.kt index 0acddd4..8d2790e 100644 --- a/app/src/main/java/usr/empty/player/PlayerService.kt +++ b/app/src/main/java/usr/empty/player/PlayerService.kt @@ -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() + var data = Channel>() + @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 { diff --git a/app/src/main/java/usr/empty/player/database/Album.kt b/app/src/main/java/usr/empty/player/database/Album.kt new file mode 100644 index 0000000..51c97dd --- /dev/null +++ b/app/src/main/java/usr/empty/player/database/Album.kt @@ -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 + + @Query("SELECT * FROM album WHERE :uuid = uuid") + fun getAlbumByUUID(uuid: UUID): Album? +} \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/database/AppDatabase.kt b/app/src/main/java/usr/empty/player/database/AppDatabase.kt index 7abf5c6..cf97a82 100644 --- a/app/src/main/java/usr/empty/player/database/AppDatabase.kt +++ b/app/src/main/java/usr/empty/player/database/AppDatabase.kt @@ -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)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/database/Artist.kt b/app/src/main/java/usr/empty/player/database/Artist.kt new file mode 100644 index 0000000..dc531cd --- /dev/null +++ b/app/src/main/java/usr/empty/player/database/Artist.kt @@ -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 + + @Query("SELECT * FROM artist WHERE :uuid = uuid") + fun getArtistByUUID(uuid: UUID): Artist? +} \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/database/Track.kt b/app/src/main/java/usr/empty/player/database/Track.kt index 1d064be..e6d1e90 100644 --- a/app/src/main/java/usr/empty/player/database/Track.kt +++ b/app/src/main/java/usr/empty/player/database/Track.kt @@ -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 @Query("SELECT * FROM track WHERE :artistId = artistId") - fun getByArtistId(artistId: Int): List + fun getByArtistId(artistId: UUID): List @Query("SELECT * FROM track WHERE :albumId = albumId") - fun getByAlbumId(albumId: Int): List + fun getByAlbumId(albumId: UUID): List + + @Query("DELETE FROM track WHERE uuid = :uuid") + fun deleteByUUID(uuid: UUID) } \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt b/app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt new file mode 100644 index 0000000..936dc70 --- /dev/null +++ b/app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt @@ -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 +) { + 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) + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt b/app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt new file mode 100644 index 0000000..ad10641 --- /dev/null +++ b/app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt @@ -0,0 +1,17 @@ +package usr.empty.player.items + +import usr.empty.player.database.Artist + + +class ArtistDescriptor( + val name: String +) { + val albumList = ArrayList() + val trackList = ArrayList() + + companion object { + fun fromArtist(artist: Artist): ArtistDescriptor { + return ArtistDescriptor(artist.name ?: "unknown") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/usr/empty/player/items/NotaDescriptor.kt b/app/src/main/java/usr/empty/player/items/NotaDescriptor.kt new file mode 100644 index 0000000..899cb60 --- /dev/null +++ b/app/src/main/java/usr/empty/player/items/NotaDescriptor.kt @@ -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 + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e094648..4fe36c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }