collabmd 0.1.18 → 0.1.19
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.md +99 -17
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{preview-render-compiler-WD3A76I6.js → chunk-5QRWYPYT.js} +18 -18
- package/public/assets/js/chunks/chunk-HEWVH67U.js +9 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +1 -0
- package/public/assets/js/chunks/editor-session-AH6Z3MXW.js +22 -0
- package/public/assets/js/chunks/preview-render-compiler-UZ4SZDQQ.js +1 -0
- package/public/assets/js/chunks/quick-switcher-controller-J7I3CUET.js +5 -0
- package/public/assets/js/{excalidraw-editor-LKW2AZBU.js → excalidraw-editor-ZDYKMZOL.js} +33 -33
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +59 -59
- package/public/assets/js/preview-render-worker.js +19 -19
- package/src/client/application/app-shell/presence-feature.js +2 -1
- package/src/client/application/app-shell/ui-feature.js +113 -1
- package/src/client/application/app-shell/workspace-feature.js +9 -0
- package/src/client/application/preview-render-compiler.js +55 -5
- package/src/client/application/preview-render-executor.js +8 -0
- package/src/client/application/preview-render-worker.js +13 -2
- package/src/client/application/preview-renderer.js +5 -0
- package/src/client/application/workspace-chrome-controller.js +12 -1
- package/src/client/application/workspace-coordinator.js +16 -7
- package/src/client/application/workspace-preview-controller.js +45 -5
- package/src/client/application/workspace-route-controller.js +4 -0
- package/src/client/bootstrap/collabmd-app-shell.js +15 -3
- package/src/client/domain/room.js +37 -0
- package/src/client/domain/vault-utils.js +46 -0
- package/src/client/infrastructure/comment-thread-store.js +98 -0
- package/src/client/infrastructure/editor-paste-utils.js +45 -0
- package/src/client/infrastructure/editor-session.js +10 -0
- package/src/client/infrastructure/editor-view-adapter.js +34 -1
- package/src/client/infrastructure/vault-api-client.js +17 -0
- package/src/client/presentation/comment-markdown-renderer.js +68 -0
- package/src/client/presentation/comment-ui-controller.js +311 -15
- package/src/client/presentation/file-explorer-controller.js +5 -0
- package/src/client/presentation/file-explorer-view.js +10 -2
- package/src/client/presentation/file-tree-state.js +7 -1
- package/src/client/presentation/image-lightbox-controller.js +394 -0
- package/src/client/styles/style.css +539 -14
- package/src/domain/comment-threads.js +95 -13
- package/src/domain/file-kind.js +16 -1
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +48 -2
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +67 -1
- package/src/server/infrastructure/http/request-body.js +11 -2
- package/src/server/infrastructure/persistence/path-utils.js +1 -1
- package/src/server/infrastructure/persistence/vault-file-store.js +187 -2
- package/public/assets/js/chunks/chunk-BBHPYU2R.js +0 -1
- package/public/assets/js/chunks/chunk-OG2TNZEU.js +0 -9
- package/public/assets/js/chunks/chunk-QSTBGTWJ.js +0 -1
- package/public/assets/js/chunks/chunk-SR3U53EQ.js +0 -1
- package/public/assets/js/chunks/editor-session-IHFTYG5D.js +0 -22
- package/public/assets/js/chunks/quick-switcher-controller-JYDIJVAJ.js +0 -5
|
@@ -18,7 +18,8 @@ export const presenceFeature = {
|
|
|
18
18
|
|
|
19
19
|
const hasEditorSession = Boolean(this.session);
|
|
20
20
|
const isExcalidrawRoute = Boolean(this.currentFilePath && this.isExcalidrawFile?.(this.currentFilePath));
|
|
21
|
-
const
|
|
21
|
+
const isImageRoute = Boolean(this.currentFilePath && this.isImageFile?.(this.currentFilePath));
|
|
22
|
+
const shouldUseLobbyState = !hasEditorSession && (!this.currentFilePath || isExcalidrawRoute || isImageRoute);
|
|
22
23
|
const effectiveConnectionState = shouldUseLobbyState
|
|
23
24
|
? this.lobby?.getConnectionState?.() ?? this.connectionState
|
|
24
25
|
: this.connectionState;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { USER_NAME_MAX_LENGTH, normalizeUserName } from '../../domain/room.js';
|
|
2
2
|
import { isMarkdownFilePath, supportsBacklinksForFilePath } from '../../../domain/file-kind.js';
|
|
3
3
|
|
|
4
|
+
const IMAGE_FILE_PICKER_ACCEPT = 'image/png,image/jpeg,image/webp,image/gif,image/svg+xml';
|
|
5
|
+
|
|
4
6
|
export const uiFeature = {
|
|
5
7
|
initialize() {
|
|
6
8
|
this.themeController.initialize();
|
|
@@ -260,15 +262,125 @@ export const uiFeature = {
|
|
|
260
262
|
return;
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
if (action === 'image') {
|
|
266
|
+
void this.handleToolbarImageInsert();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
263
270
|
const applied = this.session.applyMarkdownToolbarAction(action);
|
|
264
271
|
if (!applied) {
|
|
265
272
|
this.toastController.show('Formatting action is unavailable');
|
|
266
273
|
}
|
|
267
274
|
},
|
|
268
275
|
|
|
276
|
+
async handleToolbarImageInsert() {
|
|
277
|
+
const file = await this.pickImageFile();
|
|
278
|
+
if (!file) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await this.handleEditorImageInsert(file);
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
pickImageFile() {
|
|
286
|
+
return new Promise((resolve) => {
|
|
287
|
+
const input = document.createElement('input');
|
|
288
|
+
input.type = 'file';
|
|
289
|
+
input.accept = IMAGE_FILE_PICKER_ACCEPT;
|
|
290
|
+
input.style.position = 'fixed';
|
|
291
|
+
input.style.left = '-9999px';
|
|
292
|
+
document.body.appendChild(input);
|
|
293
|
+
let settled = false;
|
|
294
|
+
let focusTimer = null;
|
|
295
|
+
|
|
296
|
+
const cleanup = (value) => {
|
|
297
|
+
if (settled) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
settled = true;
|
|
302
|
+
if (focusTimer) {
|
|
303
|
+
window.clearTimeout(focusTimer);
|
|
304
|
+
}
|
|
305
|
+
window.removeEventListener('focus', handleWindowFocus);
|
|
306
|
+
input.remove();
|
|
307
|
+
resolve(value);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const handleWindowFocus = () => {
|
|
311
|
+
focusTimer = window.setTimeout(() => {
|
|
312
|
+
if (settled || input.files?.length) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
cleanup(null);
|
|
317
|
+
}, 250);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
input.addEventListener('change', () => {
|
|
321
|
+
cleanup(input.files?.[0] ?? null);
|
|
322
|
+
}, { once: true });
|
|
323
|
+
|
|
324
|
+
input.addEventListener('cancel', () => {
|
|
325
|
+
cleanup(null);
|
|
326
|
+
}, { once: true });
|
|
327
|
+
|
|
328
|
+
window.addEventListener('focus', handleWindowFocus, { once: true });
|
|
329
|
+
input.click();
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
async handleEditorImageInsert(file) {
|
|
334
|
+
if (!this.session || !isMarkdownFilePath(this.currentFilePath)) {
|
|
335
|
+
console.warn('[ui] Ignoring image insert because there is no active markdown session.', {
|
|
336
|
+
currentFilePath: this.currentFilePath,
|
|
337
|
+
hasSession: Boolean(this.session),
|
|
338
|
+
});
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const activeFilePath = this.currentFilePath;
|
|
343
|
+
const activeSession = this.session;
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
console.debug('[ui] Uploading image attachment.', {
|
|
347
|
+
fileName: file?.name || '',
|
|
348
|
+
size: file?.size ?? null,
|
|
349
|
+
sourcePath: activeFilePath,
|
|
350
|
+
type: file?.type || '',
|
|
351
|
+
});
|
|
352
|
+
const result = await this.vaultApiClient.uploadImageAttachment({
|
|
353
|
+
file,
|
|
354
|
+
fileName: file?.name || '',
|
|
355
|
+
sourcePath: activeFilePath,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await this.fileExplorer.refresh();
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
this.currentFilePath === activeFilePath
|
|
362
|
+
&& this.session
|
|
363
|
+
&& this.session === activeSession
|
|
364
|
+
&& typeof result?.markdown === 'string'
|
|
365
|
+
) {
|
|
366
|
+
console.debug('[ui] Inserting uploaded image markdown into the editor.', {
|
|
367
|
+
sourcePath: activeFilePath,
|
|
368
|
+
storedPath: result.path ?? '',
|
|
369
|
+
});
|
|
370
|
+
this.session.insertText(result.markdown);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return true;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('[ui] Failed to insert image attachment:', error);
|
|
376
|
+
this.toastController.show(error.message || 'Failed to upload image');
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
|
|
269
381
|
handleThemeChange(theme) {
|
|
270
382
|
this.previewRenderer.applyTheme(theme);
|
|
271
|
-
if (!this.isExcalidrawFile(this.currentFilePath)) {
|
|
383
|
+
if (!this.isExcalidrawFile(this.currentFilePath) && !this.isImageFile?.(this.currentFilePath)) {
|
|
272
384
|
this.previewRenderer.queueRender();
|
|
273
385
|
}
|
|
274
386
|
this.session?.applyTheme(theme);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isExcalidrawFilePath,
|
|
3
|
+
isImageAttachmentFilePath,
|
|
3
4
|
isMermaidFilePath,
|
|
4
5
|
isPlantUmlFilePath,
|
|
5
6
|
stripVaultFileExtension,
|
|
@@ -10,6 +11,10 @@ export const workspaceFeature = {
|
|
|
10
11
|
return isExcalidrawFilePath(filePath);
|
|
11
12
|
},
|
|
12
13
|
|
|
14
|
+
isImageFile(filePath) {
|
|
15
|
+
return isImageAttachmentFilePath(filePath);
|
|
16
|
+
},
|
|
17
|
+
|
|
13
18
|
isMermaidFile(filePath) {
|
|
14
19
|
return isMermaidFilePath(filePath);
|
|
15
20
|
},
|
|
@@ -44,6 +49,10 @@ export const workspaceFeature = {
|
|
|
44
49
|
this.workspacePreviewController.renderExcalidrawFilePreview(filePath);
|
|
45
50
|
},
|
|
46
51
|
|
|
52
|
+
renderImageFilePreview(filePath) {
|
|
53
|
+
this.workspacePreviewController.renderImageFilePreview(filePath);
|
|
54
|
+
},
|
|
55
|
+
|
|
47
56
|
createResizeHandler() {
|
|
48
57
|
return this.workspacePreviewController.createResizeHandler(() => this.restoreSidebarState());
|
|
49
58
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import markdownIt from 'markdown-it';
|
|
2
2
|
import hljs from 'highlight.js';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { isImageAttachmentFilePath } from '../../domain/file-kind.js';
|
|
5
|
+
import { escapeHtml, resolveVaultRelativePath, resolveWikiTarget } from '../domain/vault-utils.js';
|
|
5
6
|
import { analyzeMarkdownComplexity } from './preview-render-profile.js';
|
|
6
7
|
|
|
7
8
|
const DIRECT_VIDEO_MIME_TYPES = Object.freeze({
|
|
@@ -189,6 +190,35 @@ function renderVideoEmbed(token, videoEmbedCounts) {
|
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
function isAbsoluteOrExternalUrl(source = '') {
|
|
194
|
+
const normalized = String(source ?? '').trim();
|
|
195
|
+
return (
|
|
196
|
+
!normalized
|
|
197
|
+
|| normalized.startsWith('data:')
|
|
198
|
+
|| normalized.startsWith('blob:')
|
|
199
|
+
|| normalized.startsWith('#')
|
|
200
|
+
|| normalized.startsWith('//')
|
|
201
|
+
|| /^[a-zA-Z][a-zA-Z\d+.-]*:/u.test(normalized)
|
|
202
|
+
|| normalized.startsWith('/')
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveLocalAttachmentUrl(source = '', {
|
|
207
|
+
attachmentApiPath,
|
|
208
|
+
sourceFilePath,
|
|
209
|
+
} = {}) {
|
|
210
|
+
if (!sourceFilePath || isAbsoluteOrExternalUrl(source)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const resolvedPath = resolveVaultRelativePath(sourceFilePath, source);
|
|
215
|
+
if (!resolvedPath || !isImageAttachmentFilePath(resolvedPath)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return `${attachmentApiPath}?path=${encodeURIComponent(resolvedPath)}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
192
222
|
function renderInlineWikiText(content, {
|
|
193
223
|
excalidrawEmbedCounts,
|
|
194
224
|
fileList,
|
|
@@ -253,7 +283,10 @@ function renderInlineWikiText(content, {
|
|
|
253
283
|
return html;
|
|
254
284
|
}
|
|
255
285
|
|
|
256
|
-
function createMarkdownRenderer(fileList = []
|
|
286
|
+
function createMarkdownRenderer(fileList = [], {
|
|
287
|
+
attachmentApiPath = '/api/attachment',
|
|
288
|
+
sourceFilePath = '',
|
|
289
|
+
} = {}) {
|
|
257
290
|
const markdown = markdownIt({
|
|
258
291
|
highlight(source, language) {
|
|
259
292
|
if (language === 'mermaid' || language === 'plantuml' || language === 'puml') {
|
|
@@ -396,7 +429,16 @@ function createMarkdownRenderer(fileList = []) {
|
|
|
396
429
|
};
|
|
397
430
|
|
|
398
431
|
markdown.renderer.rules.image = (tokens, index, options, env, self) => {
|
|
399
|
-
const
|
|
432
|
+
const token = tokens[index];
|
|
433
|
+
const localAttachmentUrl = resolveLocalAttachmentUrl(token.attrGet('src') || '', {
|
|
434
|
+
attachmentApiPath,
|
|
435
|
+
sourceFilePath,
|
|
436
|
+
});
|
|
437
|
+
if (localAttachmentUrl) {
|
|
438
|
+
token.attrSet('src', localAttachmentUrl);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const renderedVideo = renderVideoEmbed(token, videoEmbedCounts);
|
|
400
442
|
if (renderedVideo) {
|
|
401
443
|
return renderedVideo;
|
|
402
444
|
}
|
|
@@ -415,9 +457,17 @@ function createMarkdownRenderer(fileList = []) {
|
|
|
415
457
|
return markdown;
|
|
416
458
|
}
|
|
417
459
|
|
|
418
|
-
export function compilePreviewDocument({
|
|
460
|
+
export function compilePreviewDocument({
|
|
461
|
+
attachmentApiPath = '/api/attachment',
|
|
462
|
+
fileList = [],
|
|
463
|
+
markdownText = '',
|
|
464
|
+
sourceFilePath = '',
|
|
465
|
+
} = {}) {
|
|
419
466
|
const normalizedMarkdown = String(markdownText);
|
|
420
|
-
const renderer = createMarkdownRenderer(fileList
|
|
467
|
+
const renderer = createMarkdownRenderer(fileList, {
|
|
468
|
+
attachmentApiPath,
|
|
469
|
+
sourceFilePath,
|
|
470
|
+
});
|
|
421
471
|
|
|
422
472
|
return {
|
|
423
473
|
html: renderer.render(normalizedMarkdown),
|
|
@@ -10,17 +10,21 @@ function createPreviewWorker() {
|
|
|
10
10
|
|
|
11
11
|
export class PreviewRenderExecutor {
|
|
12
12
|
constructor({
|
|
13
|
+
attachmentApiPath = '/api/attachment',
|
|
13
14
|
cancelIdleRenderFn = cancelIdleRender,
|
|
14
15
|
compilePreviewDocumentLoader = () => import('./preview-render-compiler.js'),
|
|
15
16
|
createWorkerFn = createPreviewWorker,
|
|
16
17
|
getFileList,
|
|
18
|
+
getSourceFilePath = null,
|
|
17
19
|
idleTimeoutMs = IDLE_RENDER_TIMEOUT_MS,
|
|
18
20
|
requestIdleRenderFn = requestIdleRender,
|
|
19
21
|
} = {}) {
|
|
22
|
+
this.attachmentApiPath = attachmentApiPath;
|
|
20
23
|
this.cancelIdleRenderFn = cancelIdleRenderFn;
|
|
21
24
|
this.compilePreviewDocumentLoader = compilePreviewDocumentLoader;
|
|
22
25
|
this.createWorkerFn = createWorkerFn;
|
|
23
26
|
this.getFileList = getFileList;
|
|
27
|
+
this.getSourceFilePath = getSourceFilePath;
|
|
24
28
|
this.idleTimeoutMs = idleTimeoutMs;
|
|
25
29
|
this.requestIdleRenderFn = requestIdleRenderFn;
|
|
26
30
|
this.worker = null;
|
|
@@ -105,17 +109,21 @@ export class PreviewRenderExecutor {
|
|
|
105
109
|
return new Promise((resolve, reject) => {
|
|
106
110
|
this.workerJob = { reject, renderVersion, resolve };
|
|
107
111
|
activeWorker.postMessage({
|
|
112
|
+
attachmentApiPath: this.attachmentApiPath,
|
|
108
113
|
fileList: this.getFileList?.() ?? [],
|
|
109
114
|
markdownText,
|
|
110
115
|
renderVersion,
|
|
116
|
+
sourceFilePath: this.getSourceFilePath?.() ?? '',
|
|
111
117
|
});
|
|
112
118
|
});
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
const { compilePreviewDocument } = await this.compilePreviewDocumentLoader();
|
|
116
122
|
return compilePreviewDocument({
|
|
123
|
+
attachmentApiPath: this.attachmentApiPath,
|
|
117
124
|
fileList: this.getFileList?.() ?? [],
|
|
118
125
|
markdownText,
|
|
126
|
+
sourceFilePath: this.getSourceFilePath?.() ?? '',
|
|
119
127
|
});
|
|
120
128
|
}
|
|
121
129
|
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { compilePreviewDocument } from './preview-render-compiler.js';
|
|
2
2
|
|
|
3
3
|
self.onmessage = (event) => {
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
attachmentApiPath,
|
|
6
|
+
fileList,
|
|
7
|
+
markdownText,
|
|
8
|
+
renderVersion,
|
|
9
|
+
sourceFilePath,
|
|
10
|
+
} = event.data ?? {};
|
|
5
11
|
|
|
6
12
|
try {
|
|
7
|
-
const result = compilePreviewDocument({
|
|
13
|
+
const result = compilePreviewDocument({
|
|
14
|
+
attachmentApiPath,
|
|
15
|
+
fileList,
|
|
16
|
+
markdownText,
|
|
17
|
+
sourceFilePath,
|
|
18
|
+
});
|
|
8
19
|
self.postMessage({
|
|
9
20
|
html: result.html,
|
|
10
21
|
renderVersion,
|
|
@@ -4,11 +4,13 @@ import { IDLE_RENDER_TIMEOUT_MS } from './preview-diagram-utils.js';
|
|
|
4
4
|
import { PreviewRenderExecutor } from './preview-render-executor.js';
|
|
5
5
|
import { getRenderProfile, isLargeDocumentStats } from './preview-render-profile.js';
|
|
6
6
|
import { PreviewRenderScheduler } from './preview-render-scheduler.js';
|
|
7
|
+
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
7
8
|
|
|
8
9
|
export class PreviewRenderer {
|
|
9
10
|
constructor({
|
|
10
11
|
getContent,
|
|
11
12
|
getFileList,
|
|
13
|
+
getSourceFilePath,
|
|
12
14
|
onAfterRenderCommit,
|
|
13
15
|
onBeforeRenderCommit,
|
|
14
16
|
onRenderComplete,
|
|
@@ -18,6 +20,7 @@ export class PreviewRenderer {
|
|
|
18
20
|
}) {
|
|
19
21
|
this.getContent = getContent;
|
|
20
22
|
this.getFileList = getFileList;
|
|
23
|
+
this.getSourceFilePath = getSourceFilePath;
|
|
21
24
|
this.onAfterRenderCommit = onAfterRenderCommit;
|
|
22
25
|
this.onBeforeRenderCommit = onBeforeRenderCommit;
|
|
23
26
|
this.onRenderComplete = onRenderComplete;
|
|
@@ -67,7 +70,9 @@ export class PreviewRenderer {
|
|
|
67
70
|
this.plantUmlHydrator = new PlantUmlPreviewHydrator(this);
|
|
68
71
|
this.renderScheduler = new PreviewRenderScheduler({ getRenderProfileFn: getRenderProfile });
|
|
69
72
|
this.renderExecutor = new PreviewRenderExecutor({
|
|
73
|
+
attachmentApiPath: resolveApiUrl('/attachment'),
|
|
70
74
|
getFileList: () => this.getFileList?.() ?? [],
|
|
75
|
+
getSourceFilePath: () => this.getSourceFilePath?.() ?? '',
|
|
71
76
|
idleTimeoutMs: IDLE_RENDER_TIMEOUT_MS,
|
|
72
77
|
});
|
|
73
78
|
|
|
@@ -12,6 +12,7 @@ export class WorkspaceChromeController {
|
|
|
12
12
|
onFileOpenError,
|
|
13
13
|
onFileOpenReady,
|
|
14
14
|
onRenderExcalidrawPreview,
|
|
15
|
+
onRenderImagePreview,
|
|
15
16
|
onSyncWrapToggle,
|
|
16
17
|
onUpdateActiveFile,
|
|
17
18
|
onUpdateCurrentFile,
|
|
@@ -29,6 +30,7 @@ export class WorkspaceChromeController {
|
|
|
29
30
|
this.onFileOpenError = onFileOpenError;
|
|
30
31
|
this.onFileOpenReady = onFileOpenReady;
|
|
31
32
|
this.onRenderExcalidrawPreview = onRenderExcalidrawPreview;
|
|
33
|
+
this.onRenderImagePreview = onRenderImagePreview;
|
|
32
34
|
this.onSyncWrapToggle = onSyncWrapToggle;
|
|
33
35
|
this.onUpdateActiveFile = onUpdateActiveFile;
|
|
34
36
|
this.onUpdateCurrentFile = onUpdateCurrentFile;
|
|
@@ -68,10 +70,19 @@ export class WorkspaceChromeController {
|
|
|
68
70
|
this.onFileOpenReady(session);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
finalizeFileOpen({
|
|
73
|
+
finalizeFileOpen({
|
|
74
|
+
filePath,
|
|
75
|
+
isExcalidraw = false,
|
|
76
|
+
isImage = false,
|
|
77
|
+
session = null,
|
|
78
|
+
supportsBacklinks,
|
|
79
|
+
}) {
|
|
72
80
|
if (isExcalidraw) {
|
|
73
81
|
this.onRenderExcalidrawPreview(filePath);
|
|
74
82
|
}
|
|
83
|
+
if (isImage) {
|
|
84
|
+
this.onRenderImagePreview(filePath);
|
|
85
|
+
}
|
|
75
86
|
this.onSyncWrapToggle();
|
|
76
87
|
if (supportsBacklinks && session) {
|
|
77
88
|
this.loadBacklinks(filePath);
|
|
@@ -14,6 +14,7 @@ export class WorkspaceCoordinator {
|
|
|
14
14
|
getStoredUserName,
|
|
15
15
|
getTheme,
|
|
16
16
|
isExcalidrawFile,
|
|
17
|
+
isImageFile,
|
|
17
18
|
isMermaidFile,
|
|
18
19
|
isPlantUmlFile,
|
|
19
20
|
isTabActive,
|
|
@@ -26,9 +27,11 @@ export class WorkspaceCoordinator {
|
|
|
26
27
|
onFileAwarenessChange,
|
|
27
28
|
onFileOpenError,
|
|
28
29
|
onFileOpenReady,
|
|
30
|
+
onImagePaste,
|
|
29
31
|
onSelectionChange,
|
|
30
32
|
onSessionAssigned = null,
|
|
31
33
|
onRenderExcalidrawPreview,
|
|
34
|
+
onRenderImagePreview,
|
|
32
35
|
onSyncWrapToggle,
|
|
33
36
|
onUpdateActiveFile,
|
|
34
37
|
onUpdateCurrentFile,
|
|
@@ -50,9 +53,10 @@ export class WorkspaceCoordinator {
|
|
|
50
53
|
this.getLocalUser = getLocalUser;
|
|
51
54
|
this.getStoredUserName = getStoredUserName;
|
|
52
55
|
this.getTheme = getTheme;
|
|
53
|
-
this.isExcalidrawFile = isExcalidrawFile;
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
+
this.isExcalidrawFile = isExcalidrawFile ?? (() => false);
|
|
57
|
+
this.isImageFile = isImageFile ?? (() => false);
|
|
58
|
+
this.isMermaidFile = isMermaidFile ?? (() => false);
|
|
59
|
+
this.isPlantUmlFile = isPlantUmlFile ?? (() => false);
|
|
56
60
|
this.isTabActive = isTabActive;
|
|
57
61
|
this.loadEditorSessionClassPort = loadEditorSessionClass;
|
|
58
62
|
this.loadBacklinks = loadBacklinks;
|
|
@@ -63,6 +67,7 @@ export class WorkspaceCoordinator {
|
|
|
63
67
|
this.onFileAwarenessChange = onFileAwarenessChange;
|
|
64
68
|
this.onFileOpenError = onFileOpenError;
|
|
65
69
|
this.onFileOpenReady = onFileOpenReady;
|
|
70
|
+
this.onImagePaste = onImagePaste;
|
|
66
71
|
this.onSelectionChange = onSelectionChange;
|
|
67
72
|
this.onSessionAssigned = onSessionAssigned;
|
|
68
73
|
this.onRenderExcalidrawPreview = onRenderExcalidrawPreview;
|
|
@@ -88,6 +93,7 @@ export class WorkspaceCoordinator {
|
|
|
88
93
|
onFileOpenError,
|
|
89
94
|
onFileOpenReady,
|
|
90
95
|
onRenderExcalidrawPreview,
|
|
96
|
+
onRenderImagePreview,
|
|
91
97
|
onSyncWrapToggle,
|
|
92
98
|
onUpdateActiveFile,
|
|
93
99
|
onUpdateCurrentFile,
|
|
@@ -129,10 +135,11 @@ export class WorkspaceCoordinator {
|
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
const isExcalidraw = this.isExcalidrawFile(filePath);
|
|
138
|
+
const isImage = this.isImageFile(filePath);
|
|
132
139
|
const isMermaid = this.isMermaidFile(filePath);
|
|
133
140
|
const isPlantUml = this.isPlantUmlFile(filePath);
|
|
134
141
|
|
|
135
|
-
if (filePath === this.stateStore.get('currentFilePath') && (this.session || isExcalidraw)) {
|
|
142
|
+
if (filePath === this.stateStore.get('currentFilePath') && (this.session || isExcalidraw || isImage)) {
|
|
136
143
|
this.onUpdateActiveFile(filePath);
|
|
137
144
|
this.onUpdateLobbyCurrentFile(filePath);
|
|
138
145
|
return;
|
|
@@ -142,10 +149,10 @@ export class WorkspaceCoordinator {
|
|
|
142
149
|
|
|
143
150
|
this.cleanupSession();
|
|
144
151
|
const chromeState = this.chromeController.prepareForFileOpen(filePath, {
|
|
145
|
-
resetConnectionState: !isExcalidraw,
|
|
152
|
+
resetConnectionState: !isExcalidraw && !isImage,
|
|
146
153
|
});
|
|
147
154
|
|
|
148
|
-
if (isExcalidraw) {
|
|
155
|
+
if (isExcalidraw || isImage) {
|
|
149
156
|
this.onSessionAssigned?.(null);
|
|
150
157
|
|
|
151
158
|
if (loadToken !== this.stateStore.get('sessionLoadToken')) {
|
|
@@ -155,7 +162,8 @@ export class WorkspaceCoordinator {
|
|
|
155
162
|
this.chromeController.markFileOpenReady(null);
|
|
156
163
|
this.chromeController.finalizeFileOpen({
|
|
157
164
|
filePath,
|
|
158
|
-
isExcalidraw
|
|
165
|
+
isExcalidraw,
|
|
166
|
+
isImage,
|
|
159
167
|
session: null,
|
|
160
168
|
supportsBacklinks: chromeState.supportsBacklinks,
|
|
161
169
|
});
|
|
@@ -180,6 +188,7 @@ export class WorkspaceCoordinator {
|
|
|
180
188
|
isPlantUml,
|
|
181
189
|
});
|
|
182
190
|
},
|
|
191
|
+
onImagePaste: (file) => this.onImagePaste?.(file),
|
|
183
192
|
preferredUserName: this.getStoredUserName(),
|
|
184
193
|
onSelectionChange: (anchor) => this.onSelectionChange?.(anchor),
|
|
185
194
|
theme: this.getTheme(),
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
isDiagramFilePath,
|
|
3
3
|
isMarkdownFilePath,
|
|
4
4
|
} from '../../domain/file-kind.js';
|
|
5
|
+
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
5
6
|
|
|
6
7
|
export class WorkspacePreviewController {
|
|
7
8
|
constructor({
|
|
@@ -11,6 +12,7 @@ export class WorkspacePreviewController {
|
|
|
11
12
|
getDisplayName,
|
|
12
13
|
getSession,
|
|
13
14
|
isExcalidrawFile,
|
|
15
|
+
isImageFile,
|
|
14
16
|
isMermaidFile,
|
|
15
17
|
isPlantUmlFile,
|
|
16
18
|
layoutController,
|
|
@@ -25,9 +27,10 @@ export class WorkspacePreviewController {
|
|
|
25
27
|
this.excalidrawEmbed = excalidrawEmbed;
|
|
26
28
|
this.getDisplayName = getDisplayName;
|
|
27
29
|
this.getSession = getSession;
|
|
28
|
-
this.isExcalidrawFile = isExcalidrawFile;
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
30
|
+
this.isExcalidrawFile = isExcalidrawFile ?? (() => false);
|
|
31
|
+
this.isImageFile = isImageFile ?? (() => false);
|
|
32
|
+
this.isMermaidFile = isMermaidFile ?? (() => false);
|
|
33
|
+
this.isPlantUmlFile = isPlantUmlFile ?? (() => false);
|
|
31
34
|
this.layoutController = layoutController;
|
|
32
35
|
this.outlineController = outlineController;
|
|
33
36
|
this.previewRenderer = previewRenderer;
|
|
@@ -58,23 +61,25 @@ export class WorkspacePreviewController {
|
|
|
58
61
|
|
|
59
62
|
resetPreviewMode() {
|
|
60
63
|
this.elements.previewContent?.classList.remove('is-excalidraw-file-preview');
|
|
64
|
+
this.elements.previewContent?.classList.remove('is-image-file-preview');
|
|
61
65
|
this.elements.previewContent?.classList.remove('is-mermaid-file-preview');
|
|
62
66
|
this.elements.previewContent?.classList.remove('is-plantuml-file-preview');
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
syncFileChrome(filePath) {
|
|
66
70
|
const isExcalidraw = this.isExcalidrawFile(filePath);
|
|
71
|
+
const isImage = this.isImageFile(filePath);
|
|
67
72
|
const isMarkdown = isMarkdownFilePath(filePath);
|
|
68
73
|
const isMermaid = this.isMermaidFile(filePath);
|
|
69
74
|
const isPlantUml = this.isPlantUmlFile(filePath);
|
|
70
75
|
const isDiagramFile = isDiagramFilePath(filePath);
|
|
71
76
|
|
|
72
77
|
this.elements.markdownToolbar?.classList.toggle('hidden', !isMarkdown);
|
|
73
|
-
this.elements.outlineToggle?.classList.toggle('hidden', isDiagramFile);
|
|
78
|
+
this.elements.outlineToggle?.classList.toggle('hidden', isDiagramFile || isImage);
|
|
74
79
|
this.elements.previewContent?.classList.toggle('is-mermaid-file-preview', isMermaid);
|
|
75
80
|
this.elements.previewContent?.classList.toggle('is-plantuml-file-preview', isPlantUml);
|
|
76
81
|
|
|
77
|
-
if (isExcalidraw) {
|
|
82
|
+
if (isExcalidraw || isImage) {
|
|
78
83
|
this.layoutController.setView('preview', { persist: false });
|
|
79
84
|
this.outlineController.close();
|
|
80
85
|
this.backlinksPanel.clear();
|
|
@@ -124,6 +129,41 @@ export class WorkspacePreviewController {
|
|
|
124
129
|
this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
renderImageFilePreview(filePath) {
|
|
133
|
+
const previewElement = this.elements.previewContent;
|
|
134
|
+
if (!previewElement) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.videoEmbed?.detachForCommit();
|
|
139
|
+
this.excalidrawEmbed.detachForCommit();
|
|
140
|
+
this.resetPreviewMode();
|
|
141
|
+
previewElement.classList.add('is-image-file-preview');
|
|
142
|
+
const renderHost = this.previewRenderer.ensureRenderHost();
|
|
143
|
+
this.previewRenderer.normalizePreviewChildren(renderHost);
|
|
144
|
+
|
|
145
|
+
const shell = document.createElement('figure');
|
|
146
|
+
shell.className = 'image-file-preview-shell';
|
|
147
|
+
|
|
148
|
+
const image = document.createElement('img');
|
|
149
|
+
image.className = 'image-file-preview-image';
|
|
150
|
+
image.alt = this.getDisplayName(filePath);
|
|
151
|
+
image.src = resolveApiUrl(`/attachment?path=${encodeURIComponent(filePath)}`);
|
|
152
|
+
shell.appendChild(image);
|
|
153
|
+
|
|
154
|
+
if (renderHost) {
|
|
155
|
+
renderHost.replaceChildren(shell);
|
|
156
|
+
renderHost.style.minHeight = '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
previewElement.dataset.renderPhase = 'ready';
|
|
160
|
+
this.outlineController.refresh();
|
|
161
|
+
this.scrollSyncController.setLargeDocumentMode(false);
|
|
162
|
+
this.scrollSyncController.invalidatePreviewBlocks();
|
|
163
|
+
this.videoEmbed?.reconcileEmbeds(previewElement);
|
|
164
|
+
this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
|
|
165
|
+
}
|
|
166
|
+
|
|
127
167
|
createResizeHandler(restoreSidebarState) {
|
|
128
168
|
let resizeTimer = null;
|
|
129
169
|
return () => {
|
|
@@ -10,6 +10,7 @@ export class WorkspaceRouteController {
|
|
|
10
10
|
getSessionLoadToken,
|
|
11
11
|
gitDiffView,
|
|
12
12
|
gitPanel,
|
|
13
|
+
imageLightbox = null,
|
|
13
14
|
lobby,
|
|
14
15
|
navigation,
|
|
15
16
|
previewRenderer,
|
|
@@ -37,6 +38,7 @@ export class WorkspaceRouteController {
|
|
|
37
38
|
this.getSessionLoadToken = getSessionLoadToken;
|
|
38
39
|
this.gitDiffView = gitDiffView;
|
|
39
40
|
this.gitPanel = gitPanel;
|
|
41
|
+
this.imageLightbox = imageLightbox;
|
|
40
42
|
this.lobby = lobby;
|
|
41
43
|
this.navigation = navigation;
|
|
42
44
|
this.previewRenderer = previewRenderer;
|
|
@@ -140,6 +142,7 @@ export class WorkspaceRouteController {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
async openFile(filePath) {
|
|
145
|
+
this.imageLightbox?.close?.();
|
|
143
146
|
this.gitPanel.setSelection();
|
|
144
147
|
this.gitDiffView.hide();
|
|
145
148
|
this.syncMainChrome({ mode: 'editor' });
|
|
@@ -161,6 +164,7 @@ export class WorkspaceRouteController {
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
resetPreviewSurface() {
|
|
167
|
+
this.imageLightbox?.close?.();
|
|
164
168
|
this.previewRenderer.setHydrationPaused(false);
|
|
165
169
|
this.excalidrawEmbed.setHydrationPaused(false);
|
|
166
170
|
this.videoEmbed?.detachForCommit();
|