diff --git a/shared/src/androidMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.android.kt b/shared/src/androidMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.android.kt index c0b00a287..514722c1d 100644 --- a/shared/src/androidMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.android.kt +++ b/shared/src/androidMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.android.kt @@ -58,6 +58,11 @@ private fun convertState(exo_state: Int): MediaPlayerState { actual open class MediaPlayerService: PlatformService() { + actual interface UndoRedoAction { + actual fun undo() + actual fun redo() + } + actual open class Listener { actual open fun onSongTransition(song: Song?) {} actual open fun onStateChanged(state: MediaPlayerState) {} @@ -114,8 +119,8 @@ actual open class MediaPlayerService: PlatformService() { private var notification_manager: PlayerNotificationManager? = null // Undo - private var current_action: MutableList? = null - private val action_list: MutableList> = mutableListOf() + private var current_action: MutableList? = null + private val action_list: MutableList> = mutableListOf() private var action_head: Int = 0 private val audio_processor = FFTAudioProcessor() @@ -502,10 +507,21 @@ actual open class MediaPlayerService: PlatformService() { } actual fun undoableAction(action: MediaPlayerService.() -> Unit) { + undoableActionWithCustom { + action() + null + } + } + + actual fun undoableActionWithCustom(action: MediaPlayerService.() -> UndoRedoAction?) { synchronized(action_list) { assert(current_action == null) current_action = mutableListOf() - action(this) + + val customAction = action(this) + if (customAction != null) { + performAction(customAction) + } for (i in 0 until redo_count) { action_list.removeLast() @@ -518,7 +534,7 @@ actual open class MediaPlayerService: PlatformService() { } } - private fun performAction(action: Action) { + private fun performAction(action: UndoRedoAction) { action.redo() current_action?.add(action) } @@ -565,11 +581,8 @@ actual open class MediaPlayerService: PlatformService() { } } - private abstract inner class Action() { + private abstract inner class Action: UndoRedoAction { protected val is_undoable: Boolean get() = current_action != null - - abstract fun redo() - abstract fun undo() } private inner class AddAction(val item: ExoMediaItem, val index: Int): Action() { override fun redo() { diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/PlayerService.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/PlayerService.kt index 4cafd52fa..511bc1a5e 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/PlayerService.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/PlayerService.kt @@ -130,23 +130,34 @@ class PlayerService : MediaPlayerService() { updateActiveQueueIndex() } - fun shuffleQueue(start: Int = -1) { + fun shuffleQueue(start: Int = -1, end: Int = song_count - 1) { val range: IntRange = - if (start < 0) { - current_song_index + 1 until song_count - } - else if (song_count - start <= 1) { - return - } - else { - start until song_count + if (start < 0) { + current_song_index + 1 .. end + } + else if (song_count - start <= 1) { + return + } + else { + start .. end + } + shuffleQueue(range) + } + + fun shuffleQueueAndIndices(indices: List) { + for (i in indices.withIndex()) { + val swap_index = Random.nextInt(indices.size) + swapQueuePositions(i.value, indices[swap_index], false) +// indices.swap(i.index, swap_index) } + savePersistentQueue() + } + fun shuffleQueue(range: IntRange) { for (i in range) { val swap = Random.nextInt(range) swapQueuePositions(i, swap, false) } - savePersistentQueue() } diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/model/MediaItemPreviewInteraction.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/model/MediaItemPreviewInteraction.kt new file mode 100644 index 000000000..a32a8dca3 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/model/MediaItemPreviewInteraction.kt @@ -0,0 +1,103 @@ +package com.spectre7.spmp.model + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalViewConfiguration +import com.spectre7.spmp.platform.vibrateShort +import com.spectre7.spmp.ui.component.LongPressMenuData +import com.spectre7.spmp.ui.layout.mainpage.PlayerViewContext +import com.spectre7.spmp.platform.Platform +import com.spectre7.spmp.platform.composable.platformClickable +import com.spectre7.utils.composable.OnChangedEffect +import kotlinx.coroutines.delay + +private enum class PressStage { + INSTANT, BRIEF, LONG_1, LONG_2; + + fun execute(item: MediaItem, playerProvider: () -> PlayerViewContext, long_press_menu_data: LongPressMenuData) { + when (this) { + BRIEF -> {} + INSTANT -> { + if (long_press_menu_data.multiselect_context?.is_active == true) { + long_press_menu_data.multiselect_context.toggleItem(item, long_press_menu_data.multiselect_key) + } + else { + playerProvider().onMediaItemClicked(item) + } + } + LONG_1 -> playerProvider().showLongPressMenu(long_press_menu_data) + LONG_2 -> long_press_menu_data.multiselect_context?.apply { + setActive(true) + toggleItem(item, long_press_menu_data.multiselect_key) + } + } + } + + fun isAvailable(long_press_menu_data: LongPressMenuData): Boolean = + when (this) { + LONG_2 -> long_press_menu_data.multiselect_context != null + else -> true + } +} + +private fun getIndication(): Indication? = null + +@Composable +fun Modifier.mediaItemPreviewInteraction( + item: MediaItem, + playerProvider: () -> PlayerViewContext, + long_press_menu_data: LongPressMenuData +): Modifier { + if (Platform.is_desktop) { + return platformClickable( + onClick = { PressStage.INSTANT.execute(item, playerProvider, long_press_menu_data) }, + onAltClick = { PressStage.LONG_1.execute(item, playerProvider, long_press_menu_data) }, + indication = getIndication() + ) + } + + var current_press_stage: PressStage by remember { mutableStateOf(PressStage.INSTANT) } + val long_press_timeout = LocalViewConfiguration.current.longPressTimeoutMillis + + val interaction_source = remember { MutableInteractionSource() } + val pressed by interaction_source.collectIsPressedAsState() + + OnChangedEffect(pressed) { + if (pressed) { + var delays = 0 + for (stage in PressStage.values()) { + if (stage.ordinal == 0 || !stage.isAvailable(long_press_menu_data)) { + continue + } + + if (stage.ordinal == 1) { + current_press_stage = stage + } + else { + delay(long_press_timeout * (++delays)) + current_press_stage = stage + SpMp.context.vibrateShort() + + if (stage == PressStage.values().last { it.isAvailable(long_press_menu_data) }) { + current_press_stage.execute(item, playerProvider, long_press_menu_data) + break + } + } + } + } + else { + if (current_press_stage != PressStage.values().last { it.isAvailable(long_press_menu_data) }) { + current_press_stage.execute(item, playerProvider, long_press_menu_data) + } + current_press_stage = PressStage.INSTANT + } + } + + return clickable(interaction_source, getIndication(), onClick = { + current_press_stage.execute(item, playerProvider, long_press_menu_data) + }) +} diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/model/Settings.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/model/Settings.kt index 52d1cdb94..561f4e950 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/model/Settings.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/model/Settings.kt @@ -102,7 +102,7 @@ enum class Settings { KEY_FEED_SHOW_CHARTS_ROW, // Now playing queue - KEY_NP_QUEUE_RADIO_INFO_POSITION, + KEY_NP_QUEUE_RADIO_INFO_POSITION, // TODO prefs item // Server KEY_SPMS_PORT, diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.kt index 364fe6701..6182f7806 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/platform/MediaPlayerService.kt @@ -22,6 +22,11 @@ enum class MediaPlayerRepeatMode { } expect open class MediaPlayerService(): PlatformService { + interface UndoRedoAction { + fun undo() + fun redo() + } + open class Listener() { open fun onSongTransition(song: Song?) open fun onStateChanged(state: MediaPlayerState) @@ -61,6 +66,8 @@ expect open class MediaPlayerService(): PlatformService { fun Visualiser(colour: Color, modifier: Modifier = Modifier, opacity: Float = 1f) fun undoableAction(action: MediaPlayerService.() -> Unit) + fun undoableActionWithCustom(action: MediaPlayerService.() -> UndoRedoAction?) + fun redo() fun redoAll() fun undo() diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/ArtistPreview.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/ArtistPreview.kt index c3c46abe1..cc814d468 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/ArtistPreview.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/ArtistPreview.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.sp import com.spectre7.spmp.PlayerServiceHost import com.spectre7.spmp.model.Artist import com.spectre7.spmp.model.MediaItem +import com.spectre7.spmp.model.mediaItemPreviewInteraction import com.spectre7.spmp.platform.composable.platformClickable import com.spectre7.spmp.ui.component.multiselect.MediaItemMultiSelectContext import com.spectre7.spmp.ui.layout.ArtistSubscribeButton @@ -38,24 +39,11 @@ fun ArtistPreviewSquare( params: MediaItem.PreviewParams ) { val long_press_menu_data = remember(artist) { - getArtistLongPressMenuData(artist) + getArtistLongPressMenuData(artist, multiselect_context = params.multiselect_context) } Column( - params.modifier - .platformClickable( - onClick = { - if (params.multiselect_context?.is_active == true) { - params.multiselect_context.toggleItem(artist) - } - else { - params.playerProvider().onMediaItemClicked(artist) - } - }, - onAltClick = { - params.playerProvider().showLongPressMenu(long_press_menu_data) - } - ), + params.modifier.mediaItemPreviewInteraction(artist, params.playerProvider, long_press_menu_data), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(5.dp) ) { @@ -93,22 +81,7 @@ fun ArtistPreviewLong( Row( verticalAlignment = Alignment.CenterVertically, - modifier = params.modifier - .combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { - if (params.multiselect_context?.is_active == true) { - params.multiselect_context.toggleItem(artist) - } - else { - params.playerProvider().onMediaItemClicked(artist) - } - }, - onLongClick = { - params.playerProvider().showLongPressMenu(long_press_menu_data) - } - ) + modifier = params.modifier.mediaItemPreviewInteraction(artist, params.playerProvider, long_press_menu_data) ) { Box(Modifier.width(IntrinsicSize.Min).height(IntrinsicSize.Min), contentAlignment = Alignment.Center) { artist.Thumbnail( diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/PlaylistPreview.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/PlaylistPreview.kt index 1ac146e4b..a145569c2 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/PlaylistPreview.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/PlaylistPreview.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.sp import com.spectre7.spmp.model.MediaItem import com.spectre7.spmp.model.Playlist import com.spectre7.spmp.model.getReadable +import com.spectre7.spmp.model.mediaItemPreviewInteraction import com.spectre7.spmp.platform.composable.platformClickable import com.spectre7.utils.setAlpha @@ -27,25 +28,13 @@ fun PlaylistPreviewSquare( val long_press_menu_data = remember(playlist) { LongPressMenuData( playlist, - RoundedCornerShape(10.dp) + RoundedCornerShape(10.dp), + multiselect_context = params.multiselect_context ) { } // TODO } Column( - params.modifier - .platformClickable( - onClick = { - if (params.multiselect_context?.is_active == true) { - params.multiselect_context.toggleItem(playlist) - } - else { - params.playerProvider().onMediaItemClicked(playlist) - } - }, - onAltClick = { - params.playerProvider().showLongPressMenu(long_press_menu_data) - } - ), + params.modifier.mediaItemPreviewInteraction(playlist, params.playerProvider, long_press_menu_data), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(5.dp) ) { @@ -81,28 +70,14 @@ fun PlaylistPreviewLong( val long_press_menu_data = remember(playlist) { LongPressMenuData( playlist, - RoundedCornerShape(10.dp) + RoundedCornerShape(10.dp), + multiselect_context = params.multiselect_context ) { } // TODO } Row( verticalAlignment = Alignment.CenterVertically, - modifier = params.modifier - .combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { - if (params.multiselect_context?.is_active == true) { - params.multiselect_context.toggleItem(playlist) - } - else { - params.playerProvider().onMediaItemClicked(playlist) - } - }, - onLongClick = { - params.playerProvider().showLongPressMenu(long_press_menu_data) - } - ) + modifier = params.modifier.mediaItemPreviewInteraction(playlist, params.playerProvider, long_press_menu_data) ) { Box(Modifier.width(IntrinsicSize.Min).height(IntrinsicSize.Min), contentAlignment = Alignment.Center) { playlist.Thumbnail( diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/SongPreview.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/SongPreview.kt index 99c68d866..59896f168 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/SongPreview.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/SongPreview.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.sp import com.spectre7.spmp.PlayerServiceHost import com.spectre7.spmp.model.MediaItem import com.spectre7.spmp.model.Song +import com.spectre7.spmp.model.mediaItemPreviewInteraction import com.spectre7.spmp.platform.PlayerDownloadManager.DownloadStatus import com.spectre7.spmp.platform.composable.platformClickable import com.spectre7.spmp.platform.vibrateShort @@ -59,20 +60,7 @@ fun SongPreviewSquare( } Column( - params.modifier - .platformClickable( - onClick = { - if (params.multiselect_context?.is_active == true) { - params.multiselect_context.toggleItem(song) - } - else { - params.playerProvider().onMediaItemClicked(song) - } - }, - onAltClick = { - params.playerProvider().showLongPressMenu(long_press_menu_data) - } - ), + params.modifier.mediaItemPreviewInteraction(song, params.playerProvider, long_press_menu_data), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(5.dp) ) { @@ -118,21 +106,7 @@ fun SongPreviewLong( verticalAlignment = Alignment.CenterVertically, modifier = params.modifier .fillMaxWidth() - .combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { - if (params.multiselect_context?.is_active == true) { - params.multiselect_context.toggleItem(song) - } - else { - params.playerProvider().onMediaItemClicked(song) - } - }, - onLongClick = { - params.playerProvider().showLongPressMenu(long_press_menu_data) - } - ) + .mediaItemPreviewInteraction(song, params.playerProvider, long_press_menu_data) ) { Box(Modifier.width(IntrinsicSize.Min).height(IntrinsicSize.Min), contentAlignment = Alignment.Center) { song.Thumbnail( diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/multiselect/MediaItemMultiSelectContext.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/multiselect/MediaItemMultiSelectContext.kt index b28097ca5..5640c7652 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/multiselect/MediaItemMultiSelectContext.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/component/multiselect/MediaItemMultiSelectContext.kt @@ -2,33 +2,57 @@ package com.spectre7.spmp.ui.component.multiselect import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.spectre7.spmp.PlayerServiceHost import com.spectre7.spmp.model.MediaItem import com.spectre7.spmp.model.Settings +import com.spectre7.spmp.model.Song import com.spectre7.spmp.resources.getStringTODO +import com.spectre7.spmp.ui.layout.mainpage.PlayerViewContext import com.spectre7.utils.getContrasted +import com.spectre7.utils.lazyAssert import com.spectre7.utils.setAlpha +import com.spectre7.utils.toFloat +import kotlinx.coroutines.delay class MediaItemMultiSelectContext( - val selectedItemActions: @Composable RowScope.(MediaItemMultiSelectContext, onActionPerformed: () -> Unit) -> Unit, + val playerProvider: () -> PlayerViewContext, val allow_songs: Boolean = true, val allow_artists: Boolean = true, - val allow_playlists: Boolean = true + val allow_playlists: Boolean = true, + val selectedItemActions: @Composable RowScope.(MediaItemMultiSelectContext) -> Unit ) { private val selected_items: MutableList> = mutableStateListOf() var is_active: Boolean by mutableStateOf(false) private set + @Composable + fun getActiveHintBorder(): BorderStroke? = if (is_active) BorderStroke(hint_path_thickness, LocalContentColor.current) else null + private val hint_path_thickness = 0.5.dp + fun setActive(value: Boolean) { if (value) { selected_items.clear() @@ -37,10 +61,25 @@ class MediaItemMultiSelectContext( } fun isItemSelected(item: MediaItem, key: Int? = null): Boolean { + if (!is_active) { + return false + } return selected_items.any { it.first == item && it.second == key } } + fun onActionPerformed() { + if (Settings.KEY_MULTISELECT_CANCEL_ON_ACTION.get()) { + setActive(false) + } + } + fun getSelectedItems(): List> = selected_items + fun getUniqueSelectedItems(): Set = selected_items.map { it.first }.toSet() + + fun updateKey(index: Int, key: Int?) { + selected_items[index] = selected_items[index].copy(second = key) + lazyAssert(condition = ::areItemsValid) + } fun toggleItem(item: MediaItem, key: Int? = null) { val allowed = when (item.type) { @@ -91,7 +130,7 @@ class MediaItemMultiSelectContext( } @Composable - fun InfoDisplay(modifier: Modifier) { + fun InfoDisplay(modifier: Modifier = Modifier) { DisposableEffect(Unit) { onDispose { if (!is_active) { @@ -102,14 +141,11 @@ class MediaItemMultiSelectContext( Column(modifier.fillMaxWidth()) { Text(getStringTODO("${selected_items.size} items selected"), style = MaterialTheme.typography.labelLarge) - Divider(Modifier.fillMaxWidth().padding(top = 5.dp)) + Divider(Modifier.padding(top = 5.dp), color = LocalContentColor.current, thickness = hint_path_thickness) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - selectedItemActions(this@MediaItemMultiSelectContext) { - if (Settings.KEY_MULTISELECT_CANCEL_ON_ACTION.get()) { - setActive(false) - } - } + GeneralSelectedItemActions() + selectedItemActions(this@MediaItemMultiSelectContext) Spacer(Modifier.fillMaxWidth().weight(1f)) @@ -123,6 +159,36 @@ class MediaItemMultiSelectContext( } } + @Composable + private fun GeneralSelectedItemActions() { + println("UNIQUE ${getUniqueSelectedItems().toList()}") + val all_pinned by remember { derivedStateOf { + getUniqueSelectedItems().all { it.pinned_to_home } + } } + + IconButton({ + all_pinned.also { pinned -> + for (item in getUniqueSelectedItems()) { + item.setPinnedToHome(!pinned, playerProvider) + } + } + onActionPerformed() + }) { + Icon(if (all_pinned) Icons.Filled.PushPin else Icons.Outlined.PushPin, null) + } + + IconButton({ + for (item in getUniqueSelectedItems()) { + if (item is Song) { + PlayerServiceHost.download_manager.startDownload(item.id) + } + } + onActionPerformed() + }) { + Icon(Icons.Default.Download, null) + } + } + @Composable fun CollectionToggleButton(items: List) { AnimatedVisibility(is_active) { @@ -138,4 +204,21 @@ class MediaItemMultiSelectContext( } } } + + private fun areItemsValid(): Boolean { + val keys = mutableMapOf>() + for (item in selected_items) { + val item_keys = keys[item.first] + if (item_keys == null) { + keys[item.first] = mutableListOf(item.second) + continue + } + + if (item_keys.contains(item.second)) { + return false + } + item_keys.add(item.second) + } + return true + } } diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/mainpage/PlayerViewContextImpl.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/mainpage/PlayerViewContextImpl.kt index e269c424c..c243a3001 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/mainpage/PlayerViewContextImpl.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/mainpage/PlayerViewContextImpl.kt @@ -79,34 +79,9 @@ class PlayerViewContextImpl: PlayerViewContext(null, null, null) { top = false, left = false ) - override val main_multiselect_context: MediaItemMultiSelectContext = MediaItemMultiSelectContext({ multiselect, onActionPerformed -> - val all_pinned by remember { derivedStateOf { - multiselect.getSelectedItems().all { it.first.pinned_to_home } - } } - - IconButton({ - all_pinned.also { pinned -> - for (item in multiselect.getSelectedItems()) { - item.first.setPinnedToHome(!pinned, playerProvider) - } - } - onActionPerformed() - }) { - Icon(if (all_pinned) Icons.Filled.PushPin else Icons.Outlined.PushPin, null) - } - - IconButton({ - for (item in multiselect.getSelectedItems()) { - if (item.first is Song) { - PlayerServiceHost.download_manager.startDownload(item.first.id) - } - } - onActionPerformed() - }) { - Icon(Icons.Default.Download, null) - } + override val main_multiselect_context: MediaItemMultiSelectContext = MediaItemMultiSelectContext({ this }) { multiselect -> - }) + } init { low_memory_listener = { diff --git a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/nowplaying/NowPlayingQueueTab.kt b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/nowplaying/NowPlayingQueueTab.kt index 733221200..df85c791a 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/nowplaying/NowPlayingQueueTab.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/spmp/ui/layout/nowplaying/NowPlayingQueueTab.kt @@ -20,8 +20,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.* @@ -34,6 +33,7 @@ import com.spectre7.spmp.platform.MediaPlayerRepeatMode import com.spectre7.spmp.platform.MediaPlayerService import com.spectre7.spmp.platform.vibrateShort import com.spectre7.spmp.resources.getString +import com.spectre7.spmp.ui.component.multiselect.MediaItemMultiSelectContext import com.spectre7.spmp.ui.layout.mainpage.MINIMISED_NOW_PLAYING_HEIGHT import com.spectre7.spmp.ui.layout.mainpage.PlayerViewContext import com.spectre7.utils.* @@ -70,6 +70,7 @@ private class QueueTabItem(val song: Song, val key: Int) { index: Int, backgroundColourProvider: () -> Color, playerProvider: () -> PlayerViewContext, + multiselect_context: MediaItemMultiSelectContext, requestRemove: () -> Unit ) { val swipe_state = queueElementSwipeState(requestRemove) @@ -104,6 +105,7 @@ private class QueueTabItem(val song: Song, val key: Int) { thresholds = { _, _ -> FractionalThreshold(0.2f) } ), contentColour = { backgroundColourProvider().getContrasted() }, + multiselect_context = multiselect_context ), queue_index = index ) @@ -128,6 +130,9 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon var key_inc by remember { mutableStateOf(0) } val radio_info_position: NowPlayingQueueRadioInfoPosition = Settings.getEnum(Settings.KEY_NP_QUEUE_RADIO_INFO_POSITION) + val multiselect_context: MediaItemMultiSelectContext = remember { MediaItemMultiSelectContext(playerProvider) { multiselect -> + + } } val song_items: SnapshotStateList = remember { mutableStateListOf().also { list -> PlayerServiceHost.player.iterateSongs { _, song: Song -> @@ -144,7 +149,25 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon song_items.removeAt(index) } override fun onSongMoved(from: Int, to: Int) { + if (from == to) { + return + } + song_items.add(to, song_items.removeAt(from)) + + for (item in multiselect_context.getSelectedItems().map { it.second!! }.withIndex()) { + if (item.value == from) { + multiselect_context.updateKey(item.index, to) + } + else if (from > to) { + if (item.value in to until from) { + multiselect_context.updateKey(item.index, item.value + 1) + } + } + else if (item.value in (from + 1) .. to) { + multiselect_context.updateKey(item.index, item.value - 1) + } + } } } } @@ -197,12 +220,23 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon Button( onClick = { - PlayerServiceHost.player.clearQueue(keep_current = PlayerServiceHost.status.queue_size > 1) + PlayerServiceHost.player.undoableAction { + if (multiselect_context.is_active) { + for (item in multiselect_context.getSelectedItems().sortedByDescending { it.second!! }) { + PlayerServiceHost.player.removeFromQueue(item.second!!) + } + multiselect_context.onActionPerformed() + } + else { + PlayerServiceHost.player.clearQueue(keep_current = PlayerServiceHost.status.queue_size > 1) + } + } }, colors = ButtonDefaults.buttonColors( containerColor = background_colour, contentColor = background_colour.getContrasted() - ) + ), + border = multiselect_context.getActiveHintBorder() ) { Text(getString("queue_clear")) } @@ -210,29 +244,56 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon Surface( Modifier.combinedClickable( onClick = { - PlayerServiceHost.player.undoableAction { - PlayerServiceHost.player.shuffleQueue() + if (multiselect_context.is_active) { + PlayerServiceHost.player.undoableAction { + PlayerServiceHost.player.shuffleQueueAndIndices(multiselect_context.getSelectedItems().map { it.second!! }) + } +// PlayerServiceHost.player.undoableActionWithCustom { +// val indices_to_shuffle = multiselect_context.getSelectedItems().map { it.second!! }.toMutableList() +// val original_keys = indices_to_shuffle.withIndex().associate { it.index to it.value } +// +// PlayerServiceHost.player.shuffleQueueAndIndices(indices_to_shuffle) +// val swapped_keys = indices_to_shuffle.withIndex().associate { it.index to it.value } +// +// return@undoableActionWithCustom object : MediaPlayerService.UndoRedoAction { +// override fun undo() { +// multiselect_context.updateKeys(original_keys) +// } +// +// override fun redo() { +// multiselect_context.updateKeys(swapped_keys) +// } +// } +// } + multiselect_context.onActionPerformed() + } + else { + PlayerServiceHost.player.undoableAction { + PlayerServiceHost.player.shuffleQueue() + } } }, - onLongClick = { - SpMp.context.vibrateShort() + onLongClick = if (multiselect_context.is_active) null else ({ PlayerServiceHost.player.undoableAction { - PlayerServiceHost.player.shuffleQueue(start = 0) + if (!multiselect_context.is_active) { + SpMp.context.vibrateShort() + PlayerServiceHost.player.shuffleQueue(start = 0) + } } - } + }) ), color = background_colour, - shape = FilledButtonTokens.ContainerShape.toShape() + shape = FilledButtonTokens.ContainerShape.toShape(), + border = multiselect_context.getActiveHintBorder() ) { - Row( + Box( Modifier .defaultMinSize( minWidth = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight ) .padding(ButtonDefaults.ContentPadding), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + contentAlignment = Alignment.Center ) { Text( text = getString("queue_shuffle"), @@ -270,7 +331,7 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon } if (radio_info_position == NowPlayingQueueRadioInfoPosition.TOP_BAR) { - CurrentRadioIndicator(queueBackgroundColourProvider, backgroundColourProvider, playerProvider) + CurrentRadioIndicator(queueBackgroundColourProvider, backgroundColourProvider, playerProvider, multiselect_context) } Divider(Modifier.padding(horizontal = list_padding), 1.dp, backgroundColourProvider) @@ -296,13 +357,12 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon contentPadding = PaddingValues(top = list_padding, bottom = 60.dp), modifier = Modifier .reorderable(state) - .detectReorderAfterLongPress(state) .padding(horizontal = list_padding), horizontalAlignment = Alignment.CenterHorizontally ) { if (radio_info_position == NowPlayingQueueRadioInfoPosition.ABOVE_ITEMS) { item { - CurrentRadioIndicator(queueBackgroundColourProvider, backgroundColourProvider, playerProvider) + CurrentRadioIndicator(queueBackgroundColourProvider, backgroundColourProvider, playerProvider, multiselect_context) } } @@ -325,7 +385,8 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon if (current) backgroundColourProvider() else queueBackgroundColourProvider() }, - playerProvider + playerProvider, + multiselect_context ) { PlayerServiceHost.player.undoableAction { PlayerServiceHost.player.removeFromQueue(index) @@ -354,47 +415,54 @@ fun QueueTab(expansionProvider: () -> Float, playerProvider: () -> PlayerViewCon private fun CurrentRadioIndicator( backgroundColourProvider: () -> Color, accentColourProvider: () -> Color, - playerProvider: () -> PlayerViewContext + playerProvider: () -> PlayerViewContext, + multiselect_context: MediaItemMultiSelectContext ) { - Column { + val horizontal_padding = 15.dp + Column(Modifier.animateContentSize()) { val radio_item: MediaItem? = PlayerServiceHost.player.radio_item if (radio_item != null && radio_item !is Song) { radio_item.PreviewLong(MediaItem.PreviewParams( playerProvider, - Modifier.padding(horizontal = 15.dp), + Modifier.padding(horizontal = horizontal_padding), contentColour = { backgroundColourProvider().getContrasted() } )) } val filters = PlayerServiceHost.player.radio_filters - val current_filter = PlayerServiceHost.player.radio_current_filter - if (filters != null) { - Row( - Modifier.horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(15.dp) - ) { - Spacer(Modifier) - - for (filter in listOf(null) + filters.withIndex()) { - FilterChip( - current_filter == filter?.index, - onClick = { - if (PlayerServiceHost.player.radio_current_filter != filter?.index) { - PlayerServiceHost.player.radio_current_filter = filter?.index - } - }, - label = { - Text( - filter?.value?.joinToString("|") { it.getReadable() } - ?: getString("radio_filter_all") + Crossfade(multiselect_context.is_active) { multiselect_active -> + if (multiselect_active) { + multiselect_context.InfoDisplay(Modifier.fillMaxWidth().padding(horizontal = horizontal_padding)) + } + else if (filters != null) { + Row( + Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + Spacer(Modifier) + + val current_filter = PlayerServiceHost.player.radio_current_filter + for (filter in listOf(null) + filters.withIndex()) { + FilterChip( + current_filter == filter?.index, + onClick = { + if (PlayerServiceHost.player.radio_current_filter != filter?.index) { + PlayerServiceHost.player.radio_current_filter = filter?.index + } + }, + label = { + Text( + filter?.value?.joinToString("|") { it.getReadable() } + ?: getString("radio_filter_all") + ) + }, + colors = FilterChipDefaults.filterChipColors( + labelColor = backgroundColourProvider().getContrasted(), + selectedContainerColor = accentColourProvider(), + selectedLabelColor = accentColourProvider().getContrasted() ) - }, - colors = FilterChipDefaults.filterChipColors( - labelColor = backgroundColourProvider().getContrasted(), - selectedContainerColor = accentColourProvider(), - selectedLabelColor = accentColourProvider().getContrasted() ) - ) + } } } } diff --git a/shared/src/commonMain/kotlin/com/spectre7/utils/Common.kt b/shared/src/commonMain/kotlin/com/spectre7/utils/Common.kt index d94f22844..7a2a93312 100644 --- a/shared/src/commonMain/kotlin/com/spectre7/utils/Common.kt +++ b/shared/src/commonMain/kotlin/com/spectre7/utils/Common.kt @@ -178,3 +178,9 @@ fun String.indexOfFirstOrNull(start: Int = 0, predicate: (Char) -> Boolean): Int } return null } + +fun MutableList.swap(index_a: Int, index_b: Int){ + val a = this[index_a] + this[index_a] = this[index_b] + this[index_b] = a +}