Home.vue 31 KB

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