Queue.vue 5.6 KB

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