2
0

EditStation.vue 29 KB

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