Home.vue 29 KB

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