ViewYoutubeVideo.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. <script setup lang="ts">
  2. import { onMounted, onBeforeUnmount, ref } from "vue";
  3. import Toast from "toasters";
  4. import { storeToRefs } from "pinia";
  5. import aw from "@/aw";
  6. import ws from "@/ws";
  7. import { useWebsocketsStore } from "@/stores/websockets";
  8. import { useModalsStore } from "@/stores/modals";
  9. import { useViewYoutubeVideoStore } from "@/stores/viewYoutubeVideo";
  10. const props = defineProps({
  11. modalUuid: { type: String, default: "" }
  12. });
  13. const interval = ref(null);
  14. const loaded = ref(false);
  15. const canvasWidth = ref(760);
  16. const volumeSliderValue = ref(20);
  17. const durationCanvas = ref(null);
  18. const activityWatchVideoDataInterval = ref(null);
  19. const activityWatchVideoLastStatus = ref("");
  20. const activityWatchVideoLastStartDuration = ref(0);
  21. const viewYoutubeVideoStore = useViewYoutubeVideoStore(props);
  22. const { videoId, youtubeId, video, player } = storeToRefs(
  23. viewYoutubeVideoStore
  24. );
  25. const {
  26. updatePlayer,
  27. stopVideo,
  28. loadVideoById,
  29. pauseVideo,
  30. setPlaybackRate,
  31. viewYoutubeVideo
  32. } = viewYoutubeVideoStore;
  33. const { openModal, closeCurrentModal } = useModalsStore();
  34. const { socket } = useWebsocketsStore();
  35. const remove = () => {
  36. socket.dispatch("youtube.removeVideos", videoId.value, res => {
  37. if (res.status === "success") {
  38. new Toast("YouTube video successfully removed.");
  39. closeCurrentModal();
  40. } else {
  41. new Toast("Youtube video with that ID not found.");
  42. }
  43. });
  44. };
  45. const handleConfirmed = ({ action, params }) => {
  46. if (typeof action === "function") {
  47. if (params) action(params);
  48. else action();
  49. }
  50. };
  51. const confirmAction = ({ message, action }) => {
  52. openModal({
  53. modal: "confirm",
  54. data: {
  55. message,
  56. action,
  57. params: null,
  58. onCompleted: handleConfirmed
  59. }
  60. });
  61. };
  62. const seekTo = position => {
  63. pauseVideo(false);
  64. player.value.player.seekTo(position);
  65. };
  66. const settings = type => {
  67. switch (type) {
  68. case "stop":
  69. stopVideo();
  70. pauseVideo(true);
  71. break;
  72. case "pause":
  73. pauseVideo(true);
  74. break;
  75. case "play":
  76. pauseVideo(false);
  77. break;
  78. case "skipToLast10Secs":
  79. seekTo(Number(player.value.duration) - 10);
  80. break;
  81. default:
  82. break;
  83. }
  84. };
  85. const play = () => {
  86. if (player.value.player.getVideoData().video_id !== video.value.youtubeId) {
  87. video.value.duration = -1;
  88. loadVideoById(video.value.youtubeId);
  89. }
  90. settings("play");
  91. };
  92. const changeVolume = () => {
  93. const { volume } = player.value;
  94. localStorage.setItem("volume", `${volume}`);
  95. player.value.player.setVolume(volume);
  96. if (volume > 0) {
  97. player.value.player.unMute();
  98. player.value.muted = false;
  99. }
  100. };
  101. const toggleMute = () => {
  102. const previousVolume = parseFloat(localStorage.getItem("volume"));
  103. const volume = player.value.player.getVolume() <= 0 ? previousVolume : 0;
  104. player.value.muted = !player.value.muted;
  105. volumeSliderValue.value = volume;
  106. player.value.player.setVolume(volume);
  107. if (!player.value.muted) localStorage.setItem("volume", volume.toString());
  108. };
  109. // const increaseVolume = () => {
  110. // const previousVolume = parseFloat(localStorage.getItem("volume"));
  111. // let volume = previousVolume + 5;
  112. // player.value.muted = false;
  113. // if (volume > 100) volume = 100;
  114. // player.value.volume = volume;
  115. // player.value.player.setVolume(volume);
  116. // localStorage.setItem("volume", volume.toString());
  117. // };
  118. const drawCanvas = () => {
  119. if (!loaded.value) return;
  120. const canvasElement = durationCanvas;
  121. if (!canvasElement) return;
  122. const ctx = canvasElement.getContext("2d");
  123. const videoDuration = Number(player.value.duration);
  124. const duration = Number(video.value.duration);
  125. const afterDuration = videoDuration - duration;
  126. canvasWidth.value = Math.min(document.body.clientWidth - 40, 760);
  127. const width = canvasWidth.value;
  128. const currentTime =
  129. player.value.player && player.value.player.getCurrentTime
  130. ? player.value.player.getCurrentTime()
  131. : 0;
  132. const widthDuration = (duration / videoDuration) * width;
  133. const widthAfterDuration = (afterDuration / videoDuration) * width;
  134. const widthCurrentTime = (currentTime / videoDuration) * width;
  135. const durationColor = "#03A9F4";
  136. const afterDurationColor = "#41E841";
  137. const currentDurationColor = "#3b25e8";
  138. ctx.fillStyle = durationColor;
  139. ctx.fillRect(0, 0, widthDuration, 20);
  140. ctx.fillStyle = afterDurationColor;
  141. ctx.fillRect(widthDuration, 0, widthAfterDuration, 20);
  142. ctx.fillStyle = currentDurationColor;
  143. ctx.fillRect(widthCurrentTime, 0, 1, 20);
  144. };
  145. const setTrackPosition = event => {
  146. seekTo(
  147. Number(
  148. Number(player.value.player.getDuration()) *
  149. ((event.pageX - event.target.getBoundingClientRect().left) /
  150. canvasWidth.value)
  151. )
  152. );
  153. };
  154. const sendActivityWatchVideoData = () => {
  155. if (!player.value.paused) {
  156. if (activityWatchVideoLastStatus.value !== "playing") {
  157. activityWatchVideoLastStatus.value = "playing";
  158. activityWatchVideoLastStartDuration.value = Math.floor(
  159. Number(player.value.currentTime)
  160. );
  161. }
  162. const videoData = {
  163. title: video.value.title,
  164. artists: video.value.author,
  165. youtubeId: video.value.youtubeId,
  166. muted: player.value.muted,
  167. volume: player.value.volume,
  168. startedDuration:
  169. activityWatchVideoLastStartDuration.value <= 0
  170. ? 0
  171. : activityWatchVideoLastStartDuration.value,
  172. source: `viewYoutubeVideo#${video.value.youtubeId}`,
  173. hostname: window.location.hostname
  174. };
  175. aw.sendVideoData(videoData);
  176. } else {
  177. activityWatchVideoLastStatus.value = "not_playing";
  178. }
  179. };
  180. const init = () => {
  181. loaded.value = false;
  182. socket.dispatch(
  183. "youtube.getVideo",
  184. videoId.value || youtubeId.value,
  185. true,
  186. res => {
  187. if (res.status === "success") {
  188. const youtubeVideo = res.data;
  189. viewYoutubeVideo(youtubeVideo);
  190. loaded.value = true;
  191. interval.value = setInterval(() => {
  192. if (
  193. video.value.duration !== -1 &&
  194. player.value.paused === false &&
  195. player.value.playerReady &&
  196. (player.value.player.getCurrentTime() >
  197. video.value.duration ||
  198. (player.value.player.getCurrentTime() > 0 &&
  199. player.value.player.getCurrentTime() >=
  200. player.value.player.getDuration()))
  201. ) {
  202. stopVideo();
  203. pauseVideo(true);
  204. drawCanvas();
  205. }
  206. if (
  207. player.value.playerReady &&
  208. player.value.player.getVideoData &&
  209. player.value.player.getVideoData() &&
  210. player.value.player.getVideoData().video_id ===
  211. video.value.youtubeId
  212. ) {
  213. const currentTime =
  214. player.value.player.getCurrentTime();
  215. if (currentTime !== undefined)
  216. player.value.currentTime = currentTime.toFixed(3);
  217. if (player.value.duration.indexOf(".000") !== -1) {
  218. const duration = player.value.player.getDuration();
  219. if (duration !== undefined) {
  220. if (
  221. `${player.value.duration}` ===
  222. `${Number(video.value.duration).toFixed(3)}`
  223. )
  224. video.value.duration = duration.toFixed(3);
  225. player.value.duration = duration.toFixed(3);
  226. if (
  227. player.value.duration.indexOf(".000") !== -1
  228. )
  229. player.value.videoNote = "(~)";
  230. else player.value.videoNote = "";
  231. drawCanvas();
  232. }
  233. }
  234. }
  235. if (player.value.paused === false) drawCanvas();
  236. }, 200);
  237. activityWatchVideoDataInterval.value = setInterval(() => {
  238. sendActivityWatchVideoData();
  239. }, 1000);
  240. if (window.YT && window.YT.Player) {
  241. player.value.player = new window.YT.Player(
  242. `viewYoutubeVideoPlayer-${props.modalUuid}`,
  243. {
  244. height: 298,
  245. width: 530,
  246. videoId: null,
  247. host: "https://www.youtube-nocookie.com",
  248. playerVars: {
  249. controls: 0,
  250. iv_load_policy: 3,
  251. rel: 0,
  252. showinfo: 0,
  253. autoplay: 0
  254. },
  255. events: {
  256. onReady: () => {
  257. let volume = parseFloat(
  258. localStorage.getItem("volume")
  259. );
  260. volume =
  261. typeof volume === "number"
  262. ? volume
  263. : 20;
  264. player.value.player.setVolume(volume);
  265. if (volume > 0)
  266. player.value.player.unMute();
  267. player.value.playerReady = true;
  268. if (video.value && video.value._id)
  269. player.value.player.cueVideoById(
  270. video.value.youtubeId
  271. );
  272. setPlaybackRate(null);
  273. drawCanvas();
  274. },
  275. onStateChange: event => {
  276. drawCanvas();
  277. if (event.data === 1) {
  278. player.value.paused = false;
  279. const youtubeDuration =
  280. player.value.player.getDuration();
  281. const newYoutubeVideoDuration =
  282. youtubeDuration.toFixed(3);
  283. if (
  284. player.value.duration.indexOf(
  285. ".000"
  286. ) !== -1 &&
  287. `${player.value.duration}` !==
  288. `${newYoutubeVideoDuration}`
  289. ) {
  290. const songDurationNumber = Number(
  291. video.value.duration
  292. );
  293. const songDurationNumber2 =
  294. Number(video.value.duration) +
  295. 1;
  296. const songDurationNumber3 =
  297. Number(video.value.duration) -
  298. 1;
  299. const fixedSongDuration =
  300. songDurationNumber.toFixed(3);
  301. const fixedSongDuration2 =
  302. songDurationNumber2.toFixed(3);
  303. const fixedSongDuration3 =
  304. songDurationNumber3.toFixed(3);
  305. if (
  306. `${player.value.duration}` ===
  307. `${Number(
  308. video.value.duration
  309. ).toFixed(3)}` &&
  310. (fixedSongDuration ===
  311. player.value.duration ||
  312. fixedSongDuration2 ===
  313. player.value.duration ||
  314. fixedSongDuration3 ===
  315. player.value.duration)
  316. )
  317. video.value.duration =
  318. newYoutubeVideoDuration;
  319. player.value.duration =
  320. newYoutubeVideoDuration;
  321. if (
  322. player.value.duration.indexOf(
  323. ".000"
  324. ) !== -1
  325. )
  326. player.value.videoNote = "(~)";
  327. else player.value.videoNote = "";
  328. }
  329. if (video.value.duration === -1)
  330. video.value.duration = Number(
  331. player.value.duration
  332. );
  333. if (
  334. video.value.duration >
  335. youtubeDuration + 1
  336. ) {
  337. stopVideo();
  338. pauseVideo(true);
  339. return new Toast(
  340. "Video can't play. Specified duration is bigger than the YouTube song duration."
  341. );
  342. }
  343. if (video.value.duration <= 0) {
  344. stopVideo();
  345. pauseVideo(true);
  346. return new Toast(
  347. "Video can't play. Specified duration has to be more than 0 seconds."
  348. );
  349. }
  350. setPlaybackRate(null);
  351. } else if (event.data === 2) {
  352. player.value.paused = true;
  353. }
  354. return false;
  355. }
  356. }
  357. }
  358. );
  359. } else {
  360. updatePlayer({
  361. error: true,
  362. errorMessage: "Player could not be loaded."
  363. });
  364. }
  365. let volume = parseFloat(localStorage.getItem("volume"));
  366. volume =
  367. typeof volume === "number" && !Number.isNaN(volume)
  368. ? volume
  369. : 20;
  370. localStorage.setItem("volume", volume.toString());
  371. updatePlayer({ volume });
  372. socket.dispatch(
  373. "apis.joinRoom",
  374. `view-youtube-video.${videoId.value}`
  375. );
  376. socket.on(
  377. "event:youtubeVideo.removed",
  378. () => {
  379. new Toast("This YouTube video was removed.");
  380. closeCurrentModal();
  381. },
  382. { modalUuid: props.modalUuid }
  383. );
  384. } else {
  385. new Toast("YouTube video with that ID not found");
  386. closeCurrentModal();
  387. }
  388. }
  389. );
  390. };
  391. onMounted(() => {
  392. ws.onConnect(init);
  393. });
  394. onBeforeUnmount(() => {
  395. stopVideo();
  396. pauseVideo(true);
  397. player.value.duration = "0.000";
  398. player.value.currentTime = 0;
  399. player.value.playerReady = false;
  400. player.value.videoNote = "";
  401. clearInterval(interval.value);
  402. clearInterval(activityWatchVideoDataInterval.value);
  403. loaded.value = false;
  404. socket.dispatch(
  405. "apis.leaveRoom",
  406. `view-youtube-video.${videoId.value}`,
  407. () => {}
  408. );
  409. // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
  410. viewYoutubeVideoStore.$dispose();
  411. });
  412. </script>
  413. <template>
  414. <modal title="View YouTube Video">
  415. <template #body>
  416. <div v-if="loaded" class="top-section">
  417. <div class="left-section">
  418. <p>
  419. <strong>ID:</strong>
  420. <span :title="video._id">{{ video._id }}</span>
  421. </p>
  422. <p>
  423. <strong>YouTube ID:</strong>
  424. <a
  425. :href="
  426. 'https://www.youtube.com/watch?v=' +
  427. `${video.youtubeId}`
  428. "
  429. target="_blank"
  430. >
  431. {{ video.youtubeId }}
  432. </a>
  433. </p>
  434. <p>
  435. <strong>Title:</strong>
  436. <span :title="video.title">{{ video.title }}</span>
  437. </p>
  438. <p>
  439. <strong>Author:</strong>
  440. <span :title="video.author">{{ video.author }}</span>
  441. </p>
  442. <p>
  443. <strong>Duration:</strong>
  444. <span :title="`${video.duration}`">{{
  445. video.duration
  446. }}</span>
  447. </p>
  448. </div>
  449. <div class="right-section">
  450. <song-thumbnail :song="video" class="thumbnail-preview" />
  451. </div>
  452. </div>
  453. <div v-show="loaded" class="player-section">
  454. <div class="player-container">
  455. <div :id="`viewYoutubeVideoPlayer-${modalUuid}`" />
  456. </div>
  457. <div v-show="player.error" class="player-error">
  458. <h2>{{ player.errorMessage }}</h2>
  459. </div>
  460. <canvas
  461. ref="durationCanvas"
  462. class="duration-canvas"
  463. v-show="!player.error"
  464. height="20"
  465. :width="canvasWidth"
  466. @click="setTrackPosition($event)"
  467. />
  468. <div class="player-footer">
  469. <div class="player-footer-left">
  470. <button
  471. class="button is-primary"
  472. @click="play()"
  473. @keyup.enter="play()"
  474. v-if="player.paused"
  475. content="Resume Playback"
  476. v-tippy
  477. >
  478. <i class="material-icons">play_arrow</i>
  479. </button>
  480. <button
  481. class="button is-primary"
  482. @click="settings('pause')"
  483. @keyup.enter="settings('pause')"
  484. v-else
  485. content="Pause Playback"
  486. v-tippy
  487. >
  488. <i class="material-icons">pause</i>
  489. </button>
  490. <button
  491. class="button is-danger"
  492. @click.exact="settings('stop')"
  493. @click.shift="settings('hardStop')"
  494. @keyup.enter.exact="settings('stop')"
  495. @keyup.shift.enter="settings('hardStop')"
  496. content="Stop Playback"
  497. v-tippy
  498. >
  499. <i class="material-icons">stop</i>
  500. </button>
  501. <tippy
  502. class="playerRateDropdown"
  503. :touch="true"
  504. :interactive="true"
  505. placement="bottom"
  506. theme="dropdown"
  507. ref="dropdown"
  508. trigger="click"
  509. append-to="parent"
  510. @show="
  511. () => {
  512. player.showRateDropdown = true;
  513. }
  514. "
  515. @hide="
  516. () => {
  517. player.showRateDropdown = false;
  518. }
  519. "
  520. >
  521. <div
  522. ref="trigger"
  523. class="control has-addons"
  524. content="Set Playback Rate"
  525. v-tippy
  526. >
  527. <button class="button is-primary">
  528. <i class="material-icons">fast_forward</i>
  529. </button>
  530. <button class="button dropdown-toggle">
  531. <i class="material-icons">
  532. {{
  533. player.showRateDropdown
  534. ? "expand_more"
  535. : "expand_less"
  536. }}
  537. </i>
  538. </button>
  539. </div>
  540. <template #content>
  541. <div class="nav-dropdown-items">
  542. <button
  543. class="nav-item button"
  544. :class="{
  545. active: player.playbackRate === 0.5
  546. }"
  547. title="0.5x"
  548. @click="setPlaybackRate(0.5)"
  549. >
  550. <p>0.5x</p>
  551. </button>
  552. <button
  553. class="nav-item button"
  554. :class="{
  555. active: player.playbackRate === 1
  556. }"
  557. title="1x"
  558. @click="setPlaybackRate(1)"
  559. >
  560. <p>1x</p>
  561. </button>
  562. <button
  563. class="nav-item button"
  564. :class="{
  565. active: player.playbackRate === 2
  566. }"
  567. title="2x"
  568. @click="setPlaybackRate(2)"
  569. >
  570. <p>2x</p>
  571. </button>
  572. </div>
  573. </template>
  574. </tippy>
  575. </div>
  576. <div class="player-footer-center">
  577. <span>
  578. <span>
  579. {{ player.currentTime }}
  580. </span>
  581. /
  582. <span>
  583. {{ player.duration }}
  584. {{ player.videoNote }}
  585. </span>
  586. </span>
  587. </div>
  588. <div class="player-footer-right">
  589. <p id="volume-control">
  590. <i
  591. class="material-icons"
  592. @click="toggleMute()"
  593. :content="`${player.muted ? 'Unmute' : 'Mute'}`"
  594. v-tippy
  595. >{{
  596. player.muted
  597. ? "volume_mute"
  598. : player.volume >= 50
  599. ? "volume_up"
  600. : "volume_down"
  601. }}</i
  602. >
  603. <input
  604. v-model="player.volume"
  605. type="range"
  606. min="0"
  607. max="100"
  608. class="volume-slider active"
  609. @change="changeVolume()"
  610. @input="changeVolume()"
  611. />
  612. </p>
  613. </div>
  614. </div>
  615. </div>
  616. <div v-if="!loaded" class="vertical-padding">
  617. <p>Video hasn't loaded yet</p>
  618. </div>
  619. </template>
  620. <template #footer>
  621. <button
  622. class="button is-primary icon-with-button material-icons"
  623. @click.prevent="
  624. openModal({ modal: 'editSong', data: { song: video } })
  625. "
  626. content="Create/edit song from video"
  627. v-tippy
  628. >
  629. music_note
  630. </button>
  631. <div class="right">
  632. <button
  633. class="button is-danger icon-with-button material-icons"
  634. @click.prevent="
  635. confirmAction({
  636. message:
  637. 'Removing this video will remove it from all playlists and cause a ratings recalculation.',
  638. action: remove
  639. })
  640. "
  641. content="Delete Video"
  642. v-tippy
  643. >
  644. delete_forever
  645. </button>
  646. </div>
  647. </template>
  648. </modal>
  649. </template>
  650. <style lang="less" scoped>
  651. .night-mode {
  652. .player-section,
  653. .top-section {
  654. background-color: var(--dark-grey-3) !important;
  655. border: 0 !important;
  656. .duration-canvas {
  657. background-color: var(--dark-grey-2) !important;
  658. }
  659. }
  660. }
  661. .top-section {
  662. display: flex;
  663. margin: 0 auto;
  664. padding: 10px;
  665. border: 1px solid var(--light-grey-3);
  666. border-radius: @border-radius;
  667. .left-section {
  668. display: flex;
  669. flex-direction: column;
  670. flex-grow: 1;
  671. p {
  672. text-overflow: ellipsis;
  673. white-space: nowrap;
  674. overflow: hidden;
  675. &:first-child {
  676. margin-top: auto;
  677. }
  678. &:last-child {
  679. margin-bottom: auto;
  680. }
  681. & > span,
  682. & > a {
  683. margin-left: 5px;
  684. }
  685. }
  686. }
  687. :deep(.right-section .thumbnail-preview) {
  688. width: 120px;
  689. height: 120px;
  690. margin: 0;
  691. }
  692. @media (max-width: 600px) {
  693. flex-direction: column-reverse;
  694. .left-section {
  695. margin-top: 10px;
  696. }
  697. }
  698. }
  699. .player-section {
  700. display: flex;
  701. flex-direction: column;
  702. margin: 10px auto 0 auto;
  703. border: 1px solid var(--light-grey-3);
  704. border-radius: @border-radius;
  705. overflow: hidden;
  706. .player-container {
  707. position: relative;
  708. padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
  709. height: 0;
  710. overflow: hidden;
  711. :deep([id^="viewYoutubeVideoPlayer"]) {
  712. position: absolute;
  713. top: 0;
  714. left: 0;
  715. width: 100%;
  716. height: 100%;
  717. min-height: 200px;
  718. }
  719. }
  720. .duration-canvas {
  721. background-color: var(--light-grey-2);
  722. }
  723. .player-error {
  724. display: flex;
  725. height: 428px;
  726. align-items: center;
  727. * {
  728. margin: 0;
  729. flex: 1;
  730. font-size: 30px;
  731. text-align: center;
  732. }
  733. }
  734. .player-footer {
  735. display: flex;
  736. justify-content: space-between;
  737. height: 54px;
  738. padding-left: 10px;
  739. padding-right: 10px;
  740. > * {
  741. width: 33.3%;
  742. display: flex;
  743. align-items: center;
  744. }
  745. .player-footer-left {
  746. flex: 1;
  747. & > .button:not(:first-child) {
  748. margin-left: 5px;
  749. }
  750. :deep(& > .playerRateDropdown) {
  751. margin-left: 5px;
  752. margin-bottom: unset !important;
  753. .control.has-addons {
  754. margin-bottom: unset !important;
  755. & > .button {
  756. font-size: 24px;
  757. }
  758. }
  759. }
  760. :deep(.tippy-box[data-theme~="dropdown"]) {
  761. max-width: 100px !important;
  762. .nav-dropdown-items .nav-item {
  763. justify-content: center !important;
  764. border-radius: @border-radius !important;
  765. &.active {
  766. background-color: var(--primary-color);
  767. color: var(--white);
  768. }
  769. }
  770. }
  771. }
  772. .player-footer-center {
  773. justify-content: center;
  774. align-items: center;
  775. flex: 2;
  776. font-size: 18px;
  777. font-weight: 400;
  778. width: 200px;
  779. margin: 0 5px;
  780. img {
  781. height: 21px;
  782. margin-right: 12px;
  783. filter: invert(26%) sepia(54%) saturate(6317%) hue-rotate(2deg)
  784. brightness(92%) contrast(115%);
  785. }
  786. }
  787. .player-footer-right {
  788. justify-content: right;
  789. flex: 1;
  790. #volume-control {
  791. margin: 3px;
  792. margin-top: 0;
  793. display: flex;
  794. align-items: center;
  795. cursor: pointer;
  796. .volume-slider {
  797. width: 100%;
  798. padding: 0 15px;
  799. background: transparent;
  800. min-width: 100px;
  801. }
  802. input[type="range"] {
  803. -webkit-appearance: none;
  804. margin: 7.3px 0;
  805. }
  806. input[type="range"]:focus {
  807. outline: none;
  808. }
  809. input[type="range"]::-webkit-slider-runnable-track {
  810. width: 100%;
  811. height: 5.2px;
  812. cursor: pointer;
  813. box-shadow: 0;
  814. background: var(--light-grey-3);
  815. border-radius: @border-radius;
  816. border: 0;
  817. }
  818. input[type="range"]::-webkit-slider-thumb {
  819. box-shadow: 0;
  820. border: 0;
  821. height: 19px;
  822. width: 19px;
  823. border-radius: 100%;
  824. background: var(--primary-color);
  825. cursor: pointer;
  826. -webkit-appearance: none;
  827. margin-top: -6.5px;
  828. }
  829. input[type="range"]::-moz-range-track {
  830. width: 100%;
  831. height: 5.2px;
  832. cursor: pointer;
  833. box-shadow: 0;
  834. background: var(--light-grey-3);
  835. border-radius: @border-radius;
  836. border: 0;
  837. }
  838. input[type="range"]::-moz-range-thumb {
  839. box-shadow: 0;
  840. border: 0;
  841. height: 19px;
  842. width: 19px;
  843. border-radius: 100%;
  844. background: var(--primary-color);
  845. cursor: pointer;
  846. -webkit-appearance: none;
  847. margin-top: -6.5px;
  848. }
  849. input[type="range"]::-ms-track {
  850. width: 100%;
  851. height: 5.2px;
  852. cursor: pointer;
  853. box-shadow: 0;
  854. background: var(--light-grey-3);
  855. border-radius: @border-radius;
  856. }
  857. input[type="range"]::-ms-fill-lower {
  858. background: var(--light-grey-3);
  859. border: 0;
  860. border-radius: 0;
  861. box-shadow: 0;
  862. }
  863. input[type="range"]::-ms-fill-upper {
  864. background: var(--light-grey-3);
  865. border: 0;
  866. border-radius: 0;
  867. box-shadow: 0;
  868. }
  869. input[type="range"]::-ms-thumb {
  870. box-shadow: 0;
  871. border: 0;
  872. height: 15px;
  873. width: 15px;
  874. border-radius: 100%;
  875. background: var(--primary-color);
  876. cursor: pointer;
  877. -webkit-appearance: none;
  878. margin-top: 1.5px;
  879. }
  880. }
  881. }
  882. }
  883. }
  884. </style>