2026年3月30日月曜日

ytp-widget

 <div id="ytp-widget-a1b2c3" class="ytp-widget-a1b2c3">

  <style>

    #ytp-widget-a1b2c3 {

      --ytp-bg: #0f1115;

      --ytp-panel: #171a21;

      --ytp-text: #f3f5f7;

      --ytp-sub: #aab4c0;

      --ytp-border: #2a3140;

      --ytp-btn: #202734;

      --ytp-btn-hover: #2a3344;

      --ytp-radius: 14px;

      --ytp-gap: 10px;

      --ytp-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;


      font-family: var(--ytp-font);

      color: var(--ytp-text);

      background: var(--ytp-bg);

      border: 1px solid var(--ytp-border);

      border-radius: var(--ytp-radius);

      padding: 14px;

      box-sizing: border-box;

      width: 100%;

      max-width: 960px;

    }


    #ytp-widget-a1b2c3 *,

    #ytp-widget-a1b2c3 *::before,

    #ytp-widget-a1b2c3 *::after {

      box-sizing: border-box;

    }


    #ytp-widget-a1b2c3 .ytp-head {

      display: flex;

      justify-content: space-between;

      align-items: center;

      gap: 12px;

      margin-bottom: 10px;

      flex-wrap: wrap;

    }


    #ytp-widget-a1b2c3 .ytp-title {

      font-size: 15px;

      font-weight: 700;

      letter-spacing: 0.02em;

      margin: 0;

    }


    #ytp-widget-a1b2c3 .ytp-badge {

      font-size: 12px;

      color: var(--ytp-sub);

      border: 1px solid var(--ytp-border);

      border-radius: 999px;

      padding: 4px 10px;

      background: rgba(255,255,255,0.02);

    }


    #ytp-widget-a1b2c3 .ytp-player-wrap {

      position: relative;

      width: 100%;

      aspect-ratio: 16 / 9;

      background: #000;

      border-radius: 12px;

      overflow: hidden;

    }


    #ytp-widget-a1b2c3 .ytp-player {

      width: 100%;

      height: 100%;

    }


    #ytp-widget-a1b2c3 .ytp-controls {

      display: flex;

      flex-wrap: wrap;

      gap: 8px;

      margin-top: 12px;

    }


    #ytp-widget-a1b2c3 .ytp-btn {

      appearance: none;

      border: 1px solid var(--ytp-border);

      background: var(--ytp-btn);

      color: var(--ytp-text);

      padding: 10px 14px;

      border-radius: 10px;

      cursor: pointer;

      font: inherit;

      line-height: 1;

      transition: background 0.2s ease, transform 0.08s ease;

    }


    #ytp-widget-a1b2c3 .ytp-btn:hover {

      background: var(--ytp-btn-hover);

    }


    #ytp-widget-a1b2c3 .ytp-btn:active {

      transform: translateY(1px);

    }


    #ytp-widget-a1b2c3 .ytp-meta {

      margin-top: 12px;

      display: grid;

      grid-template-columns: repeat(2, minmax(0, 1fr));

      gap: 8px;

    }


    #ytp-widget-a1b2c3 .ytp-card {

      background: var(--ytp-panel);

      border: 1px solid var(--ytp-border);

      border-radius: 10px;

      padding: 10px 12px;

      min-width: 0;

    }


    #ytp-widget-a1b2c3 .ytp-label {

      display: block;

      font-size: 11px;

      color: var(--ytp-sub);

      margin-bottom: 4px;

    }


    #ytp-widget-a1b2c3 .ytp-value {

      display: block;

      font-size: 14px;

      font-weight: 600;

      white-space: nowrap;

      overflow: hidden;

      text-overflow: ellipsis;

    }


    #ytp-widget-a1b2c3 .ytp-debug {

      margin-top: 10px;

      font-size: 11px;

      color: var(--ytp-sub);

      white-space: normal;

      word-break: break-word;

    }


    @media (max-width: 640px) {

      #ytp-widget-a1b2c3 .ytp-meta {

        grid-template-columns: 1fr;

      }


      #ytp-widget-a1b2c3 {

        padding: 12px;

      }

    }

  </style>


  <div class="ytp-head">

    <h2 class="ytp-title">Playlist Player</h2>

    <div class="ytp-badge">GA4 direct</div>

  </div>


  <div class="ytp-player-wrap">

    <div id="ytp-player-a1b2c3" class="ytp-player"></div>

  </div>


  <div class="ytp-controls">

    <button type="button" class="ytp-btn" data-act="play">再生</button>

    <button type="button" class="ytp-btn" data-act="pause">停止</button>

    <button type="button" class="ytp-btn" data-act="prev">前へ</button>

    <button type="button" class="ytp-btn" data-act="next">次へ</button>

  </div>


  <div class="ytp-meta">

    <div class="ytp-card">

      <span class="ytp-label">状態</span>

      <span class="ytp-value" data-role="state">未初期化</span>

    </div>

    <div class="ytp-card">

      <span class="ytp-label">動画ID</span>

      <span class="ytp-value" data-role="video-id">-</span>

    </div>

    <div class="ytp-card">

      <span class="ytp-label">Playlist index</span>

      <span class="ytp-value" data-role="playlist-index">-</span>

    </div>

    <div class="ytp-card">

      <span class="ytp-label">再生秒数</span>

      <span class="ytp-value" data-role="current-time">0</span>

    </div>

  </div>


  <div class="ytp-debug" data-role="debug"></div>


  <script>

    (function () {

      const ROOT_ID = 'ytp-widget-a1b2c3';

      const PLAYER_ID = 'ytp-player-a1b2c3';

      const root = document.getElementById(ROOT_ID);

      if (!root) return;


      const CONFIG = {

        gaMeasurementId: 'G-VYN4SZK0E6',      // 例: G-XXXXXXXXXX

        playlistId: 'PLKWIg8_Q-4Urj1UYEUudqpU7ySBROEVAK',      // 例: PLxxxxxxxxxxxxxxxx

        widgetName: 'yt_playlist_widget',

        progressIntervalSec: 10

      };


      const els = {

        state: root.querySelector('[data-role="state"]'),

        videoId: root.querySelector('[data-role="video-id"]'),

        playlistIndex: root.querySelector('[data-role="playlist-index"]'),

        currentTime: root.querySelector('[data-role="current-time"]'),

        debug: root.querySelector('[data-role="debug"]'),

        buttons: root.querySelectorAll('.ytp-btn')

      };


      let player = null;

      let progressTimer = null;

      let lastProgressBucket = -1;

      let lastVideoId = '';

      let lastState = null;


      function debug(msg, extra) {

        const payload = Object.assign({ msg: msg }, extra || {});

        els.debug.textContent = JSON.stringify(payload);

      }


      function ensureGA4(callback) {

        if (window.gtag && window.__ytpGaReadyA1B2C3) {

          callback();

          return;

        }


        if (!document.getElementById('ga4-script-a1b2c3')) {

          const s = document.createElement('script');

          s.id = 'ga4-script-a1b2c3';

          s.async = true;

          s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(CONFIG.gaMeasurementId);

          document.head.appendChild(s);

        }


        window.dataLayer = window.dataLayer || [];

        window.gtag = window.gtag || function(){ window.dataLayer.push(arguments); };


        if (!window.__ytpGaReadyA1B2C3) {

          window.gtag('js', new Date());

          window.gtag('config', CONFIG.gaMeasurementId, {

            send_page_view: true

          });

          window.__ytpGaReadyA1B2C3 = true;

        }


        callback();

      }


      function track(eventName, params) {

        ensureGA4(function () {

          window.gtag('event', eventName, Object.assign({

            widget_name: CONFIG.widgetName,

            playlist_id: CONFIG.playlistId

          }, params || {}));

        });

      }


      function stateLabel(state) {

        if (!window.YT || !window.YT.PlayerState) return '不明';

        switch (state) {

          case YT.PlayerState.UNSTARTED: return '未開始';

          case YT.PlayerState.ENDED: return '終了';

          case YT.PlayerState.PLAYING: return '再生中';

          case YT.PlayerState.PAUSED: return '停止中';

          case YT.PlayerState.BUFFERING: return '読込中';

          case YT.PlayerState.CUED: return '準備完了';

          default: return '不明';

        }

      }


      function getVideoId() {

        try {

          return player && player.getVideoData ? (player.getVideoData().video_id || '') : '';

        } catch (e) {

          return '';

        }

      }


      function getPlaylistIndex() {

        try {

          const idx = player && player.getPlaylistIndex ? player.getPlaylistIndex() : null;

          return idx === null || idx === undefined ? '' : String(idx);

        } catch (e) {

          return '';

        }

      }


      function getCurrentTime() {

        try {

          return Math.floor(player && player.getCurrentTime ? player.getCurrentTime() : 0);

        } catch (e) {

          return 0;

        }

      }


      function updateUI() {

        els.videoId.textContent = getVideoId() || '-';

        els.playlistIndex.textContent = getPlaylistIndex() || '-';

        els.currentTime.textContent = String(getCurrentTime());

      }


      function stopProgressTracking() {

        if (progressTimer) {

          clearInterval(progressTimer);

          progressTimer = null;

        }

      }


      function startProgressTracking() {

        stopProgressTracking();

        progressTimer = setInterval(function () {

          if (!player || !window.YT) return;

          if (player.getPlayerState() !== YT.PlayerState.PLAYING) return;


          const sec = getCurrentTime();

          const bucket = Math.floor(sec / CONFIG.progressIntervalSec);

          updateUI();


          if (bucket !== lastProgressBucket) {

            lastProgressBucket = bucket;

            track('yt_playlist_progress', {

              video_id: getVideoId(),

              playlist_index: getPlaylistIndex(),

              progress_seconds: sec

            });

          }

        }, 1000);

      }


      function bindButtons() {

        els.buttons.forEach(function (btn) {

          btn.addEventListener('click', function () {

            if (!player) return;

            const act = btn.getAttribute('data-act');


            if (act === 'play') player.playVideo();

            if (act === 'pause') player.pauseVideo();

            if (act === 'prev') player.previousVideo();

            if (act === 'next') player.nextVideo();


            track('yt_playlist_control_click', {

              control_name: act,

              video_id: getVideoId(),

              playlist_index: getPlaylistIndex(),

              current_time_seconds: getCurrentTime()

            });

          });

        });

      }


      function onPlayerReady() {

        updateUI();

        bindButtons();


        track('yt_playlist_ready', {

          video_id: getVideoId(),

          playlist_index: getPlaylistIndex()

        });


        debug('player_ready', {

          playlistId: CONFIG.playlistId

        });

      }


      function onPlayerStateChange(event) {

        const state = event.data;

        const videoId = getVideoId();


        els.state.textContent = stateLabel(state);

        updateUI();


        if (videoId && videoId !== lastVideoId) {

          lastVideoId = videoId;

          lastProgressBucket = -1;


          track('yt_playlist_video_change', {

            video_id: videoId,

            playlist_index: getPlaylistIndex()

          });

        }


        if (state !== lastState) {

          track('yt_playlist_state_change', {

            video_id: videoId,

            playlist_index: getPlaylistIndex(),

            player_state_code: state,

            player_state_label: stateLabel(state),

            current_time_seconds: getCurrentTime()

          });

          lastState = state;

        }


        if (window.YT && state === YT.PlayerState.PLAYING) {

          startProgressTracking();

        } else {

          stopProgressTracking();

        }


        if (window.YT && state === YT.PlayerState.ENDED) {

          track('yt_playlist_video_complete', {

            video_id: videoId,

            playlist_index: getPlaylistIndex(),

            watched_seconds: getCurrentTime()

          });

        }

      }


      function onPlayerError(event) {

        track('yt_playlist_error', {

          video_id: getVideoId(),

          playlist_index: getPlaylistIndex(),

          error_code: event.data

        });


        debug('player_error', { code: event.data });

      }


      function createPlayer() {

        if (!window.YT || !window.YT.Player) return;


        player = new YT.Player(PLAYER_ID, {

          width: '100%',

          height: '100%',

          playerVars: {

            listType: 'playlist',

            list: CONFIG.playlistId,

            autoplay: 0,

            rel: 0,

            modestbranding: 1,

            playsinline: 1

          },

          events: {

            onReady: onPlayerReady,

            onStateChange: onPlayerStateChange,

            onError: onPlayerError

          }

        });

      }


      function loadYouTubeAPI() {

        if (window.YT && window.YT.Player) {

          createPlayer();

          return;

        }


        const previous = window.onYouTubeIframeAPIReady;


        window.onYouTubeIframeAPIReady = function () {

          if (typeof previous === 'function') previous();

          createPlayer();

        };


        if (!document.getElementById('yt-iframe-api-a1b2c3')) {

          const tag = document.createElement('script');

          tag.id = 'yt-iframe-api-a1b2c3';

          tag.src = 'https://www.youtube.com/iframe_api';

          document.head.appendChild(tag);

        }

      }


      ensureGA4(function () {

        track('yt_playlist_widget_view', {

          component_id: ROOT_ID

        });

      });


      loadYouTubeAPI();

    })();

  </script>

</div>


情報にとって

美とは何か