ViewYoutubeVideo.vue 24 KB

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