EditStation.vue 28 KB

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