ViewYoutubeVideo.vue 23 KB

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