chocomint 1.0.2 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +27 -1
- data/THIRD_PARTY_LICENSES.md +1 -0
- data/lib/chocomint/edit_archive_handlers.rb +323 -0
- data/lib/chocomint/edit_handlers.rb +194 -55
- data/lib/chocomint/server.rb +5 -1
- data/lib/chocomint.rb +1 -1
- data/public/edit/edit.css +72 -3
- data/public/edit/edit.js +515 -118
- data/public/edit/index.html +9 -0
- metadata +16 -1
data/public/edit/edit.js
CHANGED
|
@@ -121,35 +121,6 @@ let selectedPaths = new Set();
|
|
|
121
121
|
// Shift 範囲選択の起点 (直前に通常/Ctrl/Shift クリックした行)。
|
|
122
122
|
let lastClickedPath = null;
|
|
123
123
|
|
|
124
|
-
// フラットな相対パス配列を { dirs, files } のツリーに組み立てる。
|
|
125
|
-
// emptyDirPaths: 中身が空 (または空ディレクトリのみ) のディレクトリの相対パス。
|
|
126
|
-
// ファイルパスからは辿り着けないため、明示的にノードを掘る。
|
|
127
|
-
function buildTree(paths, emptyDirPaths) {
|
|
128
|
-
const root = { dirs: new Map(), files: [] };
|
|
129
|
-
const ensureDir = (path) => {
|
|
130
|
-
const parts = path.split('/');
|
|
131
|
-
let node = root;
|
|
132
|
-
parts.forEach(part => {
|
|
133
|
-
if (!node.dirs.has(part)) node.dirs.set(part, { dirs: new Map(), files: [] });
|
|
134
|
-
node = node.dirs.get(part);
|
|
135
|
-
});
|
|
136
|
-
};
|
|
137
|
-
paths.forEach(p => {
|
|
138
|
-
const parts = p.split('/');
|
|
139
|
-
let node = root;
|
|
140
|
-
parts.forEach((part, i) => {
|
|
141
|
-
if (i === parts.length - 1) {
|
|
142
|
-
node.files.push({ name: part, path: p });
|
|
143
|
-
} else {
|
|
144
|
-
if (!node.dirs.has(part)) node.dirs.set(part, { dirs: new Map(), files: [] });
|
|
145
|
-
node = node.dirs.get(part);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
(emptyDirPaths || []).forEach(ensureDir);
|
|
150
|
-
return root;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
124
|
// 拡張子ごとのファイルアイコン (絵文字)。未知の拡張子は汎用の📄。
|
|
154
125
|
const FILE_ICONS = {
|
|
155
126
|
py: '🐍',
|
|
@@ -194,12 +165,35 @@ const FILE_ICONS = {
|
|
|
194
165
|
jpg: '🖼️',
|
|
195
166
|
jpeg: '🖼️',
|
|
196
167
|
gif: '🖼️',
|
|
168
|
+
webp: '🖼️',
|
|
169
|
+
bmp: '🖼️',
|
|
197
170
|
svg: '🖼️',
|
|
198
171
|
ico: '🖼️',
|
|
172
|
+
avif: '🖼️',
|
|
173
|
+
tif: '🖼️',
|
|
174
|
+
tiff: '🖼️',
|
|
199
175
|
pdf: '📕',
|
|
176
|
+
mp4: '🎬',
|
|
177
|
+
m4v: '🎬',
|
|
178
|
+
webm: '🎬',
|
|
179
|
+
mkv: '🎬',
|
|
180
|
+
mov: '🎬',
|
|
181
|
+
avi: '🎬',
|
|
182
|
+
ogv: '🎬',
|
|
183
|
+
mp3: '🎵',
|
|
184
|
+
wav: '🎵',
|
|
185
|
+
ogg: '🎵',
|
|
186
|
+
m4a: '🎵',
|
|
187
|
+
aac: '🎵',
|
|
188
|
+
flac: '🎵',
|
|
189
|
+
opus: '🎵',
|
|
200
190
|
zip: '📦',
|
|
201
191
|
gz: '📦',
|
|
202
192
|
tar: '📦',
|
|
193
|
+
tgz: '📦',
|
|
194
|
+
bz2: '📦',
|
|
195
|
+
tbz: '📦',
|
|
196
|
+
tbz2: '📦',
|
|
203
197
|
lock: '🔒',
|
|
204
198
|
gitignore: '🚫',
|
|
205
199
|
dockerfile: '🐳',
|
|
@@ -215,51 +209,128 @@ function fileIcon(name) {
|
|
|
215
209
|
return FILE_ICONS[ext] || '📄';
|
|
216
210
|
}
|
|
217
211
|
|
|
218
|
-
//
|
|
219
|
-
|
|
212
|
+
// ---- 遅延展開ツリー -------------------------------------------------------
|
|
213
|
+
// ツリー全体を一括取得すると巨大ホーム (C:\Users\<name> 等) で壊れるため、
|
|
214
|
+
// 1 階層ずつ /edit/dir?path= で取得して描画する。フォルダを開いたときに初めて
|
|
215
|
+
// その中身を取得する。
|
|
216
|
+
|
|
217
|
+
// 各展開済みディレクトリの「直下 entries の署名」(変化検知用)。dir 相対パス → 署名文字列。
|
|
218
|
+
// ルートは "" をキーにする。ポーリング/更新時に署名が変わった階層だけ描き直す。
|
|
219
|
+
const dirSigs = new Map();
|
|
220
|
+
|
|
221
|
+
// /edit/dir?path=dir を取得して { entries, capped, absRoot } を返す。失敗時は null。
|
|
222
|
+
async function fetchDir(dir) {
|
|
223
|
+
try {
|
|
224
|
+
const q = dir ? '?path=' + encodeURIComponent(dir) : '';
|
|
225
|
+
const r = await fetch('/edit/dir' + q);
|
|
226
|
+
if (!r.ok) return null;
|
|
227
|
+
const data = await r.json();
|
|
228
|
+
if (data.abs_root) absRoot = data.abs_root;
|
|
229
|
+
return data;
|
|
230
|
+
} catch (e) { return null; } // ポーリング中の一時的な失敗は無視する。
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// entries の署名 (name+dir をソート結合)。直下の顔ぶれが変わったかだけを見る。
|
|
234
|
+
function entriesSig(entries) {
|
|
235
|
+
return (entries || []).map(e => (e.dir ? 'd:' : 'f:') + e.name).sort().join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 1 階層分の entries を <ul> に描画して返す。dir は親ディレクトリの相対パス ("" ならルート)。
|
|
239
|
+
// ディレクトリは (openDirs に含まれれば) 展開状態で子 <ul> も再帰的に埋める。
|
|
240
|
+
function renderEntries(entries, dir) {
|
|
220
241
|
const ul = document.createElement('ul');
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
li.className = 'dir';
|
|
226
|
-
li.dataset.path = dirPath;
|
|
227
|
-
if (openDirs.has(dirPath)) li.classList.add('open');
|
|
228
|
-
const row = document.createElement('div');
|
|
229
|
-
row.className = 'row';
|
|
230
|
-
row.innerHTML = '<span class="twist">▶</span><span class="icon">📁</span>';
|
|
231
|
-
row.appendChild(document.createTextNode(name));
|
|
232
|
-
row.onclick = (e) => {
|
|
233
|
-
if (e.ctrlKey || e.metaKey || e.shiftKey) { handleMultiSelectClick(e, dirPath); return; }
|
|
234
|
-
if (openDirs.has(dirPath)) openDirs.delete(dirPath);
|
|
235
|
-
else openDirs.add(dirPath);
|
|
236
|
-
li.classList.toggle('open');
|
|
237
|
-
selectDir(dirPath);
|
|
238
|
-
};
|
|
239
|
-
row.oncontextmenu = (e) => showContextMenu(e, dirPath, true);
|
|
240
|
-
li.appendChild(row);
|
|
241
|
-
li.appendChild(renderTree(node.dirs.get(name), dirPath));
|
|
242
|
-
ul.appendChild(li);
|
|
243
|
-
});
|
|
244
|
-
node.files.sort((a, b) => a.name.localeCompare(b.name)).forEach(f => {
|
|
245
|
-
const li = document.createElement('li');
|
|
246
|
-
li.className = 'file';
|
|
247
|
-
li.dataset.path = f.path;
|
|
248
|
-
const row = document.createElement('div');
|
|
249
|
-
row.className = 'row';
|
|
250
|
-
row.innerHTML = `<span class="twist"></span><span class="icon">${fileIcon(f.name)}</span>`;
|
|
251
|
-
row.appendChild(document.createTextNode(f.name));
|
|
252
|
-
row.onclick = (e) => {
|
|
253
|
-
if (e.ctrlKey || e.metaKey || e.shiftKey) { handleMultiSelectClick(e, f.path); return; }
|
|
254
|
-
openFile(f.path, li);
|
|
255
|
-
};
|
|
256
|
-
row.oncontextmenu = (e) => showContextMenu(e, f.path, false);
|
|
257
|
-
li.appendChild(row);
|
|
258
|
-
ul.appendChild(li);
|
|
259
|
-
});
|
|
242
|
+
for (const ent of entries) {
|
|
243
|
+
if (ent.dir) ul.appendChild(makeDirLi(ent));
|
|
244
|
+
else ul.appendChild(makeFileLi(ent));
|
|
245
|
+
}
|
|
260
246
|
return ul;
|
|
261
247
|
}
|
|
262
248
|
|
|
249
|
+
// ディレクトリ <li> を作る。展開中 (openDirs) なら子 <ul> を非同期に埋める。
|
|
250
|
+
function makeDirLi(ent) {
|
|
251
|
+
const dirPath = ent.path;
|
|
252
|
+
const li = document.createElement('li');
|
|
253
|
+
li.className = 'dir';
|
|
254
|
+
li.dataset.path = dirPath;
|
|
255
|
+
const row = document.createElement('div');
|
|
256
|
+
row.className = 'row';
|
|
257
|
+
row.innerHTML = '<span class="twist">▶</span><span class="icon">📁</span>';
|
|
258
|
+
const nameSpan = document.createElement('span');
|
|
259
|
+
nameSpan.className = 'name';
|
|
260
|
+
nameSpan.textContent = ent.name;
|
|
261
|
+
row.appendChild(nameSpan);
|
|
262
|
+
row.onclick = (e) => {
|
|
263
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey) { handleMultiSelectClick(e, dirPath); return; }
|
|
264
|
+
if (openDirs.has(dirPath)) collapseDir(li, dirPath);
|
|
265
|
+
else expandDir(li, dirPath);
|
|
266
|
+
selectDir(dirPath);
|
|
267
|
+
};
|
|
268
|
+
row.oncontextmenu = (e) => showContextMenu(e, dirPath, true);
|
|
269
|
+
li.appendChild(row);
|
|
270
|
+
// 子を入れる <ul> を用意しておく (閉じているときは空)。
|
|
271
|
+
const childUl = document.createElement('ul');
|
|
272
|
+
childUl.className = 'children';
|
|
273
|
+
li.appendChild(childUl);
|
|
274
|
+
// 描画時点で開いている扱いなら、その場で中身を取得して展開する。
|
|
275
|
+
if (openDirs.has(dirPath)) {
|
|
276
|
+
li.classList.add('open');
|
|
277
|
+
fillDirChildren(li, dirPath);
|
|
278
|
+
}
|
|
279
|
+
return li;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ファイル <li> を作る。
|
|
283
|
+
function makeFileLi(ent) {
|
|
284
|
+
const li = document.createElement('li');
|
|
285
|
+
li.className = 'file';
|
|
286
|
+
li.dataset.path = ent.path;
|
|
287
|
+
const row = document.createElement('div');
|
|
288
|
+
row.className = 'row';
|
|
289
|
+
row.innerHTML = `<span class="twist"></span><span class="icon">${fileIcon(ent.name)}</span>`;
|
|
290
|
+
const nameSpan = document.createElement('span');
|
|
291
|
+
nameSpan.className = 'name';
|
|
292
|
+
nameSpan.textContent = ent.name;
|
|
293
|
+
row.appendChild(nameSpan);
|
|
294
|
+
row.onclick = (e) => {
|
|
295
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey) { handleMultiSelectClick(e, ent.path); return; }
|
|
296
|
+
openFile(ent.path, li);
|
|
297
|
+
};
|
|
298
|
+
row.oncontextmenu = (e) => showContextMenu(e, ent.path, false);
|
|
299
|
+
li.appendChild(row);
|
|
300
|
+
return li;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// li.children (子 <ul>) を /edit/dir で取得して描画する。dir は li の相対パス。
|
|
304
|
+
async function fillDirChildren(li, dir) {
|
|
305
|
+
const data = await fetchDir(dir);
|
|
306
|
+
const childUl = li.querySelector(':scope > ul.children');
|
|
307
|
+
if (!childUl) return; // 取得中に折りたたまれた等
|
|
308
|
+
if (!data) { childUl.replaceChildren(); dirSigs.delete(dir); return; }
|
|
309
|
+
dirSigs.set(dir, entriesSig(data.entries));
|
|
310
|
+
const fresh = renderEntries(data.entries, dir);
|
|
311
|
+
childUl.replaceChildren(...fresh.childNodes);
|
|
312
|
+
restoreHighlights();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ディレクトリを展開する。openDirs に加え、子 <ul> を取得して埋める。
|
|
316
|
+
function expandDir(li, dir) {
|
|
317
|
+
openDirs.add(dir);
|
|
318
|
+
li.classList.add('open');
|
|
319
|
+
fillDirChildren(li, dir);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ディレクトリを折りたたむ。子 <ul> を空にして状態を捨てる。
|
|
323
|
+
function collapseDir(li, dir) {
|
|
324
|
+
openDirs.delete(dir);
|
|
325
|
+
li.classList.remove('open');
|
|
326
|
+
const childUl = li.querySelector(':scope > ul.children');
|
|
327
|
+
if (childUl) childUl.replaceChildren();
|
|
328
|
+
// 配下の署名も破棄する (再展開時は取り直す)。
|
|
329
|
+
for (const key of [...dirSigs.keys()]) {
|
|
330
|
+
if (key === dir || key.startsWith(dir + '/')) dirSigs.delete(key);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
263
334
|
// ディレクトリを「新規作成の基準」として選択状態にする (ファイル選択のハイライトは解除する)。
|
|
264
335
|
function selectDir(dirPath) {
|
|
265
336
|
selectedDir = dirPath;
|
|
@@ -326,52 +397,95 @@ function handleMultiSelectClick(e, path) {
|
|
|
326
397
|
renderMultiSelection();
|
|
327
398
|
}
|
|
328
399
|
|
|
329
|
-
//
|
|
330
|
-
let lastFilesSig = null;
|
|
331
|
-
// workspace ルートの絶対パス (「絶対パスをコピー」用)。/edit/files のたびに更新する。
|
|
400
|
+
// workspace ルートの絶対パス (「絶対パスをコピー」用)。/edit/dir のたびに更新する。
|
|
332
401
|
let absRoot = null;
|
|
333
402
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
const r = await fetch('/edit/files');
|
|
339
|
-
if (!r.ok) return;
|
|
340
|
-
data = await r.json();
|
|
341
|
-
} catch (e) { return; } // ポーリング中の一時的な失敗は無視する。
|
|
342
|
-
|
|
343
|
-
absRoot = data.abs_root || null;
|
|
344
|
-
const files = data.files || [];
|
|
345
|
-
const emptyDirs = data.dirs || [];
|
|
346
|
-
const sig = files.join('\n') + '\n\0\n' + emptyDirs.join('\n');
|
|
347
|
-
if (!force && sig === lastFilesSig) return; // 変化なし: 何もしない。
|
|
348
|
-
lastFilesSig = sig;
|
|
349
|
-
|
|
403
|
+
// 再描画後、単一選択・複数選択のハイライトを DOM に復元する。
|
|
404
|
+
// 消えたパスは複数選択からも取り除く。遅延展開では階層描画のたびに呼ぶ。
|
|
405
|
+
function restoreHighlights() {
|
|
350
406
|
const container = document.getElementById('file-list');
|
|
351
|
-
container.innerHTML = '';
|
|
352
|
-
const tree = buildTree(files, emptyDirs);
|
|
353
|
-
container.appendChild(renderTree(tree, ''));
|
|
354
|
-
// 空白部の右クリックは workspace ルートへの新規作成メニューを出す。
|
|
355
|
-
container.oncontextmenu = (e) => {
|
|
356
|
-
if (e.target === container) showContextMenu(e, '', true);
|
|
357
|
-
};
|
|
358
|
-
// 再描画後も選択中ファイル/ディレクトリのハイライトを維持する。
|
|
359
407
|
if (currentPath) {
|
|
360
408
|
const active = container.querySelector('li.file[data-path="' + cssEscape(currentPath) + '"]');
|
|
361
409
|
if (active) active.classList.add('active');
|
|
362
|
-
}
|
|
410
|
+
}
|
|
411
|
+
if (selectedDir) {
|
|
363
412
|
const selected = container.querySelector('li.dir[data-path="' + cssEscape(selectedDir) + '"]');
|
|
364
413
|
if (selected) selected.classList.add('selected');
|
|
365
414
|
}
|
|
366
|
-
// 消えたパスは複数選択からも取り除いてから、残りのハイライトを復元する。
|
|
367
415
|
const stillExists = (p) => container.querySelector('li[data-path="' + cssEscape(p) + '"]') != null;
|
|
368
416
|
[...selectedPaths].forEach(p => { if (!stillExists(p)) selectedPaths.delete(p); });
|
|
369
417
|
renderMultiSelection();
|
|
370
418
|
}
|
|
371
419
|
|
|
372
|
-
//
|
|
420
|
+
// ルート階層を取得して #file-list を描き直す。openDirs に残っている階層は
|
|
421
|
+
// renderEntries → makeDirLi 内で再帰的に取得・展開される。
|
|
422
|
+
async function renderRoot() {
|
|
423
|
+
const container = document.getElementById('file-list');
|
|
424
|
+
const data = await fetchDir('');
|
|
425
|
+
if (!data) return;
|
|
426
|
+
dirSigs.set('', entriesSig(data.entries));
|
|
427
|
+
const fresh = renderEntries(data.entries, '');
|
|
428
|
+
container.replaceChildren(...fresh.childNodes);
|
|
429
|
+
// 空白部の右クリックは workspace ルートへの新規作成メニューを出す。
|
|
430
|
+
container.oncontextmenu = (e) => {
|
|
431
|
+
if (e.target === container) showContextMenu(e, '', true);
|
|
432
|
+
};
|
|
433
|
+
restoreHighlights();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ルートから描き直し、開いている階層も取り直す (更新ボタン・fs 操作・AI 実行後・初回)。
|
|
437
|
+
async function refreshTree() {
|
|
438
|
+
await renderRoot();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// dir の階層だけを取り直して差し替える (fs 操作後の局所更新)。
|
|
442
|
+
// dir が現在展開中 (ルート or openDirs) でなければ何もしない。
|
|
443
|
+
async function refreshDir(dir) {
|
|
444
|
+
const container = document.getElementById('file-list');
|
|
445
|
+
if (dir === '' || dir == null) { await renderRoot(); return; }
|
|
446
|
+
if (!openDirs.has(dir)) return; // 閉じている階層は描画対象外。
|
|
447
|
+
const li = container.querySelector('li.dir[data-path="' + cssEscape(dir) + '"]');
|
|
448
|
+
if (li) await fillDirChildren(li, dir);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 展開中の各階層 (ルート + openDirs) を再取得し、直下の顔ぶれ (署名) が変わった階層だけ
|
|
452
|
+
// 描き直す。外部からのファイル追加/削除を自動反映しつつ、巨大ホームでも軽い。
|
|
453
|
+
async function refreshOpenDirs() {
|
|
454
|
+
const container = document.getElementById('file-list');
|
|
455
|
+
// ルート
|
|
456
|
+
const rootData = await fetchDir('');
|
|
457
|
+
if (rootData) {
|
|
458
|
+
const sig = entriesSig(rootData.entries);
|
|
459
|
+
if (sig !== dirSigs.get('')) {
|
|
460
|
+
dirSigs.set('', sig);
|
|
461
|
+
const fresh = renderEntries(rootData.entries, '');
|
|
462
|
+
container.replaceChildren(...fresh.childNodes);
|
|
463
|
+
restoreHighlights();
|
|
464
|
+
return; // ルートを描き直すと配下も makeDirLi 経由で取り直されるため以降は不要。
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// 開いている各サブディレクトリ (浅い順に処理して親の再描画で子を巻き込まない)。
|
|
468
|
+
const openList = [...openDirs].sort((a, b) => a.split('/').length - b.split('/').length);
|
|
469
|
+
for (const dir of openList) {
|
|
470
|
+
const li = container.querySelector('li.dir[data-path="' + cssEscape(dir) + '"]');
|
|
471
|
+
if (!li) continue; // 親が閉じている等で DOM 上に無い。
|
|
472
|
+
const data = await fetchDir(dir);
|
|
473
|
+
if (!data) continue;
|
|
474
|
+
const sig = entriesSig(data.entries);
|
|
475
|
+
if (sig === dirSigs.get(dir)) continue; // 変化なし。
|
|
476
|
+
dirSigs.set(dir, sig);
|
|
477
|
+
const childUl = li.querySelector(':scope > ul.children');
|
|
478
|
+
if (childUl) {
|
|
479
|
+
const fresh = renderEntries(data.entries, dir);
|
|
480
|
+
childUl.replaceChildren(...fresh.childNodes);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
restoreHighlights();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 作業ディレクトリの内容を定期的に監視し、変化があれば展開中の階層だけ更新する。
|
|
373
487
|
const FILES_POLL_MS = 3000;
|
|
374
|
-
setInterval(() =>
|
|
488
|
+
setInterval(() => refreshOpenDirs(), FILES_POLL_MS);
|
|
375
489
|
|
|
376
490
|
// querySelector 用に属性値をエスケープ (パスに特殊文字が入っても安全に)。
|
|
377
491
|
function cssEscape(s) {
|
|
@@ -460,7 +574,7 @@ async function createEntry(dir, isDir) {
|
|
|
460
574
|
try {
|
|
461
575
|
await fsPost(isDir ? '/edit/fs/mkdir' : '/edit/fs/touch', { path });
|
|
462
576
|
if (dir) openDirs.add(dir); // 作成先を開いて見せる
|
|
463
|
-
await
|
|
577
|
+
await refreshDir(dir); // 作成先の階層だけ描き直す (dir="" ならルート)
|
|
464
578
|
if (!isDir) openFileByPath(path); // 作ったファイルを開く
|
|
465
579
|
} catch (err) { alert(err.message); }
|
|
466
580
|
}
|
|
@@ -483,7 +597,11 @@ async function renameEntry(path) {
|
|
|
483
597
|
}
|
|
484
598
|
if (selectedDir === path) selectedDir = to;
|
|
485
599
|
renderEditorTabs();
|
|
486
|
-
|
|
600
|
+
// 移動元・移動先の親階層を描き直す (同じ親なら1回で済む)。
|
|
601
|
+
const fromParent = parentDir(path);
|
|
602
|
+
const toParent = parentDir(to);
|
|
603
|
+
await refreshDir(fromParent);
|
|
604
|
+
if (toParent !== fromParent) await refreshDir(toParent);
|
|
487
605
|
} catch (err) { alert(err.message); }
|
|
488
606
|
}
|
|
489
607
|
|
|
@@ -494,7 +612,7 @@ async function deleteEntry(path, isDir) {
|
|
|
494
612
|
try {
|
|
495
613
|
await fsPost('/edit/fs/delete', { path });
|
|
496
614
|
forgetDeletedPath(path);
|
|
497
|
-
await
|
|
615
|
+
await refreshDir(parentDir(path)); // 削除対象の親階層だけ描き直す
|
|
498
616
|
} catch (err) { alert(err.message); }
|
|
499
617
|
}
|
|
500
618
|
|
|
@@ -517,18 +635,22 @@ function removeTab(path) {
|
|
|
517
635
|
const wasActive = currentPath === path;
|
|
518
636
|
if (wasActive && autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
|
|
519
637
|
openTabs.splice(idx, 1);
|
|
520
|
-
|
|
638
|
+
// メディアタブは Monaco モデルを持たない (model=null)。テキスト/バイナリのみ破棄する。
|
|
639
|
+
if (tab.model) tab.model.dispose();
|
|
521
640
|
if (wasActive) {
|
|
522
641
|
currentPath = null;
|
|
523
642
|
const next = openTabs[idx] || openTabs[idx - 1];
|
|
524
643
|
if (next) {
|
|
525
644
|
activateEditorTab(next.path);
|
|
526
645
|
} else {
|
|
646
|
+
hideMediaView();
|
|
647
|
+
hideArchiveView();
|
|
527
648
|
editor.setModel(null);
|
|
528
649
|
document.getElementById('app').classList.add('no-tabs');
|
|
529
650
|
setPathLabel('(ファイル未選択)');
|
|
530
651
|
setDirty(false);
|
|
531
652
|
setSaveStatus('');
|
|
653
|
+
updateEolStatus();
|
|
532
654
|
document.querySelectorAll('#file-list li.file.active').forEach(x => x.classList.remove('active'));
|
|
533
655
|
}
|
|
534
656
|
}
|
|
@@ -545,16 +667,21 @@ async function deleteSelectedPaths(paths) {
|
|
|
545
667
|
if (!ok) return;
|
|
546
668
|
|
|
547
669
|
const errors = [];
|
|
670
|
+
const parents = new Set();
|
|
548
671
|
for (const path of paths) {
|
|
549
672
|
try {
|
|
550
673
|
await fsPost('/edit/fs/delete', { path });
|
|
551
674
|
forgetDeletedPath(path);
|
|
675
|
+
parents.add(parentDir(path));
|
|
552
676
|
} catch (err) {
|
|
553
677
|
errors.push(path + ': ' + err.message);
|
|
554
678
|
}
|
|
555
679
|
}
|
|
556
680
|
clearMultiSelection();
|
|
557
|
-
|
|
681
|
+
// 影響した親階層を浅い順に描き直す (親を描き直すと子は取り直されるため重複しても安全)。
|
|
682
|
+
for (const dir of [...parents].sort((a, b) => a.split('/').length - b.split('/').length)) {
|
|
683
|
+
await refreshDir(dir);
|
|
684
|
+
}
|
|
558
685
|
if (errors.length > 0) alert('一部の削除に失敗しました:\n' + errors.join('\n'));
|
|
559
686
|
}
|
|
560
687
|
|
|
@@ -577,6 +704,199 @@ function openFileByPath(path) {
|
|
|
577
704
|
return openFile(path, li);
|
|
578
705
|
}
|
|
579
706
|
|
|
707
|
+
// ---- メディアプレビュー (画像 / PDF / 動画 / 音声) ----
|
|
708
|
+
// メディアタブがアクティブなときだけ #media-view を表示し、Monaco を隠す。
|
|
709
|
+
// 生ファイルは /edit/raw?path= から取得する (種別ごとの Content-Type / Range 対応)。
|
|
710
|
+
|
|
711
|
+
// 現在 #media-view に載せている <video>/<audio> を止めるために保持する。
|
|
712
|
+
let currentMediaEl = null;
|
|
713
|
+
|
|
714
|
+
function rawUrl(path) {
|
|
715
|
+
return '/edit/raw?path=' + encodeURIComponent(path);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function showMediaView(tab) {
|
|
719
|
+
const app = document.getElementById('app');
|
|
720
|
+
const view = document.getElementById('media-view');
|
|
721
|
+
stopCurrentMedia();
|
|
722
|
+
view.innerHTML = '';
|
|
723
|
+
app.classList.add('show-media');
|
|
724
|
+
// PDF は縁まで使いたいので専用クラスで padding を外す (CSS 側で制御)。
|
|
725
|
+
app.classList.toggle('media-pdf', tab.media === 'pdf');
|
|
726
|
+
|
|
727
|
+
const url = rawUrl(tab.path);
|
|
728
|
+
const name = tab.path.split('/').pop();
|
|
729
|
+
let el;
|
|
730
|
+
switch (tab.media) {
|
|
731
|
+
case 'image':
|
|
732
|
+
el = document.createElement('img');
|
|
733
|
+
el.className = 'media-image';
|
|
734
|
+
el.alt = name;
|
|
735
|
+
el.src = url;
|
|
736
|
+
el.onerror = () => showMediaError(view, '画像を読み込めませんでした');
|
|
737
|
+
view.appendChild(el);
|
|
738
|
+
break;
|
|
739
|
+
case 'pdf':
|
|
740
|
+
el = document.createElement('iframe');
|
|
741
|
+
el.className = 'media-pdf';
|
|
742
|
+
el.title = name;
|
|
743
|
+
el.src = url;
|
|
744
|
+
view.appendChild(el);
|
|
745
|
+
break;
|
|
746
|
+
case 'video':
|
|
747
|
+
el = document.createElement('video');
|
|
748
|
+
el.className = 'media-video';
|
|
749
|
+
el.src = url;
|
|
750
|
+
el.controls = true;
|
|
751
|
+
el.onerror = () => showMediaError(view,
|
|
752
|
+
'この動画はブラウザで再生できない形式の可能性があります (' + name + ')');
|
|
753
|
+
view.appendChild(el);
|
|
754
|
+
currentMediaEl = el;
|
|
755
|
+
break;
|
|
756
|
+
case 'audio': {
|
|
757
|
+
const card = document.createElement('div');
|
|
758
|
+
card.className = 'media-audio-card';
|
|
759
|
+
const label = document.createElement('div');
|
|
760
|
+
label.className = 'media-audio-name';
|
|
761
|
+
label.textContent = '🎵 ' + name;
|
|
762
|
+
el = document.createElement('audio');
|
|
763
|
+
el.className = 'media-audio';
|
|
764
|
+
el.src = url;
|
|
765
|
+
el.controls = true;
|
|
766
|
+
el.onerror = () => showMediaError(view,
|
|
767
|
+
'この音声はブラウザで再生できない形式の可能性があります (' + name + ')');
|
|
768
|
+
card.appendChild(label);
|
|
769
|
+
card.appendChild(el);
|
|
770
|
+
view.appendChild(card);
|
|
771
|
+
currentMediaEl = el;
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
default:
|
|
775
|
+
showMediaError(view, 'このファイルはプレビューできません');
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function showMediaError(view, message) {
|
|
780
|
+
const p = document.createElement('div');
|
|
781
|
+
p.className = 'media-error';
|
|
782
|
+
p.textContent = message;
|
|
783
|
+
view.appendChild(p);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// 再生中のメディアを止める (タブ切り替え/クローズ時に音が鳴り続けないように)。
|
|
787
|
+
function stopCurrentMedia() {
|
|
788
|
+
if (currentMediaEl) {
|
|
789
|
+
try { currentMediaEl.pause(); } catch (e) { /* 既に破棄済み等は無視 */ }
|
|
790
|
+
currentMediaEl = null;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function hideMediaView() {
|
|
795
|
+
const app = document.getElementById('app');
|
|
796
|
+
if (!app.classList.contains('show-media')) return;
|
|
797
|
+
stopCurrentMedia();
|
|
798
|
+
app.classList.remove('show-media', 'media-pdf');
|
|
799
|
+
document.getElementById('media-view').innerHTML = '';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ---- アーカイブプレビュー (zip / tar / tar.gz / bz2 等) ----
|
|
803
|
+
// 第一階層 (トップレベル) のエントリとメタデータを /edit/archive?path= から取得して表示する。
|
|
804
|
+
|
|
805
|
+
// バイト数を人間可読 (KB/MB 等) に整形する。null/未定義は空文字。
|
|
806
|
+
function formatBytes(n) {
|
|
807
|
+
if (n == null || isNaN(n)) return '';
|
|
808
|
+
if (n < 1024) return n + ' B';
|
|
809
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
810
|
+
let v = n / 1024, i = 0;
|
|
811
|
+
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
|
812
|
+
return (v < 10 ? v.toFixed(1) : Math.round(v)) + ' ' + units[i];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// アーカイブ形式のバッジ表示名。
|
|
816
|
+
const ARCHIVE_KIND_LABELS = {
|
|
817
|
+
tar_or_zip: 'ARCHIVE', gz: 'GZIP', bz2: 'BZIP2'
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
async function showArchiveView(tab) {
|
|
821
|
+
const app = document.getElementById('app');
|
|
822
|
+
const view = document.getElementById('archive-view');
|
|
823
|
+
app.classList.add('show-archive');
|
|
824
|
+
const name = tab.path.split('/').pop();
|
|
825
|
+
view.innerHTML = '<div class="archive-empty">読み込み中…</div>';
|
|
826
|
+
|
|
827
|
+
let data;
|
|
828
|
+
try {
|
|
829
|
+
const r = await fetch('/edit/archive?path=' + encodeURIComponent(tab.path));
|
|
830
|
+
data = await r.json();
|
|
831
|
+
if (!r.ok) throw new Error(data.error || 'アーカイブを読み取れませんでした');
|
|
832
|
+
} catch (err) {
|
|
833
|
+
// 取得中にタブが切り替わっていたら描画しない。
|
|
834
|
+
if (currentPath !== tab.path) return;
|
|
835
|
+
view.innerHTML = '';
|
|
836
|
+
view.appendChild(el('div', 'archive-error', '⚠ ' + err.message));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (currentPath !== tab.path) return; // 取得中に別タブへ切り替わっていたら破棄。
|
|
840
|
+
renderArchive(view, name, data);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// 単純な要素生成ヘルパ (class と textContent 付き)。
|
|
844
|
+
function el(tag, cls, text) {
|
|
845
|
+
const e = document.createElement(tag);
|
|
846
|
+
if (cls) e.className = cls;
|
|
847
|
+
if (text != null) e.textContent = text;
|
|
848
|
+
return e;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function renderArchive(view, name, data) {
|
|
852
|
+
view.innerHTML = '';
|
|
853
|
+
|
|
854
|
+
// ヘッダー (ファイル名 + 形式バッジ)。
|
|
855
|
+
const head = el('div', 'archive-head');
|
|
856
|
+
head.appendChild(el('span', 'archive-name', '📦 ' + name));
|
|
857
|
+
head.appendChild(el('span', 'archive-kind', ARCHIVE_KIND_LABELS[data.archive] || 'ARCHIVE'));
|
|
858
|
+
view.appendChild(head);
|
|
859
|
+
|
|
860
|
+
// メタデータ行 (エントリ数 / 合計サイズ / 圧縮サイズ)。
|
|
861
|
+
const meta = el('div', 'archive-meta');
|
|
862
|
+
const entryCount = data.total_entries != null ? data.total_entries : (data.entries || []).length;
|
|
863
|
+
meta.appendChild(el('span', null, entryCount + ' エントリ'));
|
|
864
|
+
if (data.total_size != null) meta.appendChild(el('span', null, '展開後 ' + formatBytes(data.total_size)));
|
|
865
|
+
if (data.compressed_size != null) meta.appendChild(el('span', null, '圧縮 ' + formatBytes(data.compressed_size)));
|
|
866
|
+
if (data.capped) meta.appendChild(el('span', 'archive-capped', '(一部のみ表示)'));
|
|
867
|
+
view.appendChild(meta);
|
|
868
|
+
|
|
869
|
+
// エントリ一覧 (第一階層)。
|
|
870
|
+
const entries = data.entries || [];
|
|
871
|
+
if (entries.length === 0) {
|
|
872
|
+
view.appendChild(el('div', 'archive-empty', '(エントリがありません)'));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const ul = el('ul', 'archive-list');
|
|
876
|
+
for (const ent of entries) {
|
|
877
|
+
const li = el('li');
|
|
878
|
+
li.appendChild(el('span', 'a-icon', ent.dir ? '📁' : fileIcon(ent.name)));
|
|
879
|
+
li.appendChild(el('span', 'a-name', ent.name + (ent.dir ? '/' : '')));
|
|
880
|
+
// ディレクトリは配下件数、ファイルはサイズを右側に添える。
|
|
881
|
+
let sub = '';
|
|
882
|
+
if (ent.dir) {
|
|
883
|
+
if (ent.child_count != null) sub = ent.child_count + ' 項目';
|
|
884
|
+
} else if (ent.size != null) {
|
|
885
|
+
sub = formatBytes(ent.size);
|
|
886
|
+
}
|
|
887
|
+
if (sub) li.appendChild(el('span', 'a-sub', sub));
|
|
888
|
+
ul.appendChild(li);
|
|
889
|
+
}
|
|
890
|
+
view.appendChild(ul);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function hideArchiveView() {
|
|
894
|
+
const app = document.getElementById('app');
|
|
895
|
+
if (!app.classList.contains('show-archive')) return;
|
|
896
|
+
app.classList.remove('show-archive');
|
|
897
|
+
document.getElementById('archive-view').innerHTML = '';
|
|
898
|
+
}
|
|
899
|
+
|
|
580
900
|
async function openFile(path, li) {
|
|
581
901
|
if (!editor) return;
|
|
582
902
|
// 既に開いているファイルなら、そのタブへ切り替えるだけ (再フェッチしない)。
|
|
@@ -586,6 +906,24 @@ async function openFile(path, li) {
|
|
|
586
906
|
const data = await r.json();
|
|
587
907
|
if (!r.ok) { alert(data.error || 'open failed'); return; }
|
|
588
908
|
|
|
909
|
+
// 画像 / PDF / 動画 / 音声はメディアタブとして開く (Monaco モデルは持たない)。
|
|
910
|
+
if (data.media) {
|
|
911
|
+
const tab = { path, model: null, viewState: null, dirty: false,
|
|
912
|
+
binary: false, media: data.media, archive: false };
|
|
913
|
+
openTabs.push(tab);
|
|
914
|
+
activateEditorTab(path, li);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// zip / tar 等のアーカイブは第一階層一覧タブとして開く (Monaco モデルは持たない)。
|
|
919
|
+
if (data.archive) {
|
|
920
|
+
const tab = { path, model: null, viewState: null, dirty: false,
|
|
921
|
+
binary: false, media: null, archive: true };
|
|
922
|
+
openTabs.push(tab);
|
|
923
|
+
activateEditorTab(path, li);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
589
927
|
// 新しいモデルを作ってタブに紐づける (以後このモデルが内容・undo 履歴を保持する)。
|
|
590
928
|
const binary = !!data.binary;
|
|
591
929
|
const content = binary
|
|
@@ -593,11 +931,45 @@ async function openFile(path, li) {
|
|
|
593
931
|
: (data.content || '');
|
|
594
932
|
const lang = binary ? 'plaintext' : langOf(path);
|
|
595
933
|
const model = monaco.editor.createModel(content, lang);
|
|
596
|
-
|
|
934
|
+
// 元ファイルの改行コードをモデルへ反映する (Monaco は既定で LF に正規化してしまうため、
|
|
935
|
+
// CRLF のファイルを開いて保存すると LF に化ける。開いた時点の実際の改行を尊重する)。
|
|
936
|
+
if (!binary) {
|
|
937
|
+
const crlf = /\r\n/.test(content);
|
|
938
|
+
model.setEOL(crlf ? monaco.editor.EndOfLineSequence.CRLF
|
|
939
|
+
: monaco.editor.EndOfLineSequence.LF);
|
|
940
|
+
}
|
|
941
|
+
const tab = { path, model, viewState: null, dirty: false, binary, media: null, archive: false };
|
|
597
942
|
openTabs.push(tab);
|
|
598
943
|
activateEditorTab(path, li);
|
|
599
944
|
}
|
|
600
945
|
|
|
946
|
+
// ステータスバーの改行コード表示を更新する。テキストを編集できるタブ (通常タブ) の
|
|
947
|
+
// ときだけ CRLF/LF を表示し、それ以外 (未選択・メディア・アーカイブ・バイナリ) では隠す。
|
|
948
|
+
function updateEolStatus() {
|
|
949
|
+
const btn = document.getElementById('statusbar-eol');
|
|
950
|
+
const nameEl = document.getElementById('statusbar-eol-name');
|
|
951
|
+
if (!btn || !nameEl) return;
|
|
952
|
+
const tab = currentPath ? findTab(currentPath) : null;
|
|
953
|
+
const editable = tab && tab.model && !tab.media && !tab.archive && !tab.binary;
|
|
954
|
+
if (!editable) { btn.hidden = true; return; }
|
|
955
|
+
btn.hidden = false;
|
|
956
|
+
nameEl.textContent = tab.model.getEOL() === '\r\n' ? 'CRLF' : 'LF';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ステータスバーの改行コードをクリックしたときに CRLF ⇄ LF を切り替える。
|
|
960
|
+
// 切り替えは内容の編集にあたるので dirty 扱いにし、自動保存の有無はエディタ設定に従う
|
|
961
|
+
// (setDirty → scheduleAutoSave。自動保存が OFF なら手動 Ctrl+S まで保存されない)。
|
|
962
|
+
function toggleEol() {
|
|
963
|
+
const tab = currentPath ? findTab(currentPath) : null;
|
|
964
|
+
if (!tab || !tab.model || tab.media || tab.archive || tab.binary) return;
|
|
965
|
+
const toCrlf = tab.model.getEOL() !== '\r\n';
|
|
966
|
+
tab.model.setEOL(toCrlf ? monaco.editor.EndOfLineSequence.CRLF
|
|
967
|
+
: monaco.editor.EndOfLineSequence.LF);
|
|
968
|
+
updateEolStatus();
|
|
969
|
+
setDirty(true);
|
|
970
|
+
scheduleAutoSave();
|
|
971
|
+
}
|
|
972
|
+
|
|
601
973
|
// 指定パスのエディタタブをアクティブにする。Monaco のモデルを差し替え、ビュー状態を復元する。
|
|
602
974
|
// (ターミナルタブ用の activateTab とは別関数なので名前を分けている)
|
|
603
975
|
function activateEditorTab(path, li) {
|
|
@@ -617,15 +989,34 @@ function activateEditorTab(path, li) {
|
|
|
617
989
|
setPathLabel(path);
|
|
618
990
|
|
|
619
991
|
document.getElementById('app').classList.remove('no-tabs');
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
992
|
+
if (tab.media) {
|
|
993
|
+
// メディアタブ: Monaco はモデルを外して隠し、メディアビューアを表示する。
|
|
994
|
+
hideArchiveView();
|
|
995
|
+
suppressChangeEvent = true;
|
|
996
|
+
editor.setModel(null);
|
|
997
|
+
suppressChangeEvent = false;
|
|
998
|
+
showMediaView(tab);
|
|
999
|
+
} else if (tab.archive) {
|
|
1000
|
+
// アーカイブタブ: Monaco を隠し、第一階層一覧を表示する。
|
|
1001
|
+
hideMediaView();
|
|
1002
|
+
suppressChangeEvent = true;
|
|
1003
|
+
editor.setModel(null);
|
|
1004
|
+
suppressChangeEvent = false;
|
|
1005
|
+
showArchiveView(tab);
|
|
1006
|
+
} else {
|
|
1007
|
+
hideMediaView();
|
|
1008
|
+
hideArchiveView();
|
|
1009
|
+
suppressChangeEvent = true;
|
|
1010
|
+
editor.setModel(tab.model);
|
|
1011
|
+
suppressChangeEvent = false;
|
|
1012
|
+
if (tab.viewState) editor.restoreViewState(tab.viewState);
|
|
1013
|
+
editor.updateOptions({ readOnly: tab.binary });
|
|
1014
|
+
editor.focus();
|
|
1015
|
+
}
|
|
626
1016
|
|
|
627
1017
|
isDirty = tab.dirty;
|
|
628
1018
|
setSaveStatus('');
|
|
1019
|
+
updateEolStatus();
|
|
629
1020
|
renderEditorTabs();
|
|
630
1021
|
|
|
631
1022
|
// ファイル一覧側のハイライトを同期する (li が渡されなければパスから探す)。
|
|
@@ -693,6 +1084,9 @@ async function saveCurrent(opts) {
|
|
|
693
1084
|
// ファイル未選択で手動保存 (Ctrl+S / メニュー) されたときは、保存先を尋ねる
|
|
694
1085
|
// ダイアログを開く (自動保存はそもそも未選択時は動かないので auto では出さない)。
|
|
695
1086
|
if (!currentPath) { if (!auto) openSaveAsDialog(); return; }
|
|
1087
|
+
// メディア/アーカイブタブは編集対象ではないので保存しない。
|
|
1088
|
+
const cur = findTab(currentPath);
|
|
1089
|
+
if (cur && (cur.media || cur.archive)) return;
|
|
696
1090
|
// バイナリ表示中 (readOnly) は保存しない (プレースホルダで上書きしないため)。
|
|
697
1091
|
if (editor.getRawOptions && editor.getRawOptions().readOnly) return;
|
|
698
1092
|
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
|
|
@@ -764,7 +1158,7 @@ async function confirmSaveAs() {
|
|
|
764
1158
|
return;
|
|
765
1159
|
}
|
|
766
1160
|
closeSaveAsDialog();
|
|
767
|
-
await
|
|
1161
|
+
await refreshTree();
|
|
768
1162
|
// 新規タブとして開く (currentPath/パス表示/dirty は openFile 内で設定される)。
|
|
769
1163
|
await openFileByPath(path);
|
|
770
1164
|
setSaveStatus('saved');
|
|
@@ -892,7 +1286,7 @@ initDraggableWindow(errorDialog);
|
|
|
892
1286
|
|
|
893
1287
|
document.getElementById('btn-save').onclick = () => { closeAllMenus(); saveCurrent(); };
|
|
894
1288
|
document.getElementById('btn-autosave').onclick = () => { closeAllMenus(); setAutoSave(!autoSaveEnabled); };
|
|
895
|
-
document.getElementById('btn-reload').onclick = () =>
|
|
1289
|
+
document.getElementById('btn-reload').onclick = () => refreshTree();
|
|
896
1290
|
document.getElementById('btn-new-file').onclick = () => createEntry(selectedDir, false);
|
|
897
1291
|
document.getElementById('btn-new-dir').onclick = () => createEntry(selectedDir, true);
|
|
898
1292
|
document.getElementById('btn-menu-new-file').onclick = () => { closeAllMenus(); createEntry(selectedDir, false); };
|
|
@@ -1439,12 +1833,14 @@ function addCompactSummary(text) {
|
|
|
1439
1833
|
|
|
1440
1834
|
// 最終結果に伴うエディタ反映とファイル一覧更新 (描画とは分離した副作用処理)。
|
|
1441
1835
|
function applyResultSideEffects(data) {
|
|
1442
|
-
|
|
1836
|
+
const curTab = currentPath ? findTab(currentPath) : null;
|
|
1837
|
+
// メディア/アーカイブタブ表示中は Monaco にモデルが無いので content を流し込まない。
|
|
1838
|
+
if (data.content != null && editor && currentPath && !(curTab && (curTab.media || curTab.archive))) {
|
|
1443
1839
|
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
|
|
1444
1840
|
setEditorValue(data.content); // AI が編集した最新内容を反映 (既にディスクへ保存済みなので dirty にはしない)
|
|
1445
1841
|
setDirty(false);
|
|
1446
1842
|
}
|
|
1447
|
-
|
|
1843
|
+
refreshTree(); // AI 実行後は確実に反映する (新規ファイルが増えている可能性)
|
|
1448
1844
|
}
|
|
1449
1845
|
|
|
1450
1846
|
// pending 行に既にツール実行の進捗が描画済みのとき、それを消さずにそのまま
|
|
@@ -2072,6 +2468,7 @@ async function gitCheckout(branch, create) {
|
|
|
2072
2468
|
}
|
|
2073
2469
|
|
|
2074
2470
|
document.getElementById('statusbar-branch').onclick = () => openBranchDialog();
|
|
2471
|
+
document.getElementById('statusbar-eol').onclick = () => toggleEol();
|
|
2075
2472
|
document.getElementById('branch-cancel').onclick = closeBranchDialog;
|
|
2076
2473
|
document.getElementById('branch-close').onclick = closeBranchDialog;
|
|
2077
2474
|
document.getElementById('branch-create').onclick = () => {
|
|
@@ -2122,7 +2519,7 @@ remoteUrlInput.addEventListener('keydown', (e) => {
|
|
|
2122
2519
|
initDraggableWindow(remoteDialog);
|
|
2123
2520
|
|
|
2124
2521
|
// ---- 起動 ----
|
|
2125
|
-
|
|
2522
|
+
refreshTree();
|
|
2126
2523
|
initConsole();
|
|
2127
2524
|
initResizers();
|
|
2128
2525
|
initMenu();
|