Queue.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, ref, computed, onUpdated } from "vue";
  3. import Toast from "toasters";
  4. import { DraggableList } from "vue-draggable-list";
  5. import { useWebsocketsStore } from "@/stores/websockets";
  6. import { useStationStore } from "@/stores/station";
  7. import { useManageStationStore } from "@/stores/manageStation";
  8. const SongItem = defineAsyncComponent(
  9. () => import("@/components/SongItem.vue")
  10. );
  11. const QuickConfirm = defineAsyncComponent(
  12. () => import("@/components/QuickConfirm.vue")
  13. );
  14. const props = defineProps({
  15. modalUuid: { type: String, default: "" },
  16. sector: { type: String, default: "station" }
  17. });
  18. const { socket } = useWebsocketsStore();
  19. const stationStore = useStationStore();
  20. const manageStationStore = useManageStationStore(props);
  21. const actionableButtonVisible = ref(false);
  22. const drag = ref(false);
  23. const songItems = ref([]);
  24. const station = computed({
  25. get: () => {
  26. if (props.sector === "manageStation") return manageStationStore.station;
  27. return stationStore.station;
  28. },
  29. set: value => {
  30. if (props.sector === "manageStation")
  31. manageStationStore.updateStation(value);
  32. else stationStore.updateStation(value);
  33. }
  34. });
  35. const queue = computed({
  36. get: () => {
  37. if (props.sector === "manageStation")
  38. return manageStationStore.songsList;
  39. return stationStore.songsList;
  40. },
  41. set: value => {
  42. if (props.sector === "manageStation")
  43. manageStationStore.updateSongsList(value);
  44. else stationStore.updateSongsList(value);
  45. }
  46. });
  47. const hasPermission = permission =>
  48. props.sector === "manageStation"
  49. ? manageStationStore.hasPermission(permission)
  50. : stationStore.hasPermission(permission);
  51. const removeFromQueue = youtubeId => {
  52. socket.dispatch(
  53. "stations.removeFromQueue",
  54. station.value._id,
  55. youtubeId,
  56. res => {
  57. if (res.status === "success")
  58. new Toast("Successfully removed song from the queue.");
  59. else new Toast(res.message);
  60. }
  61. );
  62. };
  63. const repositionSongInQueue = ({ moved }) => {
  64. const { oldIndex, newIndex } = moved;
  65. if (oldIndex === newIndex) return; // we only need to update when song is moved
  66. const song = queue.value[newIndex];
  67. socket.dispatch(
  68. "stations.repositionSongInQueue",
  69. station.value._id,
  70. {
  71. ...song,
  72. oldIndex,
  73. newIndex
  74. },
  75. res => {
  76. new Toast({ content: res.message, timeout: 4000 });
  77. if (res.status !== "success")
  78. queue.value.splice(
  79. oldIndex,
  80. 0,
  81. queue.value.splice(newIndex, 1)[0]
  82. );
  83. }
  84. );
  85. };
  86. const moveSongToTop = index => {
  87. songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
  88. queue.value.splice(0, 0, queue.value.splice(index, 1)[0]);
  89. repositionSongInQueue({
  90. moved: {
  91. oldIndex: index,
  92. newIndex: 0
  93. }
  94. });
  95. };
  96. const moveSongToBottom = index => {
  97. songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
  98. queue.value.splice(
  99. queue.value.length - 1,
  100. 0,
  101. queue.value.splice(index, 1)[0]
  102. );
  103. repositionSongInQueue({
  104. moved: {
  105. oldIndex: index,
  106. newIndex: queue.value.length - 1
  107. }
  108. });
  109. };
  110. onUpdated(() => {
  111. // check if actionable button is visible, if not: set max-height of queue items to 100%
  112. actionableButtonVisible.value =
  113. document
  114. .getElementById("queue")
  115. .querySelectorAll(".tab-actionable-button").length > 0;
  116. });
  117. </script>
  118. <template>
  119. <div id="queue">
  120. <div
  121. v-if="queue.length > 0"
  122. :class="{
  123. 'actionable-button-hidden': !actionableButtonVisible,
  124. 'scrollable-list': true
  125. }"
  126. >
  127. <draggable-list
  128. v-model:list="queue"
  129. item-key="_id"
  130. @start="drag = true"
  131. @end="drag = false"
  132. @update="repositionSongInQueue"
  133. :disabled="!hasPermission('stations.queue.reposition')"
  134. >
  135. <template #item="{ element, index }">
  136. <song-item
  137. :song="element"
  138. :requested-by="true"
  139. :disabled-actions="[]"
  140. :ref="el => (songItems[`song-item-${index}`] = el)"
  141. >
  142. <template
  143. v-if="hasPermission('stations.queue.reposition')"
  144. #tippyActions
  145. >
  146. <quick-confirm
  147. v-if="hasPermission('stations.queue.remove')"
  148. placement="left"
  149. @confirm="removeFromQueue(element.youtubeId)"
  150. >
  151. <i
  152. class="material-icons delete-icon"
  153. content="Remove Song from Queue"
  154. v-tippy
  155. >delete_forever</i
  156. >
  157. </quick-confirm>
  158. <i
  159. class="material-icons"
  160. v-if="index > 0"
  161. @click="moveSongToTop(index)"
  162. content="Move to top of Queue"
  163. v-tippy
  164. >vertical_align_top</i
  165. >
  166. <i
  167. v-if="queue.length - 1 !== index"
  168. @click="moveSongToBottom(index)"
  169. class="material-icons"
  170. content="Move to bottom of Queue"
  171. v-tippy
  172. >vertical_align_bottom</i
  173. >
  174. </template>
  175. </song-item>
  176. </template>
  177. </draggable-list>
  178. </div>
  179. <p class="nothing-here-text has-text-centered" v-else>
  180. There are no songs currently queued
  181. </p>
  182. </div>
  183. </template>
  184. <style lang="less" scoped>
  185. .night-mode {
  186. #queue {
  187. background-color: var(--dark-grey-3) !important;
  188. border: 0 !important;
  189. }
  190. }
  191. #queue {
  192. background-color: var(--white);
  193. border-radius: 0 0 @border-radius @border-radius;
  194. user-select: none;
  195. .actionable-button-hidden {
  196. max-height: 100%;
  197. }
  198. #queue-locked {
  199. display: flex;
  200. justify-content: center;
  201. }
  202. button.disabled {
  203. filter: grayscale(0.4);
  204. }
  205. }
  206. </style>