AddToPlaylistDropdown.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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. socket.on(
  121. "event:playlist.song.replaced",
  122. res => {
  123. playlists.value.forEach((playlist, index) => {
  124. if (playlist._id === res.data.playlistId) {
  125. playlists.value[index].songs = playlists.value[
  126. index
  127. ].songs.map(song =>
  128. song.mediaSource === res.data.oldMediaSource
  129. ? res.data.song
  130. : song
  131. );
  132. }
  133. });
  134. },
  135. { replaceable: true }
  136. );
  137. });
  138. </script>
  139. <template>
  140. <tippy
  141. class="addToPlaylistDropdown"
  142. :touch="true"
  143. :interactive="true"
  144. :placement="placement"
  145. theme="dropdown"
  146. ref="dropdown"
  147. trigger="click"
  148. append-to="parent"
  149. @show="emit('showPlaylistDropdown', true)"
  150. @hide="emit('showPlaylistDropdown', false)"
  151. >
  152. <slot name="button" ref="trigger" />
  153. <template #content>
  154. <div class="nav-dropdown-items" v-if="playlists.length > 0">
  155. <button
  156. class="nav-item"
  157. v-for="(playlist, index) in playlists"
  158. :key="playlist._id"
  159. @click.prevent="toggleSongInPlaylist(index)"
  160. :title="playlist.displayName"
  161. >
  162. <p class="control is-expanded checkbox-control">
  163. <label class="switch">
  164. <input
  165. type="checkbox"
  166. :id="`${index}`"
  167. :checked="hasSong(playlist)"
  168. @click="toggleSongInPlaylist(index)"
  169. />
  170. <span class="slider round"></span>
  171. </label>
  172. <label :for="`${index}`">
  173. <span></span>
  174. <p>{{ playlist.displayName }}</p>
  175. </label>
  176. </p>
  177. </button>
  178. </div>
  179. <p v-else class="no-playlists">
  180. You haven't created any playlists.
  181. </p>
  182. <button
  183. id="create-playlist"
  184. class="button is-primary"
  185. @click="createPlaylist()"
  186. >
  187. <i class="material-icons icon-with-button"> edit </i>
  188. Create Playlist
  189. </button>
  190. </template>
  191. </tippy>
  192. </template>
  193. <style lang="less" scoped>
  194. .no-playlists {
  195. text-align: center;
  196. margin-top: 10px;
  197. }
  198. #create-playlist .material-icons {
  199. color: var(--white);
  200. }
  201. </style>