mirror of
https://github.com/emptyynes/EmptyPlayer.git
synced 2025-01-22 00:12:27 +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.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>
|
||||
|
|
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.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(
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
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.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))
|
||||
}
|
||||
}
|
||||
}
|
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.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)
|
||||
}
|
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"
|
||||
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…
Reference in a new issue