1/* ============================================ 2 MOVIE GALLERY 3 LINE LIFF App - Main Application 4 ============================================ */ 5 6// --- ã«ãã´ãªå®ç¾© --- 7const CATEGORIES = [ 8 { key: 'all', label: 'ãã¹ã¦' }, 9 { key: 'nakayama', label: 'ä¸å±±æå' }, 10 { key: 'taguchi', label: 'ç°å£ææµ©' }, 11 { key: 'ako', label: '赤ç©é·å¤ª' }, 12 { key: 'motoda', label: 'å ç°å¤§é½' }, 13 { key: 'wetzell', label: 'ã¤ãã¼ã»ã¦ã§ããã§ã«' }, 14]; 15 16// --- Cloud Storage ãã¼ã¹URL --- 17const GCS = 'https://storage.googleapis.com/gen-lang-client-0306469427.firebasestorage.app/movies'; 18 19// --- åç»ãã¼ã¿ --- 20const ALL_VIDEOS = [ 21 { 22 src: `${GCS}/01-nakayama.mp4`, 23 title: 'ç§ç°ã®å¿è', 24 category: 'nakayama', 25 tags: ['ä¸å±±æå', '鏿', 'ç´¹ä»'], 26 }, 27 { 28 src: `${GCS}/02-taguchi.mp4`, 29 title: 'ä½åº¦ã§ã', 30 category: 'taguchi', 31 tags: ['ç°å£ææµ©', '鏿', 'ç´¹ä»'], 32 }, 33 { 34 src: `${GCS}/03-ako.mp4`, 35 title: 'ååã®èªã', 36 category: 'ako', 37 tags: ['赤ç©é·å¤ª', '鏿', 'ç´¹ä»'], 38 }, 39 { 40 src: `${GCS}/04-motoda.mp4`, 41 title: 'ç§ç°ãç §ããå ', 42 category: 'motoda', 43 tags: ['å ç°å¤§é½', '鏿', 'ç´¹ä»'], 44 }, 45 { 46 src: `${GCS}/05-wetzell.mp4`, 47 title: 'ä¸çãç§ç°ã¸', 48 category: 'wetzell', 49 tags: ['ã¤ãã¼ã»ã¦ã§ããã§ã«', '鏿', 'ç´¹ä»'], 50 }, 51]; 52 53// --- LIFFè¨å® --- 54// LINEããã¢ããªãç§ç°ãã¼ã¶ã³ãããããã ã¼ãã¼ã®ã£ã©ãªã¼ã 55// ãããã¤ãã¼=ç§ç°ãã¼ã¶ã³ãããããæ ªå¼ä¼ç¤¾ (åç³è«ç¨ã«å使 / ãã£ãã«ID 2010426723/724/726) 56// æ¬çªç¨LIFF IDã審æ»ç¨=2010426724-SQjJAsuz / éçºç¨=2010426723-4IoTyE4N 57// â»liff.initã¯try/catchã§æ¡ãã¤ã¶ãã¦ãããIDãç°å¢ã¨ä¸ä¸è´ã§ãã¢ããªè¡¨ç¤ºã«ã¯å½±é¿ããªã 58const LIFF_ID = '2010426726-rVwJAcaH'; 59 60// --- ã°ãã¼ãã«å¤æ° --- 61let swiper = null; 62let currentSlideIndex = 0; 63let filteredVideos = [...ALL_VIDEOS]; 64let activeCategory = 'all'; 65let searchQuery = ''; 66let isFullscreenMode = false; 67let overlayTimer = null; 68let appInitialized = false; 69let searchDebounceTimer = null; 70let liffAvailable = false; 71let userInteracted = false; // ã¦ã¼ã¶ã¼ãç»é¢æä½ãããï¼ãã¥ã¼ãè§£é¤ç¨ï¼ 72 73// ============================================ 74// åæå 75// ============================================ 76 77document.addEventListener('DOMContentLoaded', initApp); 78 79async function initApp() { 80 await initLIFF(); 81 buildFilterChips(); 82 setupSearch(); 83 applyFilter(); 84 setupFullscreenHandler(); 85 setupFsControls(); 86 setupUserInteraction(); 87 setupFooterLinks(); 88 appInitialized = true; 89} 90 91// ããã¿ã¼ã®ãã©ããªã»å©ç¨è¦ç´ãªã³ã¯ï¼å¤é¨ãªãã¤ã¬ã¯ããé¿ããLINEå ãã©ã¦ã¶/å¥ã¿ãã§éãï¼ 92function setupFooterLinks() { 93 const urls = { 94 privacy: 'https://northern-happinets.com/privacy/', 95 terms: 'https://northern-happinets.com/rule/', 96 }; 97 document.querySelectorAll('.footer-link').forEach((btn) => { 98 btn.addEventListener('click', () => { 99 const url = urls[btn.dataset.doc]; 100 if (!url) return; 101 // LIFFç°å¢ã§ã¯ liff.openWindow(external:false) ã§LINEå ãã©ã¦ã¶è¡¨ç¤ºï¼ããã¢ããªããé¢è±ããªãï¼ 102 if (liffAvailable && typeof liff !== 'undefined' && liff.openWindow) { 103 try { liff.openWindow({ url, external: false }); return; } catch (e) {} 104 } 105 // é常ãã©ã¦ã¶ç¨ãã©ã¼ã«ãã㯠106 window.open(url, '_blank', 'noopener'); 107 }); 108 }); 109} 110 111async function initLIFF() { 112 if (!LIFF_ID || window.__liffLoadFailed || typeof liff === 'undefined') return; 113 try { await liff.init({ liffId: LIFF_ID }); liffAvailable = true; } catch (e) { console.warn('LIFF:', e); } 114} 115 116// ã¦ã¼ã¶ã¼ååæä½ã§ãã¥ã¼ãè§£é¤ 117function setupUserInteraction() { 118 const unlock = () => { 119 if (userInteracted) return; 120 userInteracted = true; 121 const video = document.getElementById(`video-${currentSlideIndex}`); 122 if (video) { 123 video.muted = false;
124 } 125 }; 126 document.addEventListener('touchstart', unlock, { once: true }); 127 document.addEventListener('click', unlock, { once: true }); 128} 129 130// ============================================ 131// ãã£ã«ã¿ã¼ & æ¤ç´¢ 132// ============================================ 133 134function buildFilterChips() { 135 const scroll = document.querySelector('.filter-scroll'); 136 scroll.innerHTML = CATEGORIES.map(cat => 137 `<button class="filter-chip ${cat.key === 'all' ? 'active' : ''}" data-category="${cat.key}">${cat.label}</button>` 138 ).join(''); 139 scroll.addEventListener('click', (e) => { 140 const chip = e.target.closest('.filter-chip'); 141 if (chip) setActiveCategory(chip.dataset.category); 142 }); 143} 144 145function setActiveCategory(category) { 146 activeCategory = category; 147 document.querySelectorAll('.filter-chip').forEach(chip => 148 chip.classList.toggle('active', chip.dataset.category === category) 149 ); 150 applyFilter(); 151} 152 153function setupSearch() { 154 const input = document.getElementById('searchInput'); 155 const clearBtn = document.getElementById('searchClear'); 156 const resetBtn = document.getElementById('resetFilterBtn'); 157 158 input.addEventListener('focus', () => { 159 document.getElementById('mainContent').style.overflow = 'hidden'; 160 setTimeout(() => window.scrollTo(0, 0), 50); 161 }); 162 input.addEventListener('blur', () => { 163 document.getElementById('mainContent').style.overflow = ''; 164 }); 165 input.addEventListener('touchstart', (e) => { e.stopPropagation(); }); 166 167 input.addEventListener('input', () => { 168 const val = input.value.trim(); 169 clearBtn.style.display = val ? 'flex' : 'none'; 170 clearTimeout(searchDebounceTimer); 171 searchDebounceTimer = setTimeout(() => { searchQuery = val; applyFilter(); }, 250); 172 }); 173 174 clearBtn.addEventListener('click', () => { 175 input.value = ''; clearBtn.style.display = 'none'; searchQuery = ''; applyFilter(); 176 }); 177 178 resetBtn.addEventListener('click', () => { 179 input.value = ''; clearBtn.style.display = 'none'; searchQuery = ''; setActiveCategory('all'); 180 }); 181} 182 183function applyFilter() { 184 // ç¾å¨ã®åç»ããã¹ã¦åæ¢ 185 filteredVideos.forEach((_, i) => pausePlayer(i)); 186 187 filteredVideos = ALL_VIDEOS.filter(video => { 188 const matchCat = activeCategory === 'all' || video.category === activeCategory; 189 if (!matchCat) return false; 190 if (!searchQuery) return true; 191 const q = searchQuery.toLowerCase(); 192 return video.title.toLowerCase().includes(q) || 193 video.tags.some(tag => tag.toLowerCase().includes(q)); 194 }); 195 currentSlideIndex = 0; 196 197 const hasResults = filteredVideos.length > 0; 198 document.getElementById('playerSection').classList.toggle('hidden', !hasResults); 199 document.getElementById('playerBar').classList.toggle('hidden', !hasResults); 200 document.getElementById('videoInfo').classList.toggle('hidden', !hasResults); 201 document.getElementById('videoGrid').style.display = hasResults ? '' : 'none'; 202 document.getElementById('noResults').style.display = hasResults ? 'none' : 'flex'; 203 204 const catLabel = CATEGORIES.find(c => c.key === activeCategory)?.label || 'ãã¹ã¦'; 205 document.getElementById('listHeading').textContent = activeCategory === 'all' ? 'ALL MOVIES' : catLabel; 206 document.getElementById('listCount').textContent = `${filteredVideos.length}æ¬`; 207 208 if (hasResults) { 209 buildSwiperSlides(); 210 buildVideoGrid(); 211 initSwiper(); 212 updateVideoInfo(0); 213 updateDots(0); 214 // æåã®åç»ãèªååç 215 playVideo(0); 216 } 217} 218 219// ============================================ 220// ãã¬ã¼ã¤ã¼ & ã¹ã¯ã¤ãã¼ 221// ============================================ 222 223function buildSwiperSlides() { 224 const wrapper = document.getElementById('swiperWrapper'); 225 wrapper.innerHTML = filteredVideos.map((video, i) => ` 226 <div class="swiper-slide" data-index="${i}"> 227 <div class="video-frame-wrapper" id="playerWrapper-${i}"> 228 <video 229 class="slide-video" 230 id="video-${i}" 231 preload="${i <= 1 ? 'auto' : 'metadata'}" 232 playsinline 233 webkit-playsinline 234 muted 235 loop 236 src="${video.src}" 237 ></video> 238 <div class="touch-overlay" data-index="${i}"> 239 <div class="tap-to-play" id="tapIcon-${i}"></div> 240 </div> 241 </div> 242 </div> 243 `).join(''); 244 245 // ã¿ãããªã¼ãã¼ã¬ã¤è¨å® 246 document.querySelectorAll('.touch-overlay').forEach(setupTouchOverlay); 247} 248 249function setupTouchOverlay(overlay) { 250 let sx = 0, sy = 0, st = 0; 251 let handledByTouch = false;
252 253 overlay.addEventListener('touchstart', (e) => { 254 sx = e.touches[0].clientX; 255 sy = e.touches[0].clientY; 256 st = Date.now(); 257 }, { passive: true }); 258 259 overlay.addEventListener('touchend', (e) => { 260 const t = e.changedTouches[0]; 261 if (Math.abs(t.clientX - sx) < 15 && Math.abs(t.clientY - sy) < 15 && Date.now() - st < 300) { 262 handledByTouch = true; 263 setTimeout(() => { handledByTouch = false; }, 500); 264 const idx = parseInt(overlay.dataset.index); 265 if (isFullscreenMode) { toggleFsOverlay(); } else { handleTap(idx); } 266 } 267 }, { passive: true }); 268 269 // ãã¹ã¯ãããç¨ clickï¼touchend ã§å¦çæ¸ã¿ãªãã¹ãããï¼ 270 overlay.addEventListener('click', () => { 271 if (handledByTouch) return; 272 const idx = parseInt(overlay.dataset.index); 273 if (isFullscreenMode) { toggleFsOverlay(); } else { handleTap(idx); } 274 }); 275} 276 277// ã¿ããå¦ç: ååã¯ãã¥ã¼ãè§£é¤ã以éã¯åç/䏿忢ãã°ã« 278function handleTap(index) { 279 const video = document.getElementById(`video-${index}`); 280 if (!video) return; 281 282 // ãã¥ã¼ãä¸ãªãè§£é¤ãã¦åçç¶ç¶ 283 if (video.muted) { 284 video.muted = false; 285 userInteracted = true; 286 // 忢ä¸ãªãåçéå§ 287 if (video.paused) { 288 video.play().catch(() => {}); 289 } 290 return; 291 } 292 293 // åç/䏿忢ãã°ã« 294 togglePlayPause(index); 295} 296 297function togglePlayPause(index) { 298 const video = document.getElementById(`video-${index}`); 299 if (!video) return; 300 const icon = document.getElementById(`tapIcon-${index}`); 301 302 if (video.paused) { 303 video.play().catch(() => {}); 304 if (icon) { icon.classList.remove('paused'); icon.classList.add('show'); setTimeout(() => icon.classList.remove('show'), 800); } 305 } else { 306 video.pause(); 307 if (icon) { icon.classList.add('paused', 'show'); setTimeout(() => icon.classList.remove('show'), 800); } 308 } 309} 310 311// åç»ãèªååçï¼ãã¥ã¼ããªããã¥ã¼ãã®ã¾ã¾ï¼ 312function playVideo(index) { 313 const video = document.getElementById(`video-${index}`); 314 if (!video) return; 315 316 // ã¦ã¼ã¶ã¼æä½æ¸ã¿ãªããã¥ã¼ãè§£é¤ 317 video.muted = !userInteracted; 318 video.currentTime = 0; 319 video.play().catch(() => { 320 // ãããã¯ãããããã¥ã¼ãã§åè©¦è¡ 321 video.muted = true; 322 video.play().catch(() => {}); 323 }); 324 325 // 飿¥åç»ãããªãã¼ã 326 preloadAdjacent(index); 327} 328 329function preloadAdjacent(index) {
330 [index - 1, index + 1].forEach(i => { 331 if (i >= 0 && i < filteredVideos.length) { 332 const v = document.getElementById(`video-${i}`); 333 if (v && v.preload !== 'auto') v.preload = 'auto'; 334 } 335 }); 336} 337 338function buildVideoGrid() { 339 const grid = document.getElementById('videoGrid'); 340 grid.innerHTML = filteredVideos.map((video, i) => { 341 const catLabel = CATEGORIES.find(c => c.key === video.category)?.label || ''; 342 return ` 343 <div class="video-card ${i === 0 ? 'active' : ''}" data-index="${i}" onclick="goToVideo(${i})"> 344 <div class="card-thumbnail"> 345 <video preload="metadata" muted playsinline src="${video.src}#t=0.5"></video> 346 <span class="card-category-badge">${catLabel}</span> 347 ${i === 0 ? '<span class="card-now-playing">åçä¸</span>' : ''} 348 </div> 349 <div class="card-info"> 350 <div class="card-title">${video.title}</div> 351 </div> 352 </div>`; 353 }).join(''); 354} 355 356function initSwiper() { 357 if (swiper) { swiper.destroy(true, true); swiper = null; } 358 swiper = new Swiper('#playerSwiper', { 359 slidesPerView: 1, 360 spaceBetween: 0, 361 resistance: true, 362 resistanceRatio: 0.3, 363 speed: 300, 364 threshold: 10, 365 touchEventsTarget: 'container', 366 on: { slideChange: function () { onSlideChange(this.activeIndex); } }, 367 }); 368} 369 370function onSlideChange(newIndex) { 371 pausePlayer(currentSlideIndex); 372 currentSlideIndex = newIndex; 373 updateVideoInfo(newIndex); 374 updateDots(newIndex); 375 updateVideoGrid(newIndex); 376 updateFsUI(newIndex); 377 378 // æ°ããåç»ãèªååç 379 playVideo(newIndex); 380} 381 382function goToVideo(index) { 383 if (swiper) swiper.slideTo(index); 384} 385 386function pausePlayer(index) { 387 const video = document.getElementById(`video-${index}`); 388 if (video) { try { video.pause(); } catch (e) {} } 389} 390 391// ============================================ 392// UIæ´æ° 393// ============================================ 394 395function updateVideoInfo(index) { 396 if (index >= filteredVideos.length) return; 397 const video = filteredVideos[index]; 398 const catLabel = CATEGORIES.find(c => c.key === video.category)?.label || ''; 399 document.getElementById('videoCategory').textContent = catLabel; 400 document.getElementById('videoTitle').textContent = video.title; 401} 402 403function updateDots(index) { 404 document.getElementById('swipeDots').innerHTML = filteredVideos.map((_, i) => 405 `<div class="swipe-dot ${i === index ? 'active' : ''}"></div>` 406 ).join(''); 407} 408 409function updateVideoGrid(index) { 410 document.querySelectorAll('.video-card').forEach((card, i) => { 411 card.classList.toggle('active', i === index); 412 const badge = card.querySelector('.card-now-playing'); 413 if (i === index && !badge) { 414 const span = document.createElement('span'); 415 span.className = 'card-now-playing'; 416 span.textContent = 'åçä¸'; 417 card.querySelector('.card-thumbnail').appendChild(span); 418 } else if (i !== index && badge) { badge.remove(); } 419 }); 420} 421 422// ============================================ 423// ãã«ã¹ã¯ãªã¼ã³ï¼ç¸¦ç»é¢å¯¾å¿ï¼ 424// ============================================ 425 426function setupFullscreenHandler() { 427 const fsBtn = document.getElementById('fullscreenBtn'); 428 fsBtn.addEventListener('click', (e) => { 429 e.preventDefault(); 430 e.stopPropagation(); 431 enterFullscreenMode(); 432 }); 433 fsBtn.addEventListener('touchend', (e) => { 434 e.preventDefault(); 435 e.stopPropagation(); 436 enterFullscreenMode(); 437 }); 438 439 document.addEventListener('fullscreenchange', onFullscreenChange); 440 document.addEventListener('webkitfullscreenchange', onFullscreenChange); 441} 442 443function onFullscreenChange() { 444 const fsEl = document.fullscreenElement || document.webkitFullscreenElement; 445 if (!fsEl && isFullscreenMode) { 446 isFullscreenMode = false; 447 document.body.classList.remove('fs-active'); 448 document.getElementById('fsOverlay').classList.remove('visible'); 449 clearTimeout(overlayTimer); 450 setTimeout(() => { if (swiper) swiper.update(); }, 100); 451 } 452} 453 454function enterFullscreenMode() { 455 isFullscreenMode = true; 456 document.body.classList.add('fs-active'); 457 updateFsUI(currentSlideIndex); 458 459 const container = document.getElementById('fullscreenContainer'); 460 requestFS(container); 461 462 document.getElementById('fsOverlay').classList.add('visible'); 463 resetOverlayTimer(); 464 465 setTimeout(() =>
465 { if (swiper) swiper.update(); }, 150); 466 setTimeout(() => { if (swiper) swiper.update(); }, 500); 467} 468 469function exitFullscreenMode() { 470 isFullscreenMode = false; 471 document.body.classList.remove('fs-active'); 472 document.getElementById('fsOverlay').classList.remove('visible'); 473 clearTimeout(overlayTimer); 474 exitFS(); 475 setTimeout(() => { if (swiper) swiper.update(); }, 100); 476} 477 478function requestFS(el) { 479 try { 480 if (el.requestFullscreen) return el.requestFullscreen().catch(() => {}); 481 else if (el.webkitRequestFullscreen) { el.webkitRequestFullscreen(); return Promise.resolve(); } 482 } catch (e) {} 483 return Promise.resolve(); 484} 485 486function exitFS() { 487 try { 488 const fsEl = document.fullscreenElement || document.webkitFullscreenElement; 489 if (fsEl) { 490 if (document.exitFullscreen) document.exitFullscreen().catch(() => {}); 491 else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); 492 } 493 } catch (e) {} 494} 495 496// ============================================ 497// ãã«ã¹ã¯ãªã¼ã³ UI ã³ã³ããã¼ã« 498// ============================================ 499 500function updateFsUI(index) { 501 if (index >= filteredVideos.length) return; 502 const video = filteredVideos[index]; 503 document.getElementById('fsTitle').textContent = video.title; 504 document.getElementById('fsCounter').textContent = `${index + 1} / ${filteredVideos.length}`; 505 document.getElementById('fsPrevBtn').style.visibility = index > 0 ? 'visible' : 'hidden'; 506 document.getElementById('fsNextBtn').style.visibility = index < filteredVideos.length - 1 ? 'visible' : 'hidden'; 507} 508 509function setupFsControls() { 510 document.getElementById('fsBackBtn').addEventListener('click', (e) => { 511 e.stopPropagation(); 512 exitFullscreenMode(); 513 }); 514 515 document.getElementById('fsPrevBtn').addEventListener('click', (e) => { 516 e.stopPropagation(); 517 if (currentSlideIndex > 0) goToVideo(currentSlideIndex - 1); 518 resetOverlayTimer(); 519 }); 520 521 document.getElementById('fsNextBtn').addEventListener('click', (e) => { 522 e.stopPropagation(); 523 if (currentSlideIndex < filteredVideos.length - 1) goToVideo(currentSlideIndex + 1); 524 resetOverlayTimer(); 525 }); 526} 527 528function toggleFsOverlay() { 529 const overlay = document.getElementById('fsOverlay'); 530 if (overlay.classList.contains('visible')) { 531 overlay.classList.remove('visible'); 532 clearTimeout(overlayTimer); 533 } else { 534 overlay.classList.add('visible'); 535 resetOverlayTimer(); 536 } 537} 538 539function resetOverlayTimer() { 540 clearTimeout(overlayTimer); 541 overlayTimer = setTimeout(() => { 542 document.getElementById('fsOverlay').classList.remove('visible'); 543 }, 4000); 544}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.