Queue.vue 5.8 KB

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