Skip to content

Commit

Permalink
music: implement exporting frontend
Browse files Browse the repository at this point in the history
Implement the exporting dialog and flow in all places in the app.
  • Loading branch information
OxygenCobalt committed Dec 23, 2023
1 parent 68e4da5 commit 3f1f2f5
Show file tree
Hide file tree
Showing 16 changed files with 421 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ class AlbumDetailFragment :
}
is PlaylistDecision.New,
is PlaylistDecision.Rename,
is PlaylistDecision.Delete -> error("Unexpected playlist decision $decision")
is PlaylistDecision.Delete,
is PlaylistDecision.Export -> error("Unexpected playlist decision $decision")
}
findNavController().navigateSafe(directions)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ class ArtistDetailFragment :
}
is PlaylistDecision.New,
is PlaylistDecision.Rename,
is PlaylistDecision.Export,
is PlaylistDecision.Delete -> error("Unexpected playlist decision $decision")
}
findNavController().navigateSafe(directions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ class GenreDetailFragment :
}
is PlaylistDecision.New,
is PlaylistDecision.Rename,
is PlaylistDecision.Export,
is PlaylistDecision.Delete -> error("Unexpected playlist decision $decision")
}
findNavController().navigateSafe(directions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ class PlaylistDetailFragment :
logD("Renaming ${decision.playlist}")
PlaylistDetailFragmentDirections.renamePlaylist(decision.playlist.uid)
}
is PlaylistDecision.Export -> {
logD("Exporting ${decision.playlist}")
PlaylistDetailFragmentDirections.exportPlaylist(decision.playlist.uid)
}
is PlaylistDecision.Delete -> {
logD("Deleting ${decision.playlist}")
PlaylistDetailFragmentDirections.deletePlaylist(decision.playlist.uid)
Expand Down
52 changes: 44 additions & 8 deletions app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.NoAudioPermissionException
import org.oxycblt.auxio.music.NoMusicException
import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.PlaylistError
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.external.M3U
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
Expand Down Expand Up @@ -98,7 +101,9 @@ class HomeFragment :
private val homeModel: HomeViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
private var filePickerLauncher: ActivityResultLauncher<String>? = null
private var getContentLauncher: ActivityResultLauncher<String>? = null
private var createDocumentLauncher: ActivityResultLauncher<String>? = null
private var pendingExportPlaylist: Playlist? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down Expand Up @@ -127,7 +132,7 @@ class HomeFragment :
musicModel.refresh()
}

filePickerLauncher =
getContentLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) {
logW("No URI returned from file picker")
Expand All @@ -138,6 +143,24 @@ class HomeFragment :
musicModel.importPlaylist(uri)
}

createDocumentLauncher =
registerForActivityResult(ActivityResultContracts.CreateDocument(M3U.MIME_TYPE)) { uri
->
if (uri == null) {
logW("No URI returned from file picker")
return@registerForActivityResult
}

val playlist = pendingExportPlaylist
if (playlist == null) {
logW("No playlist to export")
return@registerForActivityResult
}

logD("Received playlist URI $uri")
musicModel.exportPlaylist(playlist, uri)
}

// --- UI SETUP ---
binding.homeAppbar.addOnOffsetChangedListener(this)
binding.homeNormalToolbar.apply {
Expand Down Expand Up @@ -210,7 +233,7 @@ class HomeFragment :
collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(musicModel.indexingState, ::updateIndexerState)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(musicModel.importError.flow, ::handleImportError)
collectImmediately(musicModel.playlistError.flow, ::handlePlaylistError)
collect(detailModel.toShow.flow, ::handleShow)
}

Expand Down Expand Up @@ -295,7 +318,7 @@ class HomeFragment :
}
R.id.action_import_playlist -> {
logD("Importing playlist")
filePickerLauncher?.launch("audio/x-mpegurl")
getContentLauncher?.launch(M3U.MIME_TYPE)
}
else -> {}
}
Expand Down Expand Up @@ -475,6 +498,10 @@ class HomeFragment :
logD("Renaming ${decision.playlist}")
HomeFragmentDirections.renamePlaylist(decision.playlist.uid)
}
is PlaylistDecision.Export -> {
logD("Exporting ${decision.playlist}")
HomeFragmentDirections.exportPlaylist(decision.playlist.uid)
}
is PlaylistDecision.Delete -> {
logD("Deleting ${decision.playlist}")
HomeFragmentDirections.deletePlaylist(decision.playlist.uid)
Expand All @@ -486,13 +513,22 @@ class HomeFragment :
}
}
findNavController().navigateSafe(directions)
musicModel.playlistDecision.consume()
}

private fun handleImportError(flag: Unit?) {
if (flag != null) {
requireContext().showToast(R.string.err_import_failed)
musicModel.importError.consume()
private fun handlePlaylistError(error: PlaylistError?) {
when (error) {
is PlaylistError.ImportFailed -> {
requireContext().showToast(R.string.err_import_failed)
musicModel.importError.consume()
}
is PlaylistError.ExportFailed -> {
requireContext().showToast(R.string.err_export_failed)
musicModel.importError.consume()
}
null -> {}
}
musicModel.playlistError.consume()
}

private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ class PlaylistMenuDialogFragment : MenuDialogFragment<Menu.ForPlaylist>() {
R.id.action_play_next,
R.id.action_queue_add,
R.id.action_playlist_add,
R.id.action_playlist_export,
R.id.action_share)
} else {
setOf()
Expand Down Expand Up @@ -320,6 +321,7 @@ class PlaylistMenuDialogFragment : MenuDialogFragment<Menu.ForPlaylist>() {
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_rename -> musicModel.renamePlaylist(menu.playlist)
R.id.action_playlist_export -> musicModel.exportPlaylist(menu.playlist)
R.id.action_delete -> musicModel.deletePlaylist(menu.playlist)
R.id.action_share -> requireContext().share(menu.playlist)
else -> error("Unexpected menu item $item")
Expand Down
50 changes: 47 additions & 3 deletions app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.music.external.ExportConfig
import org.oxycblt.auxio.music.external.ExternalPlaylistManager
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
Expand Down Expand Up @@ -64,11 +65,20 @@ constructor(
val playlistDecision: Event<PlaylistDecision>
get() = _playlistDecision

private val _playlistError = MutableEvent<PlaylistError>()
val playlistError: Event<PlaylistError>
get() = _playlistError

private val _importError = MutableEvent<Unit>()
/** Flag for when playlist importing failed. Consume this and show an error if active. */
val importError: Event<Unit>
get() = _importError

private val _exportError = MutableEvent<Unit>()
/** Flag for when playlist exporting failed. Consume this and show an error if active. */
val exportError: Event<Unit>
get() = _exportError

init {
musicRepository.addUpdateListener(this)
musicRepository.addIndexingListener(this)
Expand Down Expand Up @@ -134,21 +144,42 @@ constructor(
viewModelScope.launch(Dispatchers.IO) {
val importedPlaylist = externalPlaylistManager.import(uri)
if (importedPlaylist == null) {
_importError.put(Unit)
_playlistError.put(PlaylistError.ImportFailed)
return@launch
}

val deviceLibrary = musicRepository.deviceLibrary ?: return@launch
val songs = importedPlaylist.paths.mapNotNull(deviceLibrary::findSongByPath)

if (songs.isEmpty()) {
_importError.put(Unit)
_playlistError.put(PlaylistError.ImportFailed)
return@launch
}

// TODO Require the user to name it something else if the name is a duplicate of
// a prior playlist
createPlaylist(importedPlaylist.name, songs)
}

/**
* Export a [Playlist] to a file [Uri]. Errors pushed to [exportError].
*
* @param playlist The [Playlist] to export.
* @param uri The [Uri] to export to. If null, the user will be prompted for one.
*/
fun exportPlaylist(playlist: Playlist, uri: Uri? = null, config: ExportConfig? = null) {
if (uri != null && config != null) {
logD("Exporting playlist to $uri")
viewModelScope.launch(Dispatchers.IO) {
if (!externalPlaylistManager.export(playlist, uri, config)) {
_playlistError.put(PlaylistError.ExportFailed)
}
}
} else {
logD("Launching export dialog")
_playlistDecision.put(PlaylistDecision.Export(playlist))
}
}

/**
* Rename the given playlist.
*
Expand Down Expand Up @@ -280,6 +311,13 @@ sealed interface PlaylistDecision {
*/
data class Rename(val playlist: Playlist) : PlaylistDecision

/**
* Navigate to a dialog that allows the user to export a [Playlist].
*
* @param playlist The [Playlist] to export.
*/
data class Export(val playlist: Playlist) : PlaylistDecision

/**
* Navigate to a dialog that confirms the deletion of an existing [Playlist].
*
Expand All @@ -294,3 +332,9 @@ sealed interface PlaylistDecision {
*/
data class Add(val songs: List<Song>) : PlaylistDecision
}

sealed interface PlaylistError {
data object ImportFailed : PlaylistError

data object ExportFailed : PlaylistError
}
Loading

0 comments on commit 3f1f2f5

Please sign in to comment.