AddToPlaylistDropdown.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <script setup lang="ts">
  2. import { ref, onMounted } from "vue";
  3. import Toast from "toasters";
  4. import { storeToRefs } from "pinia";
  5. import {
  6. AddSongToPlaylistResponse,
  7. IndexMyPlaylistsResponse,
  8. RemoveSongFromPlaylistResponse
  9. } from "@musare_types/actions/PlaylistsActions";
  10. import { useWebsocketsStore } from "@/stores/websockets";
  11. import { useUserPlaylistsStore } from "@/stores/userPlaylists";
  12. import { useModalsStore } from "@/stores/modals";
  13. const props = defineProps({
  14. song: {
  15. type: Object,
  16. default: () => {}
  17. },
  18. placement: {
  19. type: String,
  20. default: "left"
  21. }
  22. });
  23. const emit = defineEmits(["showPlaylistDropdown"]);
  24. const dropdown = ref(null);
  25. const { socket } = useWebsocketsStore();
  26. const userPlaylistsStore = useUserPlaylistsStore();
  27. const { playlists } = storeToRefs(userPlaylistsStore);
  28. const { setPlaylists, addPlaylist, removePlaylist } = userPlaylistsStore;
  29. const { openModal } = useModalsStore();
  30. const hasSong = playlist =>
  31. playlist.songs
  32. .map(song => song.mediaSource)
  33. .indexOf(props.song.mediaSource) !== -1;
  34. const toggleSongInPlaylist = playlistIndex => {
  35. const playlist = playlists.value[playlistIndex];
  36. if (!hasSong(playlist)) {
  37. socket.dispatch(
  38. "playlists.addSongToPlaylist",
  39. false,
  40. props.song.mediaSource,
  41. playlist._id,
  42. (res: AddSongToPlaylistResponse) => new Toast(res.message)
  43. );
  44. } else {
  45. socket.dispatch(
  46. "playlists.removeSongFromPlaylist",
  47. props.song.mediaSource,
  48. playlist._id,
  49. (res: RemoveSongFromPlaylistResponse) => new Toast(res.message)
  50. );
  51. }
  52. };
  53. const createPlaylist = () => {
  54. dropdown.value.tippy.setProps({
  55. zIndex: 0,
  56. hideOnClick: false
  57. });
  58. window.addToPlaylistDropdown = dropdown.value;
  59. openModal("createPlaylist");
  60. };
  61. onMounted(() => {
  62. socket.onConnect(() => {
  63. socket.dispatch(
  64. "playlists.indexMyPlaylists",
  65. (res: IndexMyPlaylistsResponse) => {
  66. if (res.status === "success") setPlaylists(res.data.playlists);
  67. }
  68. );
  69. });
  70. socket.on("event:playlist.created", res => addPlaylist(res.data.playlist), {
  71. replaceable: true
  72. });
  73. socket.on(
  74. "event:playlist.deleted",
  75. res => removePlaylist(res.data.playlistId),
  76. { replaceable: true }
  77. );
  78. socket.on(
  79. "event:playlist.displayName.updated",
  80. res => {
  81. playlists.value.forEach((playlist, index) => {
  82. if (playlist._id === res.data.playlistId) {
  83. playlists.value[index].displayName = res.data.displayName;
  84. }
  85. });
  86. },
  87. { replaceable: true }
  88. );
  89. socket.on(
  90. "event:playlist.song.added",
  91. res => {
  92. playlists.value.forEach((playlist, index) => {
  93. if (playlist._id === res.data.playlistId) {
  94. playlists.value[index].songs.push(res.data.song);
  95. }
  96. });
  97. },
  98. { replaceable: true }
  99. );
  100. socket.on(
  101. "event:playlist.song.removed",
  102. res => {
  103. playlists.value.forEach((playlist, playlistIndex) => {
  104. if (playlist._id === res.data.playlistId) {
  105. playlists.value[playlistIndex].songs.forEach(
  106. (song, songIndex) => {
  107. if (song.mediaSource === res.data.mediaSource) {
  108. playlists.value[playlistIndex].songs.splice(
  109. songIndex,
  110. 1
  111. );
  112. }
  113. }
  114. );
  115. }
  116. });
  117. },
  118. { replaceable: true }
  119. );
  120. });
  121. </script>
  122. <template>
  123. <tippy
  124. class="addToPlaylistDropdown"
  125. :touch="true"
  126. :interactive="true"
  127. :placement="placement"
  128. theme="dropdown"
  129. ref="dropdown"
  130. trigger="click"
  131. append-to="parent"
  132. @show="emit('showPlaylistDropdown', true)"
  133. @hide="emit('showPlaylistDropdown', false)"
  134. >
  135. <slot name="button" ref="trigger" />
  136. <template #content>
  137. <div class="nav-dropdown-items" v-if="playlists.length > 0">
  138. <button
  139. class="nav-item"
  140. v-for="(playlist, index) in playlists"
  141. :key="playlist._id"
  142. @click.prevent="toggleSongInPlaylist(index)"
  143. :title="playlist.displayName"
  144. >
  145. <p class="control is-expanded checkbox-control">
  146. <label class="switch">
  147. <input
  148. type="checkbox"
  149. :id="`${index}`"
  150. :checked="hasSong(playlist)"
  151. @click="toggleSongInPlaylist(index)"
  152. />
  153. <span class="slider round"></span>
  154. </label>
  155. <label :for="`${index}`">
  156. <span></span>
  157. <p>{{ playlist.displayName }}</p>
  158. </label>
  159. </p>
  160. </button>
  161. </div>
  162. <p v-else class="no-playlists">
  163. You haven't created any playlists.
  164. </p>
  165. <button
  166. id="create-playlist"
  167. class="button is-primary"
  168. @click="createPlaylist()"
  169. >
  170. <i class="material-icons icon-with-button"> edit </i>
  171. Create Playlist
  172. </button>
  173. </template>
  174. </tippy>
  175. </template>
  176. <style lang="less" scoped>
  177. .no-playlists {
  178. text-align: center;
  179. margin-top: 10px;
  180. }
  181. #create-playlist .material-icons {
  182. color: var(--white);
  183. }
  184. </style>