Settings.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <script setup lang="ts">
  2. import Toast from "toasters";
  3. import { storeToRefs } from "pinia";
  4. import { ref, onBeforeUnmount, onMounted, watch } from "vue";
  5. import validation from "@/validation";
  6. import { useWebsocketsStore } from "@/stores/websockets";
  7. import { useUserAuthStore } from "@/stores/userAuth";
  8. import { useEditPlaylistStore } from "@/stores/editPlaylist";
  9. import { useModalsStore } from "@/stores/modals";
  10. import { useForm } from "@/composables/useForm";
  11. const props = defineProps({
  12. modalUuid: { type: String, required: true }
  13. });
  14. const userAuthStore = useUserAuthStore();
  15. const { loggedIn, userId } = storeToRefs(userAuthStore);
  16. const { hasPermission } = userAuthStore;
  17. const { socket } = useWebsocketsStore();
  18. const editPlaylistStore = useEditPlaylistStore({ modalUuid: props.modalUuid });
  19. const { playlist } = storeToRefs(editPlaylistStore);
  20. const { preventCloseUnsaved } = useModalsStore();
  21. const featured = ref(playlist.value.featured);
  22. const isOwner = () =>
  23. loggedIn.value && userId.value === playlist.value.createdBy;
  24. const isEditable = permission => {
  25. if (permission === "playlists.update.featured")
  26. return playlist.value.type !== "station" && hasPermission(permission);
  27. if (
  28. ["user", "user-liked", "user-disliked", "admin"].includes(
  29. playlist.value.type
  30. )
  31. )
  32. return isOwner() || hasPermission(permission);
  33. if (
  34. playlist.value.type === "genre" &&
  35. permission === "playlists.update.privacy"
  36. )
  37. return hasPermission(permission);
  38. return false;
  39. };
  40. const {
  41. inputs: displayNameInputs,
  42. unsavedChanges: displayNameUnsaved,
  43. save: saveDisplayName,
  44. setOriginalValue: setDisplayName
  45. } = useForm(
  46. {
  47. displayName: {
  48. value: playlist.value.displayName,
  49. validate: value => {
  50. if (!validation.isLength(value, 1, 64))
  51. return "Display name must have between 1 and 64 characters.";
  52. if (!validation.regex.ascii.test(value))
  53. return "Invalid display name format. Only ASCII characters are allowed.";
  54. return true;
  55. }
  56. }
  57. },
  58. ({ status, messages, values }, resolve, reject) => {
  59. if (status === "success")
  60. socket.dispatch(
  61. "playlists.updateDisplayName",
  62. playlist.value._id,
  63. values.displayName,
  64. res => {
  65. playlist.value.displayName = values.displayName;
  66. if (res.status === "success") {
  67. resolve();
  68. new Toast(res.message);
  69. } else reject(new Error(res.message));
  70. }
  71. );
  72. else {
  73. Object.values(messages).forEach(message => {
  74. new Toast({ content: message, timeout: 8000 });
  75. });
  76. resolve();
  77. }
  78. },
  79. {
  80. modalUuid: props.modalUuid,
  81. preventCloseUnsaved: false
  82. }
  83. );
  84. const {
  85. inputs: privacyInputs,
  86. unsavedChanges: privacyUnsaved,
  87. save: savePrivacy,
  88. setOriginalValue: setPrivacy
  89. } = useForm(
  90. { privacy: playlist.value.privacy },
  91. ({ status, messages, values }, resolve, reject) => {
  92. if (status === "success")
  93. socket.dispatch(
  94. playlist.value.type === "genre" ||
  95. playlist.value.type === "admin"
  96. ? "playlists.updatePrivacyAdmin"
  97. : "playlists.updatePrivacy",
  98. playlist.value._id,
  99. values.privacy,
  100. res => {
  101. playlist.value.privacy = values.privacy;
  102. if (values.privacy !== "public") featured.value = false;
  103. if (res.status === "success") {
  104. resolve();
  105. new Toast(res.message);
  106. } else reject(new Error(res.message));
  107. }
  108. );
  109. else {
  110. if (messages[status]) new Toast(messages[status]);
  111. resolve();
  112. }
  113. },
  114. {
  115. modalUuid: props.modalUuid,
  116. preventCloseUnsaved: false
  117. }
  118. );
  119. const toggleFeatured = () => {
  120. if (playlist.value.privacy !== "public") return;
  121. featured.value = !featured.value;
  122. socket.dispatch(
  123. "playlists.updateFeatured",
  124. playlist.value._id,
  125. featured.value,
  126. res => {
  127. playlist.value.featured = featured.value;
  128. new Toast(res.message);
  129. }
  130. );
  131. };
  132. watch(playlist, (value, oldValue) => {
  133. if (value.displayName !== oldValue.displayName)
  134. setDisplayName({ displayName: value.displayName });
  135. if (value.privacy !== oldValue.privacy) {
  136. setPrivacy({ privacy: value.privacy });
  137. if (value.privacy !== "public") featured.value = false;
  138. }
  139. if (value.featured !== oldValue.featured) featured.value = value.featured;
  140. });
  141. onMounted(() => {
  142. preventCloseUnsaved[props.modalUuid] = () =>
  143. displayNameUnsaved.value.length + privacyUnsaved.value.length > 0;
  144. });
  145. onBeforeUnmount(() => {
  146. delete preventCloseUnsaved[props.modalUuid];
  147. });
  148. </script>
  149. <template>
  150. <div class="settings-tab section">
  151. <div
  152. v-if="
  153. isEditable('playlists.update.displayName') &&
  154. !(
  155. playlist.type === 'user-liked' ||
  156. playlist.type === 'user-disliked'
  157. )
  158. "
  159. >
  160. <label class="label"> Change display name </label>
  161. <div class="control is-grouped input-with-button">
  162. <p class="control is-expanded">
  163. <input
  164. v-model="displayNameInputs['displayName'].value"
  165. class="input"
  166. type="text"
  167. placeholder="Playlist Display Name"
  168. @keyup.enter="saveDisplayName()"
  169. />
  170. </p>
  171. <p class="control">
  172. <button
  173. class="button is-info"
  174. @click.prevent="saveDisplayName()"
  175. >
  176. Rename
  177. </button>
  178. </p>
  179. </div>
  180. </div>
  181. <div v-if="isEditable('playlists.update.privacy')">
  182. <label class="label"> Change privacy </label>
  183. <div class="control is-grouped input-with-button">
  184. <div class="control is-expanded select">
  185. <select v-model="privacyInputs['privacy'].value">
  186. <option value="private">Private</option>
  187. <option value="public">Public</option>
  188. </select>
  189. </div>
  190. <p class="control">
  191. <button
  192. class="button is-info"
  193. @click.prevent="savePrivacy()"
  194. >
  195. Update Privacy
  196. </button>
  197. </p>
  198. </div>
  199. </div>
  200. <div
  201. v-if="isEditable('playlists.update.featured')"
  202. class="control is-expanded checkbox-control"
  203. >
  204. <label class="switch">
  205. <input
  206. type="checkbox"
  207. id="featured"
  208. :checked="featured"
  209. @click="toggleFeatured"
  210. :disabled="playlist.privacy !== 'public'"
  211. />
  212. <span
  213. v-if="playlist.privacy === 'public'"
  214. class="slider round"
  215. ></span>
  216. <span
  217. v-else
  218. class="slider round disabled"
  219. content="Only public playlists can be featured"
  220. v-tippy
  221. ></span>
  222. </label>
  223. <label class="label" for="featured">Featured Playlist</label>
  224. </div>
  225. </div>
  226. </template>
  227. <style lang="less" scoped>
  228. .checkbox-control label.label {
  229. margin-left: 10px;
  230. }
  231. @media screen and (max-width: 1300px) {
  232. .section {
  233. max-width: 100% !important;
  234. }
  235. }
  236. </style>