music: automatically number new playlists
Automatically number new playlists, from Playlist 1, Playlist 2, etc. This comes with the additional requirement that all playlists have unique names.
This commit is contained in:
parent
e01ea25d0b
commit
763061c352
14 changed files with 188 additions and 145 deletions
|
@ -135,7 +135,7 @@ class MainFragment :
|
|||
collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
|
||||
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
||||
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
||||
collect(musicModel.pendingPlaylistNaming.flow, ::handlePlaylistNaming)
|
||||
collect(musicModel.pendingNewPlaylist.flow, ::handlePlaylistNaming)
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
||||
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
||||
|
@ -306,8 +306,8 @@ class MainFragment :
|
|||
|
||||
private fun handlePlaylistNaming(args: PendingName.Args?) {
|
||||
if (args != null) {
|
||||
findNavController().navigateSafe(MainFragmentDirections.actionNamePlaylist(args))
|
||||
musicModel.pendingPlaylistNaming.consume()
|
||||
findNavController().navigateSafe(MainFragmentDirections.actionNewPlaylist(args))
|
||||
musicModel.pendingNewPlaylist.consume()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,6 @@ class PlaylistDetailFragment :
|
|||
}
|
||||
|
||||
override fun onPlay() {
|
||||
// TODO: Handle
|
||||
playbackModel.play(unlikelyToBeNull(detailModel.currentPlaylist.value))
|
||||
}
|
||||
|
||||
|
|
|
@ -321,7 +321,7 @@ class HomeFragment :
|
|||
}
|
||||
} else {
|
||||
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
|
||||
musicModel.createPlaylist("New playlist")
|
||||
musicModel.createPlaylist(requireContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.room.TypeConverter
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import kotlin.math.max
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
|
@ -118,13 +119,47 @@ sealed interface Music : Item {
|
|||
|
||||
companion object {
|
||||
/**
|
||||
* Creates an Auxio-style [UID] with a [UUID] generated by internal Auxio code.
|
||||
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
||||
* unlikely-to-change metadata of the music.
|
||||
*
|
||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
||||
* @param uuid The generated [UUID] for this item.
|
||||
* @param updates Block to update the [MessageDigest] hash with the metadata of the
|
||||
* item. Make sure the metadata hashed semantically aligns with the format
|
||||
* specification.
|
||||
* @return A new auxio-style [UID].
|
||||
*/
|
||||
fun auxio(mode: MusicMode, uuid: UUID) = UID(Format.AUXIO, mode, uuid)
|
||||
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
|
||||
val digest =
|
||||
MessageDigest.getInstance("SHA-256").run {
|
||||
updates()
|
||||
digest()
|
||||
}
|
||||
// Convert the digest to a UUID. This does cleave off some of the hash, but this
|
||||
// is considered okay.
|
||||
val uuid =
|
||||
UUID(
|
||||
digest[0]
|
||||
.toLong()
|
||||
.shl(56)
|
||||
.or(digest[1].toLong().and(0xFF).shl(48))
|
||||
.or(digest[2].toLong().and(0xFF).shl(40))
|
||||
.or(digest[3].toLong().and(0xFF).shl(32))
|
||||
.or(digest[4].toLong().and(0xFF).shl(24))
|
||||
.or(digest[5].toLong().and(0xFF).shl(16))
|
||||
.or(digest[6].toLong().and(0xFF).shl(8))
|
||||
.or(digest[7].toLong().and(0xFF)),
|
||||
digest[8]
|
||||
.toLong()
|
||||
.shl(56)
|
||||
.or(digest[9].toLong().and(0xFF).shl(48))
|
||||
.or(digest[10].toLong().and(0xFF).shl(40))
|
||||
.or(digest[11].toLong().and(0xFF).shl(32))
|
||||
.or(digest[12].toLong().and(0xFF).shl(24))
|
||||
.or(digest[13].toLong().and(0xFF).shl(16))
|
||||
.or(digest[14].toLong().and(0xFF).shl(8))
|
||||
.or(digest[15].toLong().and(0xFF)))
|
||||
return UID(Format.AUXIO, mode, uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID
|
||||
|
|
|
@ -18,11 +18,13 @@
|
|||
|
||||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.dialog.PendingName
|
||||
import org.oxycblt.auxio.util.Event
|
||||
import org.oxycblt.auxio.util.MutableEvent
|
||||
|
@ -45,8 +47,8 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
|
|||
val statistics: StateFlow<Statistics?>
|
||||
get() = _statistics
|
||||
|
||||
private val _pendingPlaylistNaming = MutableEvent<PendingName.Args?>()
|
||||
val pendingPlaylistNaming: Event<PendingName.Args?> = _pendingPlaylistNaming
|
||||
private val _pendingNewPlaylist = MutableEvent<PendingName.Args?>()
|
||||
val pendingNewPlaylist: Event<PendingName.Args?> = _pendingNewPlaylist
|
||||
|
||||
init {
|
||||
musicRepository.addUpdateListener(this)
|
||||
|
@ -84,16 +86,37 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
|
|||
musicRepository.requestIndex(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new generic playlist. This will automatically generate a playlist name and then
|
||||
* prompt the user to edit the name before the creation finished.
|
||||
*
|
||||
* @param context The [Context] required to generate the playlist name.
|
||||
* @param songs The [Song]s to be contained in the new playlist.
|
||||
*/
|
||||
fun createPlaylist(context: Context, songs: List<Song> = listOf()) {
|
||||
val userLibrary = musicRepository.userLibrary ?: return
|
||||
var i = 1
|
||||
while (true) {
|
||||
val possibleName = context.getString(R.string.fmt_def_playlist, i)
|
||||
if (userLibrary.playlists.none { it.name.resolve(context) == possibleName }) {
|
||||
createPlaylist(possibleName, songs)
|
||||
return
|
||||
}
|
||||
++i
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new generic playlist. This will prompt the user to edit the name before the creation
|
||||
* finishes.
|
||||
*
|
||||
* @param name The preferred name of the new playlist.
|
||||
* @param songs The [Song]s to be contained in the new playlist.
|
||||
*/
|
||||
fun createPlaylist(name: String, songs: List<Song> = listOf()) {
|
||||
// TODO: Default to something like "Playlist 1", "Playlist 2", etc.
|
||||
// TODO: Attempt to unify playlist creation flow with dialog model
|
||||
_pendingPlaylistNaming.put(PendingName.Args(name, songs.map { it.uid }))
|
||||
_pendingNewPlaylist.put(PendingName.Args(name, songs.map { it.uid }))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
|
||||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
|
@ -35,6 +33,7 @@ import org.oxycblt.auxio.music.metadata.parseMultiValue
|
|||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
import org.oxycblt.auxio.util.update
|
||||
|
||||
/**
|
||||
* Library-backed implementation of [Song].
|
||||
|
@ -47,7 +46,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
|
|||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicMode.SONGS, it) }
|
||||
?: createHashedUid(MusicMode.SONGS) {
|
||||
?: Music.UID.auxio(MusicMode.SONGS) {
|
||||
// Song UIDs are based on the raw data without parsing so that they remain
|
||||
// consistent across music setting changes. Parents are not held up to the
|
||||
// same standard since grouping is already inherently linked to settings.
|
||||
|
@ -231,7 +230,7 @@ class AlbumImpl(
|
|||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
|
||||
?: createHashedUid(MusicMode.ALBUMS) {
|
||||
?: Music.UID.auxio(MusicMode.ALBUMS) {
|
||||
// Hash based on only names despite the presence of a date to increase stability.
|
||||
// I don't know if there is any situation where an artist will have two albums with
|
||||
// the exact same name, but if there is, I would love to know.
|
||||
|
@ -327,7 +326,7 @@ class ArtistImpl(
|
|||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
|
||||
?: createHashedUid(MusicMode.ARTISTS) { update(rawArtist.name) }
|
||||
?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) }
|
||||
override val name =
|
||||
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
|
||||
?: Name.Unknown(R.string.def_artist)
|
||||
|
@ -411,7 +410,7 @@ class GenreImpl(
|
|||
musicSettings: MusicSettings,
|
||||
override val songs: List<SongImpl>
|
||||
) : Genre {
|
||||
override val uid = createHashedUid(MusicMode.GENRES) { update(rawGenre.name) }
|
||||
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) }
|
||||
override val name =
|
||||
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
|
||||
?: Name.Unknown(R.string.def_genre)
|
||||
|
@ -467,98 +466,3 @@ class GenreImpl(
|
|||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a [Music.UID] derived from the hash of objective music metadata.
|
||||
*
|
||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
||||
* @param updates Block to update the [MessageDigest] hash with the metadata of the item. Make sure
|
||||
* the metadata hashed semantically aligns with the format specification.
|
||||
* @return A new [Music.UID] of Auxio format whose [UUID] was derived from the SHA-256 hash of the
|
||||
* metadata given.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun createHashedUid(mode: MusicMode, updates: MessageDigest.() -> Unit): Music.UID {
|
||||
val digest =
|
||||
MessageDigest.getInstance("SHA-256").run {
|
||||
updates()
|
||||
digest()
|
||||
}
|
||||
// Convert the digest to a UUID. This does cleave off some of the hash, but this
|
||||
// is considered okay.
|
||||
val uuid =
|
||||
UUID(
|
||||
digest[0]
|
||||
.toLong()
|
||||
.shl(56)
|
||||
.or(digest[1].toLong().and(0xFF).shl(48))
|
||||
.or(digest[2].toLong().and(0xFF).shl(40))
|
||||
.or(digest[3].toLong().and(0xFF).shl(32))
|
||||
.or(digest[4].toLong().and(0xFF).shl(24))
|
||||
.or(digest[5].toLong().and(0xFF).shl(16))
|
||||
.or(digest[6].toLong().and(0xFF).shl(8))
|
||||
.or(digest[7].toLong().and(0xFF)),
|
||||
digest[8]
|
||||
.toLong()
|
||||
.shl(56)
|
||||
.or(digest[9].toLong().and(0xFF).shl(48))
|
||||
.or(digest[10].toLong().and(0xFF).shl(40))
|
||||
.or(digest[11].toLong().and(0xFF).shl(32))
|
||||
.or(digest[12].toLong().and(0xFF).shl(24))
|
||||
.or(digest[13].toLong().and(0xFF).shl(16))
|
||||
.or(digest[14].toLong().and(0xFF).shl(8))
|
||||
.or(digest[15].toLong().and(0xFF)))
|
||||
return Music.UID.auxio(mode, uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with a lowercase [String].
|
||||
*
|
||||
* @param string The [String] to hash. If null, it will not be hashed.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun MessageDigest.update(string: String?) {
|
||||
if (string != null) {
|
||||
update(string.lowercase().toByteArray())
|
||||
} else {
|
||||
update(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with the string representation of a [Date].
|
||||
*
|
||||
* @param date The [Date] to hash. If null, nothing will be done.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun MessageDigest.update(date: Date?) {
|
||||
if (date != null) {
|
||||
update(date.toString().toByteArray())
|
||||
} else {
|
||||
update(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with the lowercase versions of all of the input [String]s.
|
||||
*
|
||||
* @param strings The [String]s to hash. If a [String] is null, it will not be hashed.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun MessageDigest.update(strings: List<String?>) {
|
||||
strings.forEach(::update)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with the little-endian bytes of a [Int].
|
||||
*
|
||||
* @param n The [Int] to write. If null, nothing will be done.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun MessageDigest.update(n: Int?) {
|
||||
if (n != null) {
|
||||
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
||||
} else {
|
||||
update(0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* PlaylistNamingDialog.kt is part of Auxio.
|
||||
* NewPlaylistDialog.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,7 +27,7 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.navigation.fragment.navArgs
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogPlaylistNamingBinding
|
||||
import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
|
@ -37,13 +37,13 @@ import org.oxycblt.auxio.util.collectImmediately
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class PlaylistNamingDialog : ViewBindingDialogFragment<DialogPlaylistNamingBinding>() {
|
||||
class NewPlaylistDialog : ViewBindingDialogFragment<DialogPlaylistNameBinding>() {
|
||||
// activityViewModels is intentional here as the ViewModel will do work that we
|
||||
// do not want to cancel after this dialog closes.
|
||||
private val dialogModel: PlaylistDialogViewModel by activityViewModels()
|
||||
// Information about what playlist to name for is initially within the navigation arguments
|
||||
// as UIDs, as that is the only safe way to parcel playlist information.
|
||||
private val args: PlaylistNamingDialogArgs by navArgs()
|
||||
private val args: NewPlaylistDialogArgs by navArgs()
|
||||
private var initializedInput = false
|
||||
|
||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||
|
@ -54,12 +54,9 @@ class PlaylistNamingDialog : ViewBindingDialogFragment<DialogPlaylistNamingBindi
|
|||
}
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
DialogPlaylistNamingBinding.inflate(inflater)
|
||||
DialogPlaylistNameBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(
|
||||
binding: DialogPlaylistNamingBinding,
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
override fun onBindingCreated(binding: DialogPlaylistNameBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
binding.playlistName.addTextChangedListener {
|
||||
|
@ -82,6 +79,6 @@ class PlaylistNamingDialog : ViewBindingDialogFragment<DialogPlaylistNamingBindi
|
|||
}
|
||||
// Disable the OK button if the name is invalid (empty or whitespace)
|
||||
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
|
||||
pendingName.name.isNotBlank()
|
||||
pendingName.valid
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import org.oxycblt.auxio.music.Song
|
|||
|
||||
/**
|
||||
* A [ViewModel] managing the state of the playlist editing dialogs.
|
||||
*
|
||||
* @author Alexander Capehart
|
||||
*/
|
||||
@HiltViewModel
|
||||
|
@ -44,15 +45,25 @@ class PlaylistDialogViewModel @Inject constructor(private val musicRepository: M
|
|||
}
|
||||
|
||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||
if (!changes.deviceLibrary) return
|
||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||
// Update the pending name to reflect new information in the music library.
|
||||
_currentPendingName.value =
|
||||
_currentPendingName.value?.let { pendingName ->
|
||||
PendingName(
|
||||
pendingName.name,
|
||||
pendingName.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
|
||||
val pendingName = _currentPendingName.value ?: return
|
||||
|
||||
val deviceLibrary = musicRepository.deviceLibrary
|
||||
val newSongs =
|
||||
if (changes.deviceLibrary && deviceLibrary != null) {
|
||||
pendingName.songs.mapNotNull { deviceLibrary.findSong(it.uid) }
|
||||
} else {
|
||||
pendingName.songs
|
||||
}
|
||||
|
||||
val userLibrary = musicRepository.userLibrary
|
||||
val newValid =
|
||||
if (changes.userLibrary && userLibrary != null) {
|
||||
validateName(pendingName.name)
|
||||
} else {
|
||||
pendingName.valid
|
||||
}
|
||||
|
||||
_currentPendingName.value = PendingName(pendingName.name, newSongs, newValid)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
@ -61,17 +72,22 @@ class PlaylistDialogViewModel @Inject constructor(private val musicRepository: M
|
|||
|
||||
/**
|
||||
* Update the current [PendingName] based on the given [PendingName.Args].
|
||||
*
|
||||
* @param args The [PendingName.Args] to update with.
|
||||
*/
|
||||
fun setPendingName(args: PendingName.Args) {
|
||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||
val name =
|
||||
PendingName(args.preferredName, args.songUids.mapNotNull(deviceLibrary::findSong))
|
||||
PendingName(
|
||||
args.preferredName,
|
||||
args.songUids.mapNotNull(deviceLibrary::findSong),
|
||||
validateName(args.preferredName))
|
||||
_currentPendingName.value = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current [PendingName] based on new user input.
|
||||
*
|
||||
* @param name The new user-inputted name, directly from the UI.
|
||||
*/
|
||||
fun updatePendingName(name: String?) {
|
||||
|
@ -79,25 +95,31 @@ class PlaylistDialogViewModel @Inject constructor(private val musicRepository: M
|
|||
// music items.
|
||||
val normalized = (name ?: return).trim()
|
||||
_currentPendingName.value =
|
||||
_currentPendingName.value?.run { PendingName(normalized, songs) }
|
||||
_currentPendingName.value?.run { PendingName(normalized, songs, validateName(name)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the current [PendingName] operation and write it to the database.
|
||||
*/
|
||||
/** Confirm the current [PendingName] operation and write it to the database. */
|
||||
fun confirmPendingName() {
|
||||
val pendingName = _currentPendingName.value ?: return
|
||||
musicRepository.createPlaylist(pendingName.name, pendingName.songs)
|
||||
_currentPendingName.value = null
|
||||
}
|
||||
|
||||
private fun validateName(name: String) =
|
||||
name.isNotBlank() && musicRepository.userLibrary?.findPlaylist(name) == null
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a name operation
|
||||
* Represents the current state of a name operation.
|
||||
*
|
||||
* @param name The name of the playlist.
|
||||
* @param songs Any songs that will be in the playlist when added.
|
||||
* @param valid Whether the current configuration is valid.
|
||||
*/
|
||||
data class PendingName(val name: String, val songs: List<Song>) {
|
||||
data class PendingName(val name: String, val songs: List<Song>, val valid: Boolean) {
|
||||
/**
|
||||
* A [Parcelable] version of [PendingName], to be used as a dialog argument.
|
||||
*
|
||||
* @param preferredName The name to be used initially by the dialog.
|
||||
* @param songUids The [Music.UID] of any pending [Song]s that will be put in the playlist.
|
||||
*/
|
||||
|
|
|
@ -22,11 +22,12 @@ import java.util.*
|
|||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.util.update
|
||||
|
||||
class PlaylistImpl
|
||||
private constructor(
|
||||
override val uid: Music.UID,
|
||||
override val name: Name,
|
||||
override val name: Name.Known,
|
||||
override val songs: List<Song>
|
||||
) : Playlist {
|
||||
override val durationMs = songs.sumOf { it.durationMs }
|
||||
|
@ -62,7 +63,7 @@ private constructor(
|
|||
*/
|
||||
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
||||
PlaylistImpl(
|
||||
Music.UID.auxio(MusicMode.PLAYLISTS, UUID.randomUUID()),
|
||||
Music.UID.auxio(MusicMode.PLAYLISTS) { update(name) },
|
||||
Name.Known.from(name, null, musicSettings),
|
||||
songs)
|
||||
|
||||
|
|
|
@ -44,6 +44,14 @@ interface UserLibrary {
|
|||
*/
|
||||
fun findPlaylist(uid: Music.UID): Playlist?
|
||||
|
||||
/**
|
||||
* Finds a playlist by it's [name]. Since all [Playlist] names must be unique, this will always
|
||||
* return at most 1 value.
|
||||
*
|
||||
* @param name The name [String] to search for.
|
||||
*/
|
||||
fun findPlaylist(name: String): Playlist?
|
||||
|
||||
/** Constructs a [UserLibrary] implementation in an asynchronous manner. */
|
||||
interface Factory {
|
||||
/**
|
||||
|
@ -104,6 +112,8 @@ private class UserLibraryImpl(
|
|||
|
||||
override fun findPlaylist(uid: Music.UID) = playlistMap[uid]
|
||||
|
||||
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
|
||||
|
||||
@Synchronized
|
||||
override fun createPlaylist(name: String, songs: List<Song>) {
|
||||
val playlistImpl = PlaylistImpl.from(name, songs, musicSettings)
|
||||
|
|
|
@ -18,9 +18,11 @@
|
|||
|
||||
package org.oxycblt.auxio.util
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.KClass
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
|
||||
/**
|
||||
* Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull],
|
||||
|
@ -89,3 +91,51 @@ fun String.toUuidOrNull(): UUID? =
|
|||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with a lowercase [String].
|
||||
*
|
||||
* @param string The [String] to hash. If null, it will not be hashed.
|
||||
*/
|
||||
fun MessageDigest.update(string: String?) {
|
||||
if (string != null) {
|
||||
update(string.lowercase().toByteArray())
|
||||
} else {
|
||||
update(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with the string representation of a [Date].
|
||||
*
|
||||
* @param date The [Date] to hash. If null, nothing will be done.
|
||||
*/
|
||||
fun MessageDigest.update(date: Date?) {
|
||||
if (date != null) {
|
||||
update(date.toString().toByteArray())
|
||||
} else {
|
||||
update(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with the lowercase versions of all of the input [String]s.
|
||||
*
|
||||
* @param strings The [String]s to hash. If a [String] is null, it will not be hashed.
|
||||
*/
|
||||
fun MessageDigest.update(strings: List<String?>) {
|
||||
strings.forEach(::update)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a [MessageDigest] with the little-endian bytes of a [Int].
|
||||
*
|
||||
* @param n The [Int] to write. If null, nothing will be done.
|
||||
*/
|
||||
fun MessageDigest.update(n: Int?) {
|
||||
if (n != null) {
|
||||
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
|
||||
} else {
|
||||
update(0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
android:id="@+id/action_show_details"
|
||||
app:destination="@id/song_detail_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_name_playlist"
|
||||
app:destination="@id/playlist_naming_dialog" />
|
||||
android:id="@+id/action_new_playlist"
|
||||
app:destination="@id/new_playlist_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_pick_navigation_artist"
|
||||
app:destination="@id/artist_navigation_picker_dialog" />
|
||||
|
@ -42,10 +42,10 @@
|
|||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/playlist_naming_dialog"
|
||||
android:name="org.oxycblt.auxio.music.dialog.PlaylistNamingDialog"
|
||||
android:label="playlist_naming_dialog"
|
||||
tools:layout="@layout/dialog_song_detail">
|
||||
android:id="@+id/new_playlist_dialog"
|
||||
android:name="org.oxycblt.auxio.music.dialog.NewPlaylistDialog"
|
||||
android:label="new_playlist_dialog"
|
||||
tools:layout="@layout/dialog_playlist_name">
|
||||
<argument
|
||||
android:name="pendingName"
|
||||
app:argType="org.oxycblt.auxio.music.dialog.PendingName$Args" />
|
||||
|
|
|
@ -376,6 +376,8 @@
|
|||
|
||||
<!-- As in "Disc 1", "Disc 2", etc. in a set -->
|
||||
<string name="fmt_disc_no">Disc %d</string>
|
||||
<!-- As in "Playlist 1", "Playlist 2", etc. -->
|
||||
<string name="fmt_def_playlist">Playlist %d</string>
|
||||
|
||||
<!-- Use your native country's abbreviation for decibel units. -->
|
||||
<string name="fmt_db_pos">+%.1f dB</string>
|
||||
|
|
Loading…
Reference in a new issue