ViewYoutubeVideo.vue 24 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  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 activityWatchVideoDataInterval = ref(null);
  33. const activityWatchVideoLastStatus = ref("");
  34. const activityWatchVideoLastStartDuration = 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 sendActivityWatchVideoData = () => {
  163. if (
  164. !player.value.paused &&
  165. player.value.player.getPlayerState() === window.YT.PlayerState.PLAYING
  166. ) {
  167. if (activityWatchVideoLastStatus.value !== "playing") {
  168. activityWatchVideoLastStatus.value = "playing";
  169. activityWatchVideoLastStartDuration.value = Math.floor(
  170. Number(player.value.currentTime)
  171. );
  172. }
  173. const videoData = {
  174. title: video.value.title,
  175. artists: video.value.author,
  176. youtubeId: video.value.youtubeId,
  177. muted: player.value.muted,
  178. volume: player.value.volume,
  179. startedDuration:
  180. activityWatchVideoLastStartDuration.value <= 0
  181. ? 0
  182. : activityWatchVideoLastStartDuration.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.sendVideoData(videoData);
  193. } else {
  194. activityWatchVideoLastStatus.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. activityWatchVideoDataInterval.value = setInterval(() => {
  252. sendActivityWatchVideoData();
  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: null,
  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(activityWatchVideoDataInterval.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({ modal: 'editSong', props: { song: video } })
  658. "
  659. content="Create/edit song from video"
  660. v-tippy
  661. >
  662. music_note
  663. </button>
  664. <div class="right">
  665. <button
  666. v-if="hasPermission('youtube.removeVideos')"
  667. class="button is-danger icon-with-button material-icons"
  668. @click.prevent="
  669. openModal({
  670. modal: 'confirm',
  671. props: {
  672. message:
  673. 'Removing this video will remove it from all playlists and cause a ratings recalculation.',
  674. onCompleted: remove
  675. }
  676. })
  677. "
  678. content="Delete Video"
  679. v-tippy
  680. >
  681. delete_forever
  682. </button>
  683. </div>
  684. </template>
  685. </modal>
  686. </template>
  687. <style lang="less" scoped>
  688. .night-mode {
  689. .player-section,
  690. .top-section {
  691. background-color: var(--dark-grey-3) !important;
  692. border: 0 !important;
  693. .duration-canvas {
  694. background-color: var(--dark-grey-2) !important;
  695. }
  696. }
  697. }
  698. .top-section {
  699. display: flex;
  700. margin: 0 auto;
  701. padding: 10px;
  702. border: 1px solid var(--light-grey-3);
  703. border-radius: @border-radius;
  704. .left-section {
  705. display: flex;
  706. flex-direction: column;
  707. flex-grow: 1;
  708. p {
  709. text-overflow: ellipsis;
  710. white-space: nowrap;
  711. overflow: hidden;
  712. &:first-child {
  713. margin-top: auto;
  714. }
  715. &:last-child {
  716. margin-bottom: auto;
  717. }
  718. & > span,
  719. & > a {
  720. margin-left: 5px;
  721. }
  722. }
  723. }
  724. :deep(.right-section .thumbnail-preview) {
  725. width: 120px;
  726. height: 120px;
  727. margin: 0;
  728. }
  729. @media (max-width: 600px) {
  730. flex-direction: column-reverse;
  731. .left-section {
  732. margin-top: 10px;
  733. }
  734. }
  735. }
  736. .player-section {
  737. display: flex;
  738. flex-direction: column;
  739. margin: 10px auto 0 auto;
  740. border: 1px solid var(--light-grey-3);
  741. border-radius: @border-radius;
  742. overflow: hidden;
  743. .player-container {
  744. position: relative;
  745. padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
  746. height: 0;
  747. overflow: hidden;
  748. :deep([id^="viewYoutubeVideoPlayer"]) {
  749. position: absolute;
  750. top: 0;
  751. left: 0;
  752. width: 100%;
  753. height: 100%;
  754. min-height: 200px;
  755. }
  756. }
  757. .duration-canvas {
  758. background-color: var(--light-grey-2);
  759. }
  760. .player-error {
  761. display: flex;
  762. height: 428px;
  763. align-items: center;
  764. * {
  765. margin: 0;
  766. flex: 1;
  767. font-size: 30px;
  768. text-align: center;
  769. }
  770. }
  771. .player-footer {
  772. display: flex;
  773. justify-content: space-between;
  774. height: 54px;
  775. padding-left: 10px;
  776. padding-right: 10px;
  777. > * {
  778. width: 33.3%;
  779. display: flex;
  780. align-items: center;
  781. }
  782. .player-footer-left {
  783. flex: 1;
  784. & > .button:not(:first-child) {
  785. margin-left: 5px;
  786. }
  787. :deep(& > .playerRateDropdown) {
  788. margin-left: 5px;
  789. margin-bottom: unset !important;
  790. .control.has-addons {
  791. margin-bottom: unset !important;
  792. & > .button {
  793. font-size: 24px;
  794. }
  795. }
  796. }
  797. :deep(.tippy-box[data-theme~="dropdown"]) {
  798. max-width: 100px !important;
  799. .nav-dropdown-items .nav-item {
  800. justify-content: center !important;
  801. border-radius: @border-radius !important;
  802. &.active {
  803. background-color: var(--primary-color);
  804. color: var(--white);
  805. }
  806. }
  807. }
  808. }
  809. .player-footer-center {
  810. justify-content: center;
  811. align-items: center;
  812. flex: 2;
  813. font-size: 18px;
  814. font-weight: 400;
  815. width: 200px;
  816. margin: 0 5px;
  817. img {
  818. height: 21px;
  819. margin-right: 12px;
  820. filter: invert(26%) sepia(54%) saturate(6317%) hue-rotate(2deg)
  821. brightness(92%) contrast(115%);
  822. }
  823. }
  824. .player-footer-right {
  825. justify-content: right;
  826. flex: 1;
  827. #volume-control {
  828. margin: 3px;
  829. margin-top: 0;
  830. display: flex;
  831. align-items: center;
  832. cursor: pointer;
  833. .volume-slider {
  834. width: 100%;
  835. padding: 0 15px;
  836. background: transparent;
  837. min-width: 100px;
  838. }
  839. input[type="range"] {
  840. -webkit-appearance: none;
  841. margin: 7.3px 0;
  842. }
  843. input[type="range"]:focus {
  844. outline: none;
  845. }
  846. input[type="range"]::-webkit-slider-runnable-track {
  847. width: 100%;
  848. height: 5.2px;
  849. cursor: pointer;
  850. box-shadow: 0;
  851. background: var(--light-grey-3);
  852. border-radius: @border-radius;
  853. border: 0;
  854. }
  855. input[type="range"]::-webkit-slider-thumb {
  856. box-shadow: 0;
  857. border: 0;
  858. height: 19px;
  859. width: 19px;
  860. border-radius: 100%;
  861. background: var(--primary-color);
  862. cursor: pointer;
  863. -webkit-appearance: none;
  864. margin-top: -6.5px;
  865. }
  866. input[type="range"]::-moz-range-track {
  867. width: 100%;
  868. height: 5.2px;
  869. cursor: pointer;
  870. box-shadow: 0;
  871. background: var(--light-grey-3);
  872. border-radius: @border-radius;
  873. border: 0;
  874. }
  875. input[type="range"]::-moz-range-thumb {
  876. box-shadow: 0;
  877. border: 0;
  878. height: 19px;
  879. width: 19px;
  880. border-radius: 100%;
  881. background: var(--primary-color);
  882. cursor: pointer;
  883. -webkit-appearance: none;
  884. margin-top: -6.5px;
  885. }
  886. input[type="range"]::-ms-track {
  887. width: 100%;
  888. height: 5.2px;
  889. cursor: pointer;
  890. box-shadow: 0;
  891. background: var(--light-grey-3);
  892. border-radius: @border-radius;
  893. }
  894. input[type="range"]::-ms-fill-lower {
  895. background: var(--light-grey-3);
  896. border: 0;
  897. border-radius: 0;
  898. box-shadow: 0;
  899. }
  900. input[type="range"]::-ms-fill-upper {
  901. background: var(--light-grey-3);
  902. border: 0;
  903. border-radius: 0;
  904. box-shadow: 0;
  905. }
  906. input[type="range"]::-ms-thumb {
  907. box-shadow: 0;
  908. border: 0;
  909. height: 15px;
  910. width: 15px;
  911. border-radius: 100%;
  912. background: var(--primary-color);
  913. cursor: pointer;
  914. -webkit-appearance: none;
  915. margin-top: 1.5px;
  916. }
  917. }
  918. }
  919. }
  920. }
  921. </style>