routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+134
@@ -0,0 +1,134 @@
|
||||
@layer vuetify-components {
|
||||
.v-video {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
--v-video-aspect-ratio: 1.7777777778;
|
||||
--v-video-controls-height: 58px;
|
||||
--v-video-controls-pill-border-radius: 99px;
|
||||
}
|
||||
.v-video--density-default {
|
||||
--v-video-controls-height: 58px;
|
||||
}
|
||||
.v-video--density-comfortable {
|
||||
--v-video-controls-height: 50px;
|
||||
}
|
||||
.v-video--density-compact {
|
||||
--v-video-controls-height: 42px;
|
||||
}
|
||||
.v-video__video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
}
|
||||
.v-video__video::-webkit-media-controls {
|
||||
display: none !important;
|
||||
}
|
||||
.v-video__video ~ * {
|
||||
z-index: 1;
|
||||
}
|
||||
.v-video__header {
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
.v-video__header > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.v-video__content {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
z-index: 0;
|
||||
}
|
||||
.v-video__content {
|
||||
box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 2px 6px 2px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
|
||||
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 4%, transparent);
|
||||
}
|
||||
.v-video:not(.v-video--idle):not(.v-video--error) .v-video__content .v-video__overlay-fill, .v-video:not(.v-video--idle):not(.v-video--error) .v-video__content .v-video__overlay-fill > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
.v-video:not(.v-video--error) .v-video__content {
|
||||
cursor: pointer;
|
||||
}
|
||||
.v-video__overlay-fill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.v-video__overlay-fill > .v-img {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
inset: 0;
|
||||
}
|
||||
.v-video:has(.v-video-controls:not(.v-video-controls--detached)) .v-video__content .v-video__overlay-fill {
|
||||
padding-bottom: var(--v-video-controls-height);
|
||||
}
|
||||
.v-video__center-icon.v-icon-btn {
|
||||
border: 5px solid currentColor;
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 0.28s;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.v-video__center-icon.v-icon-btn.v-video__center-icon--play {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.v-video__content:hover .v-video__center-icon--play {
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
.v-video--variant-player {
|
||||
width: 100%;
|
||||
}
|
||||
.v-video--variant-player > .v-video__content {
|
||||
width: 100%;
|
||||
aspect-ratio: var(--v-video-aspect-ratio);
|
||||
}
|
||||
.v-video--variant-background {
|
||||
outline: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.v-video--variant-background > .v-video__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.v-video .v-video-controls {
|
||||
opacity: 0;
|
||||
}
|
||||
.v-video .v-video-controls:not(.v-video-controls--detached) {
|
||||
margin-top: calc(-1 * var(--v-video-controls-height));
|
||||
}
|
||||
.v-video .v-video-controls--detached {
|
||||
opacity: 1;
|
||||
}
|
||||
.v-video .v-video-controls--floating:not(.v-video-controls--detached) {
|
||||
margin-top: calc(-1 * var(--v-video-controls-height) - 12px);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.v-video:not(.v-video--playing) .v-video__header,
|
||||
.v-video:not(.v-video--playing) .v-video-controls, .v-video:hover .v-video__header,
|
||||
.v-video:hover .v-video-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
.v-video--rounded {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.v-video:fullscreen .v-video__content {
|
||||
min-height: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
.poster-fade-out-leave-active {
|
||||
transition: opacity 1s linear 0.3s;
|
||||
}
|
||||
.poster-fade-out-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
+8144
File diff suppressed because it is too large
Load Diff
+474
@@ -0,0 +1,474 @@
|
||||
import { createVNode as _createVNode, normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, mergeProps as _mergeProps, normalizeStyle as _normalizeStyle } from "vue";
|
||||
// Styles
|
||||
import "./VVideo.css";
|
||||
|
||||
// Components
|
||||
import { makeVVideoControlsProps, VVideoControls } from "./VVideoControls.js";
|
||||
import { VFadeTransition } from "../../components/transitions/index.js";
|
||||
import { VDefaultsProvider } from "../../components/VDefaultsProvider/index.js";
|
||||
import { VIcon } from "../../components/VIcon/index.js";
|
||||
import { VImg } from "../../components/VImg/VImg.js";
|
||||
import { VProgressCircular } from "../../components/VProgressCircular/VProgressCircular.js";
|
||||
import { VIconBtn } from "../VIconBtn/VIconBtn.js"; // Composables
|
||||
import { useDisplay } from "../../composables/index.js";
|
||||
import { makeComponentProps } from "../../composables/component.js";
|
||||
import { makeDensityProps, useDensity } from "../../composables/density.js";
|
||||
import { makeDimensionProps, useDimension } from "../../composables/dimensions.js";
|
||||
import { useElevation } from "../../composables/elevation.js";
|
||||
import { forwardRefs } from "../../composables/forwardRefs.js";
|
||||
import { useProxiedModel } from "../../composables/proxiedModel.js";
|
||||
import { useRounded } from "../../composables/rounded.js";
|
||||
import { makeThemeProps, provideTheme } from "../../composables/theme.js";
|
||||
import { MaybeTransition } from "../../composables/transition.js"; // Utilities
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef, toRef, Transition, watch } from 'vue';
|
||||
import { createRange, genericComponent, omit, pick, propsFactory, useRender } from "../../util/index.js"; // Types
|
||||
const allowedVariants = ['background', 'player'];
|
||||
export const makeVVideoProps = propsFactory({
|
||||
aspectRatio: [String, Number],
|
||||
autoplay: Boolean,
|
||||
muted: Boolean,
|
||||
eager: Boolean,
|
||||
error: [Object, Boolean],
|
||||
src: String,
|
||||
srcObject: Object,
|
||||
type: String,
|
||||
// e.g. video/mp4
|
||||
image: String,
|
||||
hideOverlay: Boolean,
|
||||
noFullscreen: Boolean,
|
||||
startAt: [Number, String],
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'player',
|
||||
validator: v => allowedVariants.includes(v)
|
||||
},
|
||||
controlsTransition: {
|
||||
type: [Boolean, String, Object],
|
||||
component: VFadeTransition
|
||||
},
|
||||
controlsVariant: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
controlsProps: {
|
||||
type: Object
|
||||
},
|
||||
rounded: [Boolean, Number, String, Array],
|
||||
...makeComponentProps(),
|
||||
...makeDensityProps(),
|
||||
...makeDimensionProps(),
|
||||
...makeThemeProps(),
|
||||
...omit(makeVVideoControlsProps(), ['fullscreen', 'variant'])
|
||||
}, 'VVideo');
|
||||
export const VVideo = genericComponent()({
|
||||
name: 'VVideo',
|
||||
inheritAttrs: false,
|
||||
props: makeVVideoProps(),
|
||||
emits: {
|
||||
error: val => true,
|
||||
loaded: element => true,
|
||||
'update:error': val => true,
|
||||
'update:playing': val => true,
|
||||
'update:progress': val => true,
|
||||
'update:volume': val => true
|
||||
},
|
||||
setup(props, {
|
||||
attrs,
|
||||
emit,
|
||||
slots
|
||||
}) {
|
||||
const {
|
||||
themeClasses
|
||||
} = provideTheme(props);
|
||||
const {
|
||||
densityClasses
|
||||
} = useDensity(props);
|
||||
const {
|
||||
dimensionStyles
|
||||
} = useDimension(props);
|
||||
const {
|
||||
elevationClasses
|
||||
} = useElevation(props);
|
||||
const {
|
||||
ssr
|
||||
} = useDisplay();
|
||||
const roundedForContainer = toRef(() => Array.isArray(props.rounded) ? props.rounded[0] : props.rounded);
|
||||
const roundedForControls = toRef(() => Array.isArray(props.rounded) ? props.rounded.at(-1) : props.rounded ?? false);
|
||||
const {
|
||||
roundedClasses: roundedContainerClasses
|
||||
} = useRounded(roundedForContainer);
|
||||
const {
|
||||
roundedClasses: roundedControlsClasses
|
||||
} = useRounded(roundedForControls);
|
||||
const containerRef = ref();
|
||||
const videoRef = ref();
|
||||
const controlsRef = ref();
|
||||
const playing = useProxiedModel(props, 'playing');
|
||||
const progress = useProxiedModel(props, 'progress');
|
||||
const volume = useProxiedModel(props, 'volume', 0, v => Number(v ?? 0));
|
||||
const fullscreen = shallowRef(false);
|
||||
const waiting = shallowRef(false);
|
||||
const triggered = shallowRef(false);
|
||||
const startAfterLoad = shallowRef(false);
|
||||
const error = useProxiedModel(props, 'error');
|
||||
const state = shallowRef(props.autoplay ? 'loading' : 'idle');
|
||||
const duration = shallowRef(0);
|
||||
const fullscreenEnabled = toRef(() => !props.noFullscreen && !String(attrs.controlsList ?? '').includes('nofullscreen'));
|
||||
function onTimeupdate() {
|
||||
const {
|
||||
currentTime,
|
||||
duration
|
||||
} = videoRef.value;
|
||||
progress.value = duration === 0 ? 0 : 100 * currentTime / duration;
|
||||
}
|
||||
async function onTriggered() {
|
||||
await nextTick();
|
||||
if (!videoRef.value) return;
|
||||
videoRef.value.addEventListener('timeupdate', onTimeupdate);
|
||||
videoRef.value.volume = volume.value / 100;
|
||||
if (state.value !== 'loaded') {
|
||||
state.value = 'loading';
|
||||
}
|
||||
}
|
||||
function onVideoLoaded() {
|
||||
state.value = 'loaded';
|
||||
duration.value = videoRef.value.duration;
|
||||
const startTime = Number(props.startAt ?? 0);
|
||||
if (startTime && startTime <= duration.value) {
|
||||
videoRef.value.currentTime = startTime;
|
||||
progress.value = duration.value === 0 ? 0 : 100 * startTime / duration.value;
|
||||
}
|
||||
if (startAfterLoad.value) {
|
||||
setTimeout(() => playing.value = true, 100);
|
||||
}
|
||||
emit('loaded', videoRef.value);
|
||||
}
|
||||
function onVideoError(e) {
|
||||
state.value = 'error';
|
||||
error.value = videoRef.value.error;
|
||||
}
|
||||
watch(error, v => {
|
||||
if (v && state.value !== 'error') {
|
||||
videoRef.value?.pause();
|
||||
state.value = 'error';
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
function retry() {
|
||||
if (state.value !== 'error') return;
|
||||
error.value = false;
|
||||
state.value = 'loading';
|
||||
triggered.value = true;
|
||||
videoRef.value?.load();
|
||||
if (!props.srcObject) {
|
||||
videoRef.value?.play();
|
||||
}
|
||||
}
|
||||
function onClick() {
|
||||
if (['loaded', 'error'].includes(state.value)) return;
|
||||
triggered.value = true;
|
||||
startAfterLoad.value = !startAfterLoad.value;
|
||||
}
|
||||
function onKeydown(e) {
|
||||
if (!videoRef.value || e.ctrlKey) return;
|
||||
if (e.key.startsWith('Arrow')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
switch (true) {
|
||||
case e.key === ' ':
|
||||
{
|
||||
if (!['A', 'BUTTON'].includes(e.target?.tagName)) {
|
||||
e.preventDefault();
|
||||
playing.value = !playing.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case e.key === 'ArrowRight':
|
||||
{
|
||||
const step = 10 * (e.shiftKey ? 6 : 1);
|
||||
videoRef.value.currentTime = Math.min(videoRef.value.currentTime + step, duration.value);
|
||||
// TODO: show skip indicator
|
||||
break;
|
||||
}
|
||||
case e.key === 'ArrowLeft':
|
||||
{
|
||||
const step = 10 * (e.shiftKey ? 6 : 1);
|
||||
videoRef.value.currentTime = Math.max(videoRef.value.currentTime - step, 0);
|
||||
// TODO: show skip indicator
|
||||
break;
|
||||
}
|
||||
case createRange(10).map(String).includes(e.key):
|
||||
{
|
||||
skipTo(Number(e.key) * 10);
|
||||
break;
|
||||
}
|
||||
case e.key === 'ArrowUp':
|
||||
{
|
||||
volume.value = Math.min(volume.value + 10, 100);
|
||||
// TODO: show volume change indicator
|
||||
break;
|
||||
}
|
||||
case e.key === 'ArrowDown':
|
||||
{
|
||||
volume.value = Math.max(volume.value - 10, 0);
|
||||
// TODO: show volume change indicator
|
||||
break;
|
||||
}
|
||||
case e.key === 'm':
|
||||
{
|
||||
controlsRef.value?.toggleMuted();
|
||||
break;
|
||||
}
|
||||
case e.key === 'f':
|
||||
{
|
||||
toggleFullscreen();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function skipTo(v) {
|
||||
if (!videoRef.value) return;
|
||||
progress.value = v;
|
||||
videoRef.value.currentTime = duration.value * v / 100;
|
||||
}
|
||||
watch(() => props.src, v => {
|
||||
progress.value = 0;
|
||||
});
|
||||
watch(() => props.srcObject, async v => {
|
||||
if (v) triggered.value = true;
|
||||
await nextTick();
|
||||
if (videoRef.value) videoRef.value.srcObject = v ?? null;
|
||||
});
|
||||
watch(videoRef, v => {
|
||||
if (v && props.srcObject) v.srcObject = props.srcObject;
|
||||
});
|
||||
watch(playing, v => {
|
||||
if (!videoRef.value) return;
|
||||
if (v) {
|
||||
videoRef.value.play();
|
||||
} else {
|
||||
videoRef.value.pause();
|
||||
}
|
||||
});
|
||||
watch(volume, v => {
|
||||
if (!videoRef.value) return;
|
||||
videoRef.value.volume = v / 100;
|
||||
});
|
||||
watch(triggered, () => onTriggered(), {
|
||||
once: true
|
||||
});
|
||||
watch(() => props.eager, v => v && (triggered.value = true), {
|
||||
immediate: true
|
||||
});
|
||||
onMounted(() => {
|
||||
if (props.autoplay && !ssr) {
|
||||
triggered.value = true;
|
||||
startAfterLoad.value = true;
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
videoRef.value?.removeEventListener('timeupdate', onTimeupdate);
|
||||
document.body.removeEventListener('keydown', fullscreenExitShortcut);
|
||||
document.removeEventListener('fullscreenchange', onFullscreenExit);
|
||||
});
|
||||
function focusSlider() {
|
||||
const container = videoRef.value?.closest('.v-video');
|
||||
const innerSlider = container?.querySelector('[role="slider"]');
|
||||
innerSlider?.focus();
|
||||
}
|
||||
function fullscreenExitShortcut(e) {
|
||||
if (['ESC', 'f'].includes(e.key)) {
|
||||
toggleFullscreen();
|
||||
document.body.removeEventListener('keydown', fullscreenExitShortcut);
|
||||
}
|
||||
}
|
||||
async function toggleFullscreen() {
|
||||
if (!fullscreenEnabled.value || !document.fullscreenEnabled) {
|
||||
return;
|
||||
}
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
onFullscreenExit();
|
||||
} else {
|
||||
await containerRef.value?.requestFullscreen();
|
||||
document.body.addEventListener('keydown', fullscreenExitShortcut);
|
||||
document.addEventListener('fullscreenchange', onFullscreenExit);
|
||||
fullscreen.value = true;
|
||||
}
|
||||
}
|
||||
function onFullscreenExit() {
|
||||
// event fires with a delay after requestFullscreen(), ignore first run
|
||||
if (document.fullscreenElement) return;
|
||||
focusSlider();
|
||||
fullscreen.value = false;
|
||||
document.body.removeEventListener('keydown', fullscreenExitShortcut);
|
||||
document.removeEventListener('fullscreenchange', onFullscreenExit);
|
||||
}
|
||||
function onVideoClick(e) {
|
||||
e.preventDefault();
|
||||
if (state.value === 'loaded') {
|
||||
playing.value = !playing.value;
|
||||
focusSlider();
|
||||
}
|
||||
}
|
||||
function onDoubleClick(e) {
|
||||
e.preventDefault();
|
||||
toggleFullscreen();
|
||||
}
|
||||
let lastTap = 0;
|
||||
function onTouchend(e) {
|
||||
const now = performance.now();
|
||||
if (now - lastTap < 500) {
|
||||
e.preventDefault();
|
||||
toggleFullscreen();
|
||||
} else {
|
||||
lastTap = now;
|
||||
}
|
||||
}
|
||||
useRender(() => {
|
||||
const showControls = state.value === 'loaded' && props.variant === 'player' && props.controlsVariant !== 'hidden';
|
||||
const posterTransition = props.variant === 'background' ? 'poster-fade-out' : 'fade-transition';
|
||||
const controlsProps = {
|
||||
...VVideoControls.filterProps(omit(props, ['variant', 'rounded', 'hideVolume'])),
|
||||
rounded: Array.isArray(props.rounded) ? props.rounded.at(-1) : props.rounded,
|
||||
fullscreen: fullscreen.value,
|
||||
hideVolume: props.hideVolume || props.muted,
|
||||
hideFullscreen: props.hideFullscreen || !fullscreenEnabled.value,
|
||||
density: props.density,
|
||||
variant: props.controlsVariant,
|
||||
playing: playing.value,
|
||||
progress: progress.value,
|
||||
duration: duration.value,
|
||||
volume: volume.value,
|
||||
...props.controlsProps
|
||||
};
|
||||
const controlsEventHandlers = {
|
||||
onSkip: v => skipTo(v),
|
||||
'onClick:fullscreen': () => toggleFullscreen(),
|
||||
'onUpdate:playing': v => playing.value = v,
|
||||
'onUpdate:progress': v => skipTo(v),
|
||||
'onUpdate:volume': v => volume.value = v,
|
||||
onClick: e => e.stopPropagation()
|
||||
};
|
||||
const controlslist = [attrs.controlslist, props.noFullscreen ? 'nofullscreen' : ''].filter(Boolean).join(' ');
|
||||
const loadingIndicator = _createVNode(VProgressCircular, {
|
||||
"indeterminate": true,
|
||||
"color": props.color,
|
||||
"width": "3",
|
||||
"size": Math.min(100, Number(props.height) / 2 || 50)
|
||||
}, null);
|
||||
const overlayPlayIcon = _createVNode(VIconBtn, {
|
||||
"icon": "$play",
|
||||
"size": "80",
|
||||
"color": "#fff",
|
||||
"variant": "outlined",
|
||||
"iconSize": "50",
|
||||
"class": _normalizeClass(['v-video__center-icon', 'v-video__center-icon--play']),
|
||||
"onClick": onVideoClick
|
||||
}, null);
|
||||
const errorIconProps = {
|
||||
icon: '$warning',
|
||||
size: '70'
|
||||
};
|
||||
const activeOverlays = {
|
||||
playIcon: props.variant === 'player' && state.value === 'loaded' && !props.hideOverlay && !playing.value,
|
||||
poster: state.value !== 'loaded' && state.value !== 'error',
|
||||
loading: props.variant === 'player' && state.value !== 'error' && (state.value === 'loading' || waiting.value),
|
||||
error: props.variant === 'player' && state.value === 'error'
|
||||
};
|
||||
return _createElementVNode("div", {
|
||||
"ref": containerRef,
|
||||
"class": _normalizeClass(['v-video', `v-video--variant-${props.variant}`, `v-video--${state.value}`, {
|
||||
'v-video--playing': playing.value
|
||||
}, themeClasses.value, densityClasses.value, roundedContainerClasses.value, props.class]),
|
||||
"style": _normalizeStyle([{
|
||||
'--v-video-aspect-ratio': props.aspectRatio
|
||||
}, props.variant === 'background' ? [] : pick(dimensionStyles.value, ['width', 'minWidth', 'maxWidth']), props.style]),
|
||||
"onKeydown": onKeydown,
|
||||
"onClick": onClick
|
||||
}, [_createElementVNode("div", {
|
||||
"class": _normalizeClass(['v-video__content', elevationClasses.value]),
|
||||
"style": _normalizeStyle([props.variant === 'background' ? [] : dimensionStyles.value])
|
||||
}, [(props.eager || triggered.value) && _createElementVNode("video", _mergeProps({
|
||||
"key": "video-element",
|
||||
"class": ['v-video__video', roundedContainerClasses.value]
|
||||
}, omit(attrs, ['controlslist', 'class', 'style']), {
|
||||
"controlslist": controlslist,
|
||||
"autoplay": props.autoplay,
|
||||
"muted": props.muted,
|
||||
"playsinline": true,
|
||||
"ref": videoRef,
|
||||
"onLoadeddata": onVideoLoaded,
|
||||
"onError": onVideoError,
|
||||
"onPlay": () => playing.value = true,
|
||||
"onPause": () => playing.value = false,
|
||||
"onWaiting": () => waiting.value = true,
|
||||
"onPlaying": () => waiting.value = false,
|
||||
"onClick": onVideoClick,
|
||||
"onDblclick": onDoubleClick,
|
||||
"onTouchend": onTouchend
|
||||
}), [slots.sources?.() ?? _createElementVNode("source", {
|
||||
"src": props.src,
|
||||
"type": props.type
|
||||
}, null)]), _createVNode(Transition, {
|
||||
"name": "fade-transition"
|
||||
}, {
|
||||
default: () => [activeOverlays.playIcon && _createElementVNode("div", {
|
||||
"class": "v-video__overlay-fill"
|
||||
}, [overlayPlayIcon])]
|
||||
}), props.variant === 'player' && !!slots.header && _createElementVNode("div", {
|
||||
"key": "header",
|
||||
"class": "v-video__header"
|
||||
}, [slots.header()]), _createVNode(MaybeTransition, {
|
||||
"transition": posterTransition
|
||||
}, {
|
||||
default: () => [activeOverlays.poster && _createElementVNode("div", {
|
||||
"class": "v-video__overlay-fill"
|
||||
}, [_createVNode(VImg, {
|
||||
"cover": true,
|
||||
"src": props.image
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", {
|
||||
"class": _normalizeClass(['v-video__overlay-fill', ...roundedContainerClasses.value])
|
||||
}, [props.variant === 'player' && overlayPlayIcon])]
|
||||
})])]
|
||||
}), activeOverlays.loading && _createElementVNode("div", {
|
||||
"key": "loading-overlay",
|
||||
"class": "v-video__overlay-fill"
|
||||
}, [loadingIndicator]), activeOverlays.error && _createElementVNode("div", {
|
||||
"key": "error-overlay",
|
||||
"class": "v-video__overlay-fill"
|
||||
}, [slots.error ? _createVNode(VDefaultsProvider, {
|
||||
"defaults": {
|
||||
VIcon: errorIconProps
|
||||
}
|
||||
}, {
|
||||
default: () => [slots.error?.({
|
||||
error: error.value
|
||||
})]
|
||||
}) : _createVNode(VIcon, errorIconProps, null)])]), _createVNode(MaybeTransition, {
|
||||
"key": "actions",
|
||||
"transition": props.controlsTransition
|
||||
}, {
|
||||
default: () => [showControls && _createVNode(VVideoControls, _mergeProps({
|
||||
"ref": controlsRef,
|
||||
"class": roundedControlsClasses.value
|
||||
}, controlsProps, controlsEventHandlers), {
|
||||
default: slots.controls,
|
||||
prepend: slots.prepend,
|
||||
append: slots.append
|
||||
})]
|
||||
})]);
|
||||
});
|
||||
return {
|
||||
video: videoRef,
|
||||
...forwardRefs({
|
||||
retry,
|
||||
skipTo,
|
||||
toggleFullscreen
|
||||
}, controlsRef)
|
||||
};
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=VVideo.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+134
@@ -0,0 +1,134 @@
|
||||
@use '../../styles/settings'
|
||||
@use '../../styles/tools'
|
||||
@use './variables' as *
|
||||
|
||||
@include tools.layer('components')
|
||||
.v-video
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
position: relative
|
||||
--v-video-aspect-ratio: #{$video-aspect-ratio}
|
||||
--v-video-controls-height: #{$video-controls-height}
|
||||
--v-video-controls-pill-border-radius: #{$video-controls-pill-border-radius}
|
||||
|
||||
@at-root
|
||||
@include tools.density('v-video', $video-density) using ($modifier)
|
||||
--v-video-controls-height: #{$video-controls-height + $modifier * 2}
|
||||
|
||||
&__video
|
||||
position: absolute
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
z-index: 0
|
||||
|
||||
&::-webkit-media-controls
|
||||
display: none !important
|
||||
|
||||
& ~ *
|
||||
z-index: 1
|
||||
|
||||
&__header
|
||||
position: relative
|
||||
opacity: 0
|
||||
transition: opacity .6s ease-in-out
|
||||
pointer-events: none
|
||||
> *
|
||||
pointer-events: auto
|
||||
|
||||
&__content
|
||||
position: relative
|
||||
border-radius: inherit
|
||||
z-index: 0
|
||||
@include tools.elevation($video-elevation)
|
||||
|
||||
&:not(.v-video--idle):not(.v-video--error)
|
||||
.v-video__content .v-video__overlay-fill
|
||||
&, > *
|
||||
pointer-events: none
|
||||
|
||||
&:not(.v-video--error)
|
||||
.v-video__content
|
||||
cursor: pointer
|
||||
|
||||
&__overlay-fill
|
||||
position: absolute
|
||||
inset: 0
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
border-radius: inherit
|
||||
|
||||
> .v-img
|
||||
position: absolute
|
||||
border-radius: inherit
|
||||
inset: 0
|
||||
|
||||
&:has(.v-video-controls:not(.v-video-controls--detached))
|
||||
.v-video__content .v-video__overlay-fill
|
||||
padding-bottom: var(--v-video-controls-height) // controls size
|
||||
|
||||
&__center-icon.v-icon-btn
|
||||
border: $video-center-icon-border
|
||||
transition-property: transform, opacity
|
||||
transition-duration: 0.28s
|
||||
transition-timing-function: settings.$standard-easing
|
||||
|
||||
&.v-video__center-icon--play
|
||||
opacity: $video-center-icon-opacity
|
||||
|
||||
&__content:hover &__center-icon--play
|
||||
transform: $video-center-icon-hover-transform
|
||||
opacity: 1
|
||||
|
||||
&--variant-player
|
||||
width: $video-player-width
|
||||
|
||||
> .v-video__content
|
||||
width: 100%
|
||||
aspect-ratio: var(--v-video-aspect-ratio)
|
||||
|
||||
&--variant-background
|
||||
outline: none
|
||||
position: absolute
|
||||
inset: 0
|
||||
pointer-events: none
|
||||
|
||||
> .v-video__content
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
// Integration with VVideoControls: pull controls up to overlap the video,
|
||||
// and hide them by default until hover / pause.
|
||||
.v-video-controls
|
||||
opacity: 0
|
||||
|
||||
&:not(.v-video-controls--detached)
|
||||
margin-top: calc(-1 * var(--v-video-controls-height))
|
||||
|
||||
&--detached
|
||||
opacity: 1
|
||||
|
||||
&--floating:not(.v-video-controls--detached)
|
||||
margin-top: calc(-1 * var(--v-video-controls-height) - #{$video-controls-floating-margin})
|
||||
margin-bottom: $video-controls-floating-margin
|
||||
|
||||
&:not(.v-video--playing),
|
||||
&:hover
|
||||
.v-video__header,
|
||||
.v-video-controls
|
||||
opacity: 1
|
||||
|
||||
&--rounded
|
||||
border-radius: $video-rounded-border-radius
|
||||
|
||||
&:fullscreen .v-video__content
|
||||
min-height: 100% !important
|
||||
min-width: 100% !important
|
||||
|
||||
.poster-fade-out
|
||||
&-leave-active
|
||||
transition: opacity 1s linear 0.3s
|
||||
&-leave-to
|
||||
opacity: 0
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
@layer vuetify-components {
|
||||
.v-video-controls {
|
||||
--v-video-controls-height: 58px;
|
||||
--v-video-controls-pill-border-radius: 99px;
|
||||
--v-video-controls-gap: 16px;
|
||||
--v-background-opacity: 0.8;
|
||||
--v-video-controls-pill-height: 42px;
|
||||
flex: 1 0 100%;
|
||||
align-self: stretch;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
gap: var(--v-video-controls-gap);
|
||||
min-height: var(--v-video-controls-height);
|
||||
backdrop-filter: blur(5px);
|
||||
background-color: rgba(var(--v-theme-surface), var(--v-background-opacity));
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
.v-video-controls--density-default {
|
||||
--v-video-controls-gap: 16px;
|
||||
--v-video-controls-height: 58px;
|
||||
}
|
||||
.v-video-controls--density-comfortable {
|
||||
--v-video-controls-gap: 12px;
|
||||
--v-video-controls-height: 50px;
|
||||
}
|
||||
.v-video-controls--density-compact {
|
||||
--v-video-controls-gap: 8px;
|
||||
--v-video-controls-height: 42px;
|
||||
}
|
||||
.v-video-controls, .v-video-controls--pills > .v-video-control__pill {
|
||||
transition: 0.6s ease-in-out;
|
||||
transition-property: opacity, background-color;
|
||||
}
|
||||
.v-video-controls--pills > .v-video-control__pill {
|
||||
box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 2px 6px 2px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
|
||||
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 4%, transparent);
|
||||
}
|
||||
.v-video-controls:hover {
|
||||
--v-background-opacity: 1;
|
||||
}
|
||||
.v-video-controls .v-video-control__pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.v-video-controls--pills {
|
||||
--v-background-opacity: 1;
|
||||
--v-video-controls-height: calc(var(--v-video-controls-pill-height) + 24px);
|
||||
backdrop-filter: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.v-video-controls--pills > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.v-video-controls--pills:not(:empty) {
|
||||
background: transparent;
|
||||
}
|
||||
.v-video-controls--pills > .v-video-control__pill {
|
||||
backdrop-filter: blur(5px);
|
||||
background: rgba(var(--v-theme-surface), var(--v-background-opacity));
|
||||
border-radius: var(--v-video-controls-pill-border-radius);
|
||||
min-height: var(--v-video-controls-pill-height);
|
||||
min-width: var(--v-video-controls-pill-height);
|
||||
padding: 0 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
.v-video-controls--pills > .v-video-control__pill > .v-icon-btn {
|
||||
border-radius: inherit;
|
||||
}
|
||||
.v-video-controls--pills > .v-video-control__pill:empty {
|
||||
display: none;
|
||||
}
|
||||
.v-video-controls--pills > .v-video-control__pill:has(> *:only-child) {
|
||||
padding-inline: 0;
|
||||
justify-content: center;
|
||||
border-radius: var(--v-video-controls-pill-border-radius);
|
||||
}
|
||||
.v-video-controls--pills > .v-video__time {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.v-video-controls--variant-tube .v-slider.v-video__track {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.v-video-controls--variant-tube .v-slider.v-video__track .v-slider-track {
|
||||
height: calc(var(--v-slider-track-size) + 10px);
|
||||
}
|
||||
.v-video-controls--variant-tube .v-slider.v-video__track .v-slider-track__fill {
|
||||
height: var(--v-slider-track-size);
|
||||
}
|
||||
.v-video-controls--split-time {
|
||||
padding-inline: 20px;
|
||||
}
|
||||
.v-video-controls:not(.v-video-controls--floating) {
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
.v-video-controls:not(.v-video-controls--floating):not(.v-video-controls--pills) {
|
||||
border-top: thin solid color-mix(in srgb, rgb(var(--v-theme-surface-variant)) 20%, transparent);
|
||||
}
|
||||
.v-video-controls--floating {
|
||||
--v-background-opacity: 1;
|
||||
border-radius: inherit;
|
||||
margin-inline: 12px;
|
||||
width: calc(100% - 2 * 12px);
|
||||
}
|
||||
.v-video-controls--detached {
|
||||
--v-background-opacity: 1;
|
||||
position: relative;
|
||||
margin-block-start: 12px;
|
||||
padding-inline: 12px;
|
||||
backdrop-filter: none;
|
||||
border-top: none;
|
||||
border-radius: inherit;
|
||||
opacity: 1;
|
||||
}
|
||||
.v-video-controls--detached:not(.v-video-controls--pills) {
|
||||
box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 2px 6px 2px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
|
||||
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 4%, transparent);
|
||||
}
|
||||
.v-video-controls--fullscreen {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
.v-video-controls--fullscreen.v-video-controls--floating {
|
||||
bottom: 12px;
|
||||
}
|
||||
.v-video__track {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.v-video__track.v-slider.v-input--horizontal > .v-input__control {
|
||||
min-height: 4px;
|
||||
}
|
||||
.v-video__track .v-slider-thumb:not(:hover) .v-slider-thumb__label {
|
||||
opacity: 0;
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
.v-video__track .v-slider-thumb__label {
|
||||
color: rgb(var(--v-theme-on-surface-variant)) !important;
|
||||
}
|
||||
.v-video__time {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
}
|
||||
+3694
File diff suppressed because it is too large
Load Diff
+259
@@ -0,0 +1,259 @@
|
||||
import { Fragment as _Fragment, createVNode as _createVNode, withDirectives as _withDirectives, normalizeClass as _normalizeClass, normalizeStyle as _normalizeStyle, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, mergeProps as _mergeProps } from "vue";
|
||||
/* eslint-disable complexity */
|
||||
|
||||
// Styles
|
||||
import "./VVideoControls.css";
|
||||
|
||||
// Components
|
||||
import { VVideoVolume } from "./VVideoVolume.js";
|
||||
import { VDefaultsProvider } from "../../components/VDefaultsProvider/VDefaultsProvider.js";
|
||||
import { VSpacer } from "../../components/VGrid/VSpacer.js";
|
||||
import { VSlider } from "../../components/VSlider/VSlider.js";
|
||||
import { VIconBtn } from "../VIconBtn/VIconBtn.js"; // Composables
|
||||
import { useBackgroundColor } from "../../composables/color.js";
|
||||
import { makeDensityProps, useDensity } from "../../composables/density.js";
|
||||
import { makeElevationProps, useElevation } from "../../composables/elevation.js";
|
||||
import { useLocale } from "../../composables/locale.js";
|
||||
import { useProxiedModel } from "../../composables/proxiedModel.js";
|
||||
import { makeThemeProps, provideTheme } from "../../composables/theme.js"; // Directives
|
||||
import vTooltip from "../../directives/tooltip/index.js"; // Utilities
|
||||
import { computed, shallowRef, toRef } from 'vue';
|
||||
import { formatTime, genericComponent, propsFactory, useRender } from "../../util/index.js"; // Types
|
||||
const allowedVariants = ['hidden', 'default', 'tube', 'mini'];
|
||||
export const makeVVideoControlsProps = propsFactory({
|
||||
color: String,
|
||||
backgroundColor: String,
|
||||
trackColor: String,
|
||||
playing: Boolean,
|
||||
hidePlay: Boolean,
|
||||
hideVolume: Boolean,
|
||||
hideFullscreen: Boolean,
|
||||
hideProgressBar: Boolean,
|
||||
fullscreen: Boolean,
|
||||
floating: Boolean,
|
||||
splitTime: Boolean,
|
||||
pills: Boolean,
|
||||
detached: Boolean,
|
||||
progress: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
volume: [Number, String],
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: v => allowedVariants.includes(v)
|
||||
},
|
||||
volumeProps: Object,
|
||||
...makeDensityProps(),
|
||||
...makeElevationProps(),
|
||||
...makeThemeProps()
|
||||
}, 'VVideoControls');
|
||||
export const VVideoControls = genericComponent()({
|
||||
name: 'VVideoControls',
|
||||
directives: {
|
||||
vTooltip: vTooltip
|
||||
},
|
||||
props: makeVVideoControlsProps(),
|
||||
emits: {
|
||||
'update:playing': val => true,
|
||||
'update:progress': val => true,
|
||||
'update:volume': val => true,
|
||||
skip: val => true,
|
||||
'click:fullscreen': () => true
|
||||
},
|
||||
setup(props, {
|
||||
emit,
|
||||
slots
|
||||
}) {
|
||||
const {
|
||||
t
|
||||
} = useLocale();
|
||||
const {
|
||||
themeClasses,
|
||||
current: currentTheme
|
||||
} = provideTheme(props);
|
||||
const {
|
||||
densityClasses
|
||||
} = useDensity(props);
|
||||
const {
|
||||
elevationClasses
|
||||
} = useElevation(props);
|
||||
const {
|
||||
backgroundColorClasses,
|
||||
backgroundColorStyles
|
||||
} = useBackgroundColor(() => {
|
||||
const fallbackBackground = props.detached ? 'surface' : undefined;
|
||||
return props.backgroundColor ?? fallbackBackground;
|
||||
});
|
||||
const trackColor = toRef(() => {
|
||||
if (props.trackColor) {
|
||||
return props.trackColor;
|
||||
}
|
||||
const fallback = currentTheme.value.dark || !props.pills ? undefined : 'surface';
|
||||
return (props.pills ? props.backgroundColor : props.color) ?? fallback;
|
||||
});
|
||||
const playing = useProxiedModel(props, 'playing');
|
||||
const progress = useProxiedModel(props, 'progress');
|
||||
const volume = useProxiedModel(props, 'volume', 0, v => Number(v ?? 0));
|
||||
const lastVolume = shallowRef();
|
||||
const currentTime = computed(() => {
|
||||
const secondsElapsed = Math.round(props.progress / 100 * props.duration);
|
||||
return {
|
||||
elapsed: formatTime(secondsElapsed),
|
||||
remaining: formatTime(props.duration - secondsElapsed),
|
||||
total: formatTime(props.duration)
|
||||
};
|
||||
});
|
||||
const labels = computed(() => {
|
||||
const playIconLocaleKey = playing.value ? 'pause' : 'play';
|
||||
const volumeIconLocaleKey = props.volumeProps?.inline ? volume.value ? 'mute' : 'unmute' : 'showVolume';
|
||||
const fullscreenIconLocaleKey = props.fullscreen ? 'exitFullscreen' : 'enterFullscreen';
|
||||
return {
|
||||
seek: t('$vuetify.video.seek'),
|
||||
volume: t('$vuetify.video.volume'),
|
||||
playAction: t(`$vuetify.video.${playIconLocaleKey}`),
|
||||
volumeAction: t(`$vuetify.video.${volumeIconLocaleKey}`),
|
||||
fullscreenAction: t(`$vuetify.video.${fullscreenIconLocaleKey}`)
|
||||
};
|
||||
});
|
||||
function play() {
|
||||
playing.value = true;
|
||||
}
|
||||
function pause() {
|
||||
playing.value = false;
|
||||
}
|
||||
function skipTo(v) {
|
||||
progress.value = v;
|
||||
}
|
||||
function toggleMuted() {
|
||||
if (volume.value) {
|
||||
lastVolume.value = volume.value;
|
||||
volume.value = 0;
|
||||
} else {
|
||||
volume.value = lastVolume.value ?? 100;
|
||||
}
|
||||
}
|
||||
function toggleFullscreen() {
|
||||
emit('click:fullscreen');
|
||||
}
|
||||
useRender(() => {
|
||||
const sizes = props.pills ? [42, 36, 30] : [32, 28, 24];
|
||||
const innerDefaults = {
|
||||
VIconBtn: {
|
||||
size: props.density === 'compact' ? sizes[2] : props.density === 'comfortable' ? sizes[1] : sizes[0],
|
||||
iconSize: props.density === 'compact' ? 20 : props.density === 'comfortable' ? 24 : 26,
|
||||
variant: 'text',
|
||||
color: props.color
|
||||
},
|
||||
VSlider: {
|
||||
thumbSize: props.variant === 'tube' ? 10 : 16,
|
||||
hideDetails: true
|
||||
}
|
||||
};
|
||||
const regularBtnSize = innerDefaults.VIconBtn.size;
|
||||
const playBtnSize = props.pills ? regularBtnSize + 8 : regularBtnSize;
|
||||
const pillClasses = ['v-video-control__pill', props.pills ? elevationClasses.value : [], props.pills ? backgroundColorClasses.value : []];
|
||||
const pillStyles = props.pills ? backgroundColorStyles.value : [];
|
||||
const slotProps = {
|
||||
play,
|
||||
pause,
|
||||
playing: playing.value,
|
||||
progress: progress.value,
|
||||
currentTime: currentTime.value,
|
||||
skipTo,
|
||||
volume,
|
||||
toggleMuted,
|
||||
fullscreen: props.fullscreen,
|
||||
toggleFullscreen,
|
||||
labels: labels.value
|
||||
};
|
||||
return _createElementVNode("div", {
|
||||
"class": _normalizeClass(['v-video-controls', `v-video-controls--variant-${props.variant}`, {
|
||||
'v-video-controls--pills': props.pills
|
||||
}, {
|
||||
'v-video-controls--detached': props.detached
|
||||
}, {
|
||||
'v-video-controls--floating': props.floating
|
||||
}, {
|
||||
'v-video-controls--fullscreen': props.fullscreen
|
||||
}, {
|
||||
'v-video-controls--split-time': props.splitTime
|
||||
}, !props.pills ? backgroundColorClasses.value : [], props.detached && !props.pills ? elevationClasses.value : [], densityClasses.value, themeClasses.value]),
|
||||
"style": _normalizeStyle([!props.pills ? backgroundColorStyles.value : [], {
|
||||
'--v-video-controls-pill-height': `${regularBtnSize}px`
|
||||
}])
|
||||
}, [_createVNode(VDefaultsProvider, {
|
||||
"defaults": innerDefaults
|
||||
}, {
|
||||
default: () => [slots.default?.(slotProps) ?? _createElementVNode(_Fragment, null, [props.variant !== 'mini' && _createElementVNode(_Fragment, null, [!props.hidePlay && _createElementVNode("div", {
|
||||
"class": _normalizeClass([pillClasses, 'v-video__action-play']),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [_withDirectives(_createVNode(VIconBtn, {
|
||||
"icon": playing.value ? '$pause' : '$play',
|
||||
"size": playBtnSize,
|
||||
"aria-label": labels.value.playAction,
|
||||
"onClick": () => playing.value = !playing.value
|
||||
}, null), [[vTooltip, labels.value.playAction, 'top']])]), slots.prepend && _createElementVNode("div", {
|
||||
"class": _normalizeClass(pillClasses),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [slots.prepend(slotProps)]), props.splitTime ? _createElementVNode("span", {
|
||||
"class": _normalizeClass([pillClasses, 'v-video__time']),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [currentTime.value.elapsed]) : props.variant !== 'default' ? _createElementVNode("span", {
|
||||
"class": _normalizeClass([pillClasses, 'v-video__time']),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [currentTime.value.elapsed, _createTextVNode(" / "), currentTime.value.total]) : '', props.hideProgressBar ? _createVNode(VSpacer, null, null) : _createVNode(VSlider, {
|
||||
"modelValue": props.progress,
|
||||
"noKeyboard": true,
|
||||
"color": trackColor.value ?? 'surface-variant',
|
||||
"trackColor": props.variant === 'tube' ? 'white' : undefined,
|
||||
"class": "v-video__track",
|
||||
"thumbLabel": "always",
|
||||
"aria-label": labels.value.seek,
|
||||
"onUpdate:modelValue": skipTo
|
||||
}, {
|
||||
'thumb-label': () => currentTime.value.elapsed
|
||||
}), props.variant === 'tube' && _createVNode(VSpacer, null, null), props.splitTime ? _createElementVNode("span", {
|
||||
"class": _normalizeClass([pillClasses, 'v-video__time']),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [currentTime.value.remaining]) : '']), props.variant === 'mini' && _createElementVNode(_Fragment, null, [_createVNode(VSpacer, null, null), slots.prepend && _createElementVNode("div", {
|
||||
"class": _normalizeClass(pillClasses),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [slots.prepend(slotProps)]), !props.hidePlay && _createElementVNode("div", {
|
||||
"class": _normalizeClass([pillClasses, 'v-video__action-play']),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [_withDirectives(_createVNode(VIconBtn, {
|
||||
"icon": playing.value ? '$pause' : '$play',
|
||||
"size": playBtnSize,
|
||||
"aria-label": labels.value.playAction,
|
||||
"onClick": () => playing.value = !playing.value
|
||||
}, null), [[vTooltip, labels.value.playAction, 'top']])])]), (!props.hideVolume || !props.hideFullscreen || slots.append) && _createElementVNode("div", {
|
||||
"class": _normalizeClass(pillClasses),
|
||||
"style": _normalizeStyle(pillStyles)
|
||||
}, [!props.hideVolume && _createVNode(VVideoVolume, _mergeProps({
|
||||
"key": "volume-control",
|
||||
"sliderProps": {
|
||||
color: props.color
|
||||
},
|
||||
"modelValue": volume.value,
|
||||
"label": labels.value.volumeAction,
|
||||
"onUpdate:modelValue": v => volume.value = v,
|
||||
"onClick": () => props.volumeProps?.inline && toggleMuted()
|
||||
}, props.volumeProps), null), slots.append?.(slotProps), !props.hideFullscreen && _withDirectives(_createVNode(VIconBtn, {
|
||||
"icon": props.fullscreen ? '$fullscreenExit' : '$fullscreen',
|
||||
"aria-label": labels.value.fullscreenAction,
|
||||
"onClick": toggleFullscreen
|
||||
}, null), [[vTooltip, labels.value.fullscreenAction, 'top']])]), props.variant === 'mini' && _createVNode(VSpacer, null, null)])]
|
||||
})]);
|
||||
});
|
||||
return {
|
||||
toggleMuted
|
||||
};
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=VVideoControls.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+151
@@ -0,0 +1,151 @@
|
||||
@use '../../styles/tools'
|
||||
@use './variables' as *
|
||||
|
||||
@include tools.layer('components')
|
||||
.v-video-controls
|
||||
--v-video-controls-height: #{$video-controls-height}
|
||||
--v-video-controls-pill-border-radius: #{$video-controls-pill-border-radius}
|
||||
--v-video-controls-gap: #{$video-controls-gap}
|
||||
--v-background-opacity: #{$video-controls-default-background-opacity}
|
||||
|
||||
// override by density logic
|
||||
--v-video-controls-pill-height: 42px
|
||||
|
||||
flex: 1 0 100%
|
||||
align-self: stretch
|
||||
padding: $video-controls-padding
|
||||
display: flex
|
||||
align-items: center
|
||||
position: relative
|
||||
gap: var(--v-video-controls-gap)
|
||||
min-height: var(--v-video-controls-height)
|
||||
backdrop-filter: $video-controls-default-backdrop-filter
|
||||
background-color: rgba($video-controls-default-background-color, var(--v-background-opacity))
|
||||
color: $video-controls-default-color
|
||||
width: 100%
|
||||
opacity: 1
|
||||
|
||||
@at-root
|
||||
@include tools.density('v-video-controls', $video-density) using ($modifier)
|
||||
--v-video-controls-gap: #{$video-controls-gap + $modifier}
|
||||
--v-video-controls-height: #{$video-controls-height + $modifier * 2}
|
||||
|
||||
&, &--pills > .v-video-control__pill
|
||||
transition: .6s ease-in-out
|
||||
transition-property: opacity, background-color
|
||||
|
||||
&--pills > .v-video-control__pill
|
||||
@include tools.elevation($video-elevation)
|
||||
|
||||
&:hover
|
||||
--v-background-opacity: 1
|
||||
|
||||
.v-video-control__pill
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: $video-controls-pill-gap
|
||||
|
||||
&--pills
|
||||
--v-background-opacity: 1
|
||||
--v-video-controls-height: calc(var(--v-video-controls-pill-height) + 24px)
|
||||
backdrop-filter: none
|
||||
pointer-events: none
|
||||
|
||||
> *
|
||||
pointer-events: auto
|
||||
|
||||
&:not(:empty)
|
||||
background: transparent
|
||||
|
||||
> .v-video-control__pill
|
||||
backdrop-filter: $video-controls-pill-backdrop-filter
|
||||
background: rgba($video-controls-default-background-color, var(--v-background-opacity))
|
||||
border-radius: var(--v-video-controls-pill-border-radius)
|
||||
min-height: var(--v-video-controls-pill-height)
|
||||
min-width: var(--v-video-controls-pill-height)
|
||||
padding: $video-controls-pill-padding
|
||||
z-index: 1
|
||||
|
||||
> .v-icon-btn
|
||||
border-radius: inherit
|
||||
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
&:has(> *:only-child)
|
||||
padding-inline: 0
|
||||
justify-content: center
|
||||
border-radius: var(--v-video-controls-pill-border-radius)
|
||||
|
||||
> .v-video__time
|
||||
padding: $video-time-pill-padding
|
||||
|
||||
&--variant-tube
|
||||
.v-slider.v-video__track
|
||||
position: absolute
|
||||
top: $video-tube-track-top
|
||||
left: 0
|
||||
right: 0
|
||||
|
||||
.v-slider-track
|
||||
height: calc(var(--v-slider-track-size) + #{$video-tube-track-hover-offset})
|
||||
|
||||
.v-slider-track__fill
|
||||
height: var(--v-slider-track-size)
|
||||
|
||||
&--split-time
|
||||
padding-inline: $video-controls-split-time-padding-inline
|
||||
|
||||
&:not(.v-video-controls--floating)
|
||||
border-bottom-left-radius: inherit
|
||||
border-bottom-right-radius: inherit
|
||||
|
||||
&:not(.v-video-controls--floating):not(.v-video-controls--pills)
|
||||
border-top: thin solid tools.theme-color('surface-variant', .2)
|
||||
|
||||
&--floating
|
||||
--v-background-opacity: 1
|
||||
border-radius: inherit
|
||||
margin-inline: $video-controls-floating-margin
|
||||
width: calc(100% - 2 * #{$video-controls-floating-margin})
|
||||
|
||||
&--detached
|
||||
--v-background-opacity: 1
|
||||
position: relative
|
||||
margin-block-start: $video-controls-detached-offset
|
||||
padding-inline: $video-controls-detached-padding-inline
|
||||
backdrop-filter: none
|
||||
border-top: none
|
||||
border-radius: inherit
|
||||
opacity: 1
|
||||
|
||||
&:not(.v-video-controls--pills)
|
||||
@include tools.elevation($video-elevation)
|
||||
|
||||
&--fullscreen
|
||||
position: absolute
|
||||
bottom: 0
|
||||
z-index: 2147483647
|
||||
|
||||
&.v-video-controls--floating
|
||||
bottom: $video-controls-floating-margin
|
||||
|
||||
.v-video__track
|
||||
position: relative
|
||||
z-index: 1
|
||||
|
||||
&.v-slider.v-input--horizontal > .v-input__control
|
||||
min-height: 4px
|
||||
|
||||
.v-slider-thumb:not(:hover)
|
||||
.v-slider-thumb__label
|
||||
opacity: 0
|
||||
transition-delay: .2s
|
||||
|
||||
.v-slider-thumb__label
|
||||
color: rgb(var(--v-theme-on-surface-variant)) !important
|
||||
|
||||
.v-video__time
|
||||
font-size: $video-time-font-size
|
||||
line-height: $video-time-line-height
|
||||
letter-spacing: $video-time-letter-spacing
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
@layer vuetify-components {
|
||||
.v-video-volume--inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.v-video-volume__menu {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.v-video-volume__menu {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
}
|
||||
.v-video-volume__menu--border {
|
||||
border-width: thin;
|
||||
box-shadow: none;
|
||||
}
|
||||
.v-video-volume__menu {
|
||||
box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 1px 3px 1px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
|
||||
--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 2%, transparent);
|
||||
}
|
||||
.v-video-volume__menu {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.v-video-volume__menu:has(.v-input--horizontal) {
|
||||
height: 40px;
|
||||
}
|
||||
.v-video-volume__menu:has(.v-input--vertical) {
|
||||
width: 40px;
|
||||
}
|
||||
.v-video-volume__menu .v-slider.v-input--horizontal {
|
||||
margin: 0 16px;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
}
|
||||
.v-video-volume__menu .v-slider.v-input--vertical {
|
||||
margin: 16px 0;
|
||||
height: 100px;
|
||||
width: 40px;
|
||||
}
|
||||
.v-video-volume__menu .v-slider.v-input--vertical > .v-input__control {
|
||||
min-height: 50px;
|
||||
}
|
||||
}
|
||||
+3237
File diff suppressed because it is too large
Load Diff
+103
@@ -0,0 +1,103 @@
|
||||
import { createVNode as _createVNode, mergeProps as _mergeProps, normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, withDirectives as _withDirectives, normalizeStyle as _normalizeStyle } from "vue";
|
||||
// Styles
|
||||
import "./VVideoVolume.css";
|
||||
|
||||
// Components
|
||||
import { VIcon } from "../../components/VIcon/VIcon.js";
|
||||
import { VMenu } from "../../components/VMenu/VMenu.js";
|
||||
import { VSlider } from "../../components/VSlider/VSlider.js";
|
||||
import { VIconBtn } from "../VIconBtn/VIconBtn.js"; // Composables
|
||||
import { makeComponentProps } from "../../composables/component.js";
|
||||
import { useLocale } from "../../composables/locale.js";
|
||||
import { useProxiedModel } from "../../composables/proxiedModel.js"; // Directives
|
||||
import vTooltip from "../../directives/tooltip/index.js"; // Utilities
|
||||
import { ref, shallowRef, toRef } from 'vue';
|
||||
import { EventProp, genericComponent, propsFactory, useRender } from "../../util/index.js"; // Types
|
||||
export const makeVVideoVolumeProps = propsFactory({
|
||||
inline: Boolean,
|
||||
label: String,
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'vertical'
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
menuProps: Object,
|
||||
sliderProps: Object,
|
||||
onClick: EventProp(),
|
||||
...makeComponentProps()
|
||||
}, 'VVideoVolume');
|
||||
export const VVideoVolume = genericComponent()({
|
||||
name: 'VVideoVolume',
|
||||
directives: {
|
||||
vTooltip: vTooltip
|
||||
},
|
||||
props: makeVVideoVolumeProps(),
|
||||
emits: {
|
||||
'update:modelValue': val => true
|
||||
},
|
||||
setup(props, {
|
||||
attrs
|
||||
}) {
|
||||
const {
|
||||
t
|
||||
} = useLocale();
|
||||
const volume = useProxiedModel(props, 'modelValue');
|
||||
const volumeIcon = toRef(() => volume.value > 70 ? '$volumeHigh' : volume.value > 40 ? '$volumeMedium' : volume.value > 10 ? '$volumeLow' : '$volumeOff');
|
||||
const containerRef = ref();
|
||||
const menu = shallowRef(false);
|
||||
useRender(() => {
|
||||
const sliderDefaults = {
|
||||
hideDetails: true,
|
||||
step: 5,
|
||||
thumbSize: 16
|
||||
};
|
||||
return _createElementVNode("div", {
|
||||
"class": _normalizeClass(['v-video-volume', {
|
||||
'v-video-volume--inline': props.inline
|
||||
}, props.class]),
|
||||
"style": _normalizeStyle(props.style),
|
||||
"ref": containerRef
|
||||
}, [_withDirectives(_createVNode(VIconBtn, _mergeProps({
|
||||
"icon": volumeIcon.value,
|
||||
"aria-label": props.label,
|
||||
"onClick": props.onClick
|
||||
}, attrs), {
|
||||
default: () => [_createVNode(VIcon, null, null), !props.inline && _createVNode(VMenu, {
|
||||
"modelValue": menu.value,
|
||||
"onUpdate:modelValue": $event => menu.value = $event,
|
||||
"activator": "parent",
|
||||
"attach": containerRef.value,
|
||||
"closeOnContentClick": false,
|
||||
"location": props.menuProps?.location ?? 'top center',
|
||||
"offset": "8"
|
||||
}, {
|
||||
default: () => [_createElementVNode("div", {
|
||||
"class": _normalizeClass(['v-video-volume__menu', `v-video-volume__menu--${props.direction}`])
|
||||
}, [_createVNode(VSlider, _mergeProps({
|
||||
"direction": props.direction,
|
||||
"aria-label": t('$vuetify.video.volume'),
|
||||
"modelValue": volume.value,
|
||||
"onUpdate:modelValue": v => volume.value = v
|
||||
}, sliderDefaults, props.sliderProps), null)])]
|
||||
})]
|
||||
}), [[vTooltip, {
|
||||
text: props.label,
|
||||
location: 'top',
|
||||
disabled: menu.value
|
||||
}]]), props.inline && _createVNode(VSlider, _mergeProps({
|
||||
"class": "v-video-volume-inline__slider",
|
||||
"minWidth": "50",
|
||||
"aria-label": t('$vuetify.video.volume'),
|
||||
"modelValue": volume.value,
|
||||
"onUpdate:modelValue": v => volume.value = v,
|
||||
"onKeydown": e => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, sliderDefaults, props.sliderProps), null)]);
|
||||
});
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=VVideoVolume.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+40
@@ -0,0 +1,40 @@
|
||||
@use '../../styles/tools'
|
||||
@use './variables' as *
|
||||
|
||||
@include tools.layer('components')
|
||||
.v-video-volume
|
||||
&--inline
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
&__menu
|
||||
background: rgb(var(--v-theme-surface))
|
||||
color: rgb(var(--v-theme-on-surface))
|
||||
overflow: hidden
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
@include tools.border($video-volume-menu-border...)
|
||||
@include tools.elevation($video-volume-menu-elevation)
|
||||
@include tools.rounded($video-volume-menu-border-radius)
|
||||
|
||||
&:has(.v-input--horizontal)
|
||||
height: $video-volume-horizontal-menu-height
|
||||
|
||||
&:has(.v-input--vertical)
|
||||
width: $video-volume-vertical-menu-width
|
||||
|
||||
.v-slider.v-input--horizontal
|
||||
margin: $video-volume-horizontal-margin
|
||||
width: $video-volume-horizontal-menu-width
|
||||
height: $video-volume-horizontal-menu-height
|
||||
|
||||
.v-slider.v-input--vertical
|
||||
margin: $video-volume-vertical-margin
|
||||
height: $video-volume-vertical-menu-height
|
||||
width: $video-volume-vertical-menu-width
|
||||
|
||||
> .v-input__control
|
||||
// just smaller than 300px default
|
||||
min-height: 50px
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
@use 'sass:math';
|
||||
@use '../../styles/settings';
|
||||
|
||||
// Defaults
|
||||
$video-player-width: 100% !default;
|
||||
$video-aspect-ratio: math.div(16, 9) !default;
|
||||
|
||||
$video-elevation: 2 !default;
|
||||
$video-padding: 0px !default;
|
||||
$video-rounded-border-radius: settings.$border-radius-root !default;
|
||||
$video-controls-floating-margin: 12px !default;
|
||||
$video-density: ('default': 0, 'comfortable': -1, 'compact': -2) !default;
|
||||
|
||||
$video-center-icon-border: 5px solid currentColor !default;
|
||||
$video-center-icon-opacity: .6 !default;
|
||||
$video-center-icon-hover-transform: scale(1.05) !default;
|
||||
|
||||
$video-tube-track-top: -8px !default;
|
||||
$video-tube-track-hover-offset: 10px !default;
|
||||
|
||||
$video-time-font-size: 0.875rem !default;
|
||||
$video-time-line-height: 1 !default;
|
||||
$video-time-letter-spacing: 0.02em !default;
|
||||
$video-time-pill-padding: 0 16px !default;
|
||||
|
||||
$video-controls-height: 58px !default;
|
||||
$video-controls-padding: 8px !default;
|
||||
$video-controls-gap: 16px !default;
|
||||
$video-controls-pill-gap: 8px !default;
|
||||
$video-controls-pill-padding: 0 6px !default;
|
||||
$video-controls-pill-border-radius: 99px !default;
|
||||
$video-controls-pill-backdrop-filter: blur(5px) !default;
|
||||
$video-controls-default-background-opacity: .8 !default;
|
||||
$video-controls-default-background-color: var(--v-theme-surface) !default;
|
||||
$video-controls-default-color: rgb(var(--v-theme-on-surface)) !default;
|
||||
$video-controls-default-backdrop-filter: blur(5px) !default;
|
||||
$video-controls-split-time-padding-inline: 20px !default;
|
||||
$video-controls-detached-offset: 12px !default;
|
||||
$video-controls-detached-padding-inline: 12px !default;
|
||||
|
||||
$video-volume-menu-elevation: 1 !default;
|
||||
$video-volume-menu-border-radius: settings.$border-radius-root !default;
|
||||
$video-volume-horizontal-margin: 0 16px !default;
|
||||
$video-volume-horizontal-menu-width: 100px !default;
|
||||
$video-volume-horizontal-menu-height: 40px !default;
|
||||
$video-volume-vertical-margin: 16px 0 !default;
|
||||
$video-volume-vertical-menu-width: 40px !default;
|
||||
$video-volume-vertical-menu-height: 100px !default;
|
||||
|
||||
$video-volume-menu-border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !default;
|
||||
$video-volume-menu-border-style: solid !default;
|
||||
$video-volume-menu-border-width: thin !default;
|
||||
$video-volume-menu-border-thin-width: thin !default;
|
||||
$video-volume-menu-border: (
|
||||
$video-volume-menu-border-color,
|
||||
$video-volume-menu-border-style,
|
||||
$video-volume-menu-border-width,
|
||||
$video-volume-menu-border-thin-width
|
||||
) !default;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export { VVideo } from './VVideo.js';
|
||||
export { VVideoControls } from './VVideoControls.js';
|
||||
export { VVideoVolume } from './VVideoVolume.js';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export { VVideo } from "./VVideo.js";
|
||||
export { VVideoControls } from "./VVideoControls.js";
|
||||
export { VVideoVolume } from "./VVideoVolume.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","names":["VVideo","VVideoControls","VVideoVolume"],"sources":["../../../src/labs/VVideo/index.ts"],"sourcesContent":["export { VVideo } from './VVideo'\nexport { VVideoControls } from './VVideoControls'\nexport { VVideoVolume } from './VVideoVolume'\n"],"mappings":"SAASA,MAAM;AAAA,SACNC,cAAc;AAAA,SACdC,YAAY","ignoreList":[]}
|
||||
Reference in New Issue
Block a user