- migliorata la grafica dell'aggiungi elemento.

This commit is contained in:
Surya Paolo
2025-09-08 20:42:36 +02:00
parent ac84755dbb
commit cb3baf3dbb
13 changed files with 871 additions and 470 deletions

View 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;
}
}

View 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
};
}
});

View 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>

View File

@@ -0,0 +1 @@
export {default as CMyVideoYoutube} from './CMyVideoYoutube.vue'