reviw 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +4 -4
- package/README.md +4 -4
- package/cli.cjs +539 -16
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -177,7 +177,7 @@ plugin/
|
|
|
177
177
|
|
|
178
178
|
| 種類 | 名前 | 説明 |
|
|
179
179
|
|------|------|------|
|
|
180
|
-
| **コマンド** | `/reviw:do` | タスク開始 -
|
|
180
|
+
| **コマンド** | `/reviw:do` | タスク開始 - git wtでworktree作成、計画、todo登録 |
|
|
181
181
|
| **コマンド** | `/reviw:done` | 完了チェックリスト - 7レビューエージェント実行、エビデンス収集、レビュー開始 |
|
|
182
182
|
| **エージェント** | `report-builder` | ユーザーレビュー用レポート準備 |
|
|
183
183
|
| **エージェント** | `review-code-quality` | コード品質: 可読性、DRY、型安全性、エラーハンドリング |
|
|
@@ -201,14 +201,14 @@ plugin/
|
|
|
201
201
|
適切な環境セットアップで新しいタスクを開始します。
|
|
202
202
|
|
|
203
203
|
**処理内容:**
|
|
204
|
-
1.
|
|
204
|
+
1. git wtを使用して分離開発用のgit worktreeを作成(`feature/<name>`、`fix/<name>`など)
|
|
205
205
|
2. エビデンス用の`.artifacts/<feature>/`ディレクトリをセットアップ
|
|
206
206
|
3. 計画とTODOチェックリスト付きの`REPORT.md`を作成
|
|
207
207
|
4. 進捗追跡用にTodoWriteにtodoを登録
|
|
208
208
|
|
|
209
209
|
**作成されるディレクトリ構成:**
|
|
210
210
|
```
|
|
211
|
-
<worktree>/ # 例:
|
|
211
|
+
<worktree>/ # 例: .worktree/feature-auth/
|
|
212
212
|
└── .artifacts/
|
|
213
213
|
└── <feature>/ # 例: auth(feature/authから)
|
|
214
214
|
├── REPORT.md # 計画、進捗、エビデンスリンク
|
|
@@ -216,7 +216,7 @@ plugin/
|
|
|
216
216
|
└── videos/ # 動画録画
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
**タスク再開:** セッション開始時またはコンテキスト圧縮後、コマンドは既存のworktreeを確認(`
|
|
219
|
+
**タスク再開:** セッション開始時またはコンテキスト圧縮後、コマンドは既存のworktreeを確認(`git wt`)し、`REPORT.md`から再開します。
|
|
220
220
|
|
|
221
221
|
#### `/reviw:done`
|
|
222
222
|
|
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ plugin/
|
|
|
180
180
|
|
|
181
181
|
| Type | Name | Description |
|
|
182
182
|
|------|------|-------------|
|
|
183
|
-
| **Command** | `/reviw:do` | Start a task - create worktree with
|
|
183
|
+
| **Command** | `/reviw:do` | Start a task - create worktree with git wt, plan, register todos |
|
|
184
184
|
| **Command** | `/reviw:done` | Complete checklist - run 7 review agents, collect evidence, start review |
|
|
185
185
|
| **Agent** | `report-builder` | Prepare reports and evidence for user review |
|
|
186
186
|
| **Agent** | `review-code-quality` | Code quality: readability, DRY, type safety, error handling |
|
|
@@ -204,14 +204,14 @@ plugin/
|
|
|
204
204
|
Starts a new task with proper environment setup.
|
|
205
205
|
|
|
206
206
|
**What it does:**
|
|
207
|
-
1. Creates a git worktree using
|
|
207
|
+
1. Creates a git worktree using git wt for isolated development (`feature/<name>`, `fix/<name>`, etc.)
|
|
208
208
|
2. Sets up `.artifacts/<feature>/` directory for evidence
|
|
209
209
|
3. Creates `REPORT.md` with plan and TODO checklist
|
|
210
210
|
4. Registers todos in TodoWrite for progress tracking
|
|
211
211
|
|
|
212
212
|
**Directory structure created:**
|
|
213
213
|
```
|
|
214
|
-
<worktree>/ # e.g.,
|
|
214
|
+
<worktree>/ # e.g., .worktree/feature-auth/
|
|
215
215
|
└── .artifacts/
|
|
216
216
|
└── <feature>/ # e.g., auth (from feature/auth)
|
|
217
217
|
├── REPORT.md # Plan, progress, evidence links
|
|
@@ -219,7 +219,7 @@ Starts a new task with proper environment setup.
|
|
|
219
219
|
└── videos/ # Video recordings
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
-
**Task resumption:** When a session starts or after context compaction, the command checks for existing worktrees (via `
|
|
222
|
+
**Task resumption:** When a session starts or after context compaction, the command checks for existing worktrees (via `git wt`) and resumes from `REPORT.md`.
|
|
223
223
|
|
|
224
224
|
#### `/reviw:done`
|
|
225
225
|
|
package/cli.cjs
CHANGED
|
@@ -37,9 +37,10 @@ const timelineTempDirs = new Set();
|
|
|
37
37
|
|
|
38
38
|
// Extract video timeline thumbnails using ffmpeg scene detection
|
|
39
39
|
// Uses "stabilization filter" - groups consecutive similar frames and takes the last frame of each group
|
|
40
|
-
function extractVideoTimeline(videoPath, tmpDir, res, onComplete) {
|
|
41
|
-
|
|
42
|
-
const
|
|
40
|
+
function extractVideoTimeline(videoPath, tmpDir, res, onComplete, options = {}) {
|
|
41
|
+
// Use provided thresholds or defaults
|
|
42
|
+
const STABILIZATION_THRESHOLD = options.stabilizationThreshold || 0.5; // seconds - consecutive changes within this are grouped
|
|
43
|
+
const SCENE_THRESHOLD = options.sceneThreshold || 0.005; // Lower default threshold (was 0.01) to catch more subtle changes
|
|
43
44
|
const MIN_INTERVAL = 1.0; // Minimum interval for additional keyframes (seconds)
|
|
44
45
|
|
|
45
46
|
// First, get video duration using ffprobe
|
|
@@ -373,6 +374,13 @@ function sanitizeHtml(html) {
|
|
|
373
374
|
});
|
|
374
375
|
}
|
|
375
376
|
|
|
377
|
+
// [TDD Fix] 重複見出しキーを防ぐためのカウンター
|
|
378
|
+
// 同じテキストの見出しが複数ある場合でも、ユニークなキーを生成する
|
|
379
|
+
var headingKeyCounter = {};
|
|
380
|
+
function resetHeadingKeyCounter() {
|
|
381
|
+
headingKeyCounter = {};
|
|
382
|
+
}
|
|
383
|
+
|
|
376
384
|
marked.use({
|
|
377
385
|
hooks: {
|
|
378
386
|
// テーブルをスクロールラッパーで囲む(後処理)
|
|
@@ -382,6 +390,21 @@ marked.use({
|
|
|
382
390
|
}
|
|
383
391
|
},
|
|
384
392
|
renderer: {
|
|
393
|
+
// 見出しをトグル可能なセクションとしてレンダリング
|
|
394
|
+
// markedのrenderer.headingは (text, level, raw) を引数に取る
|
|
395
|
+
heading: function(text, level, raw) {
|
|
396
|
+
// トグル用のdata属性を追加(見出しテキスト + 出現順でユニークキーを生成)
|
|
397
|
+
var baseKey = (text || '').replace(/[^a-zA-Z0-9\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\u3400-\u4DBF]/g, '-').toLowerCase();
|
|
398
|
+
// 同じbaseKeyが出現した回数をカウントしてユニーク化
|
|
399
|
+
if (headingKeyCounter[baseKey] === undefined) {
|
|
400
|
+
headingKeyCounter[baseKey] = 0;
|
|
401
|
+
}
|
|
402
|
+
var headingKey = baseKey + '-' + headingKeyCounter[baseKey]++;
|
|
403
|
+
return '<h' + level + ' class="md-heading-toggle" data-heading-key="' + escapeHtmlForXss(headingKey) + '" data-heading-level="' + level + '">' +
|
|
404
|
+
'<span class="heading-toggle-icon">▼</span>' +
|
|
405
|
+
text +
|
|
406
|
+
'</h' + level + '>';
|
|
407
|
+
},
|
|
385
408
|
// 生HTMLブロックをサニタイズ
|
|
386
409
|
html: function(token) {
|
|
387
410
|
var text = token.raw || token.text || token;
|
|
@@ -862,6 +885,9 @@ function loadText(filePath) {
|
|
|
862
885
|
}
|
|
863
886
|
|
|
864
887
|
function loadMarkdown(filePath) {
|
|
888
|
+
// [TDD Fix] カウンターをリセットして新しいMarkdownファイルの見出しキーを初期化
|
|
889
|
+
resetHeadingKeyCounter();
|
|
890
|
+
|
|
865
891
|
const raw = fs.readFileSync(filePath);
|
|
866
892
|
const text = decodeBuffer(raw);
|
|
867
893
|
const lines = text.split(/\r?\n/);
|
|
@@ -3020,6 +3046,35 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3020
3046
|
.md-preview h1, .md-preview h2, .md-preview h3, .md-preview h4 {
|
|
3021
3047
|
margin: 0.4em 0 0.2em;
|
|
3022
3048
|
}
|
|
3049
|
+
/* Heading toggle feature */
|
|
3050
|
+
.md-preview .md-heading-toggle {
|
|
3051
|
+
display: flex;
|
|
3052
|
+
align-items: center;
|
|
3053
|
+
gap: 6px;
|
|
3054
|
+
}
|
|
3055
|
+
.md-preview .heading-toggle-icon {
|
|
3056
|
+
font-size: 0.6em;
|
|
3057
|
+
transition: transform 150ms ease;
|
|
3058
|
+
color: var(--muted);
|
|
3059
|
+
flex-shrink: 0;
|
|
3060
|
+
cursor: pointer;
|
|
3061
|
+
padding: 4px 8px;
|
|
3062
|
+
margin: -4px 0 -4px -8px;
|
|
3063
|
+
border-radius: 4px;
|
|
3064
|
+
}
|
|
3065
|
+
.md-preview .heading-toggle-icon:hover {
|
|
3066
|
+
background: var(--hover-bg);
|
|
3067
|
+
color: var(--accent);
|
|
3068
|
+
}
|
|
3069
|
+
.md-preview .md-heading-toggle.collapsed .heading-toggle-icon {
|
|
3070
|
+
transform: rotate(-90deg);
|
|
3071
|
+
}
|
|
3072
|
+
.md-preview .heading-section-content {
|
|
3073
|
+
/* Content wrapper for collapsible sections */
|
|
3074
|
+
}
|
|
3075
|
+
.md-preview .heading-section-content.hidden {
|
|
3076
|
+
display: none;
|
|
3077
|
+
}
|
|
3023
3078
|
.md-preview p { margin: 0.3em 0; line-height: 1.5; }
|
|
3024
3079
|
.md-preview img { max-width: 100%; height: auto; border-radius: 8px; }
|
|
3025
3080
|
.md-preview video.video-preview { max-width: 100%; height: auto; border-radius: 8px; background: #000; }
|
|
@@ -3425,6 +3480,111 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3425
3480
|
position: relative;
|
|
3426
3481
|
flex-shrink: 0;
|
|
3427
3482
|
}
|
|
3483
|
+
/* Video threshold settings panel */
|
|
3484
|
+
.video-settings-btn {
|
|
3485
|
+
position: absolute;
|
|
3486
|
+
top: 14px;
|
|
3487
|
+
right: 64px;
|
|
3488
|
+
width: 40px;
|
|
3489
|
+
height: 40px;
|
|
3490
|
+
display: flex;
|
|
3491
|
+
align-items: center;
|
|
3492
|
+
justify-content: center;
|
|
3493
|
+
background: rgba(0, 0, 0, 0.55);
|
|
3494
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
3495
|
+
border-radius: 50%;
|
|
3496
|
+
cursor: pointer;
|
|
3497
|
+
color: #fff;
|
|
3498
|
+
font-size: 18px;
|
|
3499
|
+
z-index: 10;
|
|
3500
|
+
backdrop-filter: blur(4px);
|
|
3501
|
+
transition: background 120ms ease, transform 120ms ease;
|
|
3502
|
+
}
|
|
3503
|
+
.video-settings-btn:hover {
|
|
3504
|
+
background: rgba(0, 0, 0, 0.75);
|
|
3505
|
+
transform: scale(1.04);
|
|
3506
|
+
}
|
|
3507
|
+
.video-settings-panel {
|
|
3508
|
+
position: absolute;
|
|
3509
|
+
top: 60px;
|
|
3510
|
+
right: 14px;
|
|
3511
|
+
background: rgba(0, 0, 0, 0.9);
|
|
3512
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
3513
|
+
border-radius: 12px;
|
|
3514
|
+
padding: 16px;
|
|
3515
|
+
z-index: 15;
|
|
3516
|
+
min-width: 280px;
|
|
3517
|
+
display: none;
|
|
3518
|
+
backdrop-filter: blur(8px);
|
|
3519
|
+
}
|
|
3520
|
+
.video-settings-panel.visible {
|
|
3521
|
+
display: block;
|
|
3522
|
+
}
|
|
3523
|
+
.video-settings-panel h4 {
|
|
3524
|
+
margin: 0 0 8px;
|
|
3525
|
+
color: #fff;
|
|
3526
|
+
font-size: 14px;
|
|
3527
|
+
font-weight: 500;
|
|
3528
|
+
}
|
|
3529
|
+
.video-settings-desc {
|
|
3530
|
+
margin: 0 0 12px;
|
|
3531
|
+
color: rgba(255, 255, 255, 0.6);
|
|
3532
|
+
font-size: 11px;
|
|
3533
|
+
line-height: 1.4;
|
|
3534
|
+
}
|
|
3535
|
+
.video-settings-buttons {
|
|
3536
|
+
display: flex;
|
|
3537
|
+
gap: 6px;
|
|
3538
|
+
margin-bottom: 12px;
|
|
3539
|
+
}
|
|
3540
|
+
.video-settings-buttons button {
|
|
3541
|
+
flex: 1;
|
|
3542
|
+
padding: 8px 4px;
|
|
3543
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
3544
|
+
border-radius: 6px;
|
|
3545
|
+
background: rgba(255, 255, 255, 0.1);
|
|
3546
|
+
color: rgba(255, 255, 255, 0.8);
|
|
3547
|
+
font-size: 11px;
|
|
3548
|
+
cursor: pointer;
|
|
3549
|
+
transition: all 120ms ease;
|
|
3550
|
+
}
|
|
3551
|
+
.video-settings-buttons button:hover {
|
|
3552
|
+
background: rgba(255, 255, 255, 0.2);
|
|
3553
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
3554
|
+
}
|
|
3555
|
+
.video-settings-buttons button.selected {
|
|
3556
|
+
background: #3b82f6;
|
|
3557
|
+
border-color: #3b82f6;
|
|
3558
|
+
color: #fff;
|
|
3559
|
+
}
|
|
3560
|
+
.video-settings-actions {
|
|
3561
|
+
display: flex;
|
|
3562
|
+
gap: 8px;
|
|
3563
|
+
margin-top: 8px;
|
|
3564
|
+
}
|
|
3565
|
+
.video-settings-actions button {
|
|
3566
|
+
flex: 1;
|
|
3567
|
+
padding: 8px 12px;
|
|
3568
|
+
border: none;
|
|
3569
|
+
border-radius: 6px;
|
|
3570
|
+
font-size: 12px;
|
|
3571
|
+
cursor: pointer;
|
|
3572
|
+
transition: background 120ms ease;
|
|
3573
|
+
}
|
|
3574
|
+
.video-settings-actions .regenerate-btn {
|
|
3575
|
+
background: #3b82f6;
|
|
3576
|
+
color: #fff;
|
|
3577
|
+
}
|
|
3578
|
+
.video-settings-actions .regenerate-btn:hover {
|
|
3579
|
+
background: #2563eb;
|
|
3580
|
+
}
|
|
3581
|
+
.video-settings-actions .reset-btn {
|
|
3582
|
+
background: rgba(255, 255, 255, 0.15);
|
|
3583
|
+
color: #fff;
|
|
3584
|
+
}
|
|
3585
|
+
.video-settings-actions .reset-btn:hover {
|
|
3586
|
+
background: rgba(255, 255, 255, 0.25);
|
|
3587
|
+
}
|
|
3428
3588
|
/* Video Shortcuts Help */
|
|
3429
3589
|
.video-shortcuts-help {
|
|
3430
3590
|
opacity: 0.85;
|
|
@@ -4358,6 +4518,21 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4358
4518
|
</div>
|
|
4359
4519
|
<div class="video-fullscreen-overlay" id="video-fullscreen">
|
|
4360
4520
|
<button class="video-close-btn" id="video-close" aria-label="Close video" title="Close (ESC)">✕</button>
|
|
4521
|
+
<button class="video-settings-btn" id="video-settings-btn" aria-label="Timeline settings" title="Timeline settings">⚙</button>
|
|
4522
|
+
<div class="video-settings-panel" id="video-settings-panel">
|
|
4523
|
+
<h4>サムネイル数の調整</h4>
|
|
4524
|
+
<p class="video-settings-desc">重要シーンの見逃しを防ぐため、サムネイル数を調整できます</p>
|
|
4525
|
+
<div class="video-settings-buttons" id="scene-buttons">
|
|
4526
|
+
<button data-scene="0.05" data-stab="1.0">少なめ</button>
|
|
4527
|
+
<button data-scene="0.02" data-stab="0.8">やや少</button>
|
|
4528
|
+
<button data-scene="0.005" data-stab="0.5" class="selected">標準</button>
|
|
4529
|
+
<button data-scene="0.002" data-stab="0.3">やや多</button>
|
|
4530
|
+
<button data-scene="0.001" data-stab="0.2">多め</button>
|
|
4531
|
+
</div>
|
|
4532
|
+
<div class="video-settings-actions">
|
|
4533
|
+
<button class="regenerate-btn" id="video-settings-regenerate">この設定で再生成</button>
|
|
4534
|
+
</div>
|
|
4535
|
+
</div>
|
|
4361
4536
|
<div class="video-container" id="video-container"></div>
|
|
4362
4537
|
</div>
|
|
4363
4538
|
|
|
@@ -6083,8 +6258,17 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6083
6258
|
let startX, startY;
|
|
6084
6259
|
let svgNaturalWidth = 0, svgNaturalHeight = 0;
|
|
6085
6260
|
let minimapScale = 1;
|
|
6261
|
+
let currentMermaidContainer = null; // Store the container for selection after close
|
|
6262
|
+
let skipNextFullscreenOpen = false; // Flag to skip opening fullscreen after close
|
|
6086
6263
|
|
|
6087
6264
|
function openFullscreen(mermaidEl) {
|
|
6265
|
+
// Skip if this is triggered by the post-close click
|
|
6266
|
+
if (skipNextFullscreenOpen) {
|
|
6267
|
+
skipNextFullscreenOpen = false;
|
|
6268
|
+
return;
|
|
6269
|
+
}
|
|
6270
|
+
// Store the container for selection when fullscreen closes
|
|
6271
|
+
currentMermaidContainer = mermaidEl.closest('.mermaid-container');
|
|
6088
6272
|
const svg = mermaidEl.querySelector('svg');
|
|
6089
6273
|
if (!svg) return;
|
|
6090
6274
|
fsWrapper.innerHTML = '';
|
|
@@ -6146,6 +6330,17 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6146
6330
|
|
|
6147
6331
|
function closeFullscreen() {
|
|
6148
6332
|
fsOverlay.classList.remove('visible');
|
|
6333
|
+
// Trigger selection of the mermaid container after closing fullscreen
|
|
6334
|
+
if (currentMermaidContainer) {
|
|
6335
|
+
const containerToSelect = currentMermaidContainer;
|
|
6336
|
+
currentMermaidContainer = null;
|
|
6337
|
+
// Set flags to skip reopening fullscreen and trigger selection
|
|
6338
|
+
setTimeout(() => {
|
|
6339
|
+
skipNextFullscreenOpen = true;
|
|
6340
|
+
window._mermaidSelectAfterClose = containerToSelect;
|
|
6341
|
+
containerToSelect.click();
|
|
6342
|
+
}, 100);
|
|
6343
|
+
}
|
|
6149
6344
|
}
|
|
6150
6345
|
|
|
6151
6346
|
function updateTransform() {
|
|
@@ -6375,6 +6570,82 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6375
6570
|
}
|
|
6376
6571
|
})();
|
|
6377
6572
|
|
|
6573
|
+
// --- Heading Toggle ---
|
|
6574
|
+
(function initHeadingToggle() {
|
|
6575
|
+
const preview = document.querySelector('.md-preview');
|
|
6576
|
+
if (!preview) return;
|
|
6577
|
+
|
|
6578
|
+
const STORAGE_PREFIX = 'reviw-heading-toggle-';
|
|
6579
|
+
|
|
6580
|
+
// Get all headings
|
|
6581
|
+
const headings = Array.from(preview.querySelectorAll('.md-heading-toggle'));
|
|
6582
|
+
if (headings.length === 0) return;
|
|
6583
|
+
|
|
6584
|
+
// Wrap content between headings into sections
|
|
6585
|
+
headings.forEach((heading, idx) => {
|
|
6586
|
+
const level = parseInt(heading.dataset.headingLevel) || 1;
|
|
6587
|
+
const key = heading.dataset.headingKey;
|
|
6588
|
+
|
|
6589
|
+
// Collect all siblings until next heading of same or higher level
|
|
6590
|
+
const content = [];
|
|
6591
|
+
let sibling = heading.nextElementSibling;
|
|
6592
|
+
|
|
6593
|
+
while (sibling) {
|
|
6594
|
+
// Check if it's a heading of same or higher level
|
|
6595
|
+
if (sibling.classList.contains('md-heading-toggle')) {
|
|
6596
|
+
const siblingLevel = parseInt(sibling.dataset.headingLevel) || 1;
|
|
6597
|
+
if (siblingLevel <= level) break;
|
|
6598
|
+
}
|
|
6599
|
+
content.push(sibling);
|
|
6600
|
+
sibling = sibling.nextElementSibling;
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
// Create wrapper for content
|
|
6604
|
+
if (content.length > 0) {
|
|
6605
|
+
const wrapper = document.createElement('div');
|
|
6606
|
+
wrapper.className = 'heading-section-content';
|
|
6607
|
+
wrapper.dataset.forHeading = key;
|
|
6608
|
+
|
|
6609
|
+
// Move content into wrapper
|
|
6610
|
+
const firstContent = content[0];
|
|
6611
|
+
heading.parentNode.insertBefore(wrapper, firstContent);
|
|
6612
|
+
content.forEach(el => wrapper.appendChild(el));
|
|
6613
|
+
|
|
6614
|
+
// Restore state from localStorage
|
|
6615
|
+
const savedState = localStorage.getItem(STORAGE_PREFIX + key);
|
|
6616
|
+
if (savedState === 'collapsed') {
|
|
6617
|
+
heading.classList.add('collapsed');
|
|
6618
|
+
wrapper.classList.add('hidden');
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
|
|
6622
|
+
// Add click handler to toggle icon only (not the whole heading)
|
|
6623
|
+
// This allows the heading text to remain selectable for comments
|
|
6624
|
+
const toggleIcon = heading.querySelector('.heading-toggle-icon');
|
|
6625
|
+
if (toggleIcon) {
|
|
6626
|
+
toggleIcon.addEventListener('click', (e) => {
|
|
6627
|
+
e.stopPropagation(); // Prevent triggering comment selection
|
|
6628
|
+
const wrapper = preview.querySelector(\`.heading-section-content[data-for-heading="\${key}"]\`);
|
|
6629
|
+
if (!wrapper) return;
|
|
6630
|
+
|
|
6631
|
+
const isCollapsed = heading.classList.toggle('collapsed');
|
|
6632
|
+
wrapper.classList.toggle('hidden', isCollapsed);
|
|
6633
|
+
|
|
6634
|
+
// Save state to localStorage
|
|
6635
|
+
localStorage.setItem(STORAGE_PREFIX + key, isCollapsed ? 'collapsed' : 'expanded');
|
|
6636
|
+
|
|
6637
|
+
// Re-render Mermaid diagrams when expanding (they may not render while hidden)
|
|
6638
|
+
if (!isCollapsed && typeof mermaid !== 'undefined') {
|
|
6639
|
+
const mermaidDivs = wrapper.querySelectorAll('.mermaid');
|
|
6640
|
+
if (mermaidDivs.length > 0) {
|
|
6641
|
+
mermaid.run({ nodes: Array.from(mermaidDivs) }).catch(() => {});
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
});
|
|
6645
|
+
}
|
|
6646
|
+
});
|
|
6647
|
+
})();
|
|
6648
|
+
|
|
6378
6649
|
// --- Image Fullscreen ---
|
|
6379
6650
|
(function initImageFullscreen() {
|
|
6380
6651
|
const preview = document.querySelector('.md-preview');
|
|
@@ -6526,8 +6797,23 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6526
6797
|
return mins + ':' + (secs < 10 ? '0' : '') + secs;
|
|
6527
6798
|
}
|
|
6528
6799
|
|
|
6800
|
+
// Default threshold values
|
|
6801
|
+
const DEFAULT_SCENE_THRESHOLD = 0.005;
|
|
6802
|
+
const DEFAULT_STABILIZATION_THRESHOLD = 0.5;
|
|
6803
|
+
let currentSceneThreshold = DEFAULT_SCENE_THRESHOLD;
|
|
6804
|
+
let currentStabilizationThreshold = DEFAULT_STABILIZATION_THRESHOLD;
|
|
6805
|
+
let currentVideoPath = null;
|
|
6806
|
+
let currentVideo = null;
|
|
6807
|
+
|
|
6529
6808
|
// Load video timeline via SSE
|
|
6530
|
-
function loadVideoTimeline(videoPath, video) {
|
|
6809
|
+
function loadVideoTimeline(videoPath, video, options = {}) {
|
|
6810
|
+
const sceneThreshold = options.sceneThreshold || currentSceneThreshold;
|
|
6811
|
+
const stabilizationThreshold = options.stabilizationThreshold || currentStabilizationThreshold;
|
|
6812
|
+
|
|
6813
|
+
// Store for regeneration
|
|
6814
|
+
currentVideoPath = videoPath;
|
|
6815
|
+
currentVideo = video;
|
|
6816
|
+
|
|
6531
6817
|
// Close existing connection
|
|
6532
6818
|
if (currentTimelineEventSource) {
|
|
6533
6819
|
currentTimelineEventSource.close();
|
|
@@ -6552,9 +6838,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6552
6838
|
|
|
6553
6839
|
videoContainer.appendChild(timeline);
|
|
6554
6840
|
|
|
6555
|
-
// Start SSE connection
|
|
6841
|
+
// Start SSE connection with threshold parameters
|
|
6556
6842
|
const encodedPath = encodeURIComponent(videoPath);
|
|
6557
|
-
const es = new EventSource(
|
|
6843
|
+
const es = new EventSource(\`/video-timeline?path=\${encodedPath}&scene=\${sceneThreshold}&stabilization=\${stabilizationThreshold}\`);
|
|
6558
6844
|
currentTimelineEventSource = es;
|
|
6559
6845
|
|
|
6560
6846
|
es.onmessage = function(e) {
|
|
@@ -6648,6 +6934,11 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6648
6934
|
}
|
|
6649
6935
|
|
|
6650
6936
|
function showVideo(index) {
|
|
6937
|
+
// Skip if this is triggered by the post-close click
|
|
6938
|
+
if (window._skipNextVideoFullscreen) {
|
|
6939
|
+
window._skipNextVideoFullscreen = false;
|
|
6940
|
+
return;
|
|
6941
|
+
}
|
|
6651
6942
|
if (index < 0 || index >= allVideoSources.length) return;
|
|
6652
6943
|
currentVideoIndex = index;
|
|
6653
6944
|
const source = allVideoSources[index];
|
|
@@ -6730,6 +7021,12 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6730
7021
|
|
|
6731
7022
|
function closeVideoOverlay() {
|
|
6732
7023
|
videoOverlay.classList.remove('visible');
|
|
7024
|
+
|
|
7025
|
+
// Save source element before resetting index (for triggering selection)
|
|
7026
|
+
const closedSource = currentVideoIndex >= 0 && currentVideoIndex < allVideoSources.length
|
|
7027
|
+
? allVideoSources[currentVideoIndex]
|
|
7028
|
+
: null;
|
|
7029
|
+
|
|
6733
7030
|
currentVideoIndex = -1;
|
|
6734
7031
|
|
|
6735
7032
|
// Close timeline SSE connection
|
|
@@ -6747,6 +7044,18 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6747
7044
|
video.remove();
|
|
6748
7045
|
}
|
|
6749
7046
|
|
|
7047
|
+
// Trigger selection on the source element's parent table cell (if any)
|
|
7048
|
+
if (closedSource && closedSource.element) {
|
|
7049
|
+
const parentCell = closedSource.element.closest('td, th');
|
|
7050
|
+
if (parentCell) {
|
|
7051
|
+
// Set global flag to skip reopening fullscreen, then trigger click for selection
|
|
7052
|
+
setTimeout(() => {
|
|
7053
|
+
window._skipNextVideoFullscreen = true;
|
|
7054
|
+
parentCell.click();
|
|
7055
|
+
}, 100);
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
|
|
6750
7059
|
// Remove timeline
|
|
6751
7060
|
const timeline = videoContainer.querySelector('.video-timeline');
|
|
6752
7061
|
if (timeline) timeline.remove();
|
|
@@ -6931,6 +7240,63 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6931
7240
|
});
|
|
6932
7241
|
}
|
|
6933
7242
|
});
|
|
7243
|
+
|
|
7244
|
+
// --- Video Settings Panel ---
|
|
7245
|
+
const settingsBtn = document.getElementById('video-settings-btn');
|
|
7246
|
+
const settingsPanel = document.getElementById('video-settings-panel');
|
|
7247
|
+
const sceneButtons = document.getElementById('scene-buttons');
|
|
7248
|
+
const regenerateBtn = document.getElementById('video-settings-regenerate');
|
|
7249
|
+
|
|
7250
|
+
if (settingsBtn && settingsPanel) {
|
|
7251
|
+
// Toggle settings panel
|
|
7252
|
+
settingsBtn.addEventListener('click', (e) => {
|
|
7253
|
+
e.stopPropagation();
|
|
7254
|
+
settingsPanel.classList.toggle('visible');
|
|
7255
|
+
});
|
|
7256
|
+
|
|
7257
|
+
// Prevent panel clicks from closing overlay
|
|
7258
|
+
settingsPanel.addEventListener('click', (e) => e.stopPropagation());
|
|
7259
|
+
|
|
7260
|
+
// Handle 5-level button clicks
|
|
7261
|
+
if (sceneButtons) {
|
|
7262
|
+
const buttons = sceneButtons.querySelectorAll('button');
|
|
7263
|
+
buttons.forEach(btn => {
|
|
7264
|
+
btn.addEventListener('click', () => {
|
|
7265
|
+
buttons.forEach(b => b.classList.remove('selected'));
|
|
7266
|
+
btn.classList.add('selected');
|
|
7267
|
+
});
|
|
7268
|
+
});
|
|
7269
|
+
}
|
|
7270
|
+
|
|
7271
|
+
// Regenerate timeline with selected settings
|
|
7272
|
+
if (regenerateBtn) {
|
|
7273
|
+
regenerateBtn.addEventListener('click', () => {
|
|
7274
|
+
if (!currentVideoPath || !currentVideo) return;
|
|
7275
|
+
|
|
7276
|
+
// Get selected button's values
|
|
7277
|
+
const selectedBtn = sceneButtons?.querySelector('button.selected');
|
|
7278
|
+
if (selectedBtn) {
|
|
7279
|
+
currentSceneThreshold = parseFloat(selectedBtn.dataset.scene) || DEFAULT_SCENE_THRESHOLD;
|
|
7280
|
+
currentStabilizationThreshold = parseFloat(selectedBtn.dataset.stab) || DEFAULT_STABILIZATION_THRESHOLD;
|
|
7281
|
+
}
|
|
7282
|
+
|
|
7283
|
+
loadVideoTimeline(currentVideoPath, currentVideo, {
|
|
7284
|
+
sceneThreshold: currentSceneThreshold,
|
|
7285
|
+
stabilizationThreshold: currentStabilizationThreshold
|
|
7286
|
+
});
|
|
7287
|
+
|
|
7288
|
+
// Hide panel after regenerating
|
|
7289
|
+
settingsPanel.classList.remove('visible');
|
|
7290
|
+
});
|
|
7291
|
+
}
|
|
7292
|
+
|
|
7293
|
+
// Close panel when overlay closes
|
|
7294
|
+
const originalCloseVideoOverlay = closeVideoOverlay;
|
|
7295
|
+
closeVideoOverlay = function() {
|
|
7296
|
+
settingsPanel.classList.remove('visible');
|
|
7297
|
+
originalCloseVideoOverlay();
|
|
7298
|
+
};
|
|
7299
|
+
}
|
|
6934
7300
|
})();
|
|
6935
7301
|
|
|
6936
7302
|
// --- Preview Commenting ---
|
|
@@ -6946,11 +7312,16 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6946
7312
|
.md-preview > p:hover, .md-preview > h1:hover, .md-preview > h2:hover,
|
|
6947
7313
|
.md-preview > h3:hover, .md-preview > h4:hover, .md-preview > h5:hover,
|
|
6948
7314
|
.md-preview > h6:hover, .md-preview > ul > li:hover, .md-preview > ol > li:hover,
|
|
6949
|
-
.md-preview > pre:hover, .md-preview > blockquote:hover
|
|
7315
|
+
.md-preview > pre:hover, .md-preview > blockquote:hover,
|
|
7316
|
+
.md-preview details summary:hover {
|
|
6950
7317
|
background: rgba(99, 102, 241, 0.08);
|
|
6951
7318
|
cursor: pointer;
|
|
6952
7319
|
border-radius: 4px;
|
|
6953
7320
|
}
|
|
7321
|
+
.md-preview td:hover, .md-preview th:hover {
|
|
7322
|
+
background: rgba(99, 102, 241, 0.12);
|
|
7323
|
+
cursor: pointer;
|
|
7324
|
+
}
|
|
6954
7325
|
.md-preview img:hover {
|
|
6955
7326
|
outline: 2px solid var(--accent);
|
|
6956
7327
|
cursor: pointer;
|
|
@@ -7013,7 +7384,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7013
7384
|
}
|
|
7014
7385
|
|
|
7015
7386
|
if (!text) return -1;
|
|
7016
|
-
|
|
7387
|
+
// Remove toggle icon characters (▼, ▶) that may be included from heading toggles
|
|
7388
|
+
const cleanText = text.replace(/[▼▶]/g, '').trim();
|
|
7389
|
+
const normalized = cleanText.replace(/\\s+/g, ' ').slice(0, 100);
|
|
7017
7390
|
if (!normalized) return -1;
|
|
7018
7391
|
|
|
7019
7392
|
for (let i = 0; i < DATA.length; i++) {
|
|
@@ -7033,6 +7406,19 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7033
7406
|
}
|
|
7034
7407
|
}
|
|
7035
7408
|
|
|
7409
|
+
// Check for HTML summary tags: <summary>text</summary>
|
|
7410
|
+
const summaryMatch = lineText.match(/<summary>([^<]*)<\\/summary>/i);
|
|
7411
|
+
if (summaryMatch) {
|
|
7412
|
+
const summaryText = summaryMatch[1].trim();
|
|
7413
|
+
if (summaryText === normalized || summaryText.toLowerCase() === normalized.toLowerCase()) {
|
|
7414
|
+
return i + 1;
|
|
7415
|
+
}
|
|
7416
|
+
// Partial match for long summary text
|
|
7417
|
+
if (summaryText.includes(normalized.slice(0, 30)) && normalized.length > 5) {
|
|
7418
|
+
return i + 1;
|
|
7419
|
+
}
|
|
7420
|
+
}
|
|
7421
|
+
|
|
7036
7422
|
// Try stripping all markdown formatting (links, bold, italic, etc.)
|
|
7037
7423
|
const strippedLine = stripMarkdown(lineText).replace(/\\s+/g, ' ').slice(0, 100);
|
|
7038
7424
|
if (strippedLine === normalized) return i + 1;
|
|
@@ -7045,10 +7431,31 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7045
7431
|
// Helper: find matching source line for table cell (prioritizes table rows)
|
|
7046
7432
|
function findTableSourceLine(text) {
|
|
7047
7433
|
if (!text) return -1;
|
|
7048
|
-
|
|
7434
|
+
// Remove toggle icon characters (▼, ▶) that may be included from heading toggles
|
|
7435
|
+
const cleanText = text.replace(/[▼▶]/g, '').trim();
|
|
7436
|
+
const normalized = cleanText.replace(/\\s+/g, ' ').slice(0, 100);
|
|
7049
7437
|
if (!normalized) return -1;
|
|
7050
7438
|
|
|
7051
|
-
// First pass: look for
|
|
7439
|
+
// First pass: look for EXACT cell text match (not inside markdown syntax)
|
|
7440
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
7441
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
7442
|
+
if (!lineText || !lineText.startsWith('|')) continue;
|
|
7443
|
+
|
|
7444
|
+
// Split into cells and check for exact match (excluding markdown syntax)
|
|
7445
|
+
const cells = lineText.split('|').map(c => c.trim());
|
|
7446
|
+
for (const cell of cells) {
|
|
7447
|
+
// Skip cells that are markdown images/links (start with ![, contain []())
|
|
7448
|
+
if (cell.match(/^!?\\[.*\\]\\(.*\\)$/)) continue;
|
|
7449
|
+
|
|
7450
|
+
// Check for exact cell text match
|
|
7451
|
+
if (cell === normalized) return i + 1;
|
|
7452
|
+
|
|
7453
|
+
// For short text (like header cells), require exact word match
|
|
7454
|
+
if (normalized.length <= 5 && cell === normalized) return i + 1;
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
|
|
7458
|
+
// Second pass: look for partial matches (including inside markdown syntax)
|
|
7052
7459
|
for (let i = 0; i < DATA.length; i++) {
|
|
7053
7460
|
const lineText = (DATA[i][0] || '').trim();
|
|
7054
7461
|
if (!lineText || !lineText.startsWith('|')) continue;
|
|
@@ -7137,6 +7544,51 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7137
7544
|
return { startLine: -1, endLine: -1 };
|
|
7138
7545
|
}
|
|
7139
7546
|
|
|
7547
|
+
// Helper: find source line range for mermaid diagram by its container
|
|
7548
|
+
function findMermaidSourceLine(mermaidContainer) {
|
|
7549
|
+
if (!mermaidContainer) return { startLine: -1, endLine: -1 };
|
|
7550
|
+
|
|
7551
|
+
// Find the index of this mermaid container among all mermaid containers
|
|
7552
|
+
const allMermaidContainers = document.querySelectorAll('.mermaid-container');
|
|
7553
|
+
let containerIndex = -1;
|
|
7554
|
+
for (let i = 0; i < allMermaidContainers.length; i++) {
|
|
7555
|
+
if (allMermaidContainers[i] === mermaidContainer) {
|
|
7556
|
+
containerIndex = i;
|
|
7557
|
+
break;
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
if (containerIndex < 0) return { startLine: -1, endLine: -1 };
|
|
7561
|
+
|
|
7562
|
+
// Find all mermaid code blocks in the source
|
|
7563
|
+
const mermaidBlocks = [];
|
|
7564
|
+
let currentBlock = null;
|
|
7565
|
+
|
|
7566
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
7567
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
7568
|
+
|
|
7569
|
+
if (lineText.match(/^\`\`\`mermaid/i) && !currentBlock) {
|
|
7570
|
+
// Start of a mermaid code block
|
|
7571
|
+
currentBlock = { startLine: i + 1, lines: [] };
|
|
7572
|
+
} else if (lineText === '\`\`\`' && currentBlock) {
|
|
7573
|
+
// End of the code block
|
|
7574
|
+
currentBlock.endLine = i + 1;
|
|
7575
|
+
mermaidBlocks.push(currentBlock);
|
|
7576
|
+
currentBlock = null;
|
|
7577
|
+
} else if (currentBlock) {
|
|
7578
|
+
// Inside the mermaid block
|
|
7579
|
+
currentBlock.lines.push(DATA[i][0] || '');
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
|
|
7583
|
+
// Return the block at the same index as the container
|
|
7584
|
+
if (containerIndex < mermaidBlocks.length) {
|
|
7585
|
+
const block = mermaidBlocks[containerIndex];
|
|
7586
|
+
return { startLine: block.startLine, endLine: block.endLine };
|
|
7587
|
+
}
|
|
7588
|
+
|
|
7589
|
+
return { startLine: -1, endLine: -1 };
|
|
7590
|
+
}
|
|
7591
|
+
|
|
7140
7592
|
// Helper: find source line for image by src
|
|
7141
7593
|
function findImageSourceLine(src) {
|
|
7142
7594
|
if (!src) return -1;
|
|
@@ -7217,6 +7669,44 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7217
7669
|
return;
|
|
7218
7670
|
}
|
|
7219
7671
|
|
|
7672
|
+
// Handle video/video-button clicks - select source line first
|
|
7673
|
+
// This includes video elements, fullscreen buttons, video wrapper clicks,
|
|
7674
|
+
// AND clicks on table cells containing videos (for post-fullscreen selection)
|
|
7675
|
+
const videoElement = e.target.closest('video.video-preview');
|
|
7676
|
+
const videoButton = e.target.closest('.video-fullscreen-btn');
|
|
7677
|
+
const videoWrapper = e.target.closest('.video-fullscreen-wrapper');
|
|
7678
|
+
|
|
7679
|
+
// Also check if clicking on a td that contains a video
|
|
7680
|
+
const targetCell = e.target.closest('td, th');
|
|
7681
|
+
const cellContainsVideo = targetCell && targetCell.querySelector('video.video-preview');
|
|
7682
|
+
|
|
7683
|
+
if (videoElement || videoButton || videoWrapper || cellContainsVideo) {
|
|
7684
|
+
// Check if this is a post-fullscreen click (for selection only, skip fullscreen)
|
|
7685
|
+
const isPostFullscreenClick = window._skipNextVideoFullscreen;
|
|
7686
|
+
if (isPostFullscreenClick) {
|
|
7687
|
+
window._skipNextVideoFullscreen = false;
|
|
7688
|
+
}
|
|
7689
|
+
|
|
7690
|
+
// Find the parent table cell
|
|
7691
|
+
const refElement = videoElement || videoButton || videoWrapper || targetCell;
|
|
7692
|
+
const parentCell = refElement.closest('td, th') || targetCell;
|
|
7693
|
+
if (parentCell) {
|
|
7694
|
+
// Use video src to find the source line
|
|
7695
|
+
const video = videoElement || parentCell.querySelector('video');
|
|
7696
|
+
if (video && video.src) {
|
|
7697
|
+
const line = findImageSourceLine(video.src);
|
|
7698
|
+
if (line > 0) {
|
|
7699
|
+
selectSourceRange(line, null, parentCell);
|
|
7700
|
+
// Return if this is a post-fullscreen click (for selection only)
|
|
7701
|
+
if (isPostFullscreenClick) {
|
|
7702
|
+
return;
|
|
7703
|
+
}
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
// Don't return for normal clicks - let fullscreen handler work too
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7220
7710
|
// Handle links - sync to source but let link work normally
|
|
7221
7711
|
const link = e.target.closest('a');
|
|
7222
7712
|
if (link) {
|
|
@@ -7233,8 +7723,23 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7233
7723
|
return;
|
|
7234
7724
|
}
|
|
7235
7725
|
|
|
7236
|
-
//
|
|
7237
|
-
|
|
7726
|
+
// Handle mermaid container clicks for post-fullscreen selection
|
|
7727
|
+
const mermaidContainer = e.target.closest('.mermaid-container');
|
|
7728
|
+
if (mermaidContainer) {
|
|
7729
|
+
// Check if this is a post-fullscreen click for selection
|
|
7730
|
+
if (window._mermaidSelectAfterClose === mermaidContainer) {
|
|
7731
|
+
window._mermaidSelectAfterClose = null;
|
|
7732
|
+
const { startLine, endLine } = findMermaidSourceLine(mermaidContainer);
|
|
7733
|
+
if (startLine > 0) {
|
|
7734
|
+
selectSourceRange(startLine, endLine, mermaidContainer);
|
|
7735
|
+
}
|
|
7736
|
+
}
|
|
7737
|
+
// Always return to prevent fullscreen from reopening or other handling
|
|
7738
|
+
return;
|
|
7739
|
+
}
|
|
7740
|
+
|
|
7741
|
+
// Ignore clicks on video overlay
|
|
7742
|
+
if (e.target.closest('.video-fullscreen-overlay')) return;
|
|
7238
7743
|
|
|
7239
7744
|
// Handle code blocks - select entire block
|
|
7240
7745
|
const pre = e.target.closest('pre');
|
|
@@ -7248,15 +7753,31 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7248
7753
|
return;
|
|
7249
7754
|
}
|
|
7250
7755
|
|
|
7251
|
-
const target = e.target.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th');
|
|
7756
|
+
const target = e.target.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th, summary');
|
|
7252
7757
|
if (!target) return;
|
|
7253
7758
|
|
|
7759
|
+
// Get text content, excluding toggle icon if present (for heading toggles)
|
|
7760
|
+
let searchText = target.textContent;
|
|
7761
|
+
|
|
7762
|
+
// For summary elements, clean up the text (may have disclosure triangle)
|
|
7763
|
+
if (target.tagName === 'SUMMARY') {
|
|
7764
|
+
searchText = searchText.replace(/^[▶▼◀►◆◇]?\\s*/, '').trim();
|
|
7765
|
+
}
|
|
7766
|
+
const toggleIcon = target.querySelector('.heading-toggle-icon');
|
|
7767
|
+
if (toggleIcon) {
|
|
7768
|
+
// Exclude the toggle icon text from search
|
|
7769
|
+
searchText = searchText.replace(toggleIcon.textContent, '').trim();
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7254
7772
|
// Use table-specific search for table cells, otherwise use element-aware search
|
|
7255
7773
|
const isTableCell = target.tagName === 'TD' || target.tagName === 'TH';
|
|
7256
|
-
const line = isTableCell ? findTableSourceLine(
|
|
7774
|
+
const line = isTableCell ? findTableSourceLine(searchText) : findSourceLine(searchText, target);
|
|
7257
7775
|
if (line <= 0) return;
|
|
7258
7776
|
|
|
7259
|
-
|
|
7777
|
+
// Don't prevent default for summary elements - let native <details> toggle work
|
|
7778
|
+
if (target.tagName !== 'SUMMARY') {
|
|
7779
|
+
e.preventDefault();
|
|
7780
|
+
}
|
|
7260
7781
|
selectSourceRange(line, null, target);
|
|
7261
7782
|
});
|
|
7262
7783
|
|
|
@@ -8090,6 +8611,8 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
8090
8611
|
if (req.method === "GET" && req.url.startsWith("/video-timeline?")) {
|
|
8091
8612
|
const urlParams = new URL(req.url, `http://localhost`);
|
|
8092
8613
|
const videoPath = urlParams.searchParams.get("path");
|
|
8614
|
+
const sceneThreshold = parseFloat(urlParams.searchParams.get("scene")) || 0.005;
|
|
8615
|
+
const stabilizationThreshold = parseFloat(urlParams.searchParams.get("stabilization")) || 0.5;
|
|
8093
8616
|
|
|
8094
8617
|
if (!videoPath) {
|
|
8095
8618
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
@@ -8148,7 +8671,7 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
8148
8671
|
// Ignore cleanup errors
|
|
8149
8672
|
}
|
|
8150
8673
|
});
|
|
8151
|
-
});
|
|
8674
|
+
}, { sceneThreshold, stabilizationThreshold });
|
|
8152
8675
|
return;
|
|
8153
8676
|
}
|
|
8154
8677
|
|