Home.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417
  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. });
  167. socket.on("event:station.created", res => {
  168. const { station } = res.data;
  169. if (stations.value.find(_station => _station._id === station._id)) {
  170. stations.value.forEach(s => {
  171. const _station = s;
  172. if (_station._id === station._id) {
  173. _station.privacy = station.privacy;
  174. }
  175. });
  176. } else {
  177. if (!station.currentSong)
  178. station.currentSong = {
  179. thumbnail: "/assets/notes-transparent.png"
  180. };
  181. if (station.currentSong && !station.currentSong.thumbnail)
  182. station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.youtubeId}/mqdefault.jpg`;
  183. stations.value.push(station);
  184. }
  185. });
  186. socket.on("event:station.deleted", res => {
  187. const { stationId } = res.data;
  188. const station = stations.value.find(
  189. station => station._id === stationId
  190. );
  191. if (station) {
  192. const stationIndex = stations.value.indexOf(station);
  193. stations.value.splice(stationIndex, 1);
  194. if (station.isFavorited)
  195. orderOfFavoriteStations.value =
  196. orderOfFavoriteStations.value.filter(
  197. favoritedId => favoritedId !== stationId
  198. );
  199. }
  200. });
  201. socket.on("event:station.userCount.updated", res => {
  202. const station = stations.value.find(
  203. station => station._id === res.data.stationId
  204. );
  205. if (station) station.userCount = res.data.userCount;
  206. });
  207. socket.on("event:station.updated", res => {
  208. const stationIndex = stations.value
  209. .map(station => station._id)
  210. .indexOf(res.data.station._id);
  211. if (stationIndex !== -1) {
  212. stations.value[stationIndex] = {
  213. ...stations.value[stationIndex],
  214. ...res.data.station
  215. };
  216. }
  217. });
  218. socket.on("event:station.nextSong", res => {
  219. const station = stations.value.find(
  220. station => station._id === res.data.stationId
  221. );
  222. if (station) {
  223. let newSong = res.data.currentSong;
  224. if (!newSong)
  225. newSong = {
  226. thumbnail: "/assets/notes-transparent.png"
  227. };
  228. station.currentSong = newSong;
  229. }
  230. });
  231. socket.on("event:station.pause", res => {
  232. const station = stations.value.find(
  233. station => station._id === res.data.stationId
  234. );
  235. if (station) station.paused = true;
  236. });
  237. socket.on("event:station.resume", res => {
  238. const station = stations.value.find(
  239. station => station._id === res.data.stationId
  240. );
  241. if (station) station.paused = false;
  242. });
  243. socket.on("event:user.station.favorited", res => {
  244. const { stationId } = res.data;
  245. const station = stations.value.find(
  246. station => station._id === stationId
  247. );
  248. if (station) {
  249. station.isFavorited = true;
  250. orderOfFavoriteStations.value.push(stationId);
  251. }
  252. });
  253. socket.on("event:user.station.unfavorited", res => {
  254. const { stationId } = res.data;
  255. const station = stations.value.find(
  256. station => station._id === stationId
  257. );
  258. if (station) {
  259. station.isFavorited = false;
  260. orderOfFavoriteStations.value =
  261. orderOfFavoriteStations.value.filter(
  262. favoritedId => favoritedId !== stationId
  263. );
  264. }
  265. });
  266. socket.on("event:user.orderOfFavoriteStations.updated", res => {
  267. orderOfFavoriteStations.value = res.data.order;
  268. });
  269. socket.on("event:station.djs.added", res => {
  270. if (res.data.user._id === userId.value) fetchStations();
  271. });
  272. socket.on("event:station.djs.removed", res => {
  273. if (res.data.user._id === userId.value) fetchStations();
  274. });
  275. socket.on("keep.event:user.role.updated", () => {
  276. fetchStations();
  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("MyFavorites") }}</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. props: {
  395. stationId:
  396. element._id,
  397. sector: 'home'
  398. }
  399. })
  400. "
  401. :content="t('ManageStation')"
  402. v-tippy
  403. >
  404. {{ t("Icons.ManageStation") }}
  405. </div>
  406. <div
  407. v-else
  408. class="material-icons manage-station"
  409. @click.prevent="
  410. openModal({
  411. modal: 'manageStation',
  412. props: {
  413. stationId:
  414. element._id,
  415. sector: 'home'
  416. }
  417. })
  418. "
  419. :content="t('ViewQueue')"
  420. v-tippy
  421. >
  422. {{ t("Icons.ViewQueue") }}
  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="t('FavoriteStation')"
  438. v-tippy
  439. >{{ t("Icons.Favorite") }}</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="t('UnfavoriteStation')"
  450. v-tippy
  451. >{{ t("Icons.Unfavorite") }}</i
  452. >
  453. <h5>{{ element.displayName }}</h5>
  454. <i
  455. v-if="element.type === 'official'"
  456. class="material-icons verified-station"
  457. :content="t('OfficialStation')"
  458. v-tippy="{
  459. theme: 'info'
  460. }"
  461. >
  462. {{ t("Icons.Verified") }}
  463. </i>
  464. </div>
  465. <div class="content">
  466. {{ element.description }}
  467. </div>
  468. <div class="under-content">
  469. <p class="hostedBy">
  470. {{ t("HostedBy") }}
  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="t('UserOwnsStation')"
  499. v-tippy="{ theme: 'info' }"
  500. >{{ t("Icons.Home") }}</i
  501. >
  502. <i
  503. v-if="
  504. element.type ===
  505. 'community' &&
  506. isDj(element)
  507. "
  508. class="djIcon material-icons"
  509. :content="t('UserIsDj')"
  510. v-tippy="{ theme: 'info' }"
  511. >{{ t("Icons.DJ") }}</i
  512. >
  513. <i
  514. v-if="
  515. element.privacy ===
  516. 'private'
  517. "
  518. class="privateIcon material-icons"
  519. :content="t('StationPrivate')"
  520. v-tippy="{ theme: 'info' }"
  521. >{{ t("Icons.Private") }}</i
  522. >
  523. <i
  524. v-if="
  525. element.privacy ===
  526. 'unlisted'
  527. "
  528. class="unlistedIcon material-icons"
  529. :content="t('StationUnlisted')"
  530. v-tippy="{ theme: 'info' }"
  531. >{{ t("Icons.Unlisted") }}</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="t('StationPaused')"
  545. v-tippy="{ theme: 'info' }"
  546. >{{ t("Icons.Pause") }}</i
  547. >
  548. <i
  549. v-else-if="element.currentSong.title"
  550. class="material-icons"
  551. >{{ t("Icons.Song") }}</i
  552. >
  553. <i v-else class="material-icons">{{
  554. t("Icons.NoSong")
  555. }}</i>
  556. <span
  557. v-if="element.currentSong.title"
  558. class="songTitle"
  559. :title="
  560. element.currentSong.artists.length > 0
  561. ? 'Now Playing: ' +
  562. element.currentSong.title +
  563. ' by ' +
  564. element.currentSong.artists.join(
  565. ', '
  566. )
  567. : 'Now Playing: ' +
  568. element.currentSong.title
  569. "
  570. >{{ element.currentSong.title }}
  571. {{
  572. element.currentSong.artists.length > 0
  573. ? " by " +
  574. element.currentSong.artists.join(
  575. ", "
  576. )
  577. : ""
  578. }}</span
  579. >
  580. <span v-else class="songTitle">{{
  581. t("NoSongsPlaying")
  582. }}</span>
  583. <i
  584. v-if="canRequest(element)"
  585. class="material-icons"
  586. content="You can request songs in this station"
  587. v-tippy="{ theme: 'info' }"
  588. >
  589. {{ t("Icons.RequestSong") }}
  590. </i>
  591. </div>
  592. </router-link>
  593. </template>
  594. </draggable-list>
  595. </div>
  596. <div class="group bottom">
  597. <div class="group-title">
  598. <div>
  599. <h1>{{ t("Station", 0) }}</h1>
  600. </div>
  601. </div>
  602. <a
  603. v-if="loggedIn"
  604. @click="openModal('createStation')"
  605. class="station-card createStation"
  606. >
  607. <div class="card-content">
  608. <div class="thumbnail">
  609. <figure class="image">
  610. <i class="material-icons">{{
  611. t("Icons.Station")
  612. }}</i>
  613. </figure>
  614. </div>
  615. <div class="media">
  616. <div class="displayName">
  617. <h5>{{ t("CreateStation") }}</h5>
  618. </div>
  619. <div class="content">
  620. {{ t("ClickToCreateStation") }}
  621. </div>
  622. </div>
  623. </div>
  624. <div class="bottomBar"></div>
  625. </a>
  626. <a
  627. v-else
  628. @click="openModal('login')"
  629. class="station-card createStation"
  630. >
  631. <div class="card-content">
  632. <div class="thumbnail">
  633. <figure class="image">
  634. <i class="material-icons">{{
  635. t("Icons.RequestSong")
  636. }}</i>
  637. </figure>
  638. </div>
  639. <div class="media">
  640. <div class="displayName">
  641. <h5>{{ t("CreateStation") }}</h5>
  642. </div>
  643. <div class="content">
  644. {{ t("LoginToCreateStation") }}
  645. </div>
  646. </div>
  647. </div>
  648. <div class="bottomBar"></div>
  649. </a>
  650. <router-link
  651. v-for="station in filteredStations"
  652. :key="station._id"
  653. :to="{
  654. name: 'station',
  655. params: { id: station.name }
  656. }"
  657. class="station-card"
  658. :class="{
  659. isPrivate: station.privacy === 'private',
  660. isMine: isOwner(station),
  661. isDj: isDj(station)
  662. }"
  663. :style="'--primary-color: var(--' + station.theme + ')'"
  664. >
  665. <div class="card-content">
  666. <song-thumbnail :song="station.currentSong">
  667. <template #icon>
  668. <div class="icon-container">
  669. <div
  670. v-if="
  671. isOwnerOrDj(station) ||
  672. hasPermission(
  673. 'stations.view.manage'
  674. )
  675. "
  676. class="material-icons manage-station"
  677. @click.prevent="
  678. openModal({
  679. modal: 'manageStation',
  680. props: {
  681. stationId: station._id,
  682. sector: 'home'
  683. }
  684. })
  685. "
  686. :content="t('ManageStation')"
  687. v-tippy
  688. >
  689. {{ t("Icons.ManageStation") }}
  690. </div>
  691. <div
  692. v-else
  693. class="material-icons manage-station"
  694. @click.prevent="
  695. openModal({
  696. modal: 'manageStation',
  697. props: {
  698. stationId: station._id,
  699. sector: 'home'
  700. }
  701. })
  702. "
  703. :content="t('ViewQueue')"
  704. v-tippy
  705. >
  706. {{ t("Icons.ViewQueue") }}
  707. </div>
  708. </div>
  709. </template>
  710. </song-thumbnail>
  711. <div class="media">
  712. <div class="displayName">
  713. <i
  714. v-if="loggedIn && !station.isFavorited"
  715. @click.prevent="
  716. favoriteStation(station._id)
  717. "
  718. class="favorite material-icons"
  719. :content="t('FavoriteStation')"
  720. v-tippy
  721. >{{ t("Icons.Favorite") }}</i
  722. >
  723. <i
  724. v-if="loggedIn && station.isFavorited"
  725. @click.prevent="
  726. unfavoriteStation(station._id)
  727. "
  728. class="favorite material-icons"
  729. :content="t('UnfavoriteStation')"
  730. v-tippy
  731. >{{ t("Icons.Unfavorite") }}</i
  732. >
  733. <h5>{{ station.displayName }}</h5>
  734. <i
  735. v-if="station.type === 'official'"
  736. class="material-icons verified-station"
  737. :content="t('OfficialStation')"
  738. v-tippy="{ theme: 'info' }"
  739. >
  740. {{ t("Icons.Verified") }}
  741. </i>
  742. </div>
  743. <div class="content">
  744. {{ station.description }}
  745. </div>
  746. <div class="under-content">
  747. <p class="hostedBy">
  748. {{ t("HostedBy") }}
  749. <span class="host">
  750. <span
  751. v-if="station.type === 'official'"
  752. :title="siteSettings.sitename"
  753. >{{ siteSettings.sitename }}</span
  754. >
  755. <user-link
  756. v-else
  757. :user-id="station.owner"
  758. />
  759. </span>
  760. </p>
  761. <div class="icons">
  762. <i
  763. v-if="
  764. station.type === 'community' &&
  765. isOwner(station)
  766. "
  767. class="homeIcon material-icons"
  768. :content="t('UserOwnsStation')"
  769. v-tippy="{ theme: 'info' }"
  770. >{{ t("Icons.Home") }}</i
  771. >
  772. <i
  773. v-if="
  774. station.type === 'community' &&
  775. isDj(station)
  776. "
  777. class="djIcon material-icons"
  778. :content="t('UserIsDj')"
  779. v-tippy="{ theme: 'info' }"
  780. >{{ t("Icons.DJ") }}</i
  781. >
  782. <i
  783. v-if="station.privacy === 'private'"
  784. class="privateIcon material-icons"
  785. :content="t('StationPrivate')"
  786. v-tippy="{ theme: 'info' }"
  787. >{{ t("Icons.Private") }}</i
  788. >
  789. <i
  790. v-if="station.privacy === 'unlisted'"
  791. class="unlistedIcon material-icons"
  792. :content="t('StationUnlisted')"
  793. v-tippy="{ theme: 'info' }"
  794. >{{ t("Icons.Unlisted") }}</i
  795. >
  796. </div>
  797. </div>
  798. </div>
  799. </div>
  800. <div class="bottomBar">
  801. <i
  802. v-if="station.paused && station.currentSong.title"
  803. class="material-icons"
  804. :content="t('StationPaused')"
  805. v-tippy="{ theme: 'info' }"
  806. >{{ t("Icons.Pause") }}</i
  807. >
  808. <i
  809. v-else-if="station.currentSong.title"
  810. class="material-icons"
  811. >{{ t("Icons.Song") }}</i
  812. >
  813. <i v-else class="material-icons">{{
  814. t("Icons.NoSong")
  815. }}</i>
  816. <span
  817. v-if="station.currentSong.title"
  818. class="songTitle"
  819. :title="
  820. station.currentSong.artists.length > 0
  821. ? 'Now Playing: ' +
  822. station.currentSong.title +
  823. ' by ' +
  824. station.currentSong.artists.join(', ')
  825. : 'Now Playing: ' +
  826. station.currentSong.title
  827. "
  828. >{{ station.currentSong.title }}
  829. {{
  830. station.currentSong.artists.length > 0
  831. ? " by " +
  832. station.currentSong.artists.join(", ")
  833. : ""
  834. }}</span
  835. >
  836. <span v-else class="songTitle">{{
  837. t("NoSongsPlaying")
  838. }}</span>
  839. <i
  840. v-if="canRequest(station)"
  841. class="material-icons"
  842. :content="t('CanRequestInStation')"
  843. v-tippy="{ theme: 'info' }"
  844. >
  845. {{ t("Icons.RequestSong") }}
  846. </i>
  847. <i
  848. v-else-if="canRequest(station, false)"
  849. class="material-icons"
  850. :content="t('LoginToRequestInStation')"
  851. v-tippy="{ theme: 'info' }"
  852. >
  853. {{ t("Icons.RequestSong") }}
  854. </i>
  855. </div>
  856. </router-link>
  857. <h4 v-if="stations.length === 0">
  858. {{ t("NoStations", 0) }}
  859. </h4>
  860. </div>
  861. <main-footer />
  862. </div>
  863. </div>
  864. </template>
  865. <style lang="less">
  866. .christmas-mode .home-page {
  867. .header .overlay {
  868. background: linear-gradient(
  869. 180deg,
  870. rgba(231, 77, 60, 0.8) 0%,
  871. rgba(231, 77, 60, 0.95) 31.25%,
  872. rgba(231, 77, 60, 0.9) 54.17%,
  873. rgba(231, 77, 60, 0.8) 100%
  874. );
  875. }
  876. .christmas-lights {
  877. top: 300px !important;
  878. &.loggedIn {
  879. top: 200px !important;
  880. }
  881. }
  882. .header {
  883. &,
  884. .background,
  885. .overlay {
  886. border-radius: unset;
  887. }
  888. }
  889. }
  890. </style>
  891. <style lang="less" scoped>
  892. * {
  893. box-sizing: border-box;
  894. }
  895. html {
  896. width: 100%;
  897. height: 100%;
  898. color: rgba(0, 0, 0, 0.87);
  899. body {
  900. width: 100%;
  901. height: 100%;
  902. margin: 0;
  903. padding: 0;
  904. }
  905. @media only screen and (min-width: 1200px) {
  906. font-size: 15px;
  907. }
  908. @media only screen and (min-width: 992px) {
  909. font-size: 14.5px;
  910. }
  911. @media only screen and (min-width: 0) {
  912. font-size: 14px;
  913. }
  914. }
  915. .night-mode {
  916. .header .overlay {
  917. background: linear-gradient(
  918. 180deg,
  919. rgba(34, 34, 34, 0.8) 0%,
  920. rgba(34, 34, 34, 0.95) 31.25%,
  921. rgba(34, 34, 34, 0.9) 54.17%,
  922. rgba(34, 34, 34, 0.8) 100%
  923. );
  924. }
  925. .station-card {
  926. background-color: var(--dark-grey-3);
  927. .thumbnail {
  928. background-color: var(--dark-grey-2);
  929. i {
  930. user-select: none;
  931. -webkit-user-select: none;
  932. }
  933. }
  934. .card-content .media {
  935. .icons i,
  936. .under-content .hostedBy {
  937. color: var(--light-grey-2) !important;
  938. }
  939. }
  940. }
  941. .group-title i {
  942. color: var(--light-grey-2);
  943. }
  944. }
  945. .header {
  946. display: flex;
  947. height: 300px;
  948. margin-top: -64px;
  949. border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
  950. img.background {
  951. height: 300px;
  952. width: 100%;
  953. object-fit: cover;
  954. object-position: center;
  955. filter: blur(1px);
  956. border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
  957. overflow: hidden;
  958. user-select: none;
  959. }
  960. .overlay {
  961. background: linear-gradient(
  962. 180deg,
  963. rgba(3, 169, 244, 0.8) 0%,
  964. rgba(3, 169, 244, 0.95) 31.25%,
  965. rgba(3, 169, 244, 0.9) 54.17%,
  966. rgba(3, 169, 244, 0.8) 100%
  967. );
  968. position: absolute;
  969. height: 300px;
  970. width: 100%;
  971. border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
  972. overflow: hidden;
  973. }
  974. .content-container {
  975. position: absolute;
  976. left: 0;
  977. right: 0;
  978. margin-left: auto;
  979. margin-right: auto;
  980. text-align: center;
  981. height: 300px;
  982. .content {
  983. position: absolute;
  984. top: 50%;
  985. left: 0;
  986. right: 0;
  987. transform: translateY(-50%);
  988. background-color: transparent !important;
  989. .logo {
  990. max-height: 90px;
  991. font-size: 50px;
  992. color: var(--white);
  993. font-family: Pacifico, cursive;
  994. user-select: none;
  995. white-space: nowrap;
  996. }
  997. .buttons {
  998. display: flex;
  999. justify-content: center;
  1000. margin-top: 20px;
  1001. flex-wrap: wrap;
  1002. .login,
  1003. .register {
  1004. margin: 5px 10px;
  1005. padding: 10px 15px;
  1006. border-radius: @border-radius;
  1007. font-size: 18px;
  1008. width: 100%;
  1009. max-width: 250px;
  1010. font-weight: 600;
  1011. border: 0;
  1012. height: inherit;
  1013. }
  1014. .login {
  1015. background: var(--white);
  1016. color: var(--primary-color);
  1017. }
  1018. .register {
  1019. background: var(--purple);
  1020. color: var(--white);
  1021. }
  1022. }
  1023. }
  1024. }
  1025. &.loggedIn {
  1026. height: 200px;
  1027. .overlay,
  1028. .content-container,
  1029. img.background {
  1030. height: 200px;
  1031. }
  1032. }
  1033. }
  1034. .app {
  1035. display: flex;
  1036. flex-direction: column;
  1037. }
  1038. .station-card {
  1039. display: inline-flex;
  1040. position: relative;
  1041. background-color: var(--white);
  1042. color: var(--dark-grey);
  1043. flex-direction: row;
  1044. overflow: hidden;
  1045. margin: 10px;
  1046. cursor: pointer;
  1047. filter: none;
  1048. height: 150px;
  1049. width: calc(100% - 30px);
  1050. max-width: 400px;
  1051. flex-wrap: wrap;
  1052. border-radius: @border-radius;
  1053. box-shadow: @box-shadow;
  1054. .card-content {
  1055. display: flex;
  1056. flex-direction: row;
  1057. flex-grow: 1;
  1058. .thumbnail {
  1059. display: flex;
  1060. position: relative;
  1061. min-width: 120px;
  1062. width: 120px;
  1063. height: 120px;
  1064. margin: 0;
  1065. .image {
  1066. display: flex;
  1067. position: relative;
  1068. padding-top: 100%;
  1069. }
  1070. .icon-container {
  1071. display: flex;
  1072. position: absolute;
  1073. z-index: 2;
  1074. top: 0;
  1075. bottom: 0;
  1076. left: 0;
  1077. right: 0;
  1078. .material-icons.manage-station {
  1079. display: inline-flex;
  1080. opacity: 0;
  1081. background: var(--primary-color);
  1082. color: var(--white);
  1083. margin: auto;
  1084. font-size: 40px;
  1085. border-radius: 100%;
  1086. padding: 10px;
  1087. transition: all 0.2s ease-in-out;
  1088. }
  1089. &:hover,
  1090. &:focus {
  1091. .material-icons.manage-station {
  1092. opacity: 1;
  1093. &:hover,
  1094. &:focus {
  1095. filter: brightness(90%);
  1096. }
  1097. }
  1098. }
  1099. }
  1100. }
  1101. .media {
  1102. display: flex;
  1103. position: relative;
  1104. padding: 10px 10px 10px 15px;
  1105. flex-direction: column;
  1106. flex-grow: 1;
  1107. -webkit-line-clamp: 2;
  1108. .displayName {
  1109. display: flex;
  1110. align-items: center;
  1111. width: 100%;
  1112. overflow: hidden;
  1113. text-overflow: ellipsis;
  1114. display: flex;
  1115. line-height: 30px;
  1116. max-height: 30px;
  1117. .favorite {
  1118. position: absolute;
  1119. color: var(--yellow);
  1120. right: 10px;
  1121. top: 10px;
  1122. font-size: 28px;
  1123. }
  1124. h5 {
  1125. font-size: 20px;
  1126. font-weight: 400;
  1127. margin: 0;
  1128. display: inline;
  1129. margin-right: 6px;
  1130. line-height: 30px;
  1131. text-overflow: ellipsis;
  1132. overflow: hidden;
  1133. white-space: nowrap;
  1134. max-width: 200px;
  1135. }
  1136. i {
  1137. font-size: 22px;
  1138. }
  1139. .verified-station {
  1140. color: var(--primary-color);
  1141. }
  1142. }
  1143. .content {
  1144. word-wrap: break-word;
  1145. overflow: hidden;
  1146. text-overflow: ellipsis;
  1147. display: -webkit-box;
  1148. -webkit-box-orient: vertical;
  1149. -webkit-line-clamp: 3;
  1150. line-height: 20px;
  1151. flex-grow: 1;
  1152. text-align: left;
  1153. word-wrap: break-word;
  1154. margin-bottom: 0;
  1155. }
  1156. .under-content {
  1157. height: 20px;
  1158. position: relative;
  1159. line-height: 1;
  1160. font-size: 24px;
  1161. display: flex;
  1162. align-items: center;
  1163. text-align: left;
  1164. margin-top: 10px;
  1165. p {
  1166. font-size: 15px;
  1167. line-height: 15px;
  1168. display: inline;
  1169. }
  1170. i {
  1171. font-size: 20px;
  1172. }
  1173. * {
  1174. z-index: 10;
  1175. position: relative;
  1176. }
  1177. .icons {
  1178. position: absolute;
  1179. right: 0;
  1180. .material-icons {
  1181. font-size: 22px;
  1182. }
  1183. .material-icons:first-child {
  1184. margin-left: 5px;
  1185. }
  1186. .unlistedIcon {
  1187. color: var(--orange);
  1188. }
  1189. .privateIcon {
  1190. color: var(--dark-pink);
  1191. }
  1192. .homeIcon {
  1193. color: var(--light-purple);
  1194. }
  1195. .djIcon {
  1196. color: var(--primary-color);
  1197. }
  1198. }
  1199. .hostedBy {
  1200. font-weight: 400;
  1201. font-size: 12px;
  1202. color: var(--black);
  1203. .host,
  1204. .host a {
  1205. font-weight: 400;
  1206. color: var(--primary-color);
  1207. &:hover,
  1208. &:focus {
  1209. filter: brightness(90%);
  1210. }
  1211. }
  1212. }
  1213. }
  1214. }
  1215. }
  1216. .bottomBar {
  1217. position: relative;
  1218. display: flex;
  1219. align-items: center;
  1220. background: var(--primary-color);
  1221. width: 100%;
  1222. height: 30px;
  1223. line-height: 30px;
  1224. color: var(--white);
  1225. font-weight: 400;
  1226. font-size: 12px;
  1227. padding: 0 5px;
  1228. flex-basis: 100%;
  1229. i.material-icons {
  1230. vertical-align: middle;
  1231. margin-left: 5px;
  1232. font-size: 22px;
  1233. }
  1234. .songTitle {
  1235. text-align: left;
  1236. vertical-align: middle;
  1237. margin-left: 5px;
  1238. line-height: 30px;
  1239. flex: 2 1 0;
  1240. overflow: hidden;
  1241. text-overflow: ellipsis;
  1242. white-space: nowrap;
  1243. }
  1244. }
  1245. &.createStation {
  1246. .card-content {
  1247. .thumbnail {
  1248. .image {
  1249. width: 120px;
  1250. .material-icons {
  1251. position: absolute;
  1252. top: 25px;
  1253. bottom: 25px;
  1254. left: 0;
  1255. right: 0;
  1256. text-align: center;
  1257. font-size: 70px;
  1258. color: var(--primary-color);
  1259. }
  1260. }
  1261. }
  1262. .media {
  1263. margin: auto 0;
  1264. .displayName h5 {
  1265. font-weight: 600;
  1266. }
  1267. .content {
  1268. flex-grow: unset;
  1269. margin-bottom: auto;
  1270. }
  1271. }
  1272. }
  1273. }
  1274. &:hover {
  1275. box-shadow: @box-shadow-hover;
  1276. transition: all ease-in-out 0.2s;
  1277. }
  1278. }
  1279. .group {
  1280. flex: 1 0 auto;
  1281. text-align: center;
  1282. width: 100%;
  1283. margin: 10px 0;
  1284. min-height: 64px;
  1285. .group-title {
  1286. display: flex;
  1287. align-items: center;
  1288. justify-content: center;
  1289. margin: 25px 0;
  1290. h1 {
  1291. display: inline-block;
  1292. font-size: 45px;
  1293. margin: 0;
  1294. }
  1295. h2 {
  1296. font-size: 35px;
  1297. margin: 0;
  1298. }
  1299. a {
  1300. display: flex;
  1301. margin-left: 8px;
  1302. }
  1303. }
  1304. &.bottom {
  1305. margin-bottom: 40px;
  1306. }
  1307. }
  1308. </style>