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.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>

@ -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 {

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

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

@ -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" 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…
Cancel
Save