Home.vue 29 KB

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