Queue.vue 5.4 KB

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