Skip to content

Commit 8b16c13

Browse files
committed
feat(web): add responsive web design and integrated markdown document reader
1 parent a3b2f6f commit 8b16c13

4 files changed

Lines changed: 666 additions & 138 deletions

File tree

docs/index.html

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="zh-TW">
33
<head>
44
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
66
<title>OcularNode - 開源去中心化安全監控系統</title>
77
<meta name="description" content="OcularNode - 將舊 Android 手機化身為企業級安全監視器,支援去中心化 WebRTC P2P 影音直連與端對端加密 (E2EE)。">
88
<meta name="theme-color" content="#0B0F19">
@@ -16,54 +16,56 @@
1616
}
1717
</script>
1818
</head>
19-
<body style="align-items: center; justify-content: center;">
19+
<body style="align-items: center; justify-content: center; min-height: 100vh; min-height: 100dvh; padding: clamp(16px, 4vw, 40px) clamp(12px, 3vw, 24px);">
2020

21-
<div style="width: 100%; max-width: 800px; padding: 40px 20px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 24px;">
22-
<div class="brand-icon" style="width: 84px; height: 84px; border-radius: 22px; background: none; box-shadow: none;">
21+
<div style="width: 100%; max-width: 860px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: clamp(16px, 3vw, 24px); margin: auto;">
22+
<div class="brand-icon" style="width: clamp(64px, 12vw, 88px); height: clamp(64px, 12vw, 88px); border-radius: 22px; background: none; box-shadow: none;">
2323
<img src="assets/icon.svg" alt="OcularNode App Icon" style="width: 100%; height: 100%; object-fit: contain;">
2424
</div>
2525

26-
<h1 style="font-size: 2.4rem; font-weight: 800; background: linear-gradient(to right, #FFFFFF, #CBD5E1); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
26+
<h1 style="font-size: clamp(1.8rem, 5vw, 2.6rem); font-weight: 800; background: linear-gradient(to right, #FFFFFF, #CBD5E1); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.2;">
2727
OcularNode
2828
</h1>
2929

30-
<p style="font-size: 1.1rem; color: var(--text-secondary); max-width: 600px; line-height: 1.6;">
30+
<p style="font-size: clamp(0.95rem, 2.5vw, 1.12rem); color: var(--text-secondary); max-width: 620px; line-height: 1.6; padding: 0 8px;">
3131
開源、去中心化、零伺服器依賴的智慧安全監控系統。<br>
3232
將閒置 Android 舊手機轉化為軍規級端對端加密 (E2EE) 網路攝影機。
3333
</p>
3434

35-
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 8px;">
36-
<a href="viewer/" class="btn btn-primary" style="padding: 14px 28px; font-size: 1.05rem;">
35+
<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; width: 100%; max-width: 500px; margin-top: 6px;">
36+
<a href="viewer/" class="btn btn-primary" style="flex: 1 1 220px; padding: 14px 24px; font-size: 1rem; text-align: center;">
3737
<span>🌐 開啟 WebRTC 網頁監控端</span>
3838
</a>
39-
<a href="https://github.com/iokkai/OcularNode" target="_blank" rel="noopener" class="btn" style="padding: 14px 24px; font-size: 1.05rem;">
39+
<a href="https://github.com/iokkai/OcularNode" target="_blank" rel="noopener" class="btn" style="flex: 1 1 180px; padding: 14px 20px; font-size: 1rem; text-align: center;">
4040
<span>⭐ GitHub 原始碼</span>
4141
</a>
4242
</div>
4343

4444
<!-- Features Grid -->
45-
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; width: 100%; margin-top: 24px; text-align: left;">
45+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; width: 100%; margin-top: 16px; text-align: left;">
4646
<div class="panel-card">
47-
<div style="font-size: 1.5rem; margin-bottom: 6px;"></div>
47+
<div style="font-size: 1.5rem; margin-bottom: 4px;"></div>
4848
<div style="font-weight: 700; color: var(--text-primary);">WebRTC P2P 直連</div>
49-
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px;">STUN / 全球雙棧 IPv6 直通打洞,毫秒級超低延遲即時影像。</div>
49+
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px; line-height: 1.5;">STUN / 全球雙棧 IPv6 直通打洞,毫秒級超低延遲即時影像。</div>
5050
</div>
5151

5252
<div class="panel-card">
53-
<div style="font-size: 1.5rem; margin-bottom: 6px;">🔒</div>
53+
<div style="font-size: 1.5rem; margin-bottom: 4px;">🔒</div>
5454
<div style="font-weight: 700; color: var(--text-primary);">端對端加密 (E2EE)</div>
55-
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px;">AES-256-GCM 本地硬體加密,絕無雲端中繼外洩疑慮。</div>
55+
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px; line-height: 1.5;">AES-256-GCM 本地硬體加密,絕無雲端中繼外洩疑慮。</div>
5656
</div>
5757

5858
<div class="panel-card">
59-
<div style="font-size: 1.5rem; margin-bottom: 6px;">🤖</div>
59+
<div style="font-size: 1.5rem; margin-bottom: 4px;">🤖</div>
6060
<div style="font-weight: 700; color: var(--text-primary);">邊緣 AI 辨識</div>
61-
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px;">Google ML Kit 晶片端人物物件偵測,即時 Telegram / Webhook 告警。</div>
61+
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px; line-height: 1.5;">Google ML Kit 晶片端人物物件偵測,即時 Telegram / Webhook 告警。</div>
6262
</div>
6363
</div>
6464

65-
<div style="font-size: 0.82rem; color: var(--text-muted); margin-top: 32px;">
66-
© 2026 OcularNode · GPL-3.0 License · 支援 Chrome / Edge / Safari / Firefox
65+
<div style="font-size: 0.82rem; color: var(--text-muted); margin-top: 20px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;">
66+
<span>© 2026 OcularNode · GPL-3.0 License</span> ·
67+
<a href="viewer/#architecture" style="color: var(--text-secondary); text-decoration: none;">架構白皮書</a> ·
68+
<a href="viewer/#privacy" style="color: var(--text-secondary); text-decoration: none;">隱私政策</a>
6769
</div>
6870
</div>
6971

docs/viewer/app.js

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

407412
function 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+
}

docs/viewer/index.html

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="zh-TW">
33
<head>
44
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
66
<title>OcularNode - 去中心化 WebRTC 遠端即時監控</title>
77

88
<!-- Meta tags for SEO & PWA -->
@@ -18,6 +18,9 @@
1818
<script src="https://unpkg.com/mqtt@5.3.4/dist/mqtt.min.js"></script>
1919
<!-- 2. QR Code WebCam Scanner -->
2020
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
21+
<!-- 3. Markdown Parser & Mermaid Diagram Engine -->
22+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
23+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
2124
</head>
2225
<body>
2326

@@ -35,12 +38,12 @@
3538

3639
<div class="header-actions">
3740
<!-- Camera Selector Dropdown -->
38-
<select id="cameraSelect" class="form-input" style="padding: 6px 12px; font-size: 0.85rem; max-width: 180px;">
41+
<select id="cameraSelect" class="form-input" style="padding: 6px 10px; font-size: 0.85rem;" title="選擇已配對鏡頭">
3942
<option value="">-- 選擇已配對鏡頭 --</option>
4043
</select>
4144

4245
<button id="btnOpenPairModal" class="btn btn-primary" title="新增 / 配對鏡頭">
43-
<span>➕ 配對鏡頭</span>
46+
<span></span><span class="btn-text btn-text-mobile-hide"> 配對鏡頭</span>
4447
</button>
4548
</div>
4649
</header>
@@ -55,7 +58,7 @@
5558

5659
<!-- Overlay Top Badges -->
5760
<div class="video-overlay-top">
58-
<div style="display: flex; gap: 8px; align-items: center;">
61+
<div style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
5962
<span class="badge badge-live" id="liveBadge" style="display: none;">
6063
<span class="pulse-dot"></span> LIVE
6164
</span>
@@ -92,16 +95,16 @@
9295

9396
<!-- Quick Action Bar below Video -->
9497
<div class="quick-control-bar">
95-
<div style="display: flex; gap: 8px; align-items: center;">
96-
<button class="btn" id="btnSnapshot">📸 截圖保存</button>
97-
<button class="btn" id="btnReconnect">🔄 重新連線</button>
98-
<button class="btn btn-danger" id="btnDisconnect" style="display: none;">⏹ 斷開</button>
98+
<div class="quick-bar-actions">
99+
<button class="btn" id="btnSnapshot"><span>📸</span><span class="btn-text"> 截圖</span></button>
100+
<button class="btn" id="btnReconnect"><span>🔄</span><span class="btn-text"> 重連</span></button>
101+
<button class="btn btn-danger" id="btnDisconnect" style="display: none;"><span></span><span class="btn-text"> 斷開</span></button>
99102
</div>
100103

101-
<div style="display: flex; gap: 8px; align-items: center;">
102-
<span style="font-size: 0.8rem; color: var(--text-muted);">數位變焦:</span>
103-
<input type="range" id="digitalZoomSlider" min="1" max="5" step="0.1" value="1" style="width: 100px;">
104-
<span id="zoomVal" style="font-size: 0.8rem; font-family: 'JetBrains Mono', monospace; width: 35px;">1.0x</span>
104+
<div class="quick-bar-zoom">
105+
<span style="font-size: 0.8rem; color: var(--text-muted); white-space: nowrap;">變焦:</span>
106+
<input type="range" id="digitalZoomSlider" min="1" max="5" step="0.1" value="1" title="數位變焦">
107+
<span id="zoomVal" style="font-size: 0.8rem; font-family: 'JetBrains Mono', monospace; min-width: 32px;">1.0x</span>
105108
</div>
106109
</div>
107110
</section>
@@ -210,12 +213,29 @@ <h3><span>🔒</span> 隱私與端對端加密 (E2EE)</h3>
210213
<div>© 2026 OcularNode. 開源去中心化安全監控系統.</div>
211214
<div>
212215
<a href="https://github.com/iokkai/OcularNode" target="_blank" rel="noopener">GitHub 儲存庫</a> ·
213-
<a href="../ARCHITECTURE.md" target="_blank">架構白皮書</a> ·
214-
<a href="../PRIVACY.md" target="_blank">隱私政策</a>
216+
<a href="javascript:void(0)" onclick="openDocModal('architecture')">架構白皮書</a> ·
217+
<a href="javascript:void(0)" onclick="openDocModal('privacy')">隱私政策</a>
215218
</div>
216219
</div>
217220
</footer>
218221

222+
<!-- ======================================================================
223+
Modal: Markdown Document Viewer (Privacy & Architecture)
224+
====================================================================== -->
225+
<div class="modal-backdrop" id="docModal">
226+
<div class="modal-box modal-box-large">
227+
<div class="modal-title">
228+
<span id="docModalTitle">文件載入中...</span>
229+
<button class="btn btn-icon-only" id="btnCloseDocModal" onclick="closeDocModal()"></button>
230+
</div>
231+
<div class="doc-modal-body" id="docModalBody">
232+
<div class="loading-spinner">
233+
<span class="pulse-dot"></span> 正在載入文件...
234+
</div>
235+
</div>
236+
</div>
237+
</div>
238+
219239
<!-- ======================================================================
220240
Modal: Add / Pair Camera
221241
====================================================================== -->

0 commit comments

Comments
 (0)