Home.vue 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363
  1. <template>
  2. <div>
  3. <page-metadata title="Home" />
  4. <div class="app home-page">
  5. <main-header
  6. :hide-logo="true"
  7. :transparent="true"
  8. :hide-logged-out="true"
  9. />
  10. <div class="header" :class="{ loggedIn }">
  11. <img class="background" src="/assets/homebg.jpeg" />
  12. <div class="overlay"></div>
  13. <div class="content-container">
  14. <div class="content">
  15. <img
  16. v-if="siteSettings.sitename === 'Musare'"
  17. :src="siteSettings.logo_white"
  18. :alt="siteSettings.sitename || `Musare`"
  19. class="logo"
  20. />
  21. <span v-else class="logo">{{
  22. siteSettings.sitename
  23. }}</span>
  24. <div v-if="!loggedIn" class="buttons">
  25. <button
  26. class="button login"
  27. @click="openModal('login')"
  28. >
  29. Login
  30. </button>
  31. <button
  32. v-if="!siteSettings.registrationDisabled"
  33. class="button register"
  34. @click="openModal('register')"
  35. >
  36. Register
  37. </button>
  38. </div>
  39. </div>
  40. </div>
  41. </div>
  42. <div class="group" v-show="favoriteStations.length > 0">
  43. <div class="group-title">
  44. <div>
  45. <h2>My Favorites</h2>
  46. </div>
  47. </div>
  48. <draggable
  49. item-key="_id"
  50. v-model="favoriteStations"
  51. v-bind="dragOptions"
  52. @change="changeFavoriteOrder"
  53. >
  54. <template #item="{ element }">
  55. <router-link
  56. :to="{
  57. name: 'station',
  58. params: { id: element.name }
  59. }"
  60. :class="{
  61. 'station-card': true,
  62. 'item-draggable': true,
  63. isPrivate: element.privacy === 'private',
  64. isMine: isOwner(element)
  65. }"
  66. :style="
  67. '--primary-color: var(--' + element.theme + ')'
  68. "
  69. >
  70. <div class="card-content">
  71. <song-thumbnail :song="element.currentSong">
  72. <template #icon>
  73. <div class="icon-container">
  74. <div
  75. v-if="isOwnerOrAdmin(element)"
  76. class="material-icons manage-station"
  77. @click.prevent="
  78. openModal({
  79. modal: 'manageStation',
  80. data: {
  81. stationId:
  82. element._id,
  83. sector: 'home'
  84. }
  85. })
  86. "
  87. content="Manage Station"
  88. v-tippy
  89. >
  90. settings
  91. </div>
  92. <div
  93. v-else
  94. class="material-icons manage-station"
  95. @click.prevent="
  96. openModal({
  97. modal: 'manageStation',
  98. data: {
  99. stationId:
  100. element._id,
  101. sector: 'home'
  102. }
  103. })
  104. "
  105. content="View Queue"
  106. v-tippy
  107. >
  108. queue_music
  109. </div>
  110. </div>
  111. </template>
  112. </song-thumbnail>
  113. <div class="media">
  114. <div class="displayName">
  115. <i
  116. v-if="
  117. loggedIn && !element.isFavorited
  118. "
  119. @click.prevent="
  120. favoriteStation(element._id)
  121. "
  122. class="favorite material-icons"
  123. content="Favorite Station"
  124. v-tippy
  125. >star_border</i
  126. >
  127. <i
  128. v-if="
  129. loggedIn && element.isFavorited
  130. "
  131. @click.prevent="
  132. unfavoriteStation(element._id)
  133. "
  134. class="favorite material-icons"
  135. content="Unfavorite Station"
  136. v-tippy
  137. >star</i
  138. >
  139. <h5>{{ element.displayName }}</h5>
  140. <i
  141. v-if="element.type === 'official'"
  142. class="material-icons verified-station"
  143. content="Verified Station"
  144. v-tippy="{
  145. theme: 'info'
  146. }"
  147. >
  148. check_circle
  149. </i>
  150. </div>
  151. <div class="content">
  152. {{ element.description }}
  153. </div>
  154. <div class="under-content">
  155. <p class="hostedBy">
  156. Hosted by
  157. <span class="host">
  158. <span
  159. v-if="
  160. element.type ===
  161. 'official'
  162. "
  163. :title="
  164. siteSettings.sitename
  165. "
  166. >{{
  167. siteSettings.sitename
  168. }}</span
  169. >
  170. <user-link
  171. v-else
  172. :user-id="element.owner"
  173. />
  174. </span>
  175. </p>
  176. <div class="icons">
  177. <i
  178. v-if="
  179. element.type ===
  180. 'community' &&
  181. isOwner(element)
  182. "
  183. class="homeIcon material-icons"
  184. content="This is your station."
  185. v-tippy="{ theme: 'info' }"
  186. >home</i
  187. >
  188. <i
  189. v-if="
  190. element.privacy ===
  191. 'private'
  192. "
  193. class="privateIcon material-icons"
  194. content="This station is not visible to other users."
  195. v-tippy="{ theme: 'info' }"
  196. >lock</i
  197. >
  198. <i
  199. v-if="
  200. element.privacy ===
  201. 'unlisted'
  202. "
  203. class="unlistedIcon material-icons"
  204. content="Unlisted Station"
  205. v-tippy="{ theme: 'info' }"
  206. >link</i
  207. >
  208. </div>
  209. </div>
  210. </div>
  211. </div>
  212. <div class="bottomBar">
  213. <i
  214. v-if="
  215. element.paused &&
  216. element.currentSong.title
  217. "
  218. class="material-icons"
  219. content="Station Paused"
  220. v-tippy="{ theme: 'info' }"
  221. >pause</i
  222. >
  223. <i
  224. v-else-if="element.currentSong.title"
  225. class="material-icons"
  226. >music_note</i
  227. >
  228. <i v-else class="material-icons">music_off</i>
  229. <span
  230. v-if="element.currentSong.title"
  231. class="songTitle"
  232. :title="
  233. element.currentSong.artists.length > 0
  234. ? 'Now Playing: ' +
  235. element.currentSong.title +
  236. ' by ' +
  237. element.currentSong.artists.join(
  238. ', '
  239. )
  240. : 'Now Playing: ' +
  241. element.currentSong.title
  242. "
  243. >{{ element.currentSong.title }}
  244. {{
  245. element.currentSong.artists.length > 0
  246. ? " by " +
  247. element.currentSong.artists.join(
  248. ", "
  249. )
  250. : ""
  251. }}</span
  252. >
  253. <span v-else class="songTitle"
  254. >No Songs Playing</span
  255. >
  256. <i
  257. v-if="canRequest(element)"
  258. class="material-icons"
  259. content="You can request songs in this station"
  260. v-tippy="{ theme: 'info' }"
  261. >
  262. queue
  263. </i>
  264. </div>
  265. </router-link>
  266. </template>
  267. </draggable>
  268. </div>
  269. <div class="group bottom">
  270. <div class="group-title">
  271. <div>
  272. <h1>Stations</h1>
  273. </div>
  274. </div>
  275. <a
  276. v-if="loggedIn"
  277. @click="openModal('createStation')"
  278. class="station-card createStation"
  279. >
  280. <div class="card-content">
  281. <div class="thumbnail">
  282. <figure class="image">
  283. <i class="material-icons">radio</i>
  284. </figure>
  285. </div>
  286. <div class="media">
  287. <div class="displayName">
  288. <h5>Create Station</h5>
  289. </div>
  290. <div class="content">
  291. Click here to create your own station!
  292. </div>
  293. </div>
  294. </div>
  295. <div class="bottomBar"></div>
  296. </a>
  297. <a
  298. v-else
  299. @click="openModal('login')"
  300. class="station-card createStation"
  301. >
  302. <div class="card-content">
  303. <div class="thumbnail">
  304. <figure class="image">
  305. <i class="material-icons">radio</i>
  306. </figure>
  307. </div>
  308. <div class="media">
  309. <div class="displayName">
  310. <h5>Create Station</h5>
  311. </div>
  312. <div class="content">
  313. Login to create a station!
  314. </div>
  315. </div>
  316. </div>
  317. <div class="bottomBar"></div>
  318. </a>
  319. <router-link
  320. v-for="station in filteredStations"
  321. :key="station._id"
  322. :to="{
  323. name: 'station',
  324. params: { id: station.name }
  325. }"
  326. class="station-card"
  327. :class="{
  328. isPrivate: station.privacy === 'private',
  329. isMine: isOwner(station)
  330. }"
  331. :style="'--primary-color: var(--' + station.theme + ')'"
  332. >
  333. <div class="card-content">
  334. <song-thumbnail :song="station.currentSong">
  335. <template #icon>
  336. <div class="icon-container">
  337. <div
  338. v-if="isOwnerOrAdmin(station)"
  339. class="material-icons manage-station"
  340. @click.prevent="
  341. openModal({
  342. modal: 'manageStation',
  343. data: {
  344. stationId: station._id,
  345. sector: 'home'
  346. }
  347. })
  348. "
  349. content="Manage Station"
  350. v-tippy
  351. >
  352. settings
  353. </div>
  354. <div
  355. v-else
  356. class="material-icons manage-station"
  357. @click.prevent="
  358. openModal({
  359. modal: 'manageStation',
  360. data: {
  361. stationId: station._id,
  362. sector: 'home'
  363. }
  364. })
  365. "
  366. content="View Queue"
  367. v-tippy
  368. >
  369. queue_music
  370. </div>
  371. </div>
  372. </template>
  373. </song-thumbnail>
  374. <div class="media">
  375. <div class="displayName">
  376. <i
  377. v-if="loggedIn && !station.isFavorited"
  378. @click.prevent="
  379. favoriteStation(station._id)
  380. "
  381. class="favorite material-icons"
  382. content="Favorite Station"
  383. v-tippy
  384. >star_border</i
  385. >
  386. <i
  387. v-if="loggedIn && station.isFavorited"
  388. @click.prevent="
  389. unfavoriteStation(station._id)
  390. "
  391. class="favorite material-icons"
  392. content="Unfavorite Station"
  393. v-tippy
  394. >star</i
  395. >
  396. <h5>{{ station.displayName }}</h5>
  397. <i
  398. v-if="station.type === 'official'"
  399. class="material-icons verified-station"
  400. content="Verified Station"
  401. v-tippy="{ theme: 'info' }"
  402. >
  403. check_circle
  404. </i>
  405. </div>
  406. <div class="content">
  407. {{ station.description }}
  408. </div>
  409. <div class="under-content">
  410. <p class="hostedBy">
  411. Hosted by
  412. <span class="host">
  413. <span
  414. v-if="station.type === 'official'"
  415. :title="siteSettings.sitename"
  416. >{{ siteSettings.sitename }}</span
  417. >
  418. <user-link
  419. v-else
  420. :user-id="station.owner"
  421. />
  422. </span>
  423. </p>
  424. <div class="icons">
  425. <i
  426. v-if="
  427. station.type === 'community' &&
  428. isOwner(station)
  429. "
  430. class="homeIcon material-icons"
  431. content="This is your station."
  432. v-tippy="{ theme: 'info' }"
  433. >home</i
  434. >
  435. <i
  436. v-if="station.privacy === 'private'"
  437. class="privateIcon material-icons"
  438. content="This station is not visible to other users."
  439. v-tippy="{ theme: 'info' }"
  440. >lock</i
  441. >
  442. <i
  443. v-if="station.privacy === 'unlisted'"
  444. class="unlistedIcon material-icons"
  445. content="Unlisted Station"
  446. v-tippy="{ theme: 'info' }"
  447. >link</i
  448. >
  449. </div>
  450. </div>
  451. </div>
  452. </div>
  453. <div class="bottomBar">
  454. <i
  455. v-if="station.paused && station.currentSong.title"
  456. class="material-icons"
  457. content="Station Paused"
  458. v-tippy="{ theme: 'info' }"
  459. >pause</i
  460. >
  461. <i
  462. v-else-if="station.currentSong.title"
  463. class="material-icons"
  464. >music_note</i
  465. >
  466. <i v-else class="material-icons">music_off</i>
  467. <span
  468. v-if="station.currentSong.title"
  469. class="songTitle"
  470. :title="
  471. station.currentSong.artists.length > 0
  472. ? 'Now Playing: ' +
  473. station.currentSong.title +
  474. ' by ' +
  475. station.currentSong.artists.join(', ')
  476. : 'Now Playing: ' +
  477. station.currentSong.title
  478. "
  479. >{{ station.currentSong.title }}
  480. {{
  481. station.currentSong.artists.length > 0
  482. ? " by " +
  483. station.currentSong.artists.join(", ")
  484. : ""
  485. }}</span
  486. >
  487. <span v-else class="songTitle">No Songs Playing</span>
  488. <i
  489. v-if="canRequest(station)"
  490. class="material-icons"
  491. content="You can request songs in this station"
  492. v-tippy="{ theme: 'info' }"
  493. >
  494. queue
  495. </i>
  496. <i
  497. v-else-if="canRequest(station, false)"
  498. class="material-icons"
  499. content="Login to request songs in this station"
  500. v-tippy="{ theme: 'info' }"
  501. >
  502. queue
  503. </i>
  504. </div>
  505. </router-link>
  506. <h4 v-if="stations.length === 0">
  507. There are no stations to display
  508. </h4>
  509. </div>
  510. <main-footer />
  511. </div>
  512. </div>
  513. </template>
  514. <script>
  515. import { mapState, mapGetters, mapActions } from "vuex";
  516. import draggable from "vuedraggable";
  517. import Toast from "toasters";
  518. import ws from "@/ws";
  519. import keyboardShortcuts from "@/keyboardShortcuts";
  520. export default {
  521. components: {
  522. draggable
  523. },
  524. data() {
  525. return {
  526. recaptcha: { key: "" },
  527. stations: [],
  528. favoriteStations: [],
  529. searchQuery: "",
  530. siteSettings: {
  531. logo_white: "",
  532. sitename: "Musare",
  533. registrationDisabled: false
  534. },
  535. orderOfFavoriteStations: [],
  536. handledLoginRegisterRedirect: false,
  537. editingStationId: null
  538. };
  539. },
  540. computed: {
  541. ...mapState({
  542. loggedIn: state => state.user.auth.loggedIn,
  543. userId: state => state.user.auth.userId,
  544. role: state => state.user.auth.role,
  545. modals: state => state.modalVisibility.modals
  546. }),
  547. ...mapGetters({
  548. socket: "websockets/getSocket"
  549. }),
  550. filteredStations() {
  551. const privacyOrder = ["public", "unlisted", "private"];
  552. return this.stations
  553. .filter(
  554. station =>
  555. JSON.stringify(Object.values(station)).indexOf(
  556. this.searchQuery
  557. ) !== -1
  558. )
  559. .sort(
  560. (a, b) =>
  561. this.isOwner(b) - this.isOwner(a) ||
  562. this.isPlaying(b) - this.isPlaying(a) ||
  563. a.paused - b.paused ||
  564. privacyOrder.indexOf(a.privacy) -
  565. privacyOrder.indexOf(b.privacy) ||
  566. b.userCount - a.userCount
  567. );
  568. },
  569. dragOptions() {
  570. return {
  571. animation: 200,
  572. group: "favoriteStations",
  573. disabled: false,
  574. ghostClass: "draggable-list-ghost",
  575. filter: ".ignore-elements",
  576. fallbackTolerance: 50
  577. };
  578. }
  579. },
  580. watch: {
  581. orderOfFavoriteStations: {
  582. deep: true,
  583. handler() {
  584. this.calculateFavoriteStations();
  585. }
  586. }
  587. },
  588. async mounted() {
  589. this.siteSettings = await lofig.get("siteSettings");
  590. if (this.$route.query.searchQuery)
  591. this.searchQuery = this.$route.query.query;
  592. if (
  593. !this.loggedIn &&
  594. this.$route.redirectedFrom &&
  595. (this.$route.redirectedFrom.name === "login" ||
  596. this.$route.redirectedFrom.name === "register") &&
  597. !this.handledLoginRegisterRedirect
  598. ) {
  599. // Makes sure the login/register modal isn't opened whenever the home page gets remounted due to a code change
  600. this.handledLoginRegisterRedirect = true;
  601. this.openModal(this.$route.redirectedFrom.name);
  602. }
  603. ws.onConnect(this.init);
  604. this.socket.on("event:station.created", res => {
  605. const { station } = res.data;
  606. if (this.stations.find(_station => _station._id === station._id)) {
  607. this.stations.forEach(s => {
  608. const _station = s;
  609. if (_station._id === station._id) {
  610. _station.privacy = station.privacy;
  611. }
  612. });
  613. } else {
  614. if (!station.currentSong)
  615. station.currentSong = {
  616. thumbnail: "/assets/notes-transparent.png"
  617. };
  618. if (station.currentSong && !station.currentSong.thumbnail)
  619. station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.youtubeId}/mqdefault.jpg`;
  620. this.stations.push(station);
  621. }
  622. });
  623. this.socket.on("event:station.deleted", res => {
  624. const { stationId } = res.data;
  625. const station = this.stations.find(
  626. station => station._id === stationId
  627. );
  628. if (station) {
  629. const stationIndex = this.stations.indexOf(station);
  630. this.stations.splice(stationIndex, 1);
  631. if (station.isFavorited)
  632. this.orderOfFavoriteStations =
  633. this.orderOfFavoriteStations.filter(
  634. favoritedId => favoritedId !== stationId
  635. );
  636. }
  637. });
  638. this.socket.on("event:station.userCount.updated", res => {
  639. const station = this.stations.find(
  640. station => station._id === res.data.stationId
  641. );
  642. if (station) station.userCount = res.data.userCount;
  643. });
  644. this.socket.on("event:station.updated", res => {
  645. const stationIndex = this.stations
  646. .map(station => station._id)
  647. .indexOf(res.data.station._id);
  648. if (stationIndex !== -1) {
  649. this.stations[stationIndex] = {
  650. ...this.stations[stationIndex],
  651. ...res.data.station
  652. };
  653. this.calculateFavoriteStations();
  654. }
  655. });
  656. this.socket.on("event:station.nextSong", res => {
  657. const station = this.stations.find(
  658. station => station._id === res.data.stationId
  659. );
  660. if (station) {
  661. let newSong = res.data.currentSong;
  662. if (!newSong)
  663. newSong = {
  664. thumbnail: "/assets/notes-transparent.png"
  665. };
  666. station.currentSong = newSong;
  667. }
  668. });
  669. this.socket.on("event:station.pause", res => {
  670. const station = this.stations.find(
  671. station => station._id === res.data.stationId
  672. );
  673. if (station) station.paused = true;
  674. });
  675. this.socket.on("event:station.resume", res => {
  676. const station = this.stations.find(
  677. station => station._id === res.data.stationId
  678. );
  679. if (station) station.paused = false;
  680. });
  681. this.socket.on("event:user.station.favorited", res => {
  682. const { stationId } = res.data;
  683. const station = this.stations.find(
  684. station => station._id === stationId
  685. );
  686. if (station) {
  687. station.isFavorited = true;
  688. this.orderOfFavoriteStations.push(stationId);
  689. }
  690. });
  691. this.socket.on("event:user.station.unfavorited", res => {
  692. const { stationId } = res.data;
  693. const station = this.stations.find(
  694. station => station._id === stationId
  695. );
  696. if (station) {
  697. station.isFavorited = false;
  698. this.orderOfFavoriteStations =
  699. this.orderOfFavoriteStations.filter(
  700. favoritedId => favoritedId !== stationId
  701. );
  702. }
  703. });
  704. this.socket.on("event:user.orderOfFavoriteStations.updated", res => {
  705. this.orderOfFavoriteStations = res.data.order;
  706. });
  707. // ctrl + alt + f
  708. keyboardShortcuts.registerShortcut("home.toggleAdminFilter", {
  709. keyCode: 70,
  710. ctrl: true,
  711. alt: true,
  712. handler: () => {
  713. if (this.isAdmin())
  714. if (this.$route.query.adminFilter === undefined)
  715. this.$router.push({
  716. query: {
  717. ...this.$route.query,
  718. adminFilter: null
  719. }
  720. });
  721. else
  722. this.$router.push({
  723. query: {
  724. ...this.$route.query,
  725. adminFilter: undefined
  726. }
  727. });
  728. }
  729. });
  730. },
  731. beforeUnmount() {
  732. this.socket.dispatch("apis.leaveRoom", "home", () => {});
  733. const shortcutNames = ["home.toggleAdminFilter"];
  734. shortcutNames.forEach(shortcutName => {
  735. keyboardShortcuts.unregisterShortcut(shortcutName);
  736. });
  737. },
  738. methods: {
  739. init() {
  740. this.socket.dispatch(
  741. "stations.index",
  742. this.$route.query.adminFilter === undefined,
  743. res => {
  744. this.stations = [];
  745. if (res.status === "success") {
  746. res.data.stations.forEach(station => {
  747. const modifiableStation = station;
  748. if (!modifiableStation.currentSong)
  749. modifiableStation.currentSong = {
  750. thumbnail: "/assets/notes-transparent.png"
  751. };
  752. if (
  753. modifiableStation.currentSong &&
  754. !modifiableStation.currentSong.thumbnail
  755. )
  756. modifiableStation.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.youtubeId}/mqdefault.jpg`;
  757. this.stations.push(modifiableStation);
  758. });
  759. this.orderOfFavoriteStations = res.data.favorited;
  760. }
  761. }
  762. );
  763. this.socket.dispatch("apis.joinRoom", "home");
  764. },
  765. isOwner(station) {
  766. return this.loggedIn && station.owner === this.userId;
  767. },
  768. isAdmin() {
  769. return this.loggedIn && this.role === "admin";
  770. },
  771. isOwnerOrAdmin(station) {
  772. return this.isOwner(station) || this.isAdmin();
  773. },
  774. canRequest(station, requireLogin = true) {
  775. return (
  776. station &&
  777. (!requireLogin || this.loggedIn) &&
  778. station.requests &&
  779. station.requests.enabled &&
  780. (station.requests.access === "user" ||
  781. (station.requests.access === "owner" &&
  782. this.isOwnerOrAdmin(station)))
  783. );
  784. },
  785. isPlaying(station) {
  786. return typeof station.currentSong.title !== "undefined";
  787. },
  788. favoriteStation(stationId) {
  789. this.socket.dispatch("stations.favoriteStation", stationId, res => {
  790. if (res.status === "success") {
  791. new Toast("Successfully favorited station.");
  792. } else new Toast(res.message);
  793. });
  794. },
  795. unfavoriteStation(stationId) {
  796. this.socket.dispatch(
  797. "stations.unfavoriteStation",
  798. stationId,
  799. res => {
  800. if (res.status === "success") {
  801. new Toast("Successfully unfavorited station.");
  802. } else new Toast(res.message);
  803. }
  804. );
  805. },
  806. calculateFavoriteStations() {
  807. this.favoriteStations = this.filteredStations
  808. .filter(station => station.isFavorited === true)
  809. .sort(
  810. (a, b) =>
  811. this.orderOfFavoriteStations.indexOf(a._id) -
  812. this.orderOfFavoriteStations.indexOf(b._id)
  813. );
  814. },
  815. changeFavoriteOrder() {
  816. const recalculatedOrder = [];
  817. this.favoriteStations.forEach(station =>
  818. recalculatedOrder.push(station._id)
  819. );
  820. this.socket.dispatch(
  821. "users.updateOrderOfFavoriteStations",
  822. recalculatedOrder,
  823. res => new Toast(res.message)
  824. );
  825. },
  826. ...mapActions("modalVisibility", ["openModal"]),
  827. ...mapActions("station", ["updateIfStationIsFavorited"])
  828. }
  829. };
  830. </script>
  831. <style lang="less">
  832. .christmas-mode .home-page {
  833. .header .overlay {
  834. background: linear-gradient(
  835. 180deg,
  836. rgba(231, 77, 60, 0.8) 0%,
  837. rgba(231, 77, 60, 0.95) 31.25%,
  838. rgba(231, 77, 60, 0.9) 54.17%,
  839. rgba(231, 77, 60, 0.8) 100%
  840. );
  841. }
  842. .christmas-lights {
  843. top: 300px !important;
  844. &.loggedIn {
  845. top: 200px !important;
  846. }
  847. }
  848. .header {
  849. &,
  850. .background,
  851. .overlay {
  852. border-radius: unset;
  853. }
  854. }
  855. }
  856. </style>
  857. <style lang="less" scoped>
  858. * {
  859. box-sizing: border-box;
  860. }
  861. html {
  862. width: 100%;
  863. height: 100%;
  864. color: rgba(0, 0, 0, 0.87);
  865. body {
  866. width: 100%;
  867. height: 100%;
  868. margin: 0;
  869. padding: 0;
  870. }
  871. @media only screen and (min-width: 1200px) {
  872. font-size: 15px;
  873. }
  874. @media only screen and (min-width: 992px) {
  875. font-size: 14.5px;
  876. }
  877. @media only screen and (min-width: 0) {
  878. font-size: 14px;
  879. }
  880. }
  881. .night-mode {
  882. .header .overlay {
  883. background: linear-gradient(
  884. 180deg,
  885. rgba(34, 34, 34, 0.8) 0%,
  886. rgba(34, 34, 34, 0.95) 31.25%,
  887. rgba(34, 34, 34, 0.9) 54.17%,
  888. rgba(34, 34, 34, 0.8) 100%
  889. );
  890. }
  891. .station-card {
  892. background-color: var(--dark-grey-3);
  893. .thumbnail {
  894. background-color: var(--dark-grey-2);
  895. i {
  896. user-select: none;
  897. -webkit-user-select: none;
  898. }
  899. }
  900. .card-content .media {
  901. .icons i,
  902. .under-content .hostedBy {
  903. color: var(--light-grey-2) !important;
  904. }
  905. }
  906. }
  907. .group-title i {
  908. color: var(--light-grey-2);
  909. }
  910. }
  911. .header {
  912. display: flex;
  913. height: 300px;
  914. margin-top: -64px;
  915. border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
  916. img.background {
  917. height: 300px;
  918. width: 100%;
  919. object-fit: cover;
  920. object-position: center;
  921. filter: blur(1px);
  922. border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
  923. overflow: hidden;
  924. user-select: none;
  925. }
  926. .overlay {
  927. background: linear-gradient(
  928. 180deg,
  929. rgba(3, 169, 244, 0.8) 0%,
  930. rgba(3, 169, 244, 0.95) 31.25%,
  931. rgba(3, 169, 244, 0.9) 54.17%,
  932. rgba(3, 169, 244, 0.8) 100%
  933. );
  934. position: absolute;
  935. height: 300px;
  936. width: 100%;
  937. border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
  938. overflow: hidden;
  939. }
  940. .content-container {
  941. position: absolute;
  942. left: 0;
  943. right: 0;
  944. margin-left: auto;
  945. margin-right: auto;
  946. text-align: center;
  947. height: 300px;
  948. .content {
  949. position: absolute;
  950. top: 50%;
  951. left: 0;
  952. right: 0;
  953. transform: translateY(-50%);
  954. background-color: transparent !important;
  955. .logo {
  956. max-height: 90px;
  957. font-size: 50px;
  958. color: var(--white);
  959. font-family: Pacifico, cursive;
  960. user-select: none;
  961. white-space: nowrap;
  962. }
  963. .buttons {
  964. display: flex;
  965. justify-content: center;
  966. margin-top: 20px;
  967. flex-wrap: wrap;
  968. .login,
  969. .register {
  970. margin: 5px 10px;
  971. padding: 10px 15px;
  972. border-radius: @border-radius;
  973. font-size: 18px;
  974. width: 100%;
  975. max-width: 250px;
  976. font-weight: 600;
  977. border: 0;
  978. height: inherit;
  979. }
  980. .login {
  981. background: var(--white);
  982. color: var(--primary-color);
  983. }
  984. .register {
  985. background: var(--purple);
  986. color: var(--white);
  987. }
  988. }
  989. }
  990. }
  991. &.loggedIn {
  992. height: 200px;
  993. .overlay,
  994. .content-container,
  995. img.background {
  996. height: 200px;
  997. }
  998. }
  999. }
  1000. .app {
  1001. display: flex;
  1002. flex-direction: column;
  1003. }
  1004. .station-card {
  1005. display: inline-flex;
  1006. position: relative;
  1007. background-color: var(--white);
  1008. color: var(--dark-grey);
  1009. flex-direction: row;
  1010. overflow: hidden;
  1011. margin: 10px;
  1012. cursor: pointer;
  1013. filter: none;
  1014. height: 150px;
  1015. width: calc(100% - 30px);
  1016. max-width: 400px;
  1017. flex-wrap: wrap;
  1018. border-radius: @border-radius;
  1019. box-shadow: @box-shadow;
  1020. .card-content {
  1021. display: flex;
  1022. flex-direction: row;
  1023. flex-grow: 1;
  1024. .thumbnail {
  1025. display: flex;
  1026. position: relative;
  1027. min-width: 120px;
  1028. width: 120px;
  1029. height: 120px;
  1030. margin: 0;
  1031. .image {
  1032. display: flex;
  1033. position: relative;
  1034. padding-top: 100%;
  1035. }
  1036. .icon-container {
  1037. display: flex;
  1038. position: absolute;
  1039. z-index: 2;
  1040. top: 0;
  1041. bottom: 0;
  1042. left: 0;
  1043. right: 0;
  1044. .material-icons.manage-station {
  1045. display: inline-flex;
  1046. opacity: 0;
  1047. background: var(--primary-color);
  1048. color: var(--white);
  1049. margin: auto;
  1050. font-size: 40px;
  1051. border-radius: 100%;
  1052. padding: 10px;
  1053. transition: all 0.2s ease-in-out;
  1054. }
  1055. &:hover,
  1056. &:focus {
  1057. .material-icons.manage-station {
  1058. opacity: 1;
  1059. &:hover,
  1060. &:focus {
  1061. filter: brightness(90%);
  1062. }
  1063. }
  1064. }
  1065. }
  1066. }
  1067. .media {
  1068. display: flex;
  1069. position: relative;
  1070. padding: 10px 10px 10px 15px;
  1071. flex-direction: column;
  1072. flex-grow: 1;
  1073. -webkit-line-clamp: 2;
  1074. .displayName {
  1075. display: flex;
  1076. align-items: center;
  1077. width: 100%;
  1078. overflow: hidden;
  1079. text-overflow: ellipsis;
  1080. display: flex;
  1081. line-height: 30px;
  1082. max-height: 30px;
  1083. .favorite {
  1084. position: absolute;
  1085. color: var(--yellow);
  1086. right: 10px;
  1087. top: 10px;
  1088. font-size: 28px;
  1089. }
  1090. h5 {
  1091. font-size: 20px;
  1092. font-weight: 400;
  1093. margin: 0;
  1094. display: inline;
  1095. margin-right: 6px;
  1096. line-height: 30px;
  1097. text-overflow: ellipsis;
  1098. overflow: hidden;
  1099. white-space: nowrap;
  1100. max-width: 200px;
  1101. }
  1102. i {
  1103. font-size: 22px;
  1104. }
  1105. .verified-station {
  1106. color: var(--primary-color);
  1107. }
  1108. }
  1109. .content {
  1110. word-wrap: break-word;
  1111. overflow: hidden;
  1112. text-overflow: ellipsis;
  1113. display: -webkit-box;
  1114. -webkit-box-orient: vertical;
  1115. -webkit-line-clamp: 3;
  1116. line-height: 20px;
  1117. flex-grow: 1;
  1118. text-align: left;
  1119. word-wrap: break-word;
  1120. margin-bottom: 0;
  1121. }
  1122. .under-content {
  1123. height: 20px;
  1124. position: relative;
  1125. line-height: 1;
  1126. font-size: 24px;
  1127. display: flex;
  1128. align-items: center;
  1129. text-align: left;
  1130. margin-top: 10px;
  1131. p {
  1132. font-size: 15px;
  1133. line-height: 15px;
  1134. display: inline;
  1135. }
  1136. i {
  1137. font-size: 20px;
  1138. }
  1139. * {
  1140. z-index: 10;
  1141. position: relative;
  1142. }
  1143. .icons {
  1144. position: absolute;
  1145. right: 0;
  1146. .material-icons {
  1147. font-size: 22px;
  1148. }
  1149. .material-icons:first-child {
  1150. margin-left: 5px;
  1151. }
  1152. .unlistedIcon {
  1153. color: var(--orange);
  1154. }
  1155. .privateIcon {
  1156. color: var(--dark-pink);
  1157. }
  1158. .homeIcon {
  1159. color: var(--light-purple);
  1160. }
  1161. }
  1162. .hostedBy {
  1163. font-weight: 400;
  1164. font-size: 12px;
  1165. color: var(--black);
  1166. .host,
  1167. .host a {
  1168. font-weight: 400;
  1169. color: var(--primary-color);
  1170. &:hover,
  1171. &:focus {
  1172. filter: brightness(90%);
  1173. }
  1174. }
  1175. }
  1176. }
  1177. }
  1178. }
  1179. .bottomBar {
  1180. position: relative;
  1181. display: flex;
  1182. align-items: center;
  1183. background: var(--primary-color);
  1184. width: 100%;
  1185. height: 30px;
  1186. line-height: 30px;
  1187. color: var(--white);
  1188. font-weight: 400;
  1189. font-size: 12px;
  1190. padding: 0 5px;
  1191. flex-basis: 100%;
  1192. i.material-icons {
  1193. vertical-align: middle;
  1194. margin-left: 5px;
  1195. font-size: 22px;
  1196. }
  1197. .songTitle {
  1198. text-align: left;
  1199. vertical-align: middle;
  1200. margin-left: 5px;
  1201. line-height: 30px;
  1202. flex: 2 1 0;
  1203. overflow: hidden;
  1204. text-overflow: ellipsis;
  1205. white-space: nowrap;
  1206. }
  1207. }
  1208. &.createStation {
  1209. .card-content {
  1210. .thumbnail {
  1211. .image {
  1212. width: 120px;
  1213. .material-icons {
  1214. position: absolute;
  1215. top: 25px;
  1216. bottom: 25px;
  1217. left: 0;
  1218. right: 0;
  1219. text-align: center;
  1220. font-size: 70px;
  1221. color: var(--primary-color);
  1222. }
  1223. }
  1224. }
  1225. .media {
  1226. margin: auto 0;
  1227. .displayName h5 {
  1228. font-weight: 600;
  1229. }
  1230. .content {
  1231. flex-grow: unset;
  1232. margin-bottom: auto;
  1233. }
  1234. }
  1235. }
  1236. }
  1237. &:hover {
  1238. box-shadow: @box-shadow-hover;
  1239. transition: all ease-in-out 0.2s;
  1240. }
  1241. }
  1242. .group {
  1243. flex: 1 0 auto;
  1244. text-align: center;
  1245. width: 100%;
  1246. margin: 10px 0;
  1247. min-height: 64px;
  1248. .group-title {
  1249. display: flex;
  1250. align-items: center;
  1251. justify-content: center;
  1252. margin: 25px 0;
  1253. h1 {
  1254. display: inline-block;
  1255. font-size: 45px;
  1256. margin: 0;
  1257. }
  1258. h2 {
  1259. font-size: 35px;
  1260. margin: 0;
  1261. }
  1262. a {
  1263. display: flex;
  1264. margin-left: 8px;
  1265. }
  1266. }
  1267. &.bottom {
  1268. margin-bottom: 40px;
  1269. }
  1270. }
  1271. </style>