|
@@ -86,9 +86,13 @@ const beforeMediaModalLocalPausedLock = ref(false);
|
|
const beforeMediaModalLocalPaused = ref(null);
|
|
const beforeMediaModalLocalPaused = ref(null);
|
|
const persistentToastCheckerInterval = ref(null);
|
|
const persistentToastCheckerInterval = ref(null);
|
|
const persistentToasts = ref([]);
|
|
const persistentToasts = ref([]);
|
|
-const mediasession = ref(false);
|
|
|
|
const christmas = ref(false);
|
|
const christmas = ref(false);
|
|
const sitename = ref("Musare");
|
|
const sitename = ref("Musare");
|
|
|
|
+// Experimental options
|
|
|
|
+const experimentalChangableListenModeEnabled = ref(false);
|
|
|
|
+const experimentalChangableListenMode = ref("listen_and_participate"); // Can be either listen_and_participate or participate
|
|
|
|
+const experimentalMediaSession = ref(false);
|
|
|
|
+// End experimental options
|
|
// NEW
|
|
// NEW
|
|
const videoLoading = ref();
|
|
const videoLoading = ref();
|
|
const startedAt = ref();
|
|
const startedAt = ref();
|
|
@@ -285,6 +289,7 @@ const resizeSeekerbar = () => {
|
|
(getTimeElapsed() / 1000 / currentSong.value.duration) * 100;
|
|
(getTimeElapsed() / 1000 / currentSong.value.duration) * 100;
|
|
};
|
|
};
|
|
const calculateTimeElapsed = () => {
|
|
const calculateTimeElapsed = () => {
|
|
|
|
+ if (experimentalChangableListenMode.value === "participate") return;
|
|
if (
|
|
if (
|
|
playerReady.value &&
|
|
playerReady.value &&
|
|
!noSong.value &&
|
|
!noSong.value &&
|
|
@@ -403,6 +408,7 @@ const toggleSkipVote = (message?) => {
|
|
});
|
|
});
|
|
};
|
|
};
|
|
const youtubeReady = () => {
|
|
const youtubeReady = () => {
|
|
|
|
+ if (experimentalChangableListenMode.value === "participate") return;
|
|
if (!player.value) {
|
|
if (!player.value) {
|
|
ms.setYTReady(false);
|
|
ms.setYTReady(false);
|
|
player.value = new window.YT.Player("stationPlayer", {
|
|
player.value = new window.YT.Player("stationPlayer", {
|
|
@@ -575,7 +581,7 @@ const setCurrentSong = data => {
|
|
|
|
|
|
clearTimeout(window.stationNextSongTimeout);
|
|
clearTimeout(window.stationNextSongTimeout);
|
|
|
|
|
|
- if (mediasession.value) updateMediaSessionData(_currentSong);
|
|
+ if (experimentalMediaSession.value) updateMediaSessionData(_currentSong);
|
|
|
|
|
|
startedAt.value = _startedAt;
|
|
startedAt.value = _startedAt;
|
|
updateStationPaused(_paused);
|
|
updateStationPaused(_paused);
|
|
@@ -697,7 +703,8 @@ const changeVolume = () => {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
const resumeLocalPlayer = () => {
|
|
const resumeLocalPlayer = () => {
|
|
- if (mediasession.value) updateMediaSessionData(currentSong.value);
|
|
+ if (experimentalMediaSession.value)
|
|
|
|
+ updateMediaSessionData(currentSong.value);
|
|
if (!noSong.value) {
|
|
if (!noSong.value) {
|
|
if (playerReady.value) {
|
|
if (playerReady.value) {
|
|
player.value.seekTo(
|
|
player.value.seekTo(
|
|
@@ -708,7 +715,8 @@ const resumeLocalPlayer = () => {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
const pauseLocalPlayer = () => {
|
|
const pauseLocalPlayer = () => {
|
|
- if (mediasession.value) updateMediaSessionData(currentSong.value);
|
|
+ if (experimentalMediaSession.value)
|
|
|
|
+ updateMediaSessionData(currentSong.value);
|
|
if (!noSong.value) {
|
|
if (!noSong.value) {
|
|
timeBeforePause.value = getTimeElapsed();
|
|
timeBeforePause.value = getTimeElapsed();
|
|
if (playerReady.value) player.value.pauseVideo();
|
|
if (playerReady.value) player.value.pauseVideo();
|
|
@@ -809,9 +817,11 @@ const resetKeyboardShortcutsHelper = () => {
|
|
const sendActivityWatchVideoData = () => {
|
|
const sendActivityWatchVideoData = () => {
|
|
if (
|
|
if (
|
|
!stationPaused.value &&
|
|
!stationPaused.value &&
|
|
- !localPaused.value &&
|
|
+ (!localPaused.value ||
|
|
|
|
+ experimentalChangableListenMode.value === "participate") &&
|
|
!noSong.value &&
|
|
!noSong.value &&
|
|
- player.value.getPlayerState() === window.YT.PlayerState.PLAYING
|
|
+ (experimentalChangableListenMode.value === "participate" ||
|
|
|
|
+ player.value.getPlayerState() === window.YT.PlayerState.PLAYING)
|
|
) {
|
|
) {
|
|
if (activityWatchVideoLastStatus.value !== "playing") {
|
|
if (activityWatchVideoLastStatus.value !== "playing") {
|
|
activityWatchVideoLastStatus.value = "playing";
|
|
activityWatchVideoLastStatus.value = "playing";
|
|
@@ -845,11 +855,17 @@ const sendActivityWatchVideoData = () => {
|
|
),
|
|
),
|
|
source: `station#${station.value.name}`,
|
|
source: `station#${station.value.name}`,
|
|
hostname: window.location.hostname,
|
|
hostname: window.location.hostname,
|
|
- playerState: Object.keys(window.YT.PlayerState).find(
|
|
+ playerState:
|
|
- key =>
|
|
+ experimentalChangableListenMode.value === "participate"
|
|
- window.YT.PlayerState[key] === player.value.getPlayerState()
|
|
+ ? "none"
|
|
- ),
|
|
+ : Object.keys(window.YT.PlayerState).find(
|
|
- playbackRate: playbackRate.value
|
|
+ key =>
|
|
|
|
+ window.YT.PlayerState[key] ===
|
|
|
|
+ player.value.getPlayerState()
|
|
|
|
+ ),
|
|
|
|
+ playbackRate: playbackRate.value,
|
|
|
|
+ experimentalChangableListenMode:
|
|
|
|
+ experimentalChangableListenMode.value
|
|
};
|
|
};
|
|
|
|
|
|
aw.sendVideoData(videoData);
|
|
aw.sendVideoData(videoData);
|
|
@@ -858,6 +874,26 @@ const sendActivityWatchVideoData = () => {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+const experimentalChangableListenModeChange = newMode => {
|
|
|
|
+ experimentalChangableListenMode.value = newMode;
|
|
|
|
+ localStorage.setItem(
|
|
|
|
+ `experimental_changeable_listen_mode_${station.value._id}`,
|
|
|
|
+ newMode
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ if (newMode === "participate") {
|
|
|
|
+ // Destroy the YouTube player
|
|
|
|
+ if (player.value) {
|
|
|
|
+ player.value.destroy();
|
|
|
|
+ player.value = null;
|
|
|
|
+ playerReady.value = false;
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ // Recreate the YouTube player
|
|
|
|
+ youtubeReady();
|
|
|
|
+ }
|
|
|
|
+};
|
|
|
|
+
|
|
watch(
|
|
watch(
|
|
() => autoRequest.value.length,
|
|
() => autoRequest.value.length,
|
|
() => {
|
|
() => {
|
|
@@ -897,6 +933,8 @@ onMounted(async () => {
|
|
);
|
|
);
|
|
}, 1000);
|
|
}, 1000);
|
|
|
|
|
|
|
|
+ const experimental = await lofig.get("experimental");
|
|
|
|
+
|
|
socket.onConnect(() => {
|
|
socket.onConnect(() => {
|
|
clearTimeout(window.stationNextSongTimeout);
|
|
clearTimeout(window.stationNextSongTimeout);
|
|
|
|
|
|
@@ -922,6 +960,28 @@ onMounted(async () => {
|
|
djs
|
|
djs
|
|
} = res.data;
|
|
} = res.data;
|
|
|
|
|
|
|
|
+ if (experimental && experimental.changable_listen_mode) {
|
|
|
|
+ if (experimental.changable_listen_mode === true)
|
|
|
|
+ experimentalChangableListenModeEnabled.value = true;
|
|
|
|
+ else if (
|
|
|
|
+ Array.isArray(experimental.changable_listen_mode) &&
|
|
|
|
+ experimental.changable_listen_mode.indexOf(_id) !== -1
|
|
|
|
+ )
|
|
|
|
+ experimentalChangableListenModeEnabled.value = true;
|
|
|
|
+ }
|
|
|
|
+ if (experimentalChangableListenModeEnabled.value) {
|
|
|
|
+ console.log(
|
|
|
|
+ `Experimental changeable listen mode is enabled`
|
|
|
|
+ );
|
|
|
|
+ const experimentalChangeableListenModeLS =
|
|
|
|
+ localStorage.getItem(
|
|
|
|
+ `experimental_changeable_listen_mode_${_id}`
|
|
|
|
+ );
|
|
|
|
+ if (experimentalChangeableListenModeLS)
|
|
|
|
+ experimentalChangableListenMode.value =
|
|
|
|
+ experimentalChangeableListenModeLS;
|
|
|
|
+ }
|
|
|
|
+
|
|
// change url to use station name instead of station id
|
|
// change url to use station name instead of station id
|
|
if (name !== stationIdentifier.value) {
|
|
if (name !== stationIdentifier.value) {
|
|
// eslint-disable-next-line no-restricted-globals
|
|
// eslint-disable-next-line no-restricted-globals
|
|
@@ -1406,9 +1466,16 @@ onMounted(async () => {
|
|
});
|
|
});
|
|
|
|
|
|
frontendDevMode.value = await lofig.get("mode");
|
|
frontendDevMode.value = await lofig.get("mode");
|
|
- mediasession.value = await lofig.get("siteSettings.mediasession");
|
|
|
|
christmas.value = await lofig.get("siteSettings.christmas");
|
|
christmas.value = await lofig.get("siteSettings.christmas");
|
|
sitename.value = await lofig.get("siteSettings.sitename");
|
|
sitename.value = await lofig.get("siteSettings.sitename");
|
|
|
|
+ lofig.get("experimental").then(experimental => {
|
|
|
|
+ if (
|
|
|
|
+ experimental &&
|
|
|
|
+ Object.hasOwn(experimental, "media_session") &&
|
|
|
|
+ experimental.media_session
|
|
|
|
+ )
|
|
|
|
+ experimentalMediaSession.value = true;
|
|
|
|
+ });
|
|
|
|
|
|
ms.setListeners(0, {
|
|
ms.setListeners(0, {
|
|
play: () => {
|
|
play: () => {
|
|
@@ -1441,7 +1508,7 @@ onMounted(async () => {
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
document.getElementsByTagName("html")[0].style.cssText = "";
|
|
document.getElementsByTagName("html")[0].style.cssText = "";
|
|
|
|
|
|
- if (mediasession.value) {
|
|
+ if (experimentalMediaSession.value) {
|
|
ms.removeListeners(0);
|
|
ms.removeListeners(0);
|
|
ms.removeMediaSessionData(0);
|
|
ms.removeMediaSessionData(0);
|
|
}
|
|
}
|
|
@@ -1564,7 +1631,184 @@ onBeforeUnmount(() => {
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="station-right-column" class="column">
|
|
<div id="station-right-column" class="column">
|
|
- <div class="player-container quadrant" v-show="!noSong">
|
|
+ <div
|
|
|
|
+ class="experimental-listen-mode-container quadrant"
|
|
|
|
+ v-if="
|
|
|
|
+ experimentalChangableListenModeEnabled &&
|
|
|
|
+ !noSong
|
|
|
|
+ "
|
|
|
|
+ v-show="
|
|
|
|
+ experimentalChangableListenMode ===
|
|
|
|
+ 'participate'
|
|
|
|
+ "
|
|
|
|
+ >
|
|
|
|
+ <button
|
|
|
|
+ class="button is-primary"
|
|
|
|
+ @click="
|
|
|
|
+ experimentalChangableListenModeChange(
|
|
|
|
+ 'listen_and_participate'
|
|
|
|
+ )
|
|
|
|
+ "
|
|
|
|
+ >
|
|
|
|
+ <i class="material-icons icon-with-button"
|
|
|
|
+ >music_note</i
|
|
|
|
+ >
|
|
|
|
+ <span>Listen to music</span>
|
|
|
|
+ </button>
|
|
|
|
+ <button
|
|
|
|
+ v-if="!skipVotesLoaded"
|
|
|
|
+ class="button is-primary disabled"
|
|
|
|
+ content="Skip votes have not been loaded yet"
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <i class="material-icons icon-with-button"
|
|
|
|
+ >skip_next</i
|
|
|
|
+ >
|
|
|
|
+ Vote to skip the current song
|
|
|
|
+ </button>
|
|
|
|
+ <button
|
|
|
|
+ v-else-if="loggedIn"
|
|
|
|
+ :class="[
|
|
|
|
+ 'button',
|
|
|
|
+ 'is-primary',
|
|
|
|
+ { voted: currentSong.voted }
|
|
|
|
+ ]"
|
|
|
|
+ @click="toggleSkipVote()"
|
|
|
|
+ :content="`${
|
|
|
|
+ currentSong.voted ? 'Remove vote' : 'Vote'
|
|
|
|
+ } to Skip Song`"
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <i class="material-icons icon-with-button"
|
|
|
|
+ >skip_next</i
|
|
|
|
+ >
|
|
|
|
+ Vote to skip the current song -
|
|
|
|
+ {{ currentSong.skipVotes }} votes
|
|
|
|
+ </button>
|
|
|
|
+ <button
|
|
|
|
+ v-else
|
|
|
|
+ class="button is-primary disabled"
|
|
|
|
+ content="Log in to vote to skip songs"
|
|
|
|
+ v-tippy="{ theme: 'info' }"
|
|
|
|
+ >
|
|
|
|
+ <i class="material-icons icon-with-button"
|
|
|
|
+ >skip_next</i
|
|
|
|
+ >
|
|
|
|
+ Vote to skip the current song -
|
|
|
|
+ {{ currentSong.skipVotes }} votes
|
|
|
|
+ </button>
|
|
|
|
+ <div class="row">
|
|
|
|
+ <!-- Ratings -->
|
|
|
|
+ <div
|
|
|
|
+ class="ratings"
|
|
|
|
+ v-if="ratingsLoaded && ownRatingsLoaded"
|
|
|
|
+ :class="{
|
|
|
|
+ liked: currentSong.liked,
|
|
|
|
+ disliked: currentSong.disliked
|
|
|
|
+ }"
|
|
|
|
+ >
|
|
|
|
+ <!-- Like Song Button -->
|
|
|
|
+ <button
|
|
|
|
+ class="button is-success like-song"
|
|
|
|
+ @click="toggleLike()"
|
|
|
|
+ content="Like Song"
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <i
|
|
|
|
+ class="material-icons icon-with-button"
|
|
|
|
+ :class="{
|
|
|
|
+ liked: currentSong.liked
|
|
|
|
+ }"
|
|
|
|
+ >thumb_up_alt</i
|
|
|
|
+ >{{ currentSong.likes }}
|
|
|
|
+ </button>
|
|
|
|
+
|
|
|
|
+ <!-- Dislike Song Button -->
|
|
|
|
+ <button
|
|
|
|
+ class="button is-danger dislike-song"
|
|
|
|
+ @click="toggleDislike()"
|
|
|
|
+ content="Dislike Song"
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <i
|
|
|
|
+ class="material-icons icon-with-button"
|
|
|
|
+ :class="{
|
|
|
|
+ disliked: currentSong.disliked
|
|
|
|
+ }"
|
|
|
|
+ >thumb_down_alt</i
|
|
|
|
+ >{{ currentSong.dislikes }}
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ <div id="ratings" class="disabled" v-else>
|
|
|
|
+ <!-- Like Song Button -->
|
|
|
|
+ <button
|
|
|
|
+ class="button is-success like-song disabled"
|
|
|
|
+ content="Ratings have not been loaded yet"
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <i
|
|
|
|
+ class="material-icons icon-with-button"
|
|
|
|
+ >thumb_up_alt</i
|
|
|
|
+ >
|
|
|
|
+ </button>
|
|
|
|
+
|
|
|
|
+ <!-- Dislike Song Button -->
|
|
|
|
+ <button
|
|
|
|
+ class="button is-danger dislike-song disabled"
|
|
|
|
+ content="Ratings have not been loaded yet"
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <i
|
|
|
|
+ class="material-icons icon-with-button"
|
|
|
|
+ >thumb_down_alt</i
|
|
|
|
+ >
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ <add-to-playlist-dropdown
|
|
|
|
+ :song="currentSong"
|
|
|
|
+ placement="top-end"
|
|
|
|
+ >
|
|
|
|
+ <template #button>
|
|
|
|
+ <div
|
|
|
|
+ id="add-song-to-playlist"
|
|
|
|
+ content="Add Song to Playlist"
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <div class="control has-addons">
|
|
|
|
+ <button
|
|
|
|
+ class="button is-primary"
|
|
|
|
+ >
|
|
|
|
+ <i class="material-icons">
|
|
|
|
+ playlist_add
|
|
|
|
+ </i>
|
|
|
|
+ </button>
|
|
|
|
+ <button
|
|
|
|
+ class="button"
|
|
|
|
+ id="dropdown-toggle"
|
|
|
|
+ >
|
|
|
|
+ <i class="material-icons">
|
|
|
|
+ {{
|
|
|
|
+ showPlaylistDropdown
|
|
|
|
+ ? "expand_more"
|
|
|
|
+ : "expand_less"
|
|
|
|
+ }}
|
|
|
|
+ </i>
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ </add-to-playlist-dropdown>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div
|
|
|
|
+ class="player-container quadrant"
|
|
|
|
+ v-show="
|
|
|
|
+ !noSong &&
|
|
|
|
+ (!experimentalChangableListenModeEnabled ||
|
|
|
|
+ experimentalChangableListenMode ===
|
|
|
|
+ 'listen_and_participate')
|
|
|
|
+ "
|
|
|
|
+ >
|
|
<div id="video-container">
|
|
<div id="video-container">
|
|
<div
|
|
<div
|
|
id="stationPlayer"
|
|
id="stationPlayer"
|
|
@@ -1786,6 +2030,26 @@ onBeforeUnmount(() => {
|
|
>
|
|
>
|
|
{{ currentSong.skipVotes }}
|
|
{{ currentSong.skipVotes }}
|
|
</button>
|
|
</button>
|
|
|
|
+
|
|
|
|
+ <!-- Close player window -->
|
|
|
|
+ <button
|
|
|
|
+ v-if="
|
|
|
|
+ experimentalChangableListenModeEnabled
|
|
|
|
+ "
|
|
|
|
+ class="button is-primary"
|
|
|
|
+ content="Close this player window"
|
|
|
|
+ @click="
|
|
|
|
+ experimentalChangableListenModeChange(
|
|
|
|
+ 'participate'
|
|
|
|
+ )
|
|
|
|
+ "
|
|
|
|
+ v-tippy
|
|
|
|
+ >
|
|
|
|
+ <i
|
|
|
|
+ class="material-icons icon-with-button"
|
|
|
|
+ >cancel_presentation</i
|
|
|
|
+ >
|
|
|
|
+ </button>
|
|
</div>
|
|
</div>
|
|
<div id="duration">
|
|
<div id="duration">
|
|
<p>
|
|
<p>
|
|
@@ -2022,7 +2286,7 @@ onBeforeUnmount(() => {
|
|
:class="{ 'no-currently-playing': noSong }"
|
|
:class="{ 'no-currently-playing': noSong }"
|
|
>
|
|
>
|
|
<song-item
|
|
<song-item
|
|
- :key="`songItem-currentSong-${currentSong._id}`"
|
|
+ :key="`songItem-currentSong-${currentSong.youtubeId}`"
|
|
:song="currentSong"
|
|
:song="currentSong"
|
|
:duration="false"
|
|
:duration="false"
|
|
:requested-by="true"
|
|
:requested-by="true"
|
|
@@ -2035,7 +2299,7 @@ onBeforeUnmount(() => {
|
|
class="quadrant"
|
|
class="quadrant"
|
|
>
|
|
>
|
|
<song-item
|
|
<song-item
|
|
- :key="`songItem-nextSong-${nextSong._id}`"
|
|
+ :key="`songItem-nextSong-${nextSong.youtubeId}`"
|
|
:song="nextSong"
|
|
:song="nextSong"
|
|
:duration="false"
|
|
:duration="false"
|
|
:requested-by="true"
|
|
:requested-by="true"
|
|
@@ -2682,7 +2946,7 @@ onBeforeUnmount(() => {
|
|
var(--dark-red) 1rem 2rem
|
|
var(--dark-red) 1rem 2rem
|
|
);
|
|
);
|
|
|
|
|
|
- background-size: 200% 200%;
|
|
+ background-size: 200% 100%;
|
|
animation: christmas 20s linear infinite;
|
|
animation: christmas 20s linear infinite;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -2801,11 +3065,56 @@ onBeforeUnmount(() => {
|
|
animation-delay: 11s;
|
|
animation-delay: 11s;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+.experimental-listen-mode-container {
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ justify-content: center;
|
|
|
|
+ row-gap: 16px;
|
|
|
|
+ padding: 16px 16px;
|
|
|
|
+
|
|
|
|
+ .row {
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: row;
|
|
|
|
+ column-gap: 16px;
|
|
|
|
+
|
|
|
|
+ .ratings {
|
|
|
|
+ flex: 2;
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: row;
|
|
|
|
+ column-gap: 16px;
|
|
|
|
+
|
|
|
|
+ button {
|
|
|
|
+ flex: 1;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .addToPlaylistDropdown {
|
|
|
|
+ flex: 1;
|
|
|
|
+
|
|
|
|
+ .button.is-primary {
|
|
|
|
+ flex: 1;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
/* Tablet view fix */
|
|
/* Tablet view fix */
|
|
@media (max-width: 768px) {
|
|
@media (max-width: 768px) {
|
|
.bg-bubbles li:nth-child(10) {
|
|
.bg-bubbles li:nth-child(10) {
|
|
display: none;
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ .experimental-listen-mode-container {
|
|
|
|
+ row-gap: 8px;
|
|
|
|
+
|
|
|
|
+ .row {
|
|
|
|
+ column-gap: 8px;
|
|
|
|
+
|
|
|
|
+ .ratings {
|
|
|
|
+ column-gap: 8px;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
@-webkit-keyframes square {
|
|
@-webkit-keyframes square {
|