EditStation.vue 23 KB

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