Queue.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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 removeFromQueue = youtubeId => {
  57. socket.dispatch(
  58. "stations.removeFromQueue",
  59. station.value._id,
  60. youtubeId,
  61. res => {
  62. if (res.status === "success")
  63. new Toast("Successfully removed song from the queue.");
  64. else new Toast(res.message);
  65. }
  66. );
  67. };
  68. const repositionSongInQueue = ({ moved }) => {
  69. const { oldIndex, newIndex } = moved;
  70. if (oldIndex === newIndex) return; // we only need to update when song is moved
  71. const song = queue.value[newIndex];
  72. socket.dispatch(
  73. "stations.repositionSongInQueue",
  74. station.value._id,
  75. {
  76. ...song,
  77. oldIndex,
  78. newIndex
  79. },
  80. res => {
  81. new Toast({ content: res.message, timeout: 4000 });
  82. if (res.status !== "success")
  83. queue.value.splice(
  84. oldIndex,
  85. 0,
  86. queue.value.splice(newIndex, 1)[0]
  87. );
  88. }
  89. );
  90. };
  91. const moveSongToTop = index => {
  92. songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
  93. queue.value.splice(0, 0, queue.value.splice(index, 1)[0]);
  94. repositionSongInQueue({
  95. moved: {
  96. oldIndex: index,
  97. newIndex: 0
  98. }
  99. });
  100. };
  101. const moveSongToBottom = index => {
  102. songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
  103. queue.value.splice(
  104. queue.value.length - 1,
  105. 0,
  106. queue.value.splice(index, 1)[0]
  107. );
  108. repositionSongInQueue({
  109. moved: {
  110. oldIndex: index,
  111. newIndex: queue.value.length - 1
  112. }
  113. });
  114. };
  115. onUpdated(() => {
  116. // check if actionable button is visible, if not: set max-height of queue items to 100%
  117. actionableButtonVisible.value =
  118. document
  119. .getElementById("queue")
  120. .querySelectorAll(".tab-actionable-button").length > 0;
  121. });
  122. </script>
  123. <template>
  124. <div id="queue">
  125. <div
  126. v-if="queue.length > 0"
  127. :class="{
  128. 'actionable-button-hidden': !actionableButtonVisible,
  129. 'scrollable-list': true
  130. }"
  131. >
  132. <draggable
  133. :component-data="{
  134. name: !drag ? 'draggable-list-transition' : null
  135. }"
  136. :name="`queue-${modalUuid}-${sector}`"
  137. v-model:list="queue"
  138. item-key="_id"
  139. @start="drag = true"
  140. @end="drag = false"
  141. @update="repositionSongInQueue"
  142. :disabled="!(isAdminOnly() || isOwnerOnly())"
  143. >
  144. <template #item="{ element, index }">
  145. <song-item
  146. :song="element"
  147. :requested-by="true"
  148. :disabled-actions="[]"
  149. :ref="el => (songItems[`song-item-${index}`] = el)"
  150. >
  151. <template
  152. v-if="isAdminOnly() || isOwnerOnly()"
  153. #tippyActions
  154. >
  155. <quick-confirm
  156. v-if="isOwnerOnly() || isAdminOnly()"
  157. placement="left"
  158. @confirm="removeFromQueue(element.youtubeId)"
  159. >
  160. <i
  161. class="material-icons delete-icon"
  162. content="Remove Song from Queue"
  163. v-tippy
  164. >delete_forever</i
  165. >
  166. </quick-confirm>
  167. <i
  168. class="material-icons"
  169. v-if="index > 0"
  170. @click="moveSongToTop(index)"
  171. content="Move to top of Queue"
  172. v-tippy
  173. >vertical_align_top</i
  174. >
  175. <i
  176. v-if="queue.length - 1 !== index"
  177. @click="moveSongToBottom(index)"
  178. class="material-icons"
  179. content="Move to bottom of Queue"
  180. v-tippy
  181. >vertical_align_bottom</i
  182. >
  183. </template>
  184. </song-item>
  185. </template>
  186. </draggable>
  187. </div>
  188. <p class="nothing-here-text has-text-centered" v-else>
  189. There are no songs currently queued
  190. </p>
  191. </div>
  192. </template>
  193. <style lang="less" scoped>
  194. .night-mode {
  195. #queue {
  196. background-color: var(--dark-grey-3) !important;
  197. border: 0 !important;
  198. }
  199. }
  200. #queue {
  201. background-color: var(--white);
  202. border-radius: 0 0 @border-radius @border-radius;
  203. user-select: none;
  204. .actionable-button-hidden {
  205. max-height: 100%;
  206. }
  207. :deep(.draggable-item:not(:last-of-type)) {
  208. margin-bottom: 10px;
  209. }
  210. #queue-locked {
  211. display: flex;
  212. justify-content: center;
  213. }
  214. button.disabled {
  215. filter: grayscale(0.4);
  216. }
  217. }
  218. </style>