ViewYoutubeVideo.vue 24 KB

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