Settings.vue 6.2 KB

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