Settings.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, watch } from "vue";
  3. import Toast from "toasters";
  4. import { storeToRefs } from "pinia";
  5. import validation from "@/validation";
  6. import { useWebsocketsStore } from "@/stores/websockets";
  7. import { useManageStationStore } from "@/stores/manageStation";
  8. import { useForm } from "@/composables/useForm";
  9. const InfoIcon = defineAsyncComponent(
  10. () => import("@/components/InfoIcon.vue")
  11. );
  12. const props = defineProps({
  13. modalUuid: { type: String, required: true }
  14. });
  15. const { socket } = useWebsocketsStore();
  16. const manageStationStore = useManageStationStore({
  17. modalUuid: props.modalUuid
  18. });
  19. const { station } = storeToRefs(manageStationStore);
  20. const { editStation } = manageStationStore;
  21. const { inputs, save, setOriginalValue } = useForm(
  22. {
  23. name: {
  24. value: station.value.name,
  25. validate: value => {
  26. if (!validation.isLength(value, 2, 16))
  27. return "Name must have between 2 and 16 characters.";
  28. if (!validation.regex.az09_.test(value))
  29. return "Invalid name format. Allowed characters: a-z, 0-9 and _.";
  30. return true;
  31. }
  32. },
  33. displayName: {
  34. value: station.value.displayName,
  35. validate: value => {
  36. if (!validation.isLength(value, 2, 32))
  37. return "Display name must have between 2 and 32 characters.";
  38. if (!validation.regex.ascii.test(value))
  39. return "Invalid display name format. Only ASCII characters are allowed.";
  40. return true;
  41. }
  42. },
  43. description: {
  44. value: station.value.description,
  45. validate: value => {
  46. if (
  47. value
  48. .split("")
  49. .filter(character => character.charCodeAt(0) === 21328)
  50. .length !== 0
  51. )
  52. return "Invalid description format.";
  53. return true;
  54. }
  55. },
  56. theme: station.value.theme,
  57. privacy: station.value.privacy,
  58. requestsEnabled: station.value.requests.enabled,
  59. requestsAccess: station.value.requests.access,
  60. requestsLimit: station.value.requests.limit,
  61. autofillEnabled: station.value.autofill.enabled,
  62. autofillLimit: station.value.autofill.limit,
  63. autofillMode: station.value.autofill.mode
  64. },
  65. ({ status, messages, values }, resolve, reject) => {
  66. if (status === "success") {
  67. const oldStation = JSON.parse(JSON.stringify(station.value));
  68. const updatedStation = {
  69. ...oldStation,
  70. name: values.name,
  71. displayName: values.displayName,
  72. description: values.description,
  73. theme: values.theme,
  74. privacy: values.privacy,
  75. requests: {
  76. ...oldStation.requests,
  77. enabled: values.requestsEnabled,
  78. access: values.requestsAccess,
  79. limit: values.requestsLimit
  80. },
  81. autofill: {
  82. ...oldStation.autofill,
  83. enabled: values.autofillEnabled,
  84. limit: values.autofillLimit,
  85. mode: values.autofillMode
  86. }
  87. };
  88. socket.dispatch(
  89. "stations.update",
  90. station.value._id,
  91. updatedStation,
  92. res => {
  93. new Toast(res.message);
  94. if (res.status === "success") {
  95. editStation(updatedStation);
  96. resolve();
  97. } else reject(new Error(res.message));
  98. }
  99. );
  100. } else {
  101. Object.values(messages).forEach(message => {
  102. new Toast({ content: message, timeout: 8000 });
  103. });
  104. resolve();
  105. }
  106. },
  107. {
  108. modalUuid: props.modalUuid
  109. }
  110. );
  111. watch(station, value => {
  112. setOriginalValue({
  113. name: value.name,
  114. displayName: value.displayName,
  115. description: value.description,
  116. theme: value.theme,
  117. privacy: value.privacy,
  118. requestsEnabled: value.requests.enabled,
  119. requestsAccess: value.requests.access,
  120. requestsLimit: value.requests.limit,
  121. autofillEnabled: value.autofill.enabled,
  122. autofillLimit: value.autofill.limit,
  123. autofillMode: value.autofill.mode
  124. });
  125. });
  126. </script>
  127. <template>
  128. <div class="station-settings">
  129. <label class="label">Name</label>
  130. <div class="control is-expanded">
  131. <input class="input" type="text" v-model="inputs['name'].value" />
  132. </div>
  133. <label class="label">Display Name</label>
  134. <div class="control is-expanded">
  135. <input
  136. class="input"
  137. type="text"
  138. v-model="inputs['displayName'].value"
  139. />
  140. </div>
  141. <label class="label">Description</label>
  142. <div class="control is-expanded">
  143. <input
  144. class="input"
  145. type="text"
  146. v-model="inputs['description'].value"
  147. />
  148. </div>
  149. <div class="settings-buttons">
  150. <div class="small-section">
  151. <label class="label">Theme</label>
  152. <div class="control is-expanded select">
  153. <select v-model="inputs['theme'].value">
  154. <option value="blue" selected>Blue</option>
  155. <option value="purple">Purple</option>
  156. <option value="teal">Teal</option>
  157. <option value="orange">Orange</option>
  158. <option value="red">Red</option>
  159. </select>
  160. </div>
  161. </div>
  162. <div class="small-section">
  163. <label class="label">Privacy</label>
  164. <div class="control is-expanded select">
  165. <select v-model="inputs['privacy'].value">
  166. <option value="public">Public</option>
  167. <option value="unlisted">Unlisted</option>
  168. <option value="private" selected>Private</option>
  169. </select>
  170. </div>
  171. </div>
  172. <div
  173. class="requests-settings"
  174. :class="{ enabled: inputs['requestsEnabled'].value }"
  175. >
  176. <div class="toggle-row">
  177. <label class="label">
  178. Requests
  179. <info-icon
  180. tooltip="Allow users to add songs to the queue"
  181. />
  182. </label>
  183. <p class="is-expanded checkbox-control">
  184. <label class="switch">
  185. <input
  186. type="checkbox"
  187. id="toggle-requests"
  188. v-model="inputs['requestsEnabled'].value"
  189. />
  190. <span class="slider round"></span>
  191. </label>
  192. <label for="toggle-requests">
  193. <p>
  194. {{
  195. inputs["requestsEnabled"].value
  196. ? "Enabled"
  197. : "Disabled"
  198. }}
  199. </p>
  200. </label>
  201. </p>
  202. </div>
  203. <div
  204. v-if="inputs['requestsEnabled'].value"
  205. class="small-section"
  206. >
  207. <label class="label">Minimum access</label>
  208. <div class="control is-expanded select">
  209. <select v-model="inputs['requestsAccess'].value">
  210. <option value="owner" selected>Owner</option>
  211. <option value="user">User</option>
  212. </select>
  213. </div>
  214. </div>
  215. <div
  216. v-if="inputs['requestsEnabled'].value"
  217. class="small-section"
  218. >
  219. <label class="label">Per user request limit</label>
  220. <div class="control is-expanded">
  221. <input
  222. class="input"
  223. type="number"
  224. min="1"
  225. max="50"
  226. v-model="inputs['requestsLimit'].value"
  227. />
  228. </div>
  229. </div>
  230. </div>
  231. <div
  232. class="autofill-settings"
  233. :class="{ enabled: inputs['autofillEnabled'].value }"
  234. >
  235. <div class="toggle-row">
  236. <label class="label">
  237. Autofill
  238. <info-icon
  239. tooltip="Automatically fill the queue with songs"
  240. />
  241. </label>
  242. <p class="is-expanded checkbox-control">
  243. <label class="switch">
  244. <input
  245. type="checkbox"
  246. id="toggle-autofill"
  247. v-model="inputs['autofillEnabled'].value"
  248. />
  249. <span class="slider round"></span>
  250. </label>
  251. <label for="toggle-autofill">
  252. <p>
  253. {{
  254. inputs["autofillEnabled"].value
  255. ? "Enabled"
  256. : "Disabled"
  257. }}
  258. </p>
  259. </label>
  260. </p>
  261. </div>
  262. <div
  263. v-if="inputs['autofillEnabled'].value"
  264. class="small-section"
  265. >
  266. <label class="label">Song limit</label>
  267. <div class="control is-expanded">
  268. <input
  269. class="input"
  270. type="number"
  271. min="1"
  272. max="50"
  273. v-model="inputs['autofillLimit'].value"
  274. />
  275. </div>
  276. </div>
  277. <div
  278. v-if="inputs['autofillEnabled'].value"
  279. class="small-section"
  280. >
  281. <label class="label">Play mode</label>
  282. <div class="control is-expanded select">
  283. <select v-model="inputs['autofillMode'].value">
  284. <option value="random" selected>Random</option>
  285. <option value="sequential">Sequential</option>
  286. </select>
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. <button class="control is-expanded button is-primary" @click="save()">
  292. Save Changes
  293. </button>
  294. </div>
  295. </template>
  296. <style lang="less" scoped>
  297. .night-mode {
  298. .requests-settings,
  299. .autofill-settings {
  300. background-color: var(--dark-grey-2) !important;
  301. }
  302. }
  303. .station-settings {
  304. .settings-buttons {
  305. display: flex;
  306. justify-content: center;
  307. flex-wrap: wrap;
  308. .small-section {
  309. width: calc(50% - 10px);
  310. min-width: 150px;
  311. margin: 5px auto;
  312. &:nth-child(odd) {
  313. margin-left: 0;
  314. }
  315. &:nth-child(even) {
  316. margin-right: 0;
  317. }
  318. }
  319. }
  320. .requests-settings,
  321. .autofill-settings {
  322. display: flex;
  323. flex-wrap: wrap;
  324. width: 100%;
  325. margin: 10px 0;
  326. padding: 10px;
  327. border-radius: @border-radius;
  328. box-shadow: @box-shadow;
  329. .toggle-row {
  330. display: flex;
  331. width: 100%;
  332. line-height: 36px;
  333. .label {
  334. font-size: 18px;
  335. margin: 0;
  336. }
  337. }
  338. .label {
  339. display: flex;
  340. flex-grow: 1;
  341. }
  342. .checkbox-control {
  343. justify-content: end;
  344. }
  345. .small-section {
  346. &:nth-child(even) {
  347. margin-left: 0;
  348. margin-right: auto;
  349. }
  350. &:nth-child(odd) {
  351. margin-left: auto;
  352. margin-right: 0;
  353. }
  354. }
  355. }
  356. }
  357. </style>