- migliorata la grafica dell'aggiungi elemento.
This commit is contained in:
72
src/components/CMyVideoYoutube/CMyVideoYoutube.scss
Executable file
72
src/components/CMyVideoYoutube/CMyVideoYoutube.scss
Executable file
@@ -0,0 +1,72 @@
|
||||
.cmy-yt {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cmy-yt__error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 171, 0, 0.12);
|
||||
color: #7a4f01;
|
||||
}
|
||||
|
||||
.cmy-yt__frame-wrapper {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
.cmy-yt__iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cmy-yt__thumb-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cmy-yt__thumb-img {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
.cmy-yt__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: linear-gradient(0deg, rgba(0,0,0,0.35), rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
.cmy-yt__play-btn {
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #111;
|
||||
cursor: pointer;
|
||||
transform: scale(1);
|
||||
transition: transform .15s ease, box-shadow .15s ease, background .2s ease;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,.18);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cmy-yt__play-btn:hover,
|
||||
.cmy-yt__play-btn:focus-visible {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 10px 28px rgba(0,0,0,.22);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.cmy-yt__frame-wrapper,
|
||||
.cmy-yt__thumb-img {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
134
src/components/CMyVideoYoutube/CMyVideoYoutube.ts
Executable file
134
src/components/CMyVideoYoutube/CMyVideoYoutube.ts
Executable file
@@ -0,0 +1,134 @@
|
||||
import { defineComponent, computed, ref, watch } from 'vue';
|
||||
|
||||
type Nullable<T> = T | null | undefined;
|
||||
|
||||
function extractYouTubeId(url: string): string | null {
|
||||
if (!url) return null;
|
||||
|
||||
// URL normalizzati, rimuovi spazi
|
||||
const u = url.trim();
|
||||
|
||||
// youtu.be/<id>
|
||||
const short = u.match(/^https?:\/\/(?:www\.)?youtu\.be\/([A-Za-z0-9_-]{11})/i);
|
||||
if (short?.[1]) return short[1];
|
||||
|
||||
// youtube.com/watch?v=<id> (&…)
|
||||
const watchMatch = u.match(/[?&]v=([A-Za-z0-9_-]{11})/i);
|
||||
if (watchMatch?.[1]) return watchMatch[1];
|
||||
|
||||
// youtube.com/embed/<id>
|
||||
const embed = u.match(/\/embed\/([A-Za-z0-9_-]{11})/i);
|
||||
if (embed?.[1]) return embed[1];
|
||||
|
||||
// shorts
|
||||
const shorts = u.match(/\/shorts\/([A-Za-z0-9_-]{11})/i);
|
||||
if (shorts?.[1]) return shorts[1];
|
||||
|
||||
// fallback debole: sequenza di 11 char nel path o query
|
||||
const loose = u.match(/([A-Za-z0-9_-]{11})/);
|
||||
return loose?.[1] ?? null;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CMyVideoYoutube',
|
||||
props: {
|
||||
/** Link completo YouTube (watch, youtu.be, embed, shorts) */
|
||||
url: { type: String, required: true },
|
||||
|
||||
/** Rapporto d'aspetto (es. 16/9) */
|
||||
ratio: { type: Number, default: 16 / 9 },
|
||||
|
||||
/** Modalità privacy (usa youtube-nocookie.com) */
|
||||
privacyMode: { type: Boolean, default: true },
|
||||
|
||||
/** Caricamento lazy dell'iframe */
|
||||
lazy: { type: Boolean, default: true },
|
||||
|
||||
/** Mostra thumbnail e avvia iframe solo al click */
|
||||
thumbnailClickToPlay: { type: Boolean, default: true },
|
||||
|
||||
/** Titolo accessibile (fallback al titolo standard) */
|
||||
title: { type: String, default: '' },
|
||||
|
||||
// ---- Parametri player utili ----
|
||||
autoplay: { type: Boolean, default: false },
|
||||
controls: { type: Boolean, default: true },
|
||||
mute: { type: Boolean, default: false },
|
||||
loop: { type: Boolean, default: false },
|
||||
start: { type: Number, default: 0 },
|
||||
end: { type: Number, default: 0 }, // 0 = nessun end
|
||||
rel: { type: Boolean, default: false }, // video correlati (false = solo stesso canale)
|
||||
modestBranding: { type: Boolean, default: true },
|
||||
playsinline: { type: Boolean, default: true },
|
||||
/** Lingua sottotitoli ("it", "en", ...) */
|
||||
ccLang: { type: String, default: '' },
|
||||
/** Forza sottotitoli abilitati */
|
||||
ccLoad: { type: Boolean, default: false }
|
||||
},
|
||||
emits: ['started'],
|
||||
setup(props, { emit }) {
|
||||
const isPlaying = ref(false);
|
||||
|
||||
const videoId = computed(() => extractYouTubeId(props.url));
|
||||
|
||||
const computedTitle = computed(() => {
|
||||
if (props.title) return props.title;
|
||||
return 'Video YouTube';
|
||||
});
|
||||
|
||||
const baseDomain = computed(() =>
|
||||
props.privacyMode ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com'
|
||||
);
|
||||
|
||||
const thumbUrl = computed(() => {
|
||||
if (!videoId.value) return '';
|
||||
// hqdefault.jpg è un buon compromesso tra qualità e peso
|
||||
return `https://i.ytimg.com/vi/${videoId.value}/hqdefault.jpg`;
|
||||
});
|
||||
|
||||
const embedUrl = computed(() => {
|
||||
if (!videoId.value) return '';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
params.set('autoplay', (isPlaying.value || props.autoplay) ? '1' : '0');
|
||||
params.set('controls', props.controls ? '1' : '0');
|
||||
params.set('mute', props.mute ? '1' : '0');
|
||||
params.set('modestbranding', props.modestBranding ? '1' : '0');
|
||||
params.set('playsinline', props.playsinline ? '1' : '0');
|
||||
params.set('rel', props.rel ? '1' : '0');
|
||||
|
||||
if (props.start > 0) params.set('start', String(props.start));
|
||||
if (props.end > 0) params.set('end', String(props.end));
|
||||
if (props.ccLang) params.set('cc_lang_pref', props.ccLang);
|
||||
if (props.ccLoad) params.set('cc_load_policy', '1');
|
||||
|
||||
// Loop su singolo video: serve anche playlist=id
|
||||
if (props.loop) {
|
||||
params.set('loop', '1');
|
||||
params.set('playlist', videoId.value);
|
||||
}
|
||||
|
||||
const path = `/embed/${videoId.value}`;
|
||||
return `${baseDomain.value}${path}?${params.toString()}`;
|
||||
});
|
||||
|
||||
function startPlayback() {
|
||||
isPlaying.value = true;
|
||||
emit('started');
|
||||
}
|
||||
|
||||
// Se cambia URL, resetta stato play
|
||||
watch(() => props.url, () => {
|
||||
isPlaying.value = false;
|
||||
});
|
||||
|
||||
return {
|
||||
videoId,
|
||||
computedTitle,
|
||||
embedUrl,
|
||||
thumbUrl,
|
||||
isPlaying,
|
||||
startPlayback
|
||||
};
|
||||
}
|
||||
});
|
||||
54
src/components/CMyVideoYoutube/CMyVideoYoutube.vue
Executable file
54
src/components/CMyVideoYoutube/CMyVideoYoutube.vue
Executable file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="cmy-yt">
|
||||
<!-- Stato errore URL non valido -->
|
||||
<div v-if="!videoId" class="cmy-yt__error">
|
||||
<q-icon name="warning" class="q-mr-sm" />
|
||||
Link YouTube non valido.
|
||||
<div class="text-caption text-grey-7 q-mt-xs">
|
||||
Esempi accettati: https://youtu.be/ID, https://www.youtube.com/watch?v=ID
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modalità thumbnail -> click per avviare -->
|
||||
<div v-else-if="thumbnailClickToPlay && !isPlaying" class="cmy-yt__thumb-wrapper">
|
||||
<q-responsive :ratio="ratio">
|
||||
<q-img
|
||||
:src="thumbUrl"
|
||||
:alt="computedTitle"
|
||||
class="cmy-yt__thumb-img"
|
||||
spinner-color="primary"
|
||||
loading="lazy"
|
||||
>
|
||||
<div class="cmy-yt__overlay">
|
||||
<button
|
||||
class="cmy-yt__play-btn"
|
||||
type="button"
|
||||
:aria-label="`Riproduci video: ${computedTitle}`"
|
||||
@click="startPlayback"
|
||||
>
|
||||
<q-icon name="play_arrow" size="40px" />
|
||||
</button>
|
||||
</div>
|
||||
</q-img>
|
||||
</q-responsive>
|
||||
</div>
|
||||
|
||||
<!-- Iframe diretto -->
|
||||
<q-responsive v-else :ratio="ratio" class="cmy-yt__frame-wrapper">
|
||||
<iframe
|
||||
class="cmy-yt__iframe"
|
||||
:title="computedTitle"
|
||||
:src="embedUrl"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
:loading="lazy ? 'lazy' : 'eager'"
|
||||
></iframe>
|
||||
</q-responsive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./CMyVideoYoutube.ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
@import './CMyVideoYoutube.scss';
|
||||
</style>
|
||||
1
src/components/CMyVideoYoutube/index.ts
Executable file
1
src/components/CMyVideoYoutube/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export {default as CMyVideoYoutube} from './CMyVideoYoutube.vue'
|
||||
Reference in New Issue
Block a user