Settings.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <script setup lang="ts">
  2. import Toast from "toasters";
  3. import { storeToRefs } from "pinia";
  4. import { 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 isOwner = () =>
  22. loggedIn.value && userId.value === playlist.value.createdBy;
  23. const isEditable = permission =>
  24. ((playlist.value.type === "user" ||
  25. playlist.value.type === "user-liked" ||
  26. playlist.value.type === "user-disliked" ||
  27. playlist.value.type === "admin") &&
  28. (isOwner() || hasPermission(permission))) ||
  29. (playlist.value.type === "genre" &&
  30. permission === "playlists.update.privacy" &&
  31. hasPermission(permission));
  32. const {
  33. inputs: displayNameInputs,
  34. unsavedChanges: displayNameUnsaved,
  35. save: saveDisplayName,
  36. setOriginalValue: setDisplayName
  37. } = useForm(
  38. {
  39. displayName: {
  40. value: playlist.value.displayName,
  41. validate: value => {
  42. if (!validation.isLength(value, 1, 64))
  43. return "Display name must have between 1 and 64 characters.";
  44. if (!validation.regex.ascii.test(value))
  45. return "Invalid display name format. Only ASCII characters are allowed.";
  46. return true;
  47. }
  48. }
  49. },
  50. ({ status, messages, values }, resolve, reject) => {
  51. if (status === "success")
  52. socket.dispatch(
  53. "playlists.updateDisplayName",
  54. playlist.value._id,
  55. values.displayName,
  56. res => {
  57. playlist.value.displayName = values.displayName;
  58. if (res.status === "success") {
  59. resolve();
  60. new Toast(res.message);
  61. } else reject(new Error(res.message));
  62. }
  63. );
  64. else {
  65. Object.values(messages).forEach(message => {
  66. new Toast({ content: message, timeout: 8000 });
  67. });
  68. resolve();
  69. }
  70. },
  71. {
  72. modalUuid: props.modalUuid,
  73. preventCloseUnsaved: false
  74. }
  75. );
  76. const {
  77. inputs: privacyInputs,
  78. unsavedChanges: privacyUnsaved,
  79. save: savePrivacy,
  80. setOriginalValue: setPrivacy
  81. } = useForm(
  82. { privacy: playlist.value.privacy },
  83. ({ status, messages, values }, resolve, reject) => {
  84. if (status === "success")
  85. socket.dispatch(
  86. playlist.value.type === "genre" ||
  87. playlist.value.type === "admin"
  88. ? "playlists.updatePrivacyAdmin"
  89. : "playlists.updatePrivacy",
  90. playlist.value._id,
  91. values.privacy,
  92. res => {
  93. playlist.value.privacy = values.privacy;
  94. if (res.status === "success") {
  95. resolve();
  96. new Toast(res.message);
  97. } else reject(new Error(res.message));
  98. }
  99. );
  100. else {
  101. if (messages[status]) new Toast(messages[status]);
  102. resolve();
  103. }
  104. },
  105. {
  106. modalUuid: props.modalUuid,
  107. preventCloseUnsaved: false
  108. }
  109. );
  110. watch(playlist, (value, oldValue) => {
  111. if (value.displayName !== oldValue.displayName)
  112. setDisplayName({ displayName: value.displayName });
  113. if (value.privacy !== oldValue.privacy)
  114. setPrivacy({ privacy: value.privacy });
  115. });
  116. onMounted(() => {
  117. preventCloseUnsaved[props.modalUuid] = () =>
  118. displayNameUnsaved.value.length + privacyUnsaved.value.length > 0;
  119. });
  120. onBeforeUnmount(() => {
  121. delete preventCloseUnsaved[props.modalUuid];
  122. });
  123. </script>
  124. <template>
  125. <div class="settings-tab section">
  126. <div
  127. v-if="
  128. isEditable('playlists.update.displayName') &&
  129. !(
  130. playlist.type === 'user-liked' ||
  131. playlist.type === 'user-disliked'
  132. )
  133. "
  134. >
  135. <label class="label"> Change display name </label>
  136. <div class="control is-grouped input-with-button">
  137. <p class="control is-expanded">
  138. <input
  139. v-model="displayNameInputs['displayName'].value"
  140. class="input"
  141. type="text"
  142. placeholder="Playlist Display Name"
  143. @keyup.enter="saveDisplayName()"
  144. />
  145. </p>
  146. <p class="control">
  147. <button
  148. class="button is-info"
  149. @click.prevent="saveDisplayName()"
  150. >
  151. Rename
  152. </button>
  153. </p>
  154. </div>
  155. </div>
  156. <div v-if="isEditable('playlists.update.privacy')">
  157. <label class="label"> Change privacy </label>
  158. <div class="control is-grouped input-with-button">
  159. <div class="control is-expanded select">
  160. <select v-model="privacyInputs['privacy'].value">
  161. <option value="private">Private</option>
  162. <option value="public">Public</option>
  163. </select>
  164. </div>
  165. <p class="control">
  166. <button
  167. class="button is-info"
  168. @click.prevent="savePrivacy()"
  169. >
  170. Update Privacy
  171. </button>
  172. </p>
  173. </div>
  174. </div>
  175. </div>
  176. </template>
  177. <style lang="less" scoped>
  178. @media screen and (max-width: 1300px) {
  179. .section {
  180. max-width: 100% !important;
  181. }
  182. }
  183. </style>