reviw 0.18.0 → 0.20.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 +607 -24
- 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,10 +37,23 @@ 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
|
|
43
|
-
const
|
|
40
|
+
function extractVideoTimeline(videoPath, tmpDir, res, onComplete, options = {}) {
|
|
41
|
+
// Use provided thresholds or defaults
|
|
42
|
+
const STABILIZATION_THRESHOLD = options.stabilizationThreshold || 0.1; // seconds - consecutive changes within this are grouped
|
|
43
|
+
const SCENE_THRESHOLD = options.sceneThreshold || 0.01; // Default matches "標準" button
|
|
44
|
+
// Determine target thumbnail count based on BOTH scene sensitivity and stabilization
|
|
45
|
+
// Scene: lower threshold = more detail = more thumbnails
|
|
46
|
+
const SCENE_BASE = SCENE_THRESHOLD >= 0.1 ? 3
|
|
47
|
+
: SCENE_THRESHOLD >= 0.03 ? 5
|
|
48
|
+
: SCENE_THRESHOLD >= 0.01 ? 8
|
|
49
|
+
: SCENE_THRESHOLD >= 0.003 ? 12
|
|
50
|
+
: 20;
|
|
51
|
+
// Stab: lower threshold = finer granularity = scale up target
|
|
52
|
+
const STAB_FACTOR = STABILIZATION_THRESHOLD >= 0.3 ? 0.6
|
|
53
|
+
: STABILIZATION_THRESHOLD >= 0.1 ? 1.0
|
|
54
|
+
: STABILIZATION_THRESHOLD >= 0.05 ? 1.4
|
|
55
|
+
: 1.8;
|
|
56
|
+
const TARGET_THUMBNAILS = Math.round(SCENE_BASE * STAB_FACTOR);
|
|
44
57
|
|
|
45
58
|
// First, get video duration using ffprobe
|
|
46
59
|
let videoDuration = 0;
|
|
@@ -150,14 +163,16 @@ function extractVideoTimeline(videoPath, tmpDir, res, onComplete) {
|
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
165
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
166
|
+
// Fill up to target thumbnail count with interval-based extraction
|
|
167
|
+
// This ensures scene sensitivity actually affects output even for gradient videos
|
|
168
|
+
if (videoDuration > 1 && allThumbnails.length < TARGET_THUMBNAILS) {
|
|
169
|
+
const needed = TARGET_THUMBNAILS - allThumbnails.length;
|
|
170
|
+
const intervalCount = needed + 1;
|
|
171
|
+
for (let i = 1; i <= needed; i++) {
|
|
158
172
|
const time = (videoDuration / intervalCount) * i;
|
|
159
173
|
// Check if we already have a thumbnail close to this time
|
|
160
|
-
const
|
|
174
|
+
const dedupThreshold = Math.min(STABILIZATION_THRESHOLD, 0.1);
|
|
175
|
+
const hasNearby = allThumbnails.some(t => Math.abs(t.time - time) < dedupThreshold);
|
|
161
176
|
if (!hasNearby) {
|
|
162
177
|
const intervalPath = `${tmpDir}/interval_${i}.jpg`;
|
|
163
178
|
const result = spawnSync('ffmpeg', [
|
|
@@ -181,7 +196,7 @@ function extractVideoTimeline(videoPath, tmpDir, res, onComplete) {
|
|
|
181
196
|
// Remove duplicates (same time within threshold)
|
|
182
197
|
const uniqueThumbnails = [];
|
|
183
198
|
for (const thumb of allThumbnails) {
|
|
184
|
-
const isDuplicate = uniqueThumbnails.some(t => Math.abs(t.time - thumb.time) < 0.1);
|
|
199
|
+
const isDuplicate = uniqueThumbnails.some(t => Math.abs(t.time - thumb.time) < Math.min(STABILIZATION_THRESHOLD, 0.1));
|
|
185
200
|
if (!isDuplicate) {
|
|
186
201
|
uniqueThumbnails.push(thumb);
|
|
187
202
|
}
|
|
@@ -373,6 +388,13 @@ function sanitizeHtml(html) {
|
|
|
373
388
|
});
|
|
374
389
|
}
|
|
375
390
|
|
|
391
|
+
// [TDD Fix] 重複見出しキーを防ぐためのカウンター
|
|
392
|
+
// 同じテキストの見出しが複数ある場合でも、ユニークなキーを生成する
|
|
393
|
+
var headingKeyCounter = {};
|
|
394
|
+
function resetHeadingKeyCounter() {
|
|
395
|
+
headingKeyCounter = {};
|
|
396
|
+
}
|
|
397
|
+
|
|
376
398
|
marked.use({
|
|
377
399
|
hooks: {
|
|
378
400
|
// テーブルをスクロールラッパーで囲む(後処理)
|
|
@@ -382,6 +404,21 @@ marked.use({
|
|
|
382
404
|
}
|
|
383
405
|
},
|
|
384
406
|
renderer: {
|
|
407
|
+
// 見出しをトグル可能なセクションとしてレンダリング
|
|
408
|
+
// markedのrenderer.headingは (text, level, raw) を引数に取る
|
|
409
|
+
heading: function(text, level, raw) {
|
|
410
|
+
// トグル用のdata属性を追加(見出しテキスト + 出現順でユニークキーを生成)
|
|
411
|
+
var baseKey = (text || '').replace(/[^a-zA-Z0-9\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\u3400-\u4DBF]/g, '-').toLowerCase();
|
|
412
|
+
// 同じbaseKeyが出現した回数をカウントしてユニーク化
|
|
413
|
+
if (headingKeyCounter[baseKey] === undefined) {
|
|
414
|
+
headingKeyCounter[baseKey] = 0;
|
|
415
|
+
}
|
|
416
|
+
var headingKey = baseKey + '-' + headingKeyCounter[baseKey]++;
|
|
417
|
+
return '<h' + level + ' class="md-heading-toggle" data-heading-key="' + escapeHtmlForXss(headingKey) + '" data-heading-level="' + level + '">' +
|
|
418
|
+
'<span class="heading-toggle-icon">▼</span>' +
|
|
419
|
+
text +
|
|
420
|
+
'</h' + level + '>';
|
|
421
|
+
},
|
|
385
422
|
// 生HTMLブロックをサニタイズ
|
|
386
423
|
html: function(token) {
|
|
387
424
|
var text = token.raw || token.text || token;
|
|
@@ -516,6 +553,24 @@ function parseCliArgs(argv) {
|
|
|
516
553
|
}
|
|
517
554
|
|
|
518
555
|
// ===== ファイルパス検証・解決関数(require.main時のみ呼ばれる) =====
|
|
556
|
+
// バイナリファイル拡張子(レビュー対象外)
|
|
557
|
+
const BINARY_EXTENSIONS = new Set([
|
|
558
|
+
// 動画
|
|
559
|
+
'.mp4', '.mov', '.webm', '.avi', '.mkv', '.m4v', '.ogv', '.wmv', '.flv',
|
|
560
|
+
// 画像
|
|
561
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.tiff', '.tif',
|
|
562
|
+
// 音声
|
|
563
|
+
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma',
|
|
564
|
+
// アーカイブ
|
|
565
|
+
'.zip', '.tar', '.gz', '.bz2', '.xz', '.7z', '.rar',
|
|
566
|
+
// バイナリ実行
|
|
567
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a',
|
|
568
|
+
// その他バイナリ
|
|
569
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
570
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
571
|
+
'.sqlite', '.db'
|
|
572
|
+
]);
|
|
573
|
+
|
|
519
574
|
function validateAndResolvePaths(filePaths) {
|
|
520
575
|
const resolved = [];
|
|
521
576
|
for (const fp of filePaths) {
|
|
@@ -531,6 +586,13 @@ function validateAndResolvePaths(filePaths) {
|
|
|
531
586
|
console.error(`Please specify a file, not a directory.`);
|
|
532
587
|
process.exit(1);
|
|
533
588
|
}
|
|
589
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
590
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
591
|
+
console.error(`Error: Binary file cannot be reviewed: ${path.basename(resolvedPath)} (${ext})`);
|
|
592
|
+
console.error(`reviw supports: .csv, .tsv, .md, .diff, .patch, and text files.`);
|
|
593
|
+
console.error(`Tip: To review videos/images, embed them in a Markdown file using `);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
534
596
|
resolved.push(resolvedPath);
|
|
535
597
|
}
|
|
536
598
|
return resolved;
|
|
@@ -862,6 +924,9 @@ function loadText(filePath) {
|
|
|
862
924
|
}
|
|
863
925
|
|
|
864
926
|
function loadMarkdown(filePath) {
|
|
927
|
+
// [TDD Fix] カウンターをリセットして新しいMarkdownファイルの見出しキーを初期化
|
|
928
|
+
resetHeadingKeyCounter();
|
|
929
|
+
|
|
865
930
|
const raw = fs.readFileSync(filePath);
|
|
866
931
|
const text = decodeBuffer(raw);
|
|
867
932
|
const lines = text.split(/\r?\n/);
|
|
@@ -1050,6 +1115,16 @@ function loadData(filePath) {
|
|
|
1050
1115
|
);
|
|
1051
1116
|
}
|
|
1052
1117
|
const ext = path.extname(filePath).toLowerCase();
|
|
1118
|
+
|
|
1119
|
+
// バイナリファイルはレビュー対象外 - エラーで拒否(validateAndResolvePathsでも弾くが二重チェック)
|
|
1120
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`Binary file cannot be reviewed: ${path.basename(filePath)} (${ext})\n` +
|
|
1123
|
+
`reviw supports: .csv, .tsv, .md, .diff, .patch, and text files.\n` +
|
|
1124
|
+
`Tip: To review videos/images, embed them in a Markdown file using `
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1053
1128
|
if (ext === ".csv" || ext === ".tsv") {
|
|
1054
1129
|
const data = loadCsv(filePath);
|
|
1055
1130
|
return { ...data, mode: "csv" };
|
|
@@ -3020,6 +3095,35 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3020
3095
|
.md-preview h1, .md-preview h2, .md-preview h3, .md-preview h4 {
|
|
3021
3096
|
margin: 0.4em 0 0.2em;
|
|
3022
3097
|
}
|
|
3098
|
+
/* Heading toggle feature */
|
|
3099
|
+
.md-preview .md-heading-toggle {
|
|
3100
|
+
display: flex;
|
|
3101
|
+
align-items: center;
|
|
3102
|
+
gap: 6px;
|
|
3103
|
+
}
|
|
3104
|
+
.md-preview .heading-toggle-icon {
|
|
3105
|
+
font-size: 0.6em;
|
|
3106
|
+
transition: transform 150ms ease;
|
|
3107
|
+
color: var(--muted);
|
|
3108
|
+
flex-shrink: 0;
|
|
3109
|
+
cursor: pointer;
|
|
3110
|
+
padding: 4px 8px;
|
|
3111
|
+
margin: -4px 0 -4px -8px;
|
|
3112
|
+
border-radius: 4px;
|
|
3113
|
+
}
|
|
3114
|
+
.md-preview .heading-toggle-icon:hover {
|
|
3115
|
+
background: var(--hover-bg);
|
|
3116
|
+
color: var(--accent);
|
|
3117
|
+
}
|
|
3118
|
+
.md-preview .md-heading-toggle.collapsed .heading-toggle-icon {
|
|
3119
|
+
transform: rotate(-90deg);
|
|
3120
|
+
}
|
|
3121
|
+
.md-preview .heading-section-content {
|
|
3122
|
+
/* Content wrapper for collapsible sections */
|
|
3123
|
+
}
|
|
3124
|
+
.md-preview .heading-section-content.hidden {
|
|
3125
|
+
display: none;
|
|
3126
|
+
}
|
|
3023
3127
|
.md-preview p { margin: 0.3em 0; line-height: 1.5; }
|
|
3024
3128
|
.md-preview img { max-width: 100%; height: auto; border-radius: 8px; }
|
|
3025
3129
|
.md-preview video.video-preview { max-width: 100%; height: auto; border-radius: 8px; background: #000; }
|
|
@@ -3425,6 +3529,104 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
3425
3529
|
position: relative;
|
|
3426
3530
|
flex-shrink: 0;
|
|
3427
3531
|
}
|
|
3532
|
+
/* Video threshold settings panel */
|
|
3533
|
+
.video-settings-btn {
|
|
3534
|
+
position: absolute;
|
|
3535
|
+
top: 14px;
|
|
3536
|
+
right: 64px;
|
|
3537
|
+
width: 40px;
|
|
3538
|
+
height: 40px;
|
|
3539
|
+
display: flex;
|
|
3540
|
+
align-items: center;
|
|
3541
|
+
justify-content: center;
|
|
3542
|
+
background: rgba(0, 0, 0, 0.55);
|
|
3543
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
3544
|
+
border-radius: 50%;
|
|
3545
|
+
cursor: pointer;
|
|
3546
|
+
color: #fff;
|
|
3547
|
+
font-size: 18px;
|
|
3548
|
+
z-index: 10;
|
|
3549
|
+
backdrop-filter: blur(4px);
|
|
3550
|
+
transition: background 120ms ease, transform 120ms ease;
|
|
3551
|
+
}
|
|
3552
|
+
.video-settings-btn:hover {
|
|
3553
|
+
background: rgba(0, 0, 0, 0.75);
|
|
3554
|
+
transform: scale(1.04);
|
|
3555
|
+
}
|
|
3556
|
+
.video-settings-panel {
|
|
3557
|
+
position: absolute;
|
|
3558
|
+
top: 60px;
|
|
3559
|
+
right: 14px;
|
|
3560
|
+
background: rgba(0, 0, 0, 0.9);
|
|
3561
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
3562
|
+
border-radius: 12px;
|
|
3563
|
+
padding: 16px;
|
|
3564
|
+
z-index: 15;
|
|
3565
|
+
min-width: 280px;
|
|
3566
|
+
display: none;
|
|
3567
|
+
backdrop-filter: blur(8px);
|
|
3568
|
+
}
|
|
3569
|
+
.video-settings-panel.visible {
|
|
3570
|
+
display: block;
|
|
3571
|
+
}
|
|
3572
|
+
.video-settings-panel h4 {
|
|
3573
|
+
margin: 0 0 8px;
|
|
3574
|
+
color: #fff;
|
|
3575
|
+
font-size: 14px;
|
|
3576
|
+
font-weight: 500;
|
|
3577
|
+
}
|
|
3578
|
+
.video-settings-desc {
|
|
3579
|
+
margin: 0 0 12px;
|
|
3580
|
+
color: rgba(255, 255, 255, 0.6);
|
|
3581
|
+
font-size: 11px;
|
|
3582
|
+
line-height: 1.4;
|
|
3583
|
+
}
|
|
3584
|
+
.video-settings-buttons {
|
|
3585
|
+
display: flex;
|
|
3586
|
+
gap: 6px;
|
|
3587
|
+
margin-bottom: 12px;
|
|
3588
|
+
}
|
|
3589
|
+
.video-settings-buttons button {
|
|
3590
|
+
flex: 1;
|
|
3591
|
+
padding: 8px 4px;
|
|
3592
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
3593
|
+
border-radius: 6px;
|
|
3594
|
+
background: rgba(255, 255, 255, 0.1);
|
|
3595
|
+
color: rgba(255, 255, 255, 0.8);
|
|
3596
|
+
font-size: 11px;
|
|
3597
|
+
cursor: pointer;
|
|
3598
|
+
transition: all 120ms ease;
|
|
3599
|
+
}
|
|
3600
|
+
.video-settings-buttons button:hover {
|
|
3601
|
+
background: rgba(255, 255, 255, 0.2);
|
|
3602
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
3603
|
+
}
|
|
3604
|
+
.video-settings-buttons button.selected {
|
|
3605
|
+
background: #3b82f6;
|
|
3606
|
+
border-color: #3b82f6;
|
|
3607
|
+
color: #fff;
|
|
3608
|
+
}
|
|
3609
|
+
.video-settings-actions {
|
|
3610
|
+
display: flex;
|
|
3611
|
+
gap: 8px;
|
|
3612
|
+
margin-top: 8px;
|
|
3613
|
+
}
|
|
3614
|
+
.video-settings-actions button {
|
|
3615
|
+
flex: 1;
|
|
3616
|
+
padding: 8px 12px;
|
|
3617
|
+
border: none;
|
|
3618
|
+
border-radius: 6px;
|
|
3619
|
+
font-size: 12px;
|
|
3620
|
+
cursor: pointer;
|
|
3621
|
+
transition: background 120ms ease;
|
|
3622
|
+
}
|
|
3623
|
+
.video-settings-actions .reset-btn {
|
|
3624
|
+
background: rgba(255, 255, 255, 0.15);
|
|
3625
|
+
color: #fff;
|
|
3626
|
+
}
|
|
3627
|
+
.video-settings-actions .reset-btn:hover {
|
|
3628
|
+
background: rgba(255, 255, 255, 0.25);
|
|
3629
|
+
}
|
|
3428
3630
|
/* Video Shortcuts Help */
|
|
3429
3631
|
.video-shortcuts-help {
|
|
3430
3632
|
opacity: 0.85;
|
|
@@ -4358,6 +4560,26 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
4358
4560
|
</div>
|
|
4359
4561
|
<div class="video-fullscreen-overlay" id="video-fullscreen">
|
|
4360
4562
|
<button class="video-close-btn" id="video-close" aria-label="Close video" title="Close (ESC)">✕</button>
|
|
4563
|
+
<button class="video-settings-btn" id="video-settings-btn" aria-label="Timeline settings" title="Timeline settings">⚙</button>
|
|
4564
|
+
<div class="video-settings-panel" id="video-settings-panel">
|
|
4565
|
+
<h4>タイムライン設定</h4>
|
|
4566
|
+
<p class="video-settings-desc">シーン感度: 映像変化の検出しきい値</p>
|
|
4567
|
+
<div class="video-settings-buttons" id="scene-buttons">
|
|
4568
|
+
<button data-scene="0.3">少なめ</button>
|
|
4569
|
+
<button data-scene="0.1">やや少</button>
|
|
4570
|
+
<button data-scene="0.01" class="selected">標準</button>
|
|
4571
|
+
<button data-scene="0.003">やや多</button>
|
|
4572
|
+
<button data-scene="0.001">多め</button>
|
|
4573
|
+
</div>
|
|
4574
|
+
<p class="video-settings-desc">グルーピング: 連続する変化をまとめる秒数</p>
|
|
4575
|
+
<div class="video-settings-buttons" id="stab-buttons">
|
|
4576
|
+
<button data-stab="0.5">0.5s</button>
|
|
4577
|
+
<button data-stab="0.3">0.3s</button>
|
|
4578
|
+
<button data-stab="0.1" class="selected">0.1s</button>
|
|
4579
|
+
<button data-stab="0.05">0.05s</button>
|
|
4580
|
+
<button data-stab="0.02">0.02s</button>
|
|
4581
|
+
</div>
|
|
4582
|
+
</div>
|
|
4361
4583
|
<div class="video-container" id="video-container"></div>
|
|
4362
4584
|
</div>
|
|
4363
4585
|
|
|
@@ -6083,8 +6305,17 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6083
6305
|
let startX, startY;
|
|
6084
6306
|
let svgNaturalWidth = 0, svgNaturalHeight = 0;
|
|
6085
6307
|
let minimapScale = 1;
|
|
6308
|
+
let currentMermaidContainer = null; // Store the container for selection after close
|
|
6309
|
+
let skipNextFullscreenOpen = false; // Flag to skip opening fullscreen after close
|
|
6086
6310
|
|
|
6087
6311
|
function openFullscreen(mermaidEl) {
|
|
6312
|
+
// Skip if this is triggered by the post-close click
|
|
6313
|
+
if (skipNextFullscreenOpen) {
|
|
6314
|
+
skipNextFullscreenOpen = false;
|
|
6315
|
+
return;
|
|
6316
|
+
}
|
|
6317
|
+
// Store the container for selection when fullscreen closes
|
|
6318
|
+
currentMermaidContainer = mermaidEl.closest('.mermaid-container');
|
|
6088
6319
|
const svg = mermaidEl.querySelector('svg');
|
|
6089
6320
|
if (!svg) return;
|
|
6090
6321
|
fsWrapper.innerHTML = '';
|
|
@@ -6146,6 +6377,17 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6146
6377
|
|
|
6147
6378
|
function closeFullscreen() {
|
|
6148
6379
|
fsOverlay.classList.remove('visible');
|
|
6380
|
+
// Trigger selection of the mermaid container after closing fullscreen
|
|
6381
|
+
if (currentMermaidContainer) {
|
|
6382
|
+
const containerToSelect = currentMermaidContainer;
|
|
6383
|
+
currentMermaidContainer = null;
|
|
6384
|
+
// Set flags to skip reopening fullscreen and trigger selection
|
|
6385
|
+
setTimeout(() => {
|
|
6386
|
+
skipNextFullscreenOpen = true;
|
|
6387
|
+
window._mermaidSelectAfterClose = containerToSelect;
|
|
6388
|
+
containerToSelect.click();
|
|
6389
|
+
}, 100);
|
|
6390
|
+
}
|
|
6149
6391
|
}
|
|
6150
6392
|
|
|
6151
6393
|
function updateTransform() {
|
|
@@ -6375,6 +6617,82 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6375
6617
|
}
|
|
6376
6618
|
})();
|
|
6377
6619
|
|
|
6620
|
+
// --- Heading Toggle ---
|
|
6621
|
+
(function initHeadingToggle() {
|
|
6622
|
+
const preview = document.querySelector('.md-preview');
|
|
6623
|
+
if (!preview) return;
|
|
6624
|
+
|
|
6625
|
+
const STORAGE_PREFIX = 'reviw-heading-toggle-';
|
|
6626
|
+
|
|
6627
|
+
// Get all headings
|
|
6628
|
+
const headings = Array.from(preview.querySelectorAll('.md-heading-toggle'));
|
|
6629
|
+
if (headings.length === 0) return;
|
|
6630
|
+
|
|
6631
|
+
// Wrap content between headings into sections
|
|
6632
|
+
headings.forEach((heading, idx) => {
|
|
6633
|
+
const level = parseInt(heading.dataset.headingLevel) || 1;
|
|
6634
|
+
const key = heading.dataset.headingKey;
|
|
6635
|
+
|
|
6636
|
+
// Collect all siblings until next heading of same or higher level
|
|
6637
|
+
const content = [];
|
|
6638
|
+
let sibling = heading.nextElementSibling;
|
|
6639
|
+
|
|
6640
|
+
while (sibling) {
|
|
6641
|
+
// Check if it's a heading of same or higher level
|
|
6642
|
+
if (sibling.classList.contains('md-heading-toggle')) {
|
|
6643
|
+
const siblingLevel = parseInt(sibling.dataset.headingLevel) || 1;
|
|
6644
|
+
if (siblingLevel <= level) break;
|
|
6645
|
+
}
|
|
6646
|
+
content.push(sibling);
|
|
6647
|
+
sibling = sibling.nextElementSibling;
|
|
6648
|
+
}
|
|
6649
|
+
|
|
6650
|
+
// Create wrapper for content
|
|
6651
|
+
if (content.length > 0) {
|
|
6652
|
+
const wrapper = document.createElement('div');
|
|
6653
|
+
wrapper.className = 'heading-section-content';
|
|
6654
|
+
wrapper.dataset.forHeading = key;
|
|
6655
|
+
|
|
6656
|
+
// Move content into wrapper
|
|
6657
|
+
const firstContent = content[0];
|
|
6658
|
+
heading.parentNode.insertBefore(wrapper, firstContent);
|
|
6659
|
+
content.forEach(el => wrapper.appendChild(el));
|
|
6660
|
+
|
|
6661
|
+
// Restore state from localStorage
|
|
6662
|
+
const savedState = localStorage.getItem(STORAGE_PREFIX + key);
|
|
6663
|
+
if (savedState === 'collapsed') {
|
|
6664
|
+
heading.classList.add('collapsed');
|
|
6665
|
+
wrapper.classList.add('hidden');
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
// Add click handler to toggle icon only (not the whole heading)
|
|
6670
|
+
// This allows the heading text to remain selectable for comments
|
|
6671
|
+
const toggleIcon = heading.querySelector('.heading-toggle-icon');
|
|
6672
|
+
if (toggleIcon) {
|
|
6673
|
+
toggleIcon.addEventListener('click', (e) => {
|
|
6674
|
+
e.stopPropagation(); // Prevent triggering comment selection
|
|
6675
|
+
const wrapper = preview.querySelector(\`.heading-section-content[data-for-heading="\${key}"]\`);
|
|
6676
|
+
if (!wrapper) return;
|
|
6677
|
+
|
|
6678
|
+
const isCollapsed = heading.classList.toggle('collapsed');
|
|
6679
|
+
wrapper.classList.toggle('hidden', isCollapsed);
|
|
6680
|
+
|
|
6681
|
+
// Save state to localStorage
|
|
6682
|
+
localStorage.setItem(STORAGE_PREFIX + key, isCollapsed ? 'collapsed' : 'expanded');
|
|
6683
|
+
|
|
6684
|
+
// Re-render Mermaid diagrams when expanding (they may not render while hidden)
|
|
6685
|
+
if (!isCollapsed && typeof mermaid !== 'undefined') {
|
|
6686
|
+
const mermaidDivs = wrapper.querySelectorAll('.mermaid');
|
|
6687
|
+
if (mermaidDivs.length > 0) {
|
|
6688
|
+
mermaid.run({ nodes: Array.from(mermaidDivs) }).catch(() => {});
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
});
|
|
6692
|
+
}
|
|
6693
|
+
});
|
|
6694
|
+
})();
|
|
6695
|
+
|
|
6378
6696
|
// --- Image Fullscreen ---
|
|
6379
6697
|
(function initImageFullscreen() {
|
|
6380
6698
|
const preview = document.querySelector('.md-preview');
|
|
@@ -6526,8 +6844,23 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6526
6844
|
return mins + ':' + (secs < 10 ? '0' : '') + secs;
|
|
6527
6845
|
}
|
|
6528
6846
|
|
|
6847
|
+
// Default threshold values
|
|
6848
|
+
const DEFAULT_SCENE_THRESHOLD = 0.01;
|
|
6849
|
+
const DEFAULT_STABILIZATION_THRESHOLD = 0.1;
|
|
6850
|
+
let currentSceneThreshold = DEFAULT_SCENE_THRESHOLD;
|
|
6851
|
+
let currentStabilizationThreshold = DEFAULT_STABILIZATION_THRESHOLD;
|
|
6852
|
+
let currentVideoPath = null;
|
|
6853
|
+
let currentVideo = null;
|
|
6854
|
+
|
|
6529
6855
|
// Load video timeline via SSE
|
|
6530
|
-
function loadVideoTimeline(videoPath, video) {
|
|
6856
|
+
function loadVideoTimeline(videoPath, video, options = {}) {
|
|
6857
|
+
const sceneThreshold = options.sceneThreshold || currentSceneThreshold;
|
|
6858
|
+
const stabilizationThreshold = options.stabilizationThreshold || currentStabilizationThreshold;
|
|
6859
|
+
|
|
6860
|
+
// Store for regeneration
|
|
6861
|
+
currentVideoPath = videoPath;
|
|
6862
|
+
currentVideo = video;
|
|
6863
|
+
|
|
6531
6864
|
// Close existing connection
|
|
6532
6865
|
if (currentTimelineEventSource) {
|
|
6533
6866
|
currentTimelineEventSource.close();
|
|
@@ -6552,9 +6885,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6552
6885
|
|
|
6553
6886
|
videoContainer.appendChild(timeline);
|
|
6554
6887
|
|
|
6555
|
-
// Start SSE connection
|
|
6888
|
+
// Start SSE connection with threshold parameters
|
|
6556
6889
|
const encodedPath = encodeURIComponent(videoPath);
|
|
6557
|
-
const es = new EventSource(
|
|
6890
|
+
const es = new EventSource(\`/video-timeline?path=\${encodedPath}&scene=\${sceneThreshold}&stabilization=\${stabilizationThreshold}\`);
|
|
6558
6891
|
currentTimelineEventSource = es;
|
|
6559
6892
|
|
|
6560
6893
|
es.onmessage = function(e) {
|
|
@@ -6648,6 +6981,11 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6648
6981
|
}
|
|
6649
6982
|
|
|
6650
6983
|
function showVideo(index) {
|
|
6984
|
+
// Skip if this is triggered by the post-close click
|
|
6985
|
+
if (window._skipNextVideoFullscreen) {
|
|
6986
|
+
window._skipNextVideoFullscreen = false;
|
|
6987
|
+
return;
|
|
6988
|
+
}
|
|
6651
6989
|
if (index < 0 || index >= allVideoSources.length) return;
|
|
6652
6990
|
currentVideoIndex = index;
|
|
6653
6991
|
const source = allVideoSources[index];
|
|
@@ -6730,6 +7068,12 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6730
7068
|
|
|
6731
7069
|
function closeVideoOverlay() {
|
|
6732
7070
|
videoOverlay.classList.remove('visible');
|
|
7071
|
+
|
|
7072
|
+
// Save source element before resetting index (for triggering selection)
|
|
7073
|
+
const closedSource = currentVideoIndex >= 0 && currentVideoIndex < allVideoSources.length
|
|
7074
|
+
? allVideoSources[currentVideoIndex]
|
|
7075
|
+
: null;
|
|
7076
|
+
|
|
6733
7077
|
currentVideoIndex = -1;
|
|
6734
7078
|
|
|
6735
7079
|
// Close timeline SSE connection
|
|
@@ -6747,6 +7091,18 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6747
7091
|
video.remove();
|
|
6748
7092
|
}
|
|
6749
7093
|
|
|
7094
|
+
// Trigger selection on the source element's parent table cell (if any)
|
|
7095
|
+
if (closedSource && closedSource.element) {
|
|
7096
|
+
const parentCell = closedSource.element.closest('td, th');
|
|
7097
|
+
if (parentCell) {
|
|
7098
|
+
// Set global flag to skip reopening fullscreen, then trigger click for selection
|
|
7099
|
+
setTimeout(() => {
|
|
7100
|
+
window._skipNextVideoFullscreen = true;
|
|
7101
|
+
parentCell.click();
|
|
7102
|
+
}, 100);
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
|
|
6750
7106
|
// Remove timeline
|
|
6751
7107
|
const timeline = videoContainer.querySelector('.video-timeline');
|
|
6752
7108
|
if (timeline) timeline.remove();
|
|
@@ -6931,6 +7287,76 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6931
7287
|
});
|
|
6932
7288
|
}
|
|
6933
7289
|
});
|
|
7290
|
+
|
|
7291
|
+
// --- Video Settings Panel ---
|
|
7292
|
+
const settingsBtn = document.getElementById('video-settings-btn');
|
|
7293
|
+
const settingsPanel = document.getElementById('video-settings-panel');
|
|
7294
|
+
const sceneButtons = document.getElementById('scene-buttons');
|
|
7295
|
+
const stabButtons = document.getElementById('stab-buttons');
|
|
7296
|
+
|
|
7297
|
+
// Auto-regenerate function triggered by button clicks
|
|
7298
|
+
function triggerRegenerate() {
|
|
7299
|
+
if (!currentVideoPath || !currentVideo) return;
|
|
7300
|
+
|
|
7301
|
+
const selectedScene = sceneButtons?.querySelector('button.selected');
|
|
7302
|
+
if (selectedScene) {
|
|
7303
|
+
currentSceneThreshold = parseFloat(selectedScene.dataset.scene) || DEFAULT_SCENE_THRESHOLD;
|
|
7304
|
+
}
|
|
7305
|
+
|
|
7306
|
+
const selectedStab = stabButtons?.querySelector('button.selected');
|
|
7307
|
+
if (selectedStab) {
|
|
7308
|
+
currentStabilizationThreshold = parseFloat(selectedStab.dataset.stab) || DEFAULT_STABILIZATION_THRESHOLD;
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7311
|
+
loadVideoTimeline(currentVideoPath, currentVideo, {
|
|
7312
|
+
sceneThreshold: currentSceneThreshold,
|
|
7313
|
+
stabilizationThreshold: currentStabilizationThreshold
|
|
7314
|
+
});
|
|
7315
|
+
}
|
|
7316
|
+
|
|
7317
|
+
if (settingsBtn && settingsPanel) {
|
|
7318
|
+
// Toggle settings panel
|
|
7319
|
+
settingsBtn.addEventListener('click', (e) => {
|
|
7320
|
+
e.stopPropagation();
|
|
7321
|
+
settingsPanel.classList.toggle('visible');
|
|
7322
|
+
});
|
|
7323
|
+
|
|
7324
|
+
// Prevent panel clicks from closing overlay
|
|
7325
|
+
settingsPanel.addEventListener('click', (e) => e.stopPropagation());
|
|
7326
|
+
|
|
7327
|
+
// Handle scene button clicks (auto-regenerate)
|
|
7328
|
+
if (sceneButtons) {
|
|
7329
|
+
const buttons = sceneButtons.querySelectorAll('button');
|
|
7330
|
+
buttons.forEach(btn => {
|
|
7331
|
+
btn.addEventListener('click', () => {
|
|
7332
|
+
buttons.forEach(b => b.classList.remove('selected'));
|
|
7333
|
+
btn.classList.add('selected');
|
|
7334
|
+
// Auto-regenerate on click
|
|
7335
|
+
triggerRegenerate();
|
|
7336
|
+
});
|
|
7337
|
+
});
|
|
7338
|
+
}
|
|
7339
|
+
|
|
7340
|
+
// Handle stab button clicks (auto-regenerate)
|
|
7341
|
+
if (stabButtons) {
|
|
7342
|
+
const buttons = stabButtons.querySelectorAll('button');
|
|
7343
|
+
buttons.forEach(btn => {
|
|
7344
|
+
btn.addEventListener('click', () => {
|
|
7345
|
+
buttons.forEach(b => b.classList.remove('selected'));
|
|
7346
|
+
btn.classList.add('selected');
|
|
7347
|
+
// Auto-regenerate on click
|
|
7348
|
+
triggerRegenerate();
|
|
7349
|
+
});
|
|
7350
|
+
});
|
|
7351
|
+
}
|
|
7352
|
+
|
|
7353
|
+
// Close panel when overlay closes
|
|
7354
|
+
const originalCloseVideoOverlay = closeVideoOverlay;
|
|
7355
|
+
closeVideoOverlay = function() {
|
|
7356
|
+
settingsPanel.classList.remove('visible');
|
|
7357
|
+
originalCloseVideoOverlay();
|
|
7358
|
+
};
|
|
7359
|
+
}
|
|
6934
7360
|
})();
|
|
6935
7361
|
|
|
6936
7362
|
// --- Preview Commenting ---
|
|
@@ -6946,11 +7372,16 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
6946
7372
|
.md-preview > p:hover, .md-preview > h1:hover, .md-preview > h2:hover,
|
|
6947
7373
|
.md-preview > h3:hover, .md-preview > h4:hover, .md-preview > h5:hover,
|
|
6948
7374
|
.md-preview > h6:hover, .md-preview > ul > li:hover, .md-preview > ol > li:hover,
|
|
6949
|
-
.md-preview > pre:hover, .md-preview > blockquote:hover
|
|
7375
|
+
.md-preview > pre:hover, .md-preview > blockquote:hover,
|
|
7376
|
+
.md-preview details summary:hover {
|
|
6950
7377
|
background: rgba(99, 102, 241, 0.08);
|
|
6951
7378
|
cursor: pointer;
|
|
6952
7379
|
border-radius: 4px;
|
|
6953
7380
|
}
|
|
7381
|
+
.md-preview td:hover, .md-preview th:hover {
|
|
7382
|
+
background: rgba(99, 102, 241, 0.12);
|
|
7383
|
+
cursor: pointer;
|
|
7384
|
+
}
|
|
6954
7385
|
.md-preview img:hover {
|
|
6955
7386
|
outline: 2px solid var(--accent);
|
|
6956
7387
|
cursor: pointer;
|
|
@@ -7013,7 +7444,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7013
7444
|
}
|
|
7014
7445
|
|
|
7015
7446
|
if (!text) return -1;
|
|
7016
|
-
|
|
7447
|
+
// Remove toggle icon characters (▼, ▶) that may be included from heading toggles
|
|
7448
|
+
const cleanText = text.replace(/[▼▶]/g, '').trim();
|
|
7449
|
+
const normalized = cleanText.replace(/\\s+/g, ' ').slice(0, 100);
|
|
7017
7450
|
if (!normalized) return -1;
|
|
7018
7451
|
|
|
7019
7452
|
for (let i = 0; i < DATA.length; i++) {
|
|
@@ -7033,6 +7466,19 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7033
7466
|
}
|
|
7034
7467
|
}
|
|
7035
7468
|
|
|
7469
|
+
// Check for HTML summary tags: <summary>text</summary>
|
|
7470
|
+
const summaryMatch = lineText.match(/<summary>([^<]*)<\\/summary>/i);
|
|
7471
|
+
if (summaryMatch) {
|
|
7472
|
+
const summaryText = summaryMatch[1].trim();
|
|
7473
|
+
if (summaryText === normalized || summaryText.toLowerCase() === normalized.toLowerCase()) {
|
|
7474
|
+
return i + 1;
|
|
7475
|
+
}
|
|
7476
|
+
// Partial match for long summary text
|
|
7477
|
+
if (summaryText.includes(normalized.slice(0, 30)) && normalized.length > 5) {
|
|
7478
|
+
return i + 1;
|
|
7479
|
+
}
|
|
7480
|
+
}
|
|
7481
|
+
|
|
7036
7482
|
// Try stripping all markdown formatting (links, bold, italic, etc.)
|
|
7037
7483
|
const strippedLine = stripMarkdown(lineText).replace(/\\s+/g, ' ').slice(0, 100);
|
|
7038
7484
|
if (strippedLine === normalized) return i + 1;
|
|
@@ -7045,10 +7491,31 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7045
7491
|
// Helper: find matching source line for table cell (prioritizes table rows)
|
|
7046
7492
|
function findTableSourceLine(text) {
|
|
7047
7493
|
if (!text) return -1;
|
|
7048
|
-
|
|
7494
|
+
// Remove toggle icon characters (▼, ▶) that may be included from heading toggles
|
|
7495
|
+
const cleanText = text.replace(/[▼▶]/g, '').trim();
|
|
7496
|
+
const normalized = cleanText.replace(/\\s+/g, ' ').slice(0, 100);
|
|
7049
7497
|
if (!normalized) return -1;
|
|
7050
7498
|
|
|
7051
|
-
// First pass: look for
|
|
7499
|
+
// First pass: look for EXACT cell text match (not inside markdown syntax)
|
|
7500
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
7501
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
7502
|
+
if (!lineText || !lineText.startsWith('|')) continue;
|
|
7503
|
+
|
|
7504
|
+
// Split into cells and check for exact match (excluding markdown syntax)
|
|
7505
|
+
const cells = lineText.split('|').map(c => c.trim());
|
|
7506
|
+
for (const cell of cells) {
|
|
7507
|
+
// Skip cells that are markdown images/links (start with ![, contain []())
|
|
7508
|
+
if (cell.match(/^!?\\[.*\\]\\(.*\\)$/)) continue;
|
|
7509
|
+
|
|
7510
|
+
// Check for exact cell text match
|
|
7511
|
+
if (cell === normalized) return i + 1;
|
|
7512
|
+
|
|
7513
|
+
// For short text (like header cells), require exact word match
|
|
7514
|
+
if (normalized.length <= 5 && cell === normalized) return i + 1;
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
|
|
7518
|
+
// Second pass: look for partial matches (including inside markdown syntax)
|
|
7052
7519
|
for (let i = 0; i < DATA.length; i++) {
|
|
7053
7520
|
const lineText = (DATA[i][0] || '').trim();
|
|
7054
7521
|
if (!lineText || !lineText.startsWith('|')) continue;
|
|
@@ -7137,6 +7604,51 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7137
7604
|
return { startLine: -1, endLine: -1 };
|
|
7138
7605
|
}
|
|
7139
7606
|
|
|
7607
|
+
// Helper: find source line range for mermaid diagram by its container
|
|
7608
|
+
function findMermaidSourceLine(mermaidContainer) {
|
|
7609
|
+
if (!mermaidContainer) return { startLine: -1, endLine: -1 };
|
|
7610
|
+
|
|
7611
|
+
// Find the index of this mermaid container among all mermaid containers
|
|
7612
|
+
const allMermaidContainers = document.querySelectorAll('.mermaid-container');
|
|
7613
|
+
let containerIndex = -1;
|
|
7614
|
+
for (let i = 0; i < allMermaidContainers.length; i++) {
|
|
7615
|
+
if (allMermaidContainers[i] === mermaidContainer) {
|
|
7616
|
+
containerIndex = i;
|
|
7617
|
+
break;
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
if (containerIndex < 0) return { startLine: -1, endLine: -1 };
|
|
7621
|
+
|
|
7622
|
+
// Find all mermaid code blocks in the source
|
|
7623
|
+
const mermaidBlocks = [];
|
|
7624
|
+
let currentBlock = null;
|
|
7625
|
+
|
|
7626
|
+
for (let i = 0; i < DATA.length; i++) {
|
|
7627
|
+
const lineText = (DATA[i][0] || '').trim();
|
|
7628
|
+
|
|
7629
|
+
if (lineText.match(/^\`\`\`mermaid/i) && !currentBlock) {
|
|
7630
|
+
// Start of a mermaid code block
|
|
7631
|
+
currentBlock = { startLine: i + 1, lines: [] };
|
|
7632
|
+
} else if (lineText === '\`\`\`' && currentBlock) {
|
|
7633
|
+
// End of the code block
|
|
7634
|
+
currentBlock.endLine = i + 1;
|
|
7635
|
+
mermaidBlocks.push(currentBlock);
|
|
7636
|
+
currentBlock = null;
|
|
7637
|
+
} else if (currentBlock) {
|
|
7638
|
+
// Inside the mermaid block
|
|
7639
|
+
currentBlock.lines.push(DATA[i][0] || '');
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
|
|
7643
|
+
// Return the block at the same index as the container
|
|
7644
|
+
if (containerIndex < mermaidBlocks.length) {
|
|
7645
|
+
const block = mermaidBlocks[containerIndex];
|
|
7646
|
+
return { startLine: block.startLine, endLine: block.endLine };
|
|
7647
|
+
}
|
|
7648
|
+
|
|
7649
|
+
return { startLine: -1, endLine: -1 };
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7140
7652
|
// Helper: find source line for image by src
|
|
7141
7653
|
function findImageSourceLine(src) {
|
|
7142
7654
|
if (!src) return -1;
|
|
@@ -7217,6 +7729,44 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7217
7729
|
return;
|
|
7218
7730
|
}
|
|
7219
7731
|
|
|
7732
|
+
// Handle video/video-button clicks - select source line first
|
|
7733
|
+
// This includes video elements, fullscreen buttons, video wrapper clicks,
|
|
7734
|
+
// AND clicks on table cells containing videos (for post-fullscreen selection)
|
|
7735
|
+
const videoElement = e.target.closest('video.video-preview');
|
|
7736
|
+
const videoButton = e.target.closest('.video-fullscreen-btn');
|
|
7737
|
+
const videoWrapper = e.target.closest('.video-fullscreen-wrapper');
|
|
7738
|
+
|
|
7739
|
+
// Also check if clicking on a td that contains a video
|
|
7740
|
+
const targetCell = e.target.closest('td, th');
|
|
7741
|
+
const cellContainsVideo = targetCell && targetCell.querySelector('video.video-preview');
|
|
7742
|
+
|
|
7743
|
+
if (videoElement || videoButton || videoWrapper || cellContainsVideo) {
|
|
7744
|
+
// Check if this is a post-fullscreen click (for selection only, skip fullscreen)
|
|
7745
|
+
const isPostFullscreenClick = window._skipNextVideoFullscreen;
|
|
7746
|
+
if (isPostFullscreenClick) {
|
|
7747
|
+
window._skipNextVideoFullscreen = false;
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
// Find the parent table cell
|
|
7751
|
+
const refElement = videoElement || videoButton || videoWrapper || targetCell;
|
|
7752
|
+
const parentCell = refElement.closest('td, th') || targetCell;
|
|
7753
|
+
if (parentCell) {
|
|
7754
|
+
// Use video src to find the source line
|
|
7755
|
+
const video = videoElement || parentCell.querySelector('video');
|
|
7756
|
+
if (video && video.src) {
|
|
7757
|
+
const line = findImageSourceLine(video.src);
|
|
7758
|
+
if (line > 0) {
|
|
7759
|
+
selectSourceRange(line, null, parentCell);
|
|
7760
|
+
// Return if this is a post-fullscreen click (for selection only)
|
|
7761
|
+
if (isPostFullscreenClick) {
|
|
7762
|
+
return;
|
|
7763
|
+
}
|
|
7764
|
+
}
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7767
|
+
// Don't return for normal clicks - let fullscreen handler work too
|
|
7768
|
+
}
|
|
7769
|
+
|
|
7220
7770
|
// Handle links - sync to source but let link work normally
|
|
7221
7771
|
const link = e.target.closest('a');
|
|
7222
7772
|
if (link) {
|
|
@@ -7233,8 +7783,23 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7233
7783
|
return;
|
|
7234
7784
|
}
|
|
7235
7785
|
|
|
7236
|
-
//
|
|
7237
|
-
|
|
7786
|
+
// Handle mermaid container clicks for post-fullscreen selection
|
|
7787
|
+
const mermaidContainer = e.target.closest('.mermaid-container');
|
|
7788
|
+
if (mermaidContainer) {
|
|
7789
|
+
// Check if this is a post-fullscreen click for selection
|
|
7790
|
+
if (window._mermaidSelectAfterClose === mermaidContainer) {
|
|
7791
|
+
window._mermaidSelectAfterClose = null;
|
|
7792
|
+
const { startLine, endLine } = findMermaidSourceLine(mermaidContainer);
|
|
7793
|
+
if (startLine > 0) {
|
|
7794
|
+
selectSourceRange(startLine, endLine, mermaidContainer);
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
// Always return to prevent fullscreen from reopening or other handling
|
|
7798
|
+
return;
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
// Ignore clicks on video overlay
|
|
7802
|
+
if (e.target.closest('.video-fullscreen-overlay')) return;
|
|
7238
7803
|
|
|
7239
7804
|
// Handle code blocks - select entire block
|
|
7240
7805
|
const pre = e.target.closest('pre');
|
|
@@ -7248,15 +7813,31 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
|
|
|
7248
7813
|
return;
|
|
7249
7814
|
}
|
|
7250
7815
|
|
|
7251
|
-
const target = e.target.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th');
|
|
7816
|
+
const target = e.target.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th, summary');
|
|
7252
7817
|
if (!target) return;
|
|
7253
7818
|
|
|
7819
|
+
// Get text content, excluding toggle icon if present (for heading toggles)
|
|
7820
|
+
let searchText = target.textContent;
|
|
7821
|
+
|
|
7822
|
+
// For summary elements, clean up the text (may have disclosure triangle)
|
|
7823
|
+
if (target.tagName === 'SUMMARY') {
|
|
7824
|
+
searchText = searchText.replace(/^[▶▼◀►◆◇]?\\s*/, '').trim();
|
|
7825
|
+
}
|
|
7826
|
+
const toggleIcon = target.querySelector('.heading-toggle-icon');
|
|
7827
|
+
if (toggleIcon) {
|
|
7828
|
+
// Exclude the toggle icon text from search
|
|
7829
|
+
searchText = searchText.replace(toggleIcon.textContent, '').trim();
|
|
7830
|
+
}
|
|
7831
|
+
|
|
7254
7832
|
// Use table-specific search for table cells, otherwise use element-aware search
|
|
7255
7833
|
const isTableCell = target.tagName === 'TD' || target.tagName === 'TH';
|
|
7256
|
-
const line = isTableCell ? findTableSourceLine(
|
|
7834
|
+
const line = isTableCell ? findTableSourceLine(searchText) : findSourceLine(searchText, target);
|
|
7257
7835
|
if (line <= 0) return;
|
|
7258
7836
|
|
|
7259
|
-
|
|
7837
|
+
// Don't prevent default for summary elements - let native <details> toggle work
|
|
7838
|
+
if (target.tagName !== 'SUMMARY') {
|
|
7839
|
+
e.preventDefault();
|
|
7840
|
+
}
|
|
7260
7841
|
selectSourceRange(line, null, target);
|
|
7261
7842
|
});
|
|
7262
7843
|
|
|
@@ -8090,6 +8671,8 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
8090
8671
|
if (req.method === "GET" && req.url.startsWith("/video-timeline?")) {
|
|
8091
8672
|
const urlParams = new URL(req.url, `http://localhost`);
|
|
8092
8673
|
const videoPath = urlParams.searchParams.get("path");
|
|
8674
|
+
const sceneThreshold = parseFloat(urlParams.searchParams.get("scene")) || 0.01;
|
|
8675
|
+
const stabilizationThreshold = parseFloat(urlParams.searchParams.get("stabilization")) || 0.1;
|
|
8093
8676
|
|
|
8094
8677
|
if (!videoPath) {
|
|
8095
8678
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
@@ -8148,7 +8731,7 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
8148
8731
|
// Ignore cleanup errors
|
|
8149
8732
|
}
|
|
8150
8733
|
});
|
|
8151
|
-
});
|
|
8734
|
+
}, { sceneThreshold, stabilizationThreshold });
|
|
8152
8735
|
return;
|
|
8153
8736
|
}
|
|
8154
8737
|
|