diff --git a/src/public/zorn/assets/icons/backward15-seconds.svg b/src/public/zorn/assets/icons/backward15-seconds.svg new file mode 100644 index 0000000..a1a61bd --- /dev/null +++ b/src/public/zorn/assets/icons/backward15-seconds.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/closed-captions-tag.svg b/src/public/zorn/assets/icons/closed-captions-tag.svg new file mode 100644 index 0000000..b464861 --- /dev/null +++ b/src/public/zorn/assets/icons/closed-captions-tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/forward-solid.svg b/src/public/zorn/assets/icons/forward-solid.svg new file mode 100644 index 0000000..8edb35a --- /dev/null +++ b/src/public/zorn/assets/icons/forward-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/forward15-seconds.svg b/src/public/zorn/assets/icons/forward15-seconds.svg new file mode 100644 index 0000000..f060a7c --- /dev/null +++ b/src/public/zorn/assets/icons/forward15-seconds.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/maximize.svg b/src/public/zorn/assets/icons/maximize.svg new file mode 100644 index 0000000..6a34a02 --- /dev/null +++ b/src/public/zorn/assets/icons/maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/pause-solid.svg b/src/public/zorn/assets/icons/pause-solid.svg new file mode 100644 index 0000000..f4c5d3a --- /dev/null +++ b/src/public/zorn/assets/icons/pause-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/play-solid.svg b/src/public/zorn/assets/icons/play-solid.svg new file mode 100644 index 0000000..395e8b1 --- /dev/null +++ b/src/public/zorn/assets/icons/play-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/refresh-double.svg b/src/public/zorn/assets/icons/refresh-double.svg new file mode 100644 index 0000000..2434767 --- /dev/null +++ b/src/public/zorn/assets/icons/refresh-double.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/rewind-solid.svg b/src/public/zorn/assets/icons/rewind-solid.svg new file mode 100644 index 0000000..f3c1c16 --- /dev/null +++ b/src/public/zorn/assets/icons/rewind-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/sound-high.svg b/src/public/zorn/assets/icons/sound-high.svg new file mode 100644 index 0000000..b5bfabe --- /dev/null +++ b/src/public/zorn/assets/icons/sound-high.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/sound-min.svg b/src/public/zorn/assets/icons/sound-min.svg new file mode 100644 index 0000000..c9c65f5 --- /dev/null +++ b/src/public/zorn/assets/icons/sound-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/assets/icons/sound-off.svg b/src/public/zorn/assets/icons/sound-off.svg new file mode 100644 index 0000000..402aea8 --- /dev/null +++ b/src/public/zorn/assets/icons/sound-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/zorn/dialogs/Buffering.js b/src/public/zorn/dialogs/Buffering.js new file mode 100644 index 0000000..23cdd2f --- /dev/null +++ b/src/public/zorn/dialogs/Buffering.js @@ -0,0 +1,9 @@ +import { + RefreshIcon, +} from '../get' + +export var BufferDialog = ` +
+ ${RefreshIcon} +
+` \ No newline at end of file diff --git a/src/public/zorn/events.js b/src/public/zorn/events.js new file mode 100644 index 0000000..2258ac1 --- /dev/null +++ b/src/public/zorn/events.js @@ -0,0 +1,24 @@ +import { ZornVideoPlayer } from "./get" + +export function Events() { + ZornVideoPlayer.addEventListener('error', function(event) { + document.querySelector('#invalid-src').style.display = 'inherit' + document.querySelector('.zorn-player-controls').style.display = 'none' + videoContainer.style.backgroundColor = '#101010' + setTimeout(() => { + ZornVideoPlayer.style.opacity = '0.10' + document.querySelector('#buffering').style.display = 'none' + }, 0o250); + }, true) + + ZornVideoPlayer.onwaiting = (event) => { + document.querySelector('#buffering').style.display = 'inherit' + ZornVideoPlayer.style.transition = '5s opacity' + ZornVideoPlayer.style.opacity = '0.25' + } + ZornVideoPlayer.oncanplaythrough = (event) => { + document.querySelector('#buffering').style.display = 'none' + ZornVideoPlayer.style.transition = '0.3s opacity' + ZornVideoPlayer.style.opacity = '1' + } +} \ No newline at end of file diff --git a/src/public/zorn/functions/AutoToggleControls.js b/src/public/zorn/functions/AutoToggleControls.js new file mode 100644 index 0000000..d9ee456 --- /dev/null +++ b/src/public/zorn/functions/AutoToggleControls.js @@ -0,0 +1,46 @@ +import { VideoContainer, VideoControls, ZornVideoPlayer } from '../get' + + +export function AutoToggleControls() { + function Hide_Controls() { + var ZornTitleArea = document.querySelector(".zorn-title") + if (ZornVideoPlayer.paused) { + return + } else { + document.querySelector('.zorn-player-controls').classList.add('hide') + ZornTitleArea.classList.add('hide') + } + } + + // Show_Controls displays the video controls + function Show_Controls() { + var ZornTitleArea = document.querySelector(".zorn-title") + document.querySelector('.zorn-player-controls').classList.remove('hide') + ZornTitleArea.classList.remove('hide') + } + ZornVideoPlayer.addEventListener('mouseenter', Show_Controls) + ZornVideoPlayer.addEventListener('mouseleave', Hide_Controls) + document.querySelector('.zorn-player-controls').addEventListener('mouseenter', Show_Controls) + document.querySelector('.zorn-player-controls').addEventListener('mouseleave', Hide_Controls) + + var mouseTimer = null, cursorVisible = true + + function Hide_Cursor() { + mouseTimer = null + VideoContainer.style.cursor = "none" + cursorVisible = false + Hide_Controls() + } + + document.onmousemove = function() { + if (mouseTimer) { + window.clearTimeout(mouseTimer) + Show_Controls() + } + if (!cursorVisible) { + VideoContainer.style.cursor = "default" + cursorVisible = true + } + mouseTimer = window.setTimeout(Hide_Cursor, 3200) + } +} \ No newline at end of file diff --git a/src/public/zorn/functions/Fullscreen.js b/src/public/zorn/functions/Fullscreen.js new file mode 100644 index 0000000..d4f7141 --- /dev/null +++ b/src/public/zorn/functions/Fullscreen.js @@ -0,0 +1,24 @@ +import { VideoContainer, ZornVideoPlayer } from '../get' + + +export function Fullscreen() { + const Button_Fullscreen = document.getElementById('fullscreen') + function Toggle_Fullscreen() { + if (document.fullscreenElement) {document.exitFullscreen()} + else if (document.webkitFullscreenElement) {document.webkitExitFullscreen()} + else if (VideoContainer.webkitRequestFullscreen) {VideoContainer.webkitRequestFullscreen()} + else {VideoContainer.requestFullscreen()} + } + + Button_Fullscreen.onclick = Toggle_Fullscreen + function Update_FullscreenButton() { + if (document.fullscreenElement) { + Button_Fullscreen.setAttribute('data-title', 'Exit full screen (f)') + } else { + Button_Fullscreen.setAttribute('data-title', 'Full screen (f)') + } + } + + VideoContainer.addEventListener('fullscreenchange', Update_FullscreenButton) + ZornVideoPlayer.addEventListener('dblclick', () => {Toggle_Fullscreen()}) +} \ No newline at end of file diff --git a/src/public/zorn/functions/KeyboardShortcuts.js b/src/public/zorn/functions/KeyboardShortcuts.js new file mode 100644 index 0000000..b9c184b --- /dev/null +++ b/src/public/zorn/functions/KeyboardShortcuts.js @@ -0,0 +1,82 @@ +import { ZornVideoPlayer, VideoContainer } from "../get" + +export function KeyboardShortcuts(events) { + /// Grab custom ones, if any + if (ZornVideoPlayer.hasAttribute('keyboard-shortcut-fullscreen')) { + var Fullscreen_KeyboardShortcut = ZornVideoPlayer.getAttribute('keyboard-shortcut-fullscreen') + } + else { + var Fullscreen_KeyboardShortcut = 'f' + } + + if (ZornVideoPlayer.hasAttribute('keyboard-shortcut-mute')) { + var Mute_KeyboardShortcut = ZornVideoPlayer.getAttribute('keyboard-shortcut-mute') + } + else { + var Mute_KeyboardShortcut = 'm' + } + + if (ZornVideoPlayer.hasAttribute('keyboard-shortcut-playpause')) { + var PlayPause_KeyboardShortcut = ZornVideoPlayer.getAttribute('keyboard-shortcut-playpause') + } + else { + var PlayPause_KeyboardShortcut = 'k' + } + + if (ZornVideoPlayer.hasAttribute('keyboard-shortcut-skipback')) { + var SkipBack_KeyboardShortcut = ZornVideoPlayer.getAttribute('keyboard-shortcut-skipback') + } + else { + var SkipBack_KeyboardShortcut = 'j' + } + + if (ZornVideoPlayer.hasAttribute('keyboard-shortcut-skipforth')) { + var SkipForth_KeyboardShortcut = ZornVideoPlayer.getAttribute('keyboard-shortcut-skipforth') + } + else { + var SkipForth_KeyboardShortcut = 'l' + } + + /// Defaults + function keyboardShortcuts(event) { + const { key } = event + if (key === PlayPause_KeyboardShortcut) { + if (ZornVideoPlayer.paused || ZornVideoPlayer.ended) { + ZornVideoPlayer.play() + } + else { + ZornVideoPlayer.pause() + } + if (ZornVideoPlayer.paused) { + Show_Controls() + } else { + setTimeout(() => { + Hide_Controls() + }, 1200) + } + } + else if (key === Mute_KeyboardShortcut) { + ZornVideoPlayer.muted = !ZornVideoPlayer.muted + + if (ZornVideoPlayer.muted) { + volume.setAttribute('data-volume', volume.value) + volume.value = 0 + } else { + volume.value = volume.dataset.volume + } + } + else if (key === Fullscreen_KeyboardShortcut) { + if (document.fullscreenElement) {document.exitFullscreen()} + else if (document.webkitFullscreenElement) {document.webkitExitFullscreen()} + else if (VideoContainer.webkitRequestFullscreen) {VideoContainer.webkitRequestFullscreen()} + else {VideoContainer.requestFullscreen()} + } + else if (key === SkipBack_KeyboardShortcut) { + ZornVideoPlayer.currentTime += (-10) + } + else if (key === SkipForth_KeyboardShortcut) { + ZornVideoPlayer.currentTime += (10) + } + } + document.addEventListener('keyup', keyboardShortcuts) +} \ No newline at end of file diff --git a/src/public/zorn/functions/PlayPause.js b/src/public/zorn/functions/PlayPause.js new file mode 100644 index 0000000..752a3fe --- /dev/null +++ b/src/public/zorn/functions/PlayPause.js @@ -0,0 +1,34 @@ +import { + ZornVideoPlayer, + PlayIcon, + PauseIcon +} from '../get' + + +export function PlayPause() { + const Button_PlayPause = document.querySelector('.zorn-player-controls #play-pause') + + Button_PlayPause.addEventListener('click', Toggle_PlayPause) + ZornVideoPlayer.addEventListener('click', Toggle_PlayPause) + ZornVideoPlayer.addEventListener('play', Update_PlayPauseButton) + ZornVideoPlayer.addEventListener('pause', Update_PlayPauseButton) + + function Toggle_PlayPause() { + if (ZornVideoPlayer.paused || ZornVideoPlayer.ended) { + ZornVideoPlayer.play() + } + else { + ZornVideoPlayer.pause() + } + } + + function Update_PlayPauseButton() { + if (ZornVideoPlayer.paused) { + Button_PlayPause.setAttribute('data-title', 'Play (K)') + Button_PlayPause.innerHTML = `${PlayIcon}` + } else { + Button_PlayPause.setAttribute('data-title', 'Pause (K)') + Button_PlayPause.innerHTML = `${PauseIcon}` + } + } +} \ No newline at end of file diff --git a/src/public/zorn/functions/Seek.js b/src/public/zorn/functions/Seek.js new file mode 100644 index 0000000..d026d77 --- /dev/null +++ b/src/public/zorn/functions/Seek.js @@ -0,0 +1,73 @@ +import { ZornVideoPlayer } from '../get' + +export function Seek() { + // Duration and Length of Video + const timeElapsed = document.getElementById('time-elapsed'); + const duration = document.getElementById('duration'); + + function formatTime(timeInSeconds) { + const result = new Date(timeInSeconds * 1000).toISOString().substr(11, 8); + + return { + minutes: result.substr(3, 2), + seconds: result.substr(6, 2), + }; + }; + + + function initializeVideo() { + const videoDuration = Math.round(ZornVideoPlayer.duration); + const time = formatTime(videoDuration); + duration.innerText = `${time.minutes}:${time.seconds}`; + duration.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`); + } + ZornVideoPlayer.addEventListener('loadedmetadata', initializeVideo); + function updateTimeElapsed() { + const time = formatTime(Math.round(ZornVideoPlayer.currentTime)); + timeElapsed.innerText = `${time.minutes}:${time.seconds}`; + timeElapsed.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`); + } + ZornVideoPlayer.addEventListener('timeupdate', updateTimeElapsed); + + // Progress Bar + const progressBar = document.getElementById('progress-bar'); + const seek = document.getElementById('seek'); + function initializeVideo() { + const videoDuration = Math.round(ZornVideoPlayer.duration); + seek.setAttribute('max', videoDuration); + progressBar.setAttribute('max', videoDuration); + const time = formatTime(videoDuration); + duration.innerText = `${time.minutes}:${time.seconds}`; + duration.setAttribute('datetime', `${time.minutes}m ${time.seconds}s`) + } + + function updateProgress() { + seek.value = Math.floor(ZornVideoPlayer.currentTime); + progressBar.value = Math.floor(ZornVideoPlayer.currentTime); + } + + ZornVideoPlayer.addEventListener('timeupdate', updateProgress); + + const seekTooltip = document.getElementById('seek-tooltip'); + + function updateSeekTooltip(event) { + const skipTo = Math.round((event.offsetX / event.target.clientWidth) * parseInt(event.target.getAttribute('max'), 10)); + seek.setAttribute('data-seek', skipTo) + const t = formatTime(skipTo); + seekTooltip.textContent = `${t.minutes}:${t.seconds}`; + const rect = ZornVideoPlayer.getBoundingClientRect(); + seekTooltip.style.left = `${event.pageX - rect.left}px`; + } + + seek.addEventListener('mousemove', updateSeekTooltip); + + function skipAhead(event) { + const skipTo = event.target.dataset.seek ? event.target.dataset.seek : event.target.value; + ZornVideoPlayer.currentTime = skipTo; + progressBar.value = skipTo; + seek.value = skipTo; + } + seek.addEventListener('input', skipAhead); + + initializeVideo() +} \ No newline at end of file diff --git a/src/public/zorn/functions/SkipAround.js b/src/public/zorn/functions/SkipAround.js new file mode 100644 index 0000000..8024766 --- /dev/null +++ b/src/public/zorn/functions/SkipAround.js @@ -0,0 +1,16 @@ +import { ZornVideoPlayer } from '../get' + +export function SkipAround() { + const Button_SkipBack = document.querySelector('.zorn-player-controls #skip-back') + const Button_SkipForth = document.querySelector('.zorn-player-controls #skip-forth') + + Button_SkipBack.addEventListener('click', Toggle_SkipBack) + Button_SkipForth.addEventListener('click', Toggle_SkipForth) + + function Toggle_SkipBack() {Skip(-10)} + function Toggle_SkipForth() {Skip(10)} + + function Skip(value) { + ZornVideoPlayer.currentTime += value + } +} \ No newline at end of file diff --git a/src/public/zorn/functions/Subtitles.js b/src/public/zorn/functions/Subtitles.js new file mode 100644 index 0000000..4d3b204 --- /dev/null +++ b/src/public/zorn/functions/Subtitles.js @@ -0,0 +1,51 @@ +import { VideoContainer, ZornVideoPlayer } from '../get' + + +export function Subtitles() { + var subtitles = document.querySelector('.zorn-player-controls #subtitles') + var subtitleMenuButtons = [] + var createMenuItem = function(id, lang, label) { + var listItem = document.createElement('li') + var button = listItem.appendChild(document.createElement('button')) + button.setAttribute('id', id) + button.className = 'subtitles-button' + if (lang.length > 0) button.setAttribute('lang', lang) + button.value = label + button.setAttribute('data-state', 'inactive') + button.appendChild(document.createTextNode(label)) + button.addEventListener('click', function(e) { + subtitleMenuButtons.map(function(v, i, a) { + subtitleMenuButtons[i].setAttribute('data-state', 'inactive') + }) + var lang = this.getAttribute('lang') + for (var i = 0; i < ZornVideoPlayer.textTracks.length; i++) { + if (ZornVideoPlayer.textTracks[i].language == lang) { + ZornVideoPlayer.textTracks[i].mode = 'showing' + this.setAttribute('data-state', 'active') + } + else { + ZornVideoPlayer.textTracks[i].mode = 'hidden' + } + } + subtitlesMenu.style.display = 'none' + }) + subtitleMenuButtons.push(button) + return listItem + } + var subtitlesMenu + if (ZornVideoPlayer.textTracks) { + var df = document.createDocumentFragment() + var subtitlesMenu = df.appendChild(document.createElement('ul')) + subtitlesMenu.className = 'subtitles-menu' + subtitlesMenu.appendChild(createMenuItem('subtitles-off', '', 'Off')) + for (var i = 0; i < ZornVideoPlayer.textTracks.length; i++) { + subtitlesMenu.appendChild(createMenuItem('subtitles-' + ZornVideoPlayer.textTracks[i].language, ZornVideoPlayer.textTracks[i].language, ZornVideoPlayer.textTracks[i].label)) + } + VideoContainer.appendChild(subtitlesMenu) + } + subtitles.addEventListener('click', function(e) { + if (subtitlesMenu) { + subtitlesMenu.style.display = (subtitlesMenu.style.display == 'block' ? 'none' : 'block') + } + }) +} \ No newline at end of file diff --git a/src/public/zorn/functions/Volume.js b/src/public/zorn/functions/Volume.js new file mode 100644 index 0000000..a0fa946 --- /dev/null +++ b/src/public/zorn/functions/Volume.js @@ -0,0 +1,46 @@ +import { + ZornVideoPlayer, + VolumeOffIcon, + VolumeMinIcon, + VolumeHighIcon +} from '../get' + +export function Volume() { + const Button_Volume = document.getElementById('volume-button') + const volume = document.getElementById('volume') + function Update_Volme() { + if (ZornVideoPlayer.muted) { + ZornVideoPlayer.muted = false + } + ZornVideoPlayer.volume = volume.value + } + + volume.addEventListener('input', Update_Volme) + + function Update_Volume_Icon() { + Button_Volume.setAttribute('data-title', 'Mute (M)') + + if (ZornVideoPlayer.muted || ZornVideoPlayer.volume === 0) { + Button_Volume.innerHTML = `${VolumeOffIcon}` + Button_Volume.setAttribute('data-title', 'Unmute (M)') + } else if (ZornVideoPlayer.volume > 0 && ZornVideoPlayer.volume <= 0.5) { + Button_Volume.innerHTML = `${VolumeMinIcon}` + } else { + Button_Volume.innerHTML = `${VolumeHighIcon}` + } + } + + ZornVideoPlayer.addEventListener('volumechange', Update_Volume_Icon) + + function Toggle_Mute() { + ZornVideoPlayer.muted = !ZornVideoPlayer.muted + + if (ZornVideoPlayer.muted) { + volume.setAttribute('data-volume', volume.value) + volume.value = 0 + } else { + volume.value = volume.dataset.volume + } + } + Button_Volume.addEventListener('click', Toggle_Mute) +} \ No newline at end of file diff --git a/src/public/zorn/get.js b/src/public/zorn/get.js new file mode 100644 index 0000000..dba0d17 --- /dev/null +++ b/src/public/zorn/get.js @@ -0,0 +1,29 @@ +// Set Variables for required video container and video player +export var ZornVideoPlayer = document.querySelector('.zorn-player') +export var VideoContainer = document.querySelector('.video-container') +export var VideoControls = document.querySelector('.zorn-player-controls') + +// Icons - Iconoir.com +/// Get Icons +import PlaySVG from './assets/icons/play-solid.svg' +import PauseSVG from './assets/icons/pause-solid.svg' +import FullcreenSVG from './assets/icons/maximize.svg' +import CaptionsSVG from './assets/icons/closed-captions-tag.svg' +import Backward15SVG from './assets/icons/backward15-seconds.svg' +import Forward15SVG from './assets/icons/forward15-seconds.svg' +import VolumeHighSVG from './assets/icons/sound-high.svg' +import VolumeMinSVG from './assets/icons/sound-min.svg' +import VolumeOffSVG from './assets/icons/sound-off.svg' +import RefreshSVG from './assets/icons/refresh-double.svg' + +/// Set Icons +export var PlayIcon = PlaySVG +export var PauseIcon = PauseSVG +export var FullcreenIcon = FullcreenSVG +export var CaptionsIcon = CaptionsSVG +export var Backward15Icon = Backward15SVG +export var Forward15Icon = Forward15SVG +export var VolumeHighIcon = VolumeHighSVG +export var VolumeMinIcon = VolumeMinSVG +export var VolumeOffIcon = VolumeOffSVG +export var RefreshIcon = RefreshSVG \ No newline at end of file diff --git a/src/public/zorn/index.js b/src/public/zorn/index.js new file mode 100644 index 0000000..6e4c1c4 --- /dev/null +++ b/src/public/zorn/index.js @@ -0,0 +1,35 @@ +import { ZornVideoPlayer } from "./get" +import { Controls, Title } from "./themes/default" + +// Import Functions +import { Events } from './events' +import { PlayPause } from "./functions/PlayPause" +import { SkipAround } from "./functions/SkipAround" +import { Fullscreen } from "./functions/Fullscreen" +import { Subtitles } from "./functions/Subtitles" +import { Volume } from "./functions/Volume" +import { Seek } from "./functions/Seek" +import { BufferDialog } from "./dialogs/Buffering" +import { AutoToggleControls } from "./functions/AutoToggleControls" +import { KeyboardShortcuts } from "./functions/KeyboardShortcuts" + +// Apply Controls +ZornVideoPlayer.insertAdjacentHTML("afterend", Controls) +ZornVideoPlayer.insertAdjacentHTML("afterend", BufferDialog) + +if (ZornVideoPlayer.getAttribute('layout') === 'default') { + Title() +} + + +// Init Functions +Events() +KeyboardShortcuts() +PlayPause() +AutoToggleControls() // Broken +SkipAround() +Fullscreen() +Subtitles() +Volume() +Seek() +BufferDialog() \ No newline at end of file diff --git a/src/public/zorn/themes/default.js b/src/public/zorn/themes/default.js new file mode 100644 index 0000000..7d13f82 --- /dev/null +++ b/src/public/zorn/themes/default.js @@ -0,0 +1,381 @@ +import { + PlayIcon, + FullcreenIcon, + CaptionsIcon, + Backward15Icon, + Forward15Icon, + VolumeHighIcon, + ZornVideoPlayer +} from '../get' + + +// This theme includes an optional title +export function Title() { + let VideoTitle = ZornVideoPlayer.getAttribute('video-title') + document.querySelector(".zorn-title").innerHTML = VideoTitle + if (ZornVideoPlayer.hasAttribute('video-title')) { + document.querySelector('.zorn-title').style.display = 'inherit' + } else { + document.querySelector('.zorn-title').style.display = 'none' + } +} + +export var Controls = ` +

+
+
+
+ + +
00:00
+
+
+
+
+
+ + +
+
+ + / + +
+
+
+ + + +
+
+ + +
+
+
+ +` \ No newline at end of file