joplin-plugin-explorer 1.1.1 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joplin-plugin-explorer",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "A unified sidebar that displays notebooks and notes together in a single tree view",
5
5
  "author": "lim0513",
6
6
  "homepage": "https://github.com/lim0513/joplin-explorer",
package/publish/index.js CHANGED
@@ -25,6 +25,7 @@ var i18nData = {
25
25
  confirmDeleteNote: '确定删除此笔记吗?',
26
26
  promptRename: '请输入新名称:',
27
27
  promptMoveNote: '请输入目标笔记本名称:',
28
+ cancel: '取消',
28
29
  },
29
30
  'zh_TW': {
30
31
  newNotebook: '新建筆記本', newNote: '新建筆記', newTodo: '新建待辦',
@@ -45,6 +46,7 @@ var i18nData = {
45
46
  confirmDeleteFolder: '確定刪除此筆記本及其所有內容嗎?',
46
47
  confirmDeleteNote: '確定刪除此筆記嗎?',
47
48
  promptRename: '請輸入新名稱:', promptMoveNote: '請輸入目標筆記本名稱:',
49
+ cancel: '取消',
48
50
  },
49
51
  'en_US': {
50
52
  newNotebook: 'New Notebook', newNote: 'New Note', newTodo: 'New To-do',
@@ -65,6 +67,7 @@ var i18nData = {
65
67
  confirmDeleteFolder: 'Delete this notebook and all its contents?',
66
68
  confirmDeleteNote: 'Delete this note?',
67
69
  promptRename: 'Enter new name:', promptMoveNote: 'Enter target notebook name:',
70
+ cancel: 'Cancel',
68
71
  },
69
72
  'ja_JP': {
70
73
  newNotebook: '\u65B0\u898F\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF', newNote: '\u65B0\u898F\u30CE\u30FC\u30C8', newTodo: '\u65B0\u898F\u30BF\u30B9\u30AF',
@@ -85,6 +88,7 @@ var i18nData = {
85
88
  confirmDeleteFolder: '\u3053\u306E\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF\u3068\u305D\u306E\u5185\u5BB9\u3092\u3059\u3079\u3066\u524A\u9664\u3057\u307E\u3059\u304B\uFF1F',
86
89
  confirmDeleteNote: '\u3053\u306E\u30CE\u30FC\u30C8\u3092\u524A\u9664\u3057\u307E\u3059\u304B\uFF1F',
87
90
  promptRename: '\u65B0\u3057\u3044\u540D\u524D\u3092\u5165\u529B\uFF1A', promptMoveNote: '\u79FB\u52D5\u5148\u306E\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF\u540D\uFF1A',
91
+ cancel: '\u30AD\u30E3\u30F3\u30BB\u30EB',
88
92
  },
89
93
  };
90
94
 
@@ -256,6 +260,18 @@ joplin.plugins.register({
256
260
  var allFoldersCache = [];
257
261
  var isFirstLoad = true;
258
262
 
263
+ function expandToFolder(folderId) {
264
+ var parentId = folderId;
265
+ while (parentId) {
266
+ delete collapsedFolders[parentId];
267
+ var found = null;
268
+ for (var i = 0; i < allFoldersCache.length; i++) {
269
+ if (allFoldersCache[i].id === parentId) { found = allFoldersCache[i]; break; }
270
+ }
271
+ parentId = found ? found.parent_id : null;
272
+ }
273
+ }
274
+
259
275
  function sortNotes(notes, sortMode) {
260
276
  var sorted = notes.slice();
261
277
  switch (sortMode) {
@@ -363,9 +379,13 @@ joplin.plugins.register({
363
379
  await refreshPanel();
364
380
  } else if (msg.name === 'newNote') {
365
381
  await joplin.commands.execute('newNote');
382
+ var nn = await joplin.workspace.selectedNote();
383
+ if (nn) { selectedNoteId = nn.id; expandToFolder(nn.parent_id); }
366
384
  await refreshPanel();
367
385
  } else if (msg.name === 'newTodo') {
368
386
  await joplin.commands.execute('newTodo');
387
+ var nt = await joplin.workspace.selectedNote();
388
+ if (nt) { selectedNoteId = nt.id; expandToFolder(nt.parent_id); }
369
389
  await refreshPanel();
370
390
  } else if (msg.name === 'search') {
371
391
  var query = msg.query;
@@ -447,10 +467,16 @@ joplin.plugins.register({
447
467
  if (itemType === 'folder') {
448
468
  switch (action) {
449
469
  case 'newNote':
450
- await joplin.data.post(['folders', id, 'notes'], null, { title: '' });
470
+ var newNote = await joplin.data.post(['notes'], null, { title: t.newNote, parent_id: id });
471
+ await joplin.commands.execute('openNote', newNote.id);
472
+ selectedNoteId = newNote.id;
473
+ expandToFolder(id);
451
474
  break;
452
475
  case 'newTodo':
453
- await joplin.data.post(['folders', id, 'notes'], null, { title: '', is_todo: 1 });
476
+ var newTodo = await joplin.data.post(['notes'], null, { title: t.newTodo, parent_id: id, is_todo: 1 });
477
+ await joplin.commands.execute('openNote', newTodo.id);
478
+ selectedNoteId = newTodo.id;
479
+ expandToFolder(id);
454
480
  break;
455
481
  case 'newSubNotebook':
456
482
  await joplin.data.post(['folders'], null, { title: t.newNotebook, parent_id: id });
@@ -462,28 +488,39 @@ joplin.plugins.register({
462
488
  if (msg.newTitle) await joplin.data.put(['folders', id], null, { title: msg.newTitle });
463
489
  break;
464
490
  case 'exportFolder':
465
- await joplin.commands.execute('exportFolders', [id]);
491
+ try { await joplin.commands.execute('exportFolders', [id]); } catch(e) {}
466
492
  break;
467
493
  }
468
494
  } else if (itemType === 'note') {
469
495
  switch (action) {
470
496
  case 'openNote':
471
497
  await joplin.commands.execute('openNote', id);
498
+ selectedNoteId = id;
472
499
  break;
473
500
  case 'openInNewWindow':
474
- await joplin.commands.execute('openNoteInNewWindow', id);
501
+ try { await joplin.commands.execute('openNoteInNewWindow', id); } catch(e) {
502
+ // Fallback: just open in main window
503
+ await joplin.commands.execute('openNote', id);
504
+ }
475
505
  break;
476
506
  case 'copyLink':
477
507
  var linkNote = await joplin.data.get(['notes', id], { fields: ['id', 'title'] });
478
508
  var mdLink = '[' + linkNote.title + '](:/' + linkNote.id + ')';
479
- await joplin.clipboard.writeText(mdLink);
509
+ try {
510
+ await joplin.clipboard.writeText(mdLink);
511
+ } catch(e) {
512
+ // Fallback: send link text to webview for copying
513
+ await joplin.views.panels.postMessage(panel, { name: 'copyText', text: mdLink });
514
+ }
480
515
  break;
481
516
  case 'duplicateNote':
482
517
  var srcNote = await joplin.data.get(['notes', id], { fields: ['title', 'body', 'parent_id', 'is_todo'] });
483
- await joplin.data.post(['notes'], null, {
518
+ var dupNote = await joplin.data.post(['notes'], null, {
484
519
  title: srcNote.title + ' (copy)', body: srcNote.body,
485
520
  parent_id: srcNote.parent_id, is_todo: srcNote.is_todo,
486
521
  });
522
+ await joplin.commands.execute('openNote', dupNote.id);
523
+ selectedNoteId = dupNote.id;
487
524
  break;
488
525
  case 'switchNoteType':
489
526
  var sn = await joplin.data.get(['notes', id], { fields: ['is_todo'] });
@@ -2,9 +2,17 @@
2
2
  "manifest_version": 1,
3
3
  "id": "com.github.joplin-explorer",
4
4
  "app_min_version": "2.6.0",
5
- "version": "1.1.1",
5
+ "version": "1.1.3",
6
6
  "name": "Joplin Explorer",
7
7
  "description": "A unified sidebar that displays notebooks and notes together in a single tree view",
8
- "author": "user",
9
- "homepage_url": "https://github.com"
8
+ "author": "lim0513",
9
+ "homepage_url": "https://github.com/lim0513/joplin-explorer",
10
+ "repository_url": "https://github.com/lim0513/joplin-explorer",
11
+ "keywords": ["sidebar", "explorer", "tree-view", "notebooks", "notes"],
12
+ "categories": ["appearance", "productivity"],
13
+ "screenshots": [
14
+ {"src": "screenshots/tree-view.png", "label": "Unified tree view of notebooks and notes"},
15
+ {"src": "screenshots/search.png", "label": "Full-text search with keyword highlighting"},
16
+ {"src": "screenshots/context-menu.png", "label": "Right-click context menu"}
17
+ ]
10
18
  }
Binary file
@@ -347,6 +347,76 @@ html, body {
347
347
  word-break: break-all;
348
348
  }
349
349
 
350
+ /* Inline input dialog */
351
+ .inline-input-overlay {
352
+ position: fixed;
353
+ top: 0; left: 0; right: 0; bottom: 0;
354
+ background: rgba(0, 0, 0, 0.3);
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ z-index: 10000;
359
+ }
360
+
361
+ .inline-input-dialog {
362
+ background: var(--joplin-background-color, #fff);
363
+ border: 1px solid var(--joplin-divider-color, #ccc);
364
+ border-radius: 8px;
365
+ padding: 16px;
366
+ min-width: 240px;
367
+ max-width: 90%;
368
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
369
+ }
370
+
371
+ .inline-input-label {
372
+ font-size: 12px;
373
+ margin-bottom: 8px;
374
+ color: var(--joplin-color);
375
+ }
376
+
377
+ .inline-input-field {
378
+ width: 100%;
379
+ box-sizing: border-box;
380
+ padding: 6px 8px;
381
+ border: 1px solid var(--joplin-divider-color, #ccc);
382
+ border-radius: 4px;
383
+ background: var(--joplin-background-color, #fff);
384
+ color: var(--joplin-color);
385
+ font-size: 13px;
386
+ outline: none;
387
+ }
388
+
389
+ .inline-input-field:focus {
390
+ border-color: var(--joplin-color2, #4a9cf5);
391
+ }
392
+
393
+ .inline-input-buttons {
394
+ display: flex;
395
+ gap: 8px;
396
+ margin-top: 12px;
397
+ justify-content: flex-end;
398
+ }
399
+
400
+ .inline-input-buttons button {
401
+ padding: 5px 16px;
402
+ border: 1px solid var(--joplin-divider-color, #ccc);
403
+ border-radius: 4px;
404
+ cursor: pointer;
405
+ font-size: 12px;
406
+ color: var(--joplin-color);
407
+ background: var(--joplin-background-color3, #f0f0f0);
408
+ }
409
+
410
+ .inline-input-ok {
411
+ background: #1a73e8 !important;
412
+ color: #fff !important;
413
+ border-color: #1a73e8 !important;
414
+ }
415
+
416
+ .inline-input-buttons button:hover {
417
+ opacity: 0.85;
418
+ }
419
+
350
420
  mark.search-highlight {
351
421
  background: rgba(255, 213, 0, 0.4);
352
422
  color: inherit;
@@ -46,6 +46,49 @@ function T(key) {
46
46
  return (window._i18n && window._i18n[key]) || key;
47
47
  }
48
48
 
49
+ // Inline input dialog (replaces prompt() which may not work in webview)
50
+ function showInlineInput(label, defaultValue, callback) {
51
+ var existing = document.getElementById('inline-input-overlay');
52
+ if (existing) existing.remove();
53
+
54
+ var overlay = document.createElement('div');
55
+ overlay.id = 'inline-input-overlay';
56
+ overlay.className = 'inline-input-overlay';
57
+ overlay.innerHTML = '<div class="inline-input-dialog">'
58
+ + '<div class="inline-input-label">' + label + '</div>'
59
+ + '<input class="inline-input-field" type="text" value="' + (defaultValue || '').replace(/"/g, '&quot;') + '" />'
60
+ + '<div class="inline-input-buttons">'
61
+ + '<button class="inline-input-ok">OK</button>'
62
+ + '<button class="inline-input-cancel">' + (T('cancel') || 'Cancel') + '</button>'
63
+ + '</div></div>';
64
+
65
+ document.body.appendChild(overlay);
66
+
67
+ var input = overlay.querySelector('.inline-input-field');
68
+ input.focus();
69
+ input.select();
70
+
71
+ function submit() {
72
+ var val = input.value;
73
+ overlay.remove();
74
+ callback(val);
75
+ }
76
+ function cancel() {
77
+ overlay.remove();
78
+ callback(null);
79
+ }
80
+
81
+ overlay.querySelector('.inline-input-ok').addEventListener('click', submit);
82
+ overlay.querySelector('.inline-input-cancel').addEventListener('click', cancel);
83
+ input.addEventListener('keydown', function(e) {
84
+ if (e.key === 'Enter') submit();
85
+ if (e.key === 'Escape') cancel();
86
+ });
87
+ overlay.addEventListener('click', function(e) {
88
+ if (e.target === overlay) cancel();
89
+ });
90
+ }
91
+
49
92
  // Left click: open note / toggle folder
50
93
  document.addEventListener('click', function(e) {
51
94
  var existingMenu = document.getElementById('ctx-menu');
@@ -107,7 +150,6 @@ document.addEventListener('contextmenu', function(e) {
107
150
  menuHtml += '<div class="ctx-item" data-action="toggleTodo" data-id="' + id + '" data-type="note">' + T('ctxToggleTodo') + '</div>';
108
151
  menuHtml += '<div class="ctx-sep"></div>';
109
152
  menuHtml += '<div class="ctx-item" data-action="renameNote" data-id="' + id + '" data-type="note" data-title="' + title.replace(/"/g, '&quot;') + '">' + T('ctxRenameNote') + '</div>';
110
- menuHtml += '<div class="ctx-item" data-action="moveNote" data-id="' + id + '" data-type="note">' + T('ctxMoveNote') + '</div>';
111
153
  menuHtml += '<div class="ctx-item" data-action="noteInfo" data-id="' + id + '" data-type="note">' + T('ctxNoteInfo') + '</div>';
112
154
  menuHtml += '<div class="ctx-sep"></div>';
113
155
  menuHtml += '<div class="ctx-item ctx-danger" data-action="deleteNote" data-id="' + id + '" data-type="note">' + T('ctxDeleteNote') + '</div>';
@@ -133,10 +175,11 @@ document.addEventListener('click', function(e) {
133
175
 
134
176
  if (action === 'renameFolder' || action === 'renameNote') {
135
177
  var currentTitle = ctxItem.dataset.title || '';
136
- var newTitle = prompt(T('promptRename'), currentTitle);
137
- if (newTitle !== null && newTitle.trim() !== '') {
138
- postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType, newTitle: newTitle.trim() });
139
- }
178
+ showInlineInput(T('promptRename'), currentTitle, function(newTitle) {
179
+ if (newTitle !== null && newTitle.trim() !== '') {
180
+ postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType, newTitle: newTitle.trim() });
181
+ }
182
+ });
140
183
  } else if (action === 'deleteFolder') {
141
184
  if (confirm(T('confirmDeleteFolder'))) {
142
185
  postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType });
@@ -146,10 +189,11 @@ document.addEventListener('click', function(e) {
146
189
  postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType });
147
190
  }
148
191
  } else if (action === 'moveNote') {
149
- var folderName = prompt(T('promptMoveNote'));
150
- if (folderName !== null && folderName.trim() !== '') {
151
- postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType, targetFolderName: folderName.trim() });
152
- }
192
+ showInlineInput(T('promptMoveNote'), '', function(folderName) {
193
+ if (folderName !== null && folderName.trim() !== '') {
194
+ postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType, targetFolderName: folderName.trim() });
195
+ }
196
+ });
153
197
  } else {
154
198
  postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType });
155
199
  }
@@ -219,6 +263,18 @@ webviewApi.onMessage(function(msg) {
219
263
  } else {
220
264
  renderSearchResults(m.results, m.query);
221
265
  }
266
+ } else if (m.name === 'copyText') {
267
+ // Fallback clipboard copy via webview
268
+ if (navigator.clipboard && navigator.clipboard.writeText) {
269
+ navigator.clipboard.writeText(m.text);
270
+ } else {
271
+ var ta = document.createElement('textarea');
272
+ ta.value = m.text;
273
+ document.body.appendChild(ta);
274
+ ta.select();
275
+ document.execCommand('copy');
276
+ ta.remove();
277
+ }
222
278
  } else if (m.name === 'selectNote') {
223
279
  // Update selection without full re-render
224
280
  document.querySelectorAll('.tree-item.note.selected').forEach(function(el) {