@@ -75,6 +75,11 @@ function checkUrlHashForCamera() {
7575 return ;
7676 }
7777
78+ if ( hash === 'privacy' || hash === 'architecture' ) {
79+ openDocModal ( hash ) ;
80+ return ;
81+ }
82+
7883 const params = new URLSearchParams ( hash ) ;
7984 const id = params . get ( 'id' ) ;
8085 const secret = params . get ( 'secret' ) ;
@@ -406,20 +411,31 @@ function takeSnapshot() {
406411
407412function setupAudioVisualizer ( ) {
408413 const canvas = document . getElementById ( 'audioVisualizer' ) ;
414+ if ( ! canvas ) return ;
409415 const ctx = canvas . getContext ( '2d' ) ;
410416
417+ function resizeCanvas ( ) {
418+ if ( canvas . clientWidth > 0 ) {
419+ canvas . width = canvas . clientWidth * ( window . devicePixelRatio || 1 ) ;
420+ canvas . height = canvas . clientHeight * ( window . devicePixelRatio || 1 ) ;
421+ }
422+ drawIdle ( ) ;
423+ }
424+
411425 function drawIdle ( ) {
412426 ctx . fillStyle = '#1E293B' ;
413427 ctx . fillRect ( 0 , 0 , canvas . width , canvas . height ) ;
414428
415429 ctx . strokeStyle = '#475569' ;
416- ctx . lineWidth = 2 ;
430+ ctx . lineWidth = 2 * ( window . devicePixelRatio || 1 ) ;
417431 ctx . beginPath ( ) ;
418432 ctx . moveTo ( 0 , canvas . height / 2 ) ;
419433 ctx . lineTo ( canvas . width , canvas . height / 2 ) ;
420434 ctx . stroke ( ) ;
421435 }
422- drawIdle ( ) ;
436+
437+ window . addEventListener ( 'resize' , resizeCanvas ) ;
438+ setTimeout ( resizeCanvas , 50 ) ;
423439}
424440
425441// =============================================================================
@@ -530,3 +546,104 @@ function stopQrScanner() {
530546 } catch ( _ ) { }
531547 }
532548}
549+
550+ // =============================================================================
551+ // Document Reader Modal (Dynamic Markdown Parser)
552+ // =============================================================================
553+
554+ async function openDocModal ( docType ) {
555+ const modal = document . getElementById ( 'docModal' ) ;
556+ const titleElem = document . getElementById ( 'docModalTitle' ) ;
557+ const bodyElem = document . getElementById ( 'docModalBody' ) ;
558+ if ( ! modal || ! titleElem || ! bodyElem ) return ;
559+
560+ modal . classList . add ( 'active' ) ;
561+ bodyElem . scrollTop = 0 ;
562+
563+ let fileName = '' ;
564+ let title = '' ;
565+ if ( docType === 'privacy' ) {
566+ fileName = 'PRIVACY.md' ;
567+ title = '🔒 OcularNode 隱私權政策與使用條款' ;
568+ } else if ( docType === 'architecture' ) {
569+ fileName = 'ARCHITECTURE.md' ;
570+ title = '📐 OcularNode 系統架構規格書' ;
571+ } else {
572+ fileName = docType ;
573+ title = docType ;
574+ }
575+
576+ titleElem . innerText = title ;
577+ bodyElem . innerHTML = `
578+ <div class="loading-spinner">
579+ <span class="pulse-dot"></span> 正在載入 ${ fileName } ...
580+ </div>
581+ ` ;
582+
583+ const localPath = `../../${ fileName } ` ;
584+ const parentPath = `../${ fileName } ` ;
585+ const rawGithubUrl = `https://raw.githubusercontent.com/iokkai/OcularNode/main/${ fileName } ` ;
586+
587+ let mdText = '' ;
588+ try {
589+ const res = await fetch ( parentPath ) ;
590+ if ( res . ok ) {
591+ mdText = await res . text ( ) ;
592+ } else {
593+ throw new Error ( 'Parent fetch not ok' ) ;
594+ }
595+ } catch ( e ) {
596+ try {
597+ const res2 = await fetch ( localPath ) ;
598+ if ( res2 . ok ) {
599+ mdText = await res2 . text ( ) ;
600+ } else {
601+ throw new Error ( 'Local fetch not ok' ) ;
602+ }
603+ } catch ( e2 ) {
604+ try {
605+ const githubRes = await fetch ( rawGithubUrl ) ;
606+ if ( githubRes . ok ) {
607+ mdText = await githubRes . text ( ) ;
608+ } else {
609+ throw new Error ( 'GitHub fetch failed' ) ;
610+ }
611+ } catch ( err ) {
612+ bodyElem . innerHTML = `
613+ <div style="text-align: center; padding: 24px; color: var(--accent-red);">
614+ <h3>無法載入 ${ fileName } </h3>
615+ <p style="margin-top: 8px;">請直接至 GitHub 查看:<a href="${ rawGithubUrl } " target="_blank" style="color: var(--primary);">${ fileName } </a></p>
616+ </div>
617+ ` ;
618+ return ;
619+ }
620+ }
621+ }
622+
623+ if ( window . marked ) {
624+ const renderer = new marked . Renderer ( ) ;
625+ const originalCodeRenderer = renderer . code . bind ( renderer ) ;
626+ renderer . code = function ( { text, lang } ) {
627+ if ( lang === 'mermaid' ) {
628+ return `<div class="mermaid">${ text } </div>` ;
629+ }
630+ return originalCodeRenderer ( { text, lang } ) ;
631+ } ;
632+ bodyElem . innerHTML = marked . parse ( mdText , { renderer } ) ;
633+
634+ if ( window . mermaid && docType === 'architecture' ) {
635+ try {
636+ await mermaid . run ( { nodes : bodyElem . querySelectorAll ( '.mermaid' ) } ) ;
637+ } catch ( mErr ) {
638+ console . warn ( 'Mermaid render warning:' , mErr ) ;
639+ }
640+ }
641+ } else {
642+ bodyElem . innerText = mdText ;
643+ }
644+ }
645+
646+ function closeDocModal ( ) {
647+ const modal = document . getElementById ( 'docModal' ) ;
648+ if ( modal ) modal . classList . remove ( 'active' ) ;
649+ }
0 commit comments