EditStation.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236
  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">
  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. return socket;
  428. });
  429. },
  430. data() {
  431. return {
  432. genreInputValue: "",
  433. genreInputFocussed: false,
  434. genreAutosuggestContainerFocussed: false,
  435. keydownGenreInputTimeout: 0,
  436. genreAutosuggestItems: [],
  437. blacklistGenreInputValue: "",
  438. blacklistGenreInputFocussed: false,
  439. blacklistGenreAutosuggestContainerFocussed: false,
  440. blacklistKeydownGenreInputTimeout: 0,
  441. blacklistGenreAutosuggestItems: [],
  442. privacyDropdownActive: false,
  443. modeDropdownActive: false,
  444. queueLockDropdownActive: false,
  445. genres: [
  446. "Blues",
  447. "Country",
  448. "Disco",
  449. "Funk",
  450. "Hip-Hop",
  451. "Jazz",
  452. "Metal",
  453. "Oldies",
  454. "Other",
  455. "Pop",
  456. "Rap",
  457. "Reggae",
  458. "Rock",
  459. "Techno",
  460. "Trance",
  461. "Classical",
  462. "Instrumental",
  463. "House",
  464. "Electronic",
  465. "Christian Rap",
  466. "Lo-Fi",
  467. "Musical",
  468. "Rock 'n' Roll",
  469. "Opera",
  470. "Drum & Bass",
  471. "Club-House",
  472. "Indie",
  473. "Heavy Metal",
  474. "Christian rock",
  475. "Dubstep"
  476. ],
  477. privacyButtons: {
  478. public: {
  479. style: "green",
  480. iconName: "public"
  481. },
  482. private: {
  483. style: "red",
  484. iconName: "lock"
  485. },
  486. unlisted: {
  487. style: "orange",
  488. iconName: "link"
  489. }
  490. },
  491. playlists: []
  492. };
  493. },
  494. props: ["store"],
  495. methods: {
  496. isPlaylistSelected(id) {
  497. // TODO Also change this once it changes for a station
  498. if (this.station && this.station.privatePlaylist === id)
  499. return true;
  500. return false;
  501. },
  502. selectPlaylist(playlistId) {
  503. this.socket.emit(
  504. "stations.selectPrivatePlaylist",
  505. this.station._id,
  506. playlistId,
  507. res => {
  508. if (res.status === "failure")
  509. return new Toast({
  510. content: res.message,
  511. timeout: 8000
  512. });
  513. return new Toast({ content: res.message, timeout: 4000 });
  514. }
  515. );
  516. },
  517. update() {
  518. if (this.station.name !== this.editing.name) this.updateName();
  519. if (this.station.displayName !== this.editing.displayName)
  520. this.updateDisplayName();
  521. if (this.station.description !== this.editing.description)
  522. this.updateDescription();
  523. if (this.station.privacy !== this.editing.privacy)
  524. this.updatePrivacy();
  525. if (
  526. this.station.type === "community" &&
  527. this.station.partyMode !== this.editing.partyMode
  528. )
  529. this.updatePartyMode();
  530. if (
  531. this.station.type === "community" &&
  532. this.editing.partyMode &&
  533. this.station.locked !== this.editing.locked
  534. )
  535. this.updateQueueLock();
  536. if (this.$props.store !== "station") {
  537. if (
  538. this.station.genres.toString() !==
  539. this.editing.genres.toString()
  540. )
  541. this.updateGenres();
  542. if (
  543. this.station.blacklistedGenres.toString() !==
  544. this.editing.blacklistedGenres.toString()
  545. )
  546. this.updateBlacklistedGenres();
  547. }
  548. },
  549. updateName() {
  550. const { name } = this.editing;
  551. if (!validation.isLength(name, 2, 16))
  552. return new Toast({
  553. content: "Name must have between 2 and 16 characters.",
  554. timeout: 8000
  555. });
  556. if (!validation.regex.az09_.test(name))
  557. return new Toast({
  558. content:
  559. "Invalid name format. Allowed characters: a-z, 0-9 and _.",
  560. timeout: 8000
  561. });
  562. return this.socket.emit(
  563. "stations.updateName",
  564. this.editing._id,
  565. name,
  566. res => {
  567. if (res.status === "success") {
  568. if (this.station) this.station.name = name;
  569. else {
  570. this.stations.forEach((station, index) => {
  571. if (station._id === this.editing._id) {
  572. this.stations[index].name = name;
  573. return name;
  574. }
  575. return false;
  576. });
  577. }
  578. }
  579. new Toast({ content: res.message, timeout: 8000 });
  580. }
  581. );
  582. },
  583. updateDisplayName() {
  584. const { displayName } = this.editing;
  585. if (!validation.isLength(displayName, 2, 32))
  586. return new Toast({
  587. content:
  588. "Display name must have between 2 and 32 characters.",
  589. timeout: 8000
  590. });
  591. if (!validation.regex.ascii.test(displayName))
  592. return new Toast({
  593. content:
  594. "Invalid display name format. Only ASCII characters are allowed.",
  595. timeout: 8000
  596. });
  597. return this.socket.emit(
  598. "stations.updateDisplayName",
  599. this.editing._id,
  600. displayName,
  601. res => {
  602. if (res.status === "success") {
  603. if (this.station)
  604. this.station.displayName = displayName;
  605. else {
  606. this.stations.forEach((station, index) => {
  607. if (station._id === this.editing._id) {
  608. this.stations[
  609. index
  610. ].displayName = displayName;
  611. return displayName;
  612. }
  613. return false;
  614. });
  615. }
  616. }
  617. new Toast({ content: res.message, timeout: 8000 });
  618. }
  619. );
  620. },
  621. updateDescription() {
  622. const { description } = this.editing;
  623. if (!validation.isLength(description, 2, 200))
  624. return new Toast({
  625. content:
  626. "Description must have between 2 and 200 characters.",
  627. timeout: 8000
  628. });
  629. let characters = description.split("");
  630. characters = characters.filter(character => {
  631. return character.charCodeAt(0) === 21328;
  632. });
  633. if (characters.length !== 0)
  634. return new Toast({
  635. content: "Invalid description format.",
  636. timeout: 8000
  637. });
  638. return this.socket.emit(
  639. "stations.updateDescription",
  640. this.editing._id,
  641. description,
  642. res => {
  643. if (res.status === "success") {
  644. if (this.station)
  645. this.station.description = description;
  646. else {
  647. this.stations.forEach((station, index) => {
  648. if (station._id === this.editing._id) {
  649. this.stations[
  650. index
  651. ].description = description;
  652. return description;
  653. }
  654. return false;
  655. });
  656. }
  657. return new Toast({
  658. content: res.message,
  659. timeout: 4000
  660. });
  661. }
  662. return new Toast({ content: res.message, timeout: 8000 });
  663. }
  664. );
  665. },
  666. updatePrivacyLocal(privacy) {
  667. if (this.editing.privacy === privacy) return;
  668. this.editing.privacy = privacy;
  669. this.privacyDropdownActive = false;
  670. },
  671. updatePrivacy() {
  672. this.socket.emit(
  673. "stations.updatePrivacy",
  674. this.editing._id,
  675. this.editing.privacy,
  676. res => {
  677. if (res.status === "success") {
  678. if (this.station)
  679. this.station.privacy = this.editing.privacy;
  680. else {
  681. this.stations.forEach((station, index) => {
  682. if (station._id === this.editing._id) {
  683. this.stations[
  684. index
  685. ].privacy = this.editing.privacy;
  686. return this.editing.privacy;
  687. }
  688. return false;
  689. });
  690. }
  691. return new Toast({
  692. content: res.message,
  693. timeout: 4000
  694. });
  695. }
  696. return new Toast({ content: res.message, timeout: 8000 });
  697. }
  698. );
  699. },
  700. updateGenres() {
  701. this.socket.emit(
  702. "stations.updateGenres",
  703. this.editing._id,
  704. this.editing.genres,
  705. res => {
  706. if (res.status === "success") {
  707. const genres = JSON.parse(
  708. JSON.stringify(this.editing.genres)
  709. );
  710. if (this.station) this.station.genres = genres;
  711. this.stations.forEach((station, index) => {
  712. if (station._id === this.editing._id) {
  713. this.stations[index].genres = genres;
  714. return genres;
  715. }
  716. return false;
  717. });
  718. return new Toast({
  719. content: res.message,
  720. timeout: 4000
  721. });
  722. }
  723. return new Toast({ content: res.message, timeout: 8000 });
  724. }
  725. );
  726. },
  727. updateBlacklistedGenres() {
  728. this.socket.emit(
  729. "stations.updateBlacklistedGenres",
  730. this.editing._id,
  731. this.editing.blacklistedGenres,
  732. res => {
  733. if (res.status === "success") {
  734. const blacklistedGenres = JSON.parse(
  735. JSON.stringify(this.editing.blacklistedGenres)
  736. );
  737. if (this.station)
  738. this.station.blacklistedGenres = blacklistedGenres;
  739. this.stations.forEach((station, index) => {
  740. if (station._id === this.editing._id) {
  741. this.stations[
  742. index
  743. ].blacklistedGenres = blacklistedGenres;
  744. return blacklistedGenres;
  745. }
  746. return false;
  747. });
  748. return new Toast({
  749. content: res.message,
  750. timeout: 4000
  751. });
  752. }
  753. return new Toast({ content: res.message, timeout: 8000 });
  754. }
  755. );
  756. },
  757. updatePartyModeLocal(partyMode) {
  758. if (this.editing.partyMode === partyMode) return;
  759. this.editing.partyMode = partyMode;
  760. this.modeDropdownActive = false;
  761. },
  762. updatePartyMode() {
  763. this.socket.emit(
  764. "stations.updatePartyMode",
  765. this.editing._id,
  766. this.editing.partyMode,
  767. res => {
  768. if (res.status === "success") {
  769. if (this.station)
  770. this.station.partyMode = this.editing.partyMode;
  771. // if (this.station)
  772. // this.station.partyMode = this.editing.partyMode;
  773. // this.stations.forEach((station, index) => {
  774. // if (station._id === this.editing._id) {
  775. // this.stations[
  776. // index
  777. // ].partyMode = this.editing.partyMode;
  778. // return this.editing.partyMode;
  779. // }
  780. // return false;
  781. // });
  782. return new Toast({
  783. content: res.message,
  784. timeout: 4000
  785. });
  786. }
  787. return new Toast({ content: res.message, timeout: 8000 });
  788. }
  789. );
  790. },
  791. updateQueueLockLocal(locked) {
  792. if (this.editing.locked === locked) return;
  793. this.editing.locked = locked;
  794. this.queueLockDropdownActive = false;
  795. },
  796. updateQueueLock() {
  797. this.socket.emit("stations.toggleLock", this.editing._id, res => {
  798. console.log(res);
  799. if (res.status === "success") {
  800. if (this.station) this.station.locked = res.data;
  801. return new Toast({
  802. content: `Toggled queue lock succesfully to ${res.data}`,
  803. timeout: 4000
  804. });
  805. }
  806. return new Toast({
  807. content: "Failed to toggle queue lock.",
  808. timeout: 8000
  809. });
  810. });
  811. },
  812. deleteStation() {
  813. this.socket.emit("stations.remove", this.editing._id, res => {
  814. if (res.status === "success")
  815. this.closeModal({
  816. sector: "station",
  817. modal: "editStation"
  818. });
  819. return new Toast({ content: res.message, timeout: 8000 });
  820. });
  821. },
  822. blurGenreInput() {
  823. this.genreInputFocussed = false;
  824. },
  825. focusGenreInput() {
  826. this.genreInputFocussed = true;
  827. },
  828. keydownGenreInput() {
  829. clearTimeout(this.keydownGenreInputTimeout);
  830. this.keydownGenreInputTimeout = setTimeout(() => {
  831. if (this.genreInputValue.length > 1) {
  832. this.genreAutosuggestItems = this.genres.filter(genre => {
  833. return genre
  834. .toLowerCase()
  835. .startsWith(this.genreInputValue.toLowerCase());
  836. });
  837. } else this.genreAutosuggestItems = [];
  838. }, 1000);
  839. },
  840. focusGenreContainer() {
  841. this.genreAutosuggestContainerFocussed = true;
  842. },
  843. blurGenreContainer() {
  844. this.genreAutosuggestContainerFocussed = false;
  845. },
  846. selectGenreAutosuggest(value) {
  847. this.genreInputValue = value;
  848. },
  849. blurBlacklistGenreInput() {
  850. this.blacklistGenreInputFocussed = false;
  851. },
  852. focusBlacklistGenreInput() {
  853. this.blacklistGenreInputFocussed = true;
  854. },
  855. keydownBlacklistGenreInput() {
  856. clearTimeout(this.keydownBlacklistGenreInputTimeout);
  857. this.keydownBlacklistGenreInputTimeout = setTimeout(() => {
  858. if (this.blacklistGenreInputValue.length > 1) {
  859. this.blacklistGenreAutosuggestItems = this.genres.filter(
  860. genre => {
  861. return genre
  862. .toLowerCase()
  863. .startsWith(
  864. this.blacklistGenreInputValue.toLowerCase()
  865. );
  866. }
  867. );
  868. } else this.blacklistGenreAutosuggestItems = [];
  869. }, 1000);
  870. },
  871. focusBlacklistGenreContainer() {
  872. this.blacklistGenreAutosuggestContainerFocussed = true;
  873. },
  874. blurBlacklistGenreContainer() {
  875. this.blacklistGenreAutosuggestContainerFocussed = false;
  876. },
  877. selectBlacklistGenreAutosuggest(value) {
  878. this.blacklistGenreInputValue = value;
  879. },
  880. addTag(type) {
  881. if (type === "genres") {
  882. const genre = this.genreInputValue.toLowerCase().trim();
  883. if (this.editing.genres.indexOf(genre) !== -1)
  884. return new Toast({
  885. content: "Genre already exists",
  886. timeout: 3000
  887. });
  888. if (genre) {
  889. this.editing.genres.push(genre);
  890. this.genreInputValue = "";
  891. return false;
  892. }
  893. return new Toast({
  894. content: "Genre cannot be empty",
  895. timeout: 3000
  896. });
  897. }
  898. if (type === "blacklist-genres") {
  899. const genre = this.blacklistGenreInputValue
  900. .toLowerCase()
  901. .trim();
  902. if (this.editing.blacklistedGenres.indexOf(genre) !== -1)
  903. return new Toast({
  904. content: "Blacklist genre already exists",
  905. timeout: 3000
  906. });
  907. if (genre) {
  908. this.editing.blacklistedGenres.push(genre);
  909. this.blacklistGenreInputValue = "";
  910. return false;
  911. }
  912. return new Toast({
  913. content: "Blacklist genre cannot be empty",
  914. timeout: 3000
  915. });
  916. }
  917. return false;
  918. },
  919. removeTag(type, index) {
  920. if (type === "genres") this.editing.genres.splice(index, 1);
  921. else if (type === "blacklist-genres")
  922. this.editing.blacklistedGenres.splice(index, 1);
  923. },
  924. ...mapActions("modals", ["closeModal"])
  925. },
  926. components: { Modal, PlaylistItem }
  927. };
  928. </script>
  929. <style lang="scss">
  930. @import "styles/global.scss";
  931. .night-mode {
  932. .modal-card,
  933. .modal-card-head,
  934. .modal-card-body,
  935. .modal-card-foot {
  936. background-color: $night-mode-secondary;
  937. }
  938. .section {
  939. background-color: #111 !important;
  940. border: 0 !important;
  941. }
  942. .label,
  943. p,
  944. strong {
  945. color: #ddd;
  946. }
  947. }
  948. .edit-station-modal {
  949. .modal-card-title {
  950. text-align: center;
  951. margin-left: 24px;
  952. }
  953. .modal-card {
  954. width: 800px;
  955. font-size: 16px;
  956. .modal-card-body {
  957. padding: 16px;
  958. display: flex;
  959. }
  960. }
  961. }
  962. .section {
  963. border: 1px solid #a3e0ff;
  964. background-color: #f4f4f4;
  965. border-radius: 5px;
  966. padding: 16px;
  967. }
  968. .left-section {
  969. width: 595px;
  970. display: grid;
  971. gap: 16px;
  972. grid-template-rows: min-content min-content auto;
  973. .control {
  974. input {
  975. width: 100%;
  976. height: 36px;
  977. }
  978. .add-button {
  979. width: 32px;
  980. &.blue {
  981. background-color: $musareBlue !important;
  982. }
  983. &.red {
  984. background-color: $red !important;
  985. }
  986. i {
  987. font-size: 32px;
  988. }
  989. }
  990. }
  991. .col {
  992. > div {
  993. position: relative;
  994. }
  995. }
  996. .list-item-circle {
  997. width: 16px;
  998. height: 16px;
  999. border-radius: 8px;
  1000. cursor: pointer;
  1001. margin-right: 8px;
  1002. float: left;
  1003. -webkit-touch-callout: none;
  1004. -webkit-user-select: none;
  1005. -khtml-user-select: none;
  1006. -moz-user-select: none;
  1007. -ms-user-select: none;
  1008. user-select: none;
  1009. &.blue {
  1010. background-color: $musareBlue;
  1011. i {
  1012. color: $musareBlue;
  1013. }
  1014. }
  1015. &.red {
  1016. background-color: $red;
  1017. i {
  1018. color: $red;
  1019. }
  1020. }
  1021. i {
  1022. font-size: 14px;
  1023. margin-left: 1px;
  1024. }
  1025. }
  1026. .list-item-circle:hover,
  1027. .list-item-circle:focus {
  1028. i {
  1029. color: white;
  1030. }
  1031. }
  1032. .list-item > p {
  1033. line-height: 16px;
  1034. word-wrap: break-word;
  1035. width: calc(100% - 24px);
  1036. left: 24px;
  1037. float: left;
  1038. margin-bottom: 8px;
  1039. }
  1040. .list-item:last-child > p {
  1041. margin-bottom: 0;
  1042. }
  1043. .autosuggest-container {
  1044. position: absolute;
  1045. background: white;
  1046. width: calc(100% + 1px);
  1047. top: 57px;
  1048. z-index: 200;
  1049. overflow: auto;
  1050. max-height: 100%;
  1051. clear: both;
  1052. .autosuggest-item {
  1053. padding: 8px;
  1054. display: block;
  1055. border: 1px solid #dbdbdb;
  1056. margin-top: -1px;
  1057. line-height: 16px;
  1058. cursor: pointer;
  1059. -webkit-user-select: none;
  1060. -ms-user-select: none;
  1061. -moz-user-select: none;
  1062. user-select: none;
  1063. }
  1064. .autosuggest-item:hover,
  1065. .autosuggest-item:focus {
  1066. background-color: #eee;
  1067. }
  1068. .autosuggest-item:first-child {
  1069. border-top: none;
  1070. }
  1071. .autosuggest-item:last-child {
  1072. border-radius: 0 0 3px 3px;
  1073. }
  1074. }
  1075. }
  1076. .right-section {
  1077. width: 157px;
  1078. min-height: 375px;
  1079. margin-left: 16px;
  1080. display: grid;
  1081. gap: 16px;
  1082. grid-template-rows: min-content min-content min-content;
  1083. .button-wrapper {
  1084. display: flex;
  1085. flex-direction: column;
  1086. }
  1087. button {
  1088. width: 100%;
  1089. height: 36px;
  1090. border: 0;
  1091. border-radius: 3px;
  1092. font-size: 18px;
  1093. color: white;
  1094. box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25);
  1095. display: block;
  1096. text-align: center;
  1097. justify-content: center;
  1098. display: inline-flex;
  1099. -ms-flex-align: center;
  1100. align-items: center;
  1101. -moz-user-select: none;
  1102. user-select: none;
  1103. cursor: pointer;
  1104. margin-bottom: 10px;
  1105. padding: 0;
  1106. &.red {
  1107. background-color: $red;
  1108. }
  1109. &.green {
  1110. background-color: $green;
  1111. }
  1112. &.blue {
  1113. background-color: $musareBlue;
  1114. }
  1115. &.orange {
  1116. background-color: $light-orange;
  1117. }
  1118. &.yellow {
  1119. background-color: $yellow;
  1120. }
  1121. i {
  1122. font-size: 20px;
  1123. margin-right: 4px;
  1124. }
  1125. }
  1126. }
  1127. .col {
  1128. display: grid;
  1129. grid-column-gap: 16px;
  1130. }
  1131. .col-1 {
  1132. grid-template-columns: auto;
  1133. }
  1134. .col-2 {
  1135. grid-template-columns: auto auto;
  1136. }
  1137. .slide-down-enter-active {
  1138. transition: transform 0.25s;
  1139. }
  1140. .slide-down-enter {
  1141. transform: translateY(-10px);
  1142. }
  1143. #playlists {
  1144. height: 168px;
  1145. overflow: auto;
  1146. }
  1147. .modal-card {
  1148. overflow: auto;
  1149. }
  1150. .modal-card-body {
  1151. overflow: unset;
  1152. }
  1153. </style>