Queue.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <script lang="ts" setup>
  2. import { defineAsyncComponent, onMounted, ref } from "vue";
  3. import { DraggableList } from "vue-draggable-list";
  4. import Toast from "toasters";
  5. import { Station } from "@/types/station";
  6. import { useWebsocketsStore } from "@/stores/websockets";
  7. import { useUserAuthStore } from "@/stores/userAuth";
  8. import DropdownListItem from "@/pages/NewStation/Components/DropdownListItem.vue";
  9. const MediaItem = defineAsyncComponent(
  10. () => import("@/pages/NewStation/Components/MediaItem.vue")
  11. );
  12. const props = defineProps<{
  13. station: Station;
  14. }>();
  15. const { hasPermissionForStation } = useUserAuthStore();
  16. const { socket } = useWebsocketsStore();
  17. const mediaItems = ref({});
  18. const queue = ref([]);
  19. const applyQueueOrder = queueOrder => {
  20. queue.value.sort((previous, current) => {
  21. const currentIndex = queueOrder.findIndex(
  22. mediaSource => mediaSource === previous.mediaSource
  23. );
  24. const previousIndex = queueOrder.findIndex(
  25. mediaSource => mediaSource === current.mediaSource
  26. );
  27. if (currentIndex > previousIndex) return 1;
  28. if (currentIndex < previousIndex) return -1;
  29. return 0;
  30. });
  31. };
  32. const getQueue = () => {
  33. socket.dispatch("stations.getQueue", props.station._id, res => {
  34. if (res.status === "success") {
  35. queue.value = res.data.queue;
  36. }
  37. });
  38. };
  39. const removeFromQueue = (media, index) => {
  40. mediaItems.value[`media-item-${index}`].collapseActions();
  41. socket.dispatch(
  42. "stations.removeFromQueue",
  43. props.station._id,
  44. media.mediaSource,
  45. res => {
  46. if (res.status === "success")
  47. new Toast("Successfully removed song from the queue.");
  48. else new Toast(res.message);
  49. }
  50. );
  51. };
  52. const repositionSongInQueue = ({ moved, song }) => {
  53. const { oldIndex, newIndex } = moved;
  54. if (oldIndex === newIndex) return; // we only need to update when song is moved
  55. const _song = song ?? queue.value[newIndex];
  56. socket.dispatch(
  57. "stations.repositionSongInQueue",
  58. props.station._id,
  59. {
  60. ..._song,
  61. oldIndex,
  62. newIndex
  63. },
  64. res => {
  65. new Toast({ content: res.message, timeout: 4000 });
  66. if (res.status === "success") return;
  67. queue.value.splice(oldIndex, 0, queue.value.splice(newIndex, 1)[0]);
  68. }
  69. );
  70. };
  71. const moveToQueueTop = (media, oldIndex) => {
  72. mediaItems.value[`media-item-${oldIndex}`].collapseActions();
  73. repositionSongInQueue({
  74. moved: {
  75. oldIndex,
  76. newIndex: 0
  77. },
  78. song: media
  79. });
  80. };
  81. const moveToQueueBottom = (media, oldIndex) => {
  82. mediaItems.value[`media-item-${oldIndex}`].collapseActions();
  83. repositionSongInQueue({
  84. moved: {
  85. oldIndex,
  86. newIndex: queue.value.length - 1
  87. },
  88. song: media
  89. });
  90. };
  91. onMounted(() => {
  92. socket.onConnect(() => {
  93. getQueue();
  94. socket.on("event:station.queue.updated", res => {
  95. queue.value = res.data.queue;
  96. });
  97. socket.on("event:station.queue.order.changed", res => {
  98. applyQueueOrder(res.data.queueOrder);
  99. });
  100. });
  101. });
  102. </script>
  103. <template>
  104. <ul>
  105. <DraggableList
  106. v-model:list="queue"
  107. tag="li"
  108. item-key="mediaSource"
  109. :disabled="
  110. !hasPermissionForStation(
  111. station._id,
  112. 'stations.queue.reposition'
  113. )
  114. "
  115. @update="repositionSongInQueue"
  116. >
  117. <template #item="{ element: media, index }">
  118. <MediaItem
  119. :media="media"
  120. :ref="el => (mediaItems[`media-item-${index}`] = el)"
  121. >
  122. <template
  123. v-if="
  124. hasPermissionForStation(
  125. station._id,
  126. 'stations.queue.reposition'
  127. )
  128. "
  129. #actions
  130. >
  131. <DropdownListItem
  132. v-if="index > 0"
  133. icon="vertical_align_top"
  134. label="Move to top of queue"
  135. @click="() => moveToQueueTop(media, index)"
  136. />
  137. <DropdownListItem
  138. v-if="queue.length - 1 !== index"
  139. icon="vertical_align_bottom"
  140. label="Move to bottom of queue"
  141. @click="() => moveToQueueBottom(media, index)"
  142. />
  143. <!-- TODO: Quick confirm -->
  144. <DropdownListItem
  145. v-if="
  146. hasPermissionForStation(
  147. station._id,
  148. 'stations.queue.remove'
  149. )
  150. "
  151. icon="delete"
  152. label="Remove from queue"
  153. @click="() => removeFromQueue(media, index)"
  154. />
  155. </template>
  156. </MediaItem>
  157. </template>
  158. </DraggableList>
  159. </ul>
  160. </template>
  161. <style lang="less" scoped>
  162. ul {
  163. display: flex;
  164. flex-direction: column;
  165. gap: 10px;
  166. list-style: none;
  167. padding: 0;
  168. overflow: auto;
  169. :deep(li) {
  170. margin: 0 !important;
  171. }
  172. }
  173. </style>