EditStation.vue 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267
  1. <template>
  2. <modal title="Edit Station" class="edit-station-modal">
  3. <template #body>
  4. <div class="custom-modal-body">
  5. <!-- Station Preferences -->
  6. <div class="section left-section">
  7. <div class="col col-2">
  8. <div>
  9. <label class="label">Name</label>
  10. <p class="control">
  11. <input
  12. class="input"
  13. type="text"
  14. v-model="editing.name"
  15. />
  16. </p>
  17. </div>
  18. <div>
  19. <label class="label">Display name</label>
  20. <p class="control">
  21. <input
  22. class="input"
  23. type="text"
  24. v-model="editing.displayName"
  25. />
  26. </p>
  27. </div>
  28. </div>
  29. <div class="col col-1">
  30. <div>
  31. <label class="label">Description</label>
  32. <p class="control">
  33. <input
  34. class="input"
  35. type="text"
  36. v-model="editing.description"
  37. />
  38. </p>
  39. </div>
  40. </div>
  41. <div class="col col-2" v-if="editing.genres">
  42. <div>
  43. <label class="label">Genre(s)</label>
  44. <p class="control has-addons">
  45. <input
  46. class="input"
  47. type="text"
  48. id="new-genre"
  49. v-model="genreInputValue"
  50. @blur="blurGenreInput()"
  51. @focus="focusGenreInput()"
  52. @keydown="keydownGenreInput()"
  53. @keyup.enter="addTag('genres')"
  54. />
  55. <button
  56. class="button is-info add-button blue"
  57. @click="addTag('genres')"
  58. >
  59. <i class="material-icons">add</i>
  60. </button>
  61. </p>
  62. <div
  63. class="autosuggest-container"
  64. v-if="
  65. (genreInputFocussed ||
  66. genreAutosuggestContainerFocussed) &&
  67. genreAutosuggestItems.length > 0
  68. "
  69. @mouseover="focusGenreContainer()"
  70. @mouseleave="blurGenreContainer()"
  71. >
  72. <span
  73. class="autosuggest-item"
  74. tabindex="0"
  75. @click="selectGenreAutosuggest(item)"
  76. v-for="(item,
  77. index) in genreAutosuggestItems"
  78. :key="index"
  79. >{{ item }}</span
  80. >
  81. </div>
  82. <div class="list-container">
  83. <div
  84. class="list-item"
  85. v-for="(genre, index) in editing.genres"
  86. :key="index"
  87. >
  88. <div
  89. class="list-item-circle blue"
  90. @click="removeTag('genres', index)"
  91. >
  92. <i class="material-icons">close</i>
  93. </div>
  94. <p>{{ genre }}</p>
  95. </div>
  96. </div>
  97. </div>
  98. <div>
  99. <label class="label">Blacklist genre(s)</label>
  100. <p class="control has-addons">
  101. <input
  102. class="input"
  103. type="text"
  104. v-model="blacklistGenreInputValue"
  105. @blur="blurBlacklistGenreInput()"
  106. @focus="focusBlacklistGenreInput()"
  107. @keydown="keydownBlacklistGenreInput()"
  108. @keyup.enter="addTag('blacklist-genres')"
  109. />
  110. <button
  111. class="button is-info add-button red"
  112. @click="addTag('blacklist-genres')"
  113. >
  114. <i class="material-icons">add</i>
  115. </button>
  116. </p>
  117. <div
  118. class="autosuggest-container"
  119. v-if="
  120. (blacklistGenreInputFocussed ||
  121. blacklistGenreAutosuggestContainerFocussed) &&
  122. blacklistGenreAutosuggestItems.length >
  123. 0
  124. "
  125. @mouseover="focusBlacklistGenreContainer()"
  126. @mouseleave="blurBlacklistGenreContainer()"
  127. >
  128. <span
  129. class="autosuggest-item"
  130. tabindex="0"
  131. @click="
  132. selectBlacklistGenreAutosuggest(item)
  133. "
  134. v-for="(item,
  135. index) in blacklistGenreAutosuggestItems"
  136. :key="index"
  137. >{{ item }}</span
  138. >
  139. </div>
  140. <div class="list-container">
  141. <div
  142. class="list-item"
  143. v-for="(genre,
  144. index) in editing.blacklistedGenres"
  145. :key="index"
  146. >
  147. <div
  148. class="list-item-circle red"
  149. @click="
  150. removeTag('blacklist-genres', index)
  151. "
  152. >
  153. <i class="material-icons">close</i>
  154. </div>
  155. <p>{{ genre }}</p>
  156. </div>
  157. </div>
  158. </div>
  159. </div>
  160. <!-- Choose a playlist -->
  161. <div v-if="!editing.partyMode && playlists.length > 0">
  162. <hr style="margin: 10px 0 20px 0;" />
  163. <h4 class="section-title">Choose a playlist</h4>
  164. <p class="section-description">
  165. Choose one of your playlists to add to the queue.
  166. </p>
  167. <br />
  168. <div id="playlists">
  169. <div
  170. class="playlist"
  171. v-for="(playlist, index) in playlists"
  172. :key="index"
  173. >
  174. <playlist-item :playlist="playlist">
  175. <div slot="actions">
  176. <!-- <a
  177. class="button is-danger"
  178. href="#"
  179. @click="
  180. togglePlaylistSelection(
  181. playlist._id
  182. )
  183. "
  184. v-if="isPlaylistSelected(playlist._id)"
  185. >
  186. <i
  187. class="material-icons icon-with-button"
  188. >stop</i
  189. >
  190. Stop playing
  191. </a> -->
  192. <a
  193. class="button is-success"
  194. href="#"
  195. @click="
  196. selectPlaylist(playlist._id)
  197. "
  198. ><i
  199. class="material-icons icon-with-button"
  200. >play_arrow</i
  201. >Play in queue
  202. </a>
  203. </div>
  204. </playlist-item>
  205. </div>
  206. </div>
  207. </div>
  208. </div>
  209. <!-- Buttons changing the privacy settings -->
  210. <div class="section right-section">
  211. <div>
  212. <label class="label">Privacy</label>
  213. <div
  214. @mouseenter="privacyDropdownActive = true"
  215. @mouseleave="privacyDropdownActive = false"
  216. class="button-wrapper"
  217. >
  218. <button
  219. :class="privacyButtons[editing.privacy].style"
  220. style="text-transform: capitalize"
  221. @click="updatePrivacyLocal(editing.privacy)"
  222. >
  223. <i class="material-icons">{{
  224. privacyButtons[editing.privacy].iconName
  225. }}</i>
  226. {{ editing.privacy }}
  227. </button>
  228. <transition name="slide-down">
  229. <button
  230. class="green"
  231. v-if="
  232. privacyDropdownActive &&
  233. editing.privacy !== 'public'
  234. "
  235. @click="updatePrivacyLocal('public')"
  236. >
  237. <i class="material-icons">{{
  238. privacyButtons["public"].iconName
  239. }}</i>
  240. Public
  241. </button>
  242. </transition>
  243. <transition name="slide-down">
  244. <button
  245. class="orange"
  246. v-if="
  247. privacyDropdownActive &&
  248. editing.privacy !== 'unlisted'
  249. "
  250. @click="updatePrivacyLocal('unlisted')"
  251. >
  252. <i class="material-icons">{{
  253. privacyButtons["unlisted"].iconName
  254. }}</i>
  255. Unlisted
  256. </button>
  257. </transition>
  258. <transition name="slide-down">
  259. <button
  260. class="red"
  261. v-if="
  262. privacyDropdownActive &&
  263. editing.privacy !== 'private'
  264. "
  265. @click="updatePrivacyLocal('private')"
  266. >
  267. <i class="material-icons">{{
  268. privacyButtons["private"].iconName
  269. }}</i>
  270. Private
  271. </button>
  272. </transition>
  273. </div>
  274. </div>
  275. <!-- Buttons changing the mode of the station -->
  276. <div v-if="editing.type === 'community'">
  277. <label class="label">Mode</label>
  278. <div
  279. @mouseenter="modeDropdownActive = true"
  280. @mouseleave="modeDropdownActive = false"
  281. class="button-wrapper"
  282. >
  283. <button
  284. :class="{
  285. blue: !editing.partyMode,
  286. yellow: editing.partyMode
  287. }"
  288. @click="
  289. editing.partyMode
  290. ? updatePartyModeLocal(true)
  291. : updatePartyModeLocal(false)
  292. "
  293. >
  294. <i class="material-icons">{{
  295. editing.partyMode
  296. ? "emoji_people"
  297. : "playlist_play"
  298. }}</i>
  299. {{ editing.partyMode ? "Party" : "Playlist" }}
  300. </button>
  301. <transition name="slide-down">
  302. <button
  303. class="blue"
  304. v-if="
  305. modeDropdownActive && editing.partyMode
  306. "
  307. @click="updatePartyModeLocal(false)"
  308. >
  309. <i class="material-icons">playlist_play</i>
  310. Playlist
  311. </button>
  312. </transition>
  313. <transition name="slide-down">
  314. <button
  315. class="yellow"
  316. v-if="
  317. modeDropdownActive && !editing.partyMode
  318. "
  319. @click="updatePartyModeLocal(true)"
  320. >
  321. <i class="material-icons">emoji_people</i>
  322. Party
  323. </button>
  324. </transition>
  325. </div>
  326. </div>
  327. <div
  328. v-if="
  329. editing.type === 'community' &&
  330. editing.partyMode === true
  331. "
  332. >
  333. <label class="label">Queue lock</label>
  334. <div
  335. @mouseenter="queueLockDropdownActive = true"
  336. @mouseleave="queueLockDropdownActive = false"
  337. class="button-wrapper"
  338. >
  339. <button
  340. :class="{
  341. green: editing.locked,
  342. red: !editing.locked
  343. }"
  344. @click="
  345. editing.locked
  346. ? updateQueueLockLocal(true)
  347. : updateQueueLockLocal(false)
  348. "
  349. >
  350. <i class="material-icons">{{
  351. editing.locked ? "lock" : "lock_open"
  352. }}</i>
  353. {{ editing.locked ? "Locked" : "Unlocked" }}
  354. </button>
  355. <transition name="slide-down">
  356. <button
  357. class="green"
  358. v-if="
  359. queueLockDropdownActive &&
  360. !editing.locked
  361. "
  362. @click="updateQueueLockLocal(true)"
  363. >
  364. <i class="material-icons">lock</i>
  365. Locked
  366. </button>
  367. </transition>
  368. <transition name="slide-down">
  369. <button
  370. class="red"
  371. v-if="
  372. queueLockDropdownActive &&
  373. editing.locked
  374. "
  375. @click="updateQueueLockLocal(false)"
  376. >
  377. <i class="material-icons">lock_open</i>
  378. Unlocked
  379. </button>
  380. </transition>
  381. </div>
  382. </div>
  383. </div>
  384. </div>
  385. </template>
  386. <template #footer>
  387. <button class="button is-success" @click="update()">
  388. Update Settings
  389. </button>
  390. <button
  391. v-if="station.type === 'community'"
  392. class="button is-danger"
  393. @click="deleteStation()"
  394. >
  395. Delete station
  396. </button>
  397. </template>
  398. </modal>
  399. </template>
  400. <script>
  401. import { mapState, mapActions } from "vuex";
  402. import Toast from "toasters";
  403. import PlaylistItem from "../ui/PlaylistItem.vue";
  404. import Modal from "../Modal.vue";
  405. import io from "../../io";
  406. import validation from "../../validation";
  407. export default {
  408. components: { Modal, PlaylistItem },
  409. props: { store: { type: Object, default: () => {} } },
  410. data() {
  411. return {
  412. genreInputValue: "",
  413. genreInputFocussed: false,
  414. genreAutosuggestContainerFocussed: false,
  415. keydownGenreInputTimeout: 0,
  416. genreAutosuggestItems: [],
  417. blacklistGenreInputValue: "",
  418. blacklistGenreInputFocussed: false,
  419. blacklistGenreAutosuggestContainerFocussed: false,
  420. blacklistKeydownGenreInputTimeout: 0,
  421. blacklistGenreAutosuggestItems: [],
  422. privacyDropdownActive: false,
  423. modeDropdownActive: false,
  424. queueLockDropdownActive: false,
  425. genres: [
  426. "Blues",
  427. "Country",
  428. "Disco",
  429. "Funk",
  430. "Hip-Hop",
  431. "Jazz",
  432. "Metal",
  433. "Oldies",
  434. "Other",
  435. "Pop",
  436. "Rap",
  437. "Reggae",
  438. "Rock",
  439. "Techno",
  440. "Trance",
  441. "Classical",
  442. "Instrumental",
  443. "House",
  444. "Electronic",
  445. "Christian Rap",
  446. "Lo-Fi",
  447. "Musical",
  448. "Rock 'n' Roll",
  449. "Opera",
  450. "Drum & Bass",
  451. "Club-House",
  452. "Indie",
  453. "Heavy Metal",
  454. "Christian rock",
  455. "Dubstep"
  456. ],
  457. privacyButtons: {
  458. public: {
  459. style: "green",
  460. iconName: "public"
  461. },
  462. private: {
  463. style: "red",
  464. iconName: "lock"
  465. },
  466. unlisted: {
  467. style: "orange",
  468. iconName: "link"
  469. }
  470. },
  471. playlists: []
  472. };
  473. },
  474. computed: {
  475. ...mapState("admin/station", {
  476. stations: state => state.stations
  477. }),
  478. ...mapState({
  479. editing(state) {
  480. return this.$props.store
  481. .split("/")
  482. .reduce((a, v) => a[v], state).editing;
  483. },
  484. station(state) {
  485. return this.$props.store
  486. .split("/")
  487. .reduce((a, v) => a[v], state).station;
  488. }
  489. })
  490. },
  491. mounted() {
  492. io.getSocket(socket => {
  493. this.socket = socket;
  494. this.socket.emit("playlists.indexForUser", res => {
  495. if (res.status === "success") this.playlists = res.data;
  496. });
  497. this.socket.on("event:playlist.create", playlist => {
  498. this.playlists.push(playlist);
  499. });
  500. this.socket.on("event:playlist.delete", playlistId => {
  501. this.playlists.forEach((playlist, index) => {
  502. if (playlist._id === playlistId) {
  503. this.playlists.splice(index, 1);
  504. }
  505. });
  506. });
  507. this.socket.on("event:playlist.addSong", data => {
  508. this.playlists.forEach((playlist, index) => {
  509. if (playlist._id === data.playlistId) {
  510. this.playlists[index].songs.push(data.song);
  511. }
  512. });
  513. });
  514. this.socket.on("event:playlist.removeSong", data => {
  515. this.playlists.forEach((playlist, index) => {
  516. if (playlist._id === data.playlistId) {
  517. this.playlists[index].songs.forEach((song, index2) => {
  518. if (song._id === data.songId) {
  519. this.playlists[index].songs.splice(index2, 1);
  520. }
  521. });
  522. }
  523. });
  524. });
  525. this.socket.on("event:playlist.updateDisplayName", data => {
  526. this.playlists.forEach((playlist, index) => {
  527. if (playlist._id === data.playlistId) {
  528. this.playlists[index].displayName = data.displayName;
  529. }
  530. });
  531. });
  532. return socket;
  533. });
  534. },
  535. methods: {
  536. isPlaylistSelected(id) {
  537. // TODO Also change this once it changes for a station
  538. if (this.station && this.station.privatePlaylist === id)
  539. return true;
  540. return false;
  541. },
  542. selectPlaylist(playlistId) {
  543. this.socket.emit(
  544. "stations.selectPrivatePlaylist",
  545. this.station._id,
  546. playlistId,
  547. res => {
  548. if (res.status === "failure")
  549. return new Toast({
  550. content: res.message,
  551. timeout: 8000
  552. });
  553. return new Toast({ content: res.message, timeout: 4000 });
  554. }
  555. );
  556. },
  557. update() {
  558. if (this.station.name !== this.editing.name) this.updateName();
  559. if (this.station.displayName !== this.editing.displayName)
  560. this.updateDisplayName();
  561. if (this.station.description !== this.editing.description)
  562. this.updateDescription();
  563. if (this.station.privacy !== this.editing.privacy)
  564. this.updatePrivacy();
  565. if (
  566. this.station.type === "community" &&
  567. this.station.partyMode !== this.editing.partyMode
  568. )
  569. this.updatePartyMode();
  570. if (
  571. this.station.type === "community" &&
  572. this.editing.partyMode &&
  573. this.station.locked !== this.editing.locked
  574. )
  575. this.updateQueueLock();
  576. if (this.$props.store !== "station") {
  577. if (
  578. this.station.genres.toString() !==
  579. this.editing.genres.toString()
  580. )
  581. this.updateGenres();
  582. if (
  583. this.station.blacklistedGenres.toString() !==
  584. this.editing.blacklistedGenres.toString()
  585. )
  586. this.updateBlacklistedGenres();
  587. }
  588. },
  589. updateName() {
  590. const { name } = this.editing;
  591. if (!validation.isLength(name, 2, 16))
  592. return new Toast({
  593. content: "Name must have between 2 and 16 characters.",
  594. timeout: 8000
  595. });
  596. if (!validation.regex.az09_.test(name))
  597. return new Toast({
  598. content:
  599. "Invalid name format. Allowed characters: a-z, 0-9 and _.",
  600. timeout: 8000
  601. });
  602. return this.socket.emit(
  603. "stations.updateName",
  604. this.editing._id,
  605. name,
  606. res => {
  607. if (res.status === "success") {
  608. if (this.station) this.station.name = name;
  609. else {
  610. this.stations.forEach((station, index) => {
  611. if (station._id === this.editing._id) {
  612. this.stations[index].name = name;
  613. return name;
  614. }
  615. return false;
  616. });
  617. }
  618. }
  619. new Toast({ content: res.message, timeout: 8000 });
  620. }
  621. );
  622. },
  623. updateDisplayName() {
  624. const { displayName } = this.editing;
  625. if (!validation.isLength(displayName, 2, 32))
  626. return new Toast({
  627. content:
  628. "Display name must have between 2 and 32 characters.",
  629. timeout: 8000
  630. });
  631. if (!validation.regex.ascii.test(displayName))
  632. return new Toast({
  633. content:
  634. "Invalid display name format. Only ASCII characters are allowed.",
  635. timeout: 8000
  636. });
  637. return this.socket.emit(
  638. "stations.updateDisplayName",
  639. this.editing._id,
  640. displayName,
  641. res => {
  642. if (res.status === "success") {
  643. if (this.station)
  644. this.station.displayName = displayName;
  645. else {
  646. this.stations.forEach((station, index) => {
  647. if (station._id === this.editing._id) {
  648. this.stations[
  649. index
  650. ].displayName = displayName;
  651. return displayName;
  652. }
  653. return false;
  654. });
  655. }
  656. }
  657. new Toast({ content: res.message, timeout: 8000 });
  658. }
  659. );
  660. },
  661. updateDescription() {
  662. const { description } = this.editing;
  663. if (!validation.isLength(description, 2, 200))
  664. return new Toast({
  665. content:
  666. "Description must have between 2 and 200 characters.",
  667. timeout: 8000
  668. });
  669. let characters = description.split("");
  670. characters = characters.filter(character => {
  671. return character.charCodeAt(0) === 21328;
  672. });
  673. if (characters.length !== 0)
  674. return new Toast({
  675. content: "Invalid description format.",
  676. timeout: 8000
  677. });
  678. return this.socket.emit(
  679. "stations.updateDescription",
  680. this.editing._id,
  681. description,
  682. res => {
  683. if (res.status === "success") {
  684. if (this.station)
  685. this.station.description = description;
  686. else {
  687. this.stations.forEach((station, index) => {
  688. if (station._id === this.editing._id) {
  689. this.stations[
  690. index
  691. ].description = description;
  692. return description;
  693. }
  694. return false;
  695. });
  696. }
  697. return new Toast({
  698. content: res.message,
  699. timeout: 4000
  700. });
  701. }
  702. return new Toast({ content: res.message, timeout: 8000 });
  703. }
  704. );
  705. },
  706. updatePrivacyLocal(privacy) {
  707. if (this.editing.privacy === privacy) return;
  708. this.editing.privacy = privacy;
  709. this.privacyDropdownActive = false;
  710. },
  711. updatePrivacy() {
  712. this.socket.emit(
  713. "stations.updatePrivacy",
  714. this.editing._id,
  715. this.editing.privacy,
  716. res => {
  717. if (res.status === "success") {
  718. if (this.station)
  719. this.station.privacy = this.editing.privacy;
  720. else {
  721. this.stations.forEach((station, index) => {
  722. if (station._id === this.editing._id) {
  723. this.stations[
  724. index
  725. ].privacy = this.editing.privacy;
  726. return this.editing.privacy;
  727. }
  728. return false;
  729. });
  730. }
  731. return new Toast({
  732. content: res.message,
  733. timeout: 4000
  734. });
  735. }
  736. return new Toast({ content: res.message, timeout: 8000 });
  737. }
  738. );
  739. },
  740. updateGenres() {
  741. this.socket.emit(
  742. "stations.updateGenres",
  743. this.editing._id,
  744. this.editing.genres,
  745. res => {
  746. if (res.status === "success") {
  747. const genres = JSON.parse(
  748. JSON.stringify(this.editing.genres)
  749. );
  750. if (this.station) this.station.genres = genres;
  751. this.stations.forEach((station, index) => {
  752. if (station._id === this.editing._id) {
  753. this.stations[index].genres = genres;
  754. return genres;
  755. }
  756. return false;
  757. });
  758. return new Toast({
  759. content: res.message,
  760. timeout: 4000
  761. });
  762. }
  763. return new Toast({ content: res.message, timeout: 8000 });
  764. }
  765. );
  766. },
  767. updateBlacklistedGenres() {
  768. this.socket.emit(
  769. "stations.updateBlacklistedGenres",
  770. this.editing._id,
  771. this.editing.blacklistedGenres,
  772. res => {
  773. if (res.status === "success") {
  774. const blacklistedGenres = JSON.parse(
  775. JSON.stringify(this.editing.blacklistedGenres)
  776. );
  777. if (this.station)
  778. this.station.blacklistedGenres = blacklistedGenres;
  779. this.stations.forEach((station, index) => {
  780. if (station._id === this.editing._id) {
  781. this.stations[
  782. index
  783. ].blacklistedGenres = blacklistedGenres;
  784. return blacklistedGenres;
  785. }
  786. return false;
  787. });
  788. return new Toast({
  789. content: res.message,
  790. timeout: 4000
  791. });
  792. }
  793. return new Toast({ content: res.message, timeout: 8000 });
  794. }
  795. );
  796. },
  797. updatePartyModeLocal(partyMode) {
  798. if (this.editing.partyMode === partyMode) return;
  799. this.editing.partyMode = partyMode;
  800. this.modeDropdownActive = false;
  801. },
  802. updatePartyMode() {
  803. this.socket.emit(
  804. "stations.updatePartyMode",
  805. this.editing._id,
  806. this.editing.partyMode,
  807. res => {
  808. if (res.status === "success") {
  809. if (this.station)
  810. this.station.partyMode = this.editing.partyMode;
  811. // if (this.station)
  812. // this.station.partyMode = this.editing.partyMode;
  813. // this.stations.forEach((station, index) => {
  814. // if (station._id === this.editing._id) {
  815. // this.stations[
  816. // index
  817. // ].partyMode = this.editing.partyMode;
  818. // return this.editing.partyMode;
  819. // }
  820. // return false;
  821. // });
  822. return new Toast({
  823. content: res.message,
  824. timeout: 4000
  825. });
  826. }
  827. return new Toast({ content: res.message, timeout: 8000 });
  828. }
  829. );
  830. },
  831. updateQueueLockLocal(locked) {
  832. if (this.editing.locked === locked) return;
  833. this.editing.locked = locked;
  834. this.queueLockDropdownActive = false;
  835. },
  836. updateQueueLock() {
  837. this.socket.emit("stations.toggleLock", this.editing._id, res => {
  838. console.log(res);
  839. if (res.status === "success") {
  840. if (this.station) this.station.locked = res.data;
  841. return new Toast({
  842. content: `Toggled queue lock succesfully to ${res.data}`,
  843. timeout: 4000
  844. });
  845. }
  846. return new Toast({
  847. content: "Failed to toggle queue lock.",
  848. timeout: 8000
  849. });
  850. });
  851. },
  852. deleteStation() {
  853. this.socket.emit("stations.remove", this.editing._id, res => {
  854. if (res.status === "success")
  855. this.closeModal({
  856. sector: "station",
  857. modal: "editStation"
  858. });
  859. return new Toast({ content: res.message, timeout: 8000 });
  860. });
  861. },
  862. blurGenreInput() {
  863. this.genreInputFocussed = false;
  864. },
  865. focusGenreInput() {
  866. this.genreInputFocussed = true;
  867. },
  868. keydownGenreInput() {
  869. clearTimeout(this.keydownGenreInputTimeout);
  870. this.keydownGenreInputTimeout = setTimeout(() => {
  871. if (this.genreInputValue.length > 1) {
  872. this.genreAutosuggestItems = this.genres.filter(genre => {
  873. return genre
  874. .toLowerCase()
  875. .startsWith(this.genreInputValue.toLowerCase());
  876. });
  877. } else this.genreAutosuggestItems = [];
  878. }, 1000);
  879. },
  880. focusGenreContainer() {
  881. this.genreAutosuggestContainerFocussed = true;
  882. },
  883. blurGenreContainer() {
  884. this.genreAutosuggestContainerFocussed = false;
  885. },
  886. selectGenreAutosuggest(value) {
  887. this.genreInputValue = value;
  888. },
  889. blurBlacklistGenreInput() {
  890. this.blacklistGenreInputFocussed = false;
  891. },
  892. focusBlacklistGenreInput() {
  893. this.blacklistGenreInputFocussed = true;
  894. },
  895. keydownBlacklistGenreInput() {
  896. clearTimeout(this.keydownBlacklistGenreInputTimeout);
  897. this.keydownBlacklistGenreInputTimeout = setTimeout(() => {
  898. if (this.blacklistGenreInputValue.length > 1) {
  899. this.blacklistGenreAutosuggestItems = this.genres.filter(
  900. genre => {
  901. return genre
  902. .toLowerCase()
  903. .startsWith(
  904. this.blacklistGenreInputValue.toLowerCase()
  905. );
  906. }
  907. );
  908. } else this.blacklistGenreAutosuggestItems = [];
  909. }, 1000);
  910. },
  911. focusBlacklistGenreContainer() {
  912. this.blacklistGenreAutosuggestContainerFocussed = true;
  913. },
  914. blurBlacklistGenreContainer() {
  915. this.blacklistGenreAutosuggestContainerFocussed = false;
  916. },
  917. selectBlacklistGenreAutosuggest(value) {
  918. this.blacklistGenreInputValue = value;
  919. },
  920. addTag(type) {
  921. if (type === "genres") {
  922. const genre = this.genreInputValue.toLowerCase().trim();
  923. if (this.editing.genres.indexOf(genre) !== -1)
  924. return new Toast({
  925. content: "Genre already exists",
  926. timeout: 3000
  927. });
  928. if (genre) {
  929. this.editing.genres.push(genre);
  930. this.genreInputValue = "";
  931. return false;
  932. }
  933. return new Toast({
  934. content: "Genre cannot be empty",
  935. timeout: 3000
  936. });
  937. }
  938. if (type === "blacklist-genres") {
  939. const genre = this.blacklistGenreInputValue
  940. .toLowerCase()
  941. .trim();
  942. if (this.editing.blacklistedGenres.indexOf(genre) !== -1)
  943. return new Toast({
  944. content: "Blacklist genre already exists",
  945. timeout: 3000
  946. });
  947. if (genre) {
  948. this.editing.blacklistedGenres.push(genre);
  949. this.blacklistGenreInputValue = "";
  950. return false;
  951. }
  952. return new Toast({
  953. content: "Blacklist genre cannot be empty",
  954. timeout: 3000
  955. });
  956. }
  957. return false;
  958. },
  959. removeTag(type, index) {
  960. if (type === "genres") this.editing.genres.splice(index, 1);
  961. else if (type === "blacklist-genres")
  962. this.editing.blacklistedGenres.splice(index, 1);
  963. },
  964. ...mapActions("modals", ["closeModal"])
  965. }
  966. };
  967. </script>
  968. <style lang="scss" scoped>
  969. @import "../../styles/global.scss";
  970. .night-mode {
  971. .modal-card,
  972. .modal-card-head,
  973. .modal-card-body,
  974. .modal-card-foot {
  975. background-color: $night-mode-secondary;
  976. }
  977. .section {
  978. background-color: #222 !important;
  979. border: 0 !important;
  980. }
  981. .label,
  982. p,
  983. strong {
  984. color: #ddd;
  985. }
  986. }
  987. .modal-card-title {
  988. text-align: center;
  989. margin-left: 24px;
  990. }
  991. .custom-modal-body {
  992. padding: 16px;
  993. display: flex;
  994. }
  995. .section {
  996. border: 1px solid #a3e0ff;
  997. background-color: #f4f4f4;
  998. border-radius: 5px;
  999. padding: 16px;
  1000. }
  1001. .left-section {
  1002. width: 595px;
  1003. display: grid;
  1004. gap: 16px;
  1005. grid-template-rows: min-content min-content auto;
  1006. .control {
  1007. input {
  1008. width: 100%;
  1009. height: 36px;
  1010. }
  1011. .add-button {
  1012. width: 32px;
  1013. &.blue {
  1014. background-color: $musare-blue !important;
  1015. }
  1016. &.red {
  1017. background-color: $red !important;
  1018. }
  1019. i {
  1020. font-size: 32px;
  1021. }
  1022. }
  1023. }
  1024. .col {
  1025. > div {
  1026. position: relative;
  1027. }
  1028. }
  1029. .list-item-circle {
  1030. width: 16px;
  1031. height: 16px;
  1032. border-radius: 8px;
  1033. cursor: pointer;
  1034. margin-right: 8px;
  1035. float: left;
  1036. -webkit-touch-callout: none;
  1037. -webkit-user-select: none;
  1038. -khtml-user-select: none;
  1039. -moz-user-select: none;
  1040. -ms-user-select: none;
  1041. user-select: none;
  1042. &.blue {
  1043. background-color: $musare-blue;
  1044. i {
  1045. color: $musare-blue;
  1046. }
  1047. }
  1048. &.red {
  1049. background-color: $red;
  1050. i {
  1051. color: $red;
  1052. }
  1053. }
  1054. i {
  1055. font-size: 14px;
  1056. margin-left: 1px;
  1057. }
  1058. }
  1059. .list-item-circle:hover,
  1060. .list-item-circle:focus {
  1061. i {
  1062. color: white;
  1063. }
  1064. }
  1065. .list-item > p {
  1066. line-height: 16px;
  1067. word-wrap: break-word;
  1068. width: calc(100% - 24px);
  1069. left: 24px;
  1070. float: left;
  1071. margin-bottom: 8px;
  1072. }
  1073. .list-item:last-child > p {
  1074. margin-bottom: 0;
  1075. }
  1076. .autosuggest-container {
  1077. position: absolute;
  1078. background: white;
  1079. width: calc(100% + 1px);
  1080. top: 57px;
  1081. z-index: 200;
  1082. overflow: auto;
  1083. max-height: 100%;
  1084. clear: both;
  1085. .autosuggest-item {
  1086. padding: 8px;
  1087. display: block;
  1088. border: 1px solid #dbdbdb;
  1089. margin-top: -1px;
  1090. line-height: 16px;
  1091. cursor: pointer;
  1092. -webkit-user-select: none;
  1093. -ms-user-select: none;
  1094. -moz-user-select: none;
  1095. user-select: none;
  1096. }
  1097. .autosuggest-item:hover,
  1098. .autosuggest-item:focus {
  1099. background-color: #eee;
  1100. }
  1101. .autosuggest-item:first-child {
  1102. border-top: none;
  1103. }
  1104. .autosuggest-item:last-child {
  1105. border-radius: 0 0 3px 3px;
  1106. }
  1107. }
  1108. }
  1109. .right-section {
  1110. width: 157px;
  1111. min-height: 375px;
  1112. margin-left: 16px;
  1113. display: grid;
  1114. gap: 16px;
  1115. grid-template-rows: min-content min-content min-content;
  1116. .button-wrapper {
  1117. display: flex;
  1118. flex-direction: column;
  1119. }
  1120. button {
  1121. width: 100%;
  1122. height: 36px;
  1123. border: 0;
  1124. border-radius: 3px;
  1125. font-size: 18px;
  1126. color: white;
  1127. box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25);
  1128. display: block;
  1129. text-align: center;
  1130. justify-content: center;
  1131. display: inline-flex;
  1132. -ms-flex-align: center;
  1133. align-items: center;
  1134. -moz-user-select: none;
  1135. user-select: none;
  1136. cursor: pointer;
  1137. margin-bottom: 10px;
  1138. padding: 0;
  1139. &.red {
  1140. background-color: $red;
  1141. }
  1142. &.green {
  1143. background-color: $green;
  1144. }
  1145. &.blue {
  1146. background-color: $musare-blue;
  1147. }
  1148. &.orange {
  1149. background-color: $light-orange;
  1150. }
  1151. &.yellow {
  1152. background-color: $yellow;
  1153. }
  1154. i {
  1155. font-size: 20px;
  1156. margin-right: 4px;
  1157. }
  1158. }
  1159. }
  1160. .col {
  1161. display: grid;
  1162. grid-column-gap: 16px;
  1163. }
  1164. .col-1 {
  1165. grid-template-columns: auto;
  1166. }
  1167. .col-2 {
  1168. grid-template-columns: auto auto;
  1169. }
  1170. .slide-down-enter-active {
  1171. transition: transform 0.25s;
  1172. }
  1173. .slide-down-enter {
  1174. transform: translateY(-10px);
  1175. }
  1176. #playlists {
  1177. overflow: auto;
  1178. }
  1179. .modal-card {
  1180. overflow: auto;
  1181. }
  1182. .modal-card-body {
  1183. overflow: unset;
  1184. }
  1185. </style>