joplin-plugin-explorer 1.0.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joplin-plugin-explorer",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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
@@ -5,7 +5,9 @@ var i18nData = {
5
5
  'zh_CN': {
6
6
  newNotebook: '新建笔记本', newNote: '新建笔记', newTodo: '新建待办',
7
7
  sort: '排序', collapseAll: '全部折叠', expandAll: '全部展开',
8
- search: '搜索...', sync: '同步', syncing: '同步中...', syncDone: '\u2714 同步完成', loading: '加载中...',
8
+ search: '搜索笔记内容...', searchResultCount: '找到 {count} 条结果',
9
+ searchNoResult: '没有找到匹配的笔记', searching: '搜索中...',
10
+ sync: '同步', syncing: '同步中...', syncDone: '\u2714 同步完成', loading: '加载中...',
9
11
  sortUpdatedDesc: '\u2193 修改时间', sortUpdatedAsc: '\u2191 修改时间',
10
12
  sortTitleAsc: '\u2191 标题', sortTitleDesc: '\u2193 标题',
11
13
  // Folder context menu
@@ -27,7 +29,9 @@ var i18nData = {
27
29
  'zh_TW': {
28
30
  newNotebook: '新建筆記本', newNote: '新建筆記', newTodo: '新建待辦',
29
31
  sort: '排序', collapseAll: '全部摺疊', expandAll: '全部展開',
30
- search: '搜尋...', sync: '同步', syncing: '同步中...', syncDone: '\u2714 同步完成', loading: '載入中...',
32
+ search: '搜尋筆記內容...', searchResultCount: '找到 {count} 條結果',
33
+ searchNoResult: '沒有找到匹配的筆記', searching: '搜尋中...',
34
+ sync: '同步', syncing: '同步中...', syncDone: '\u2714 同步完成', loading: '載入中...',
31
35
  sortUpdatedDesc: '\u2193 修改時間', sortUpdatedAsc: '\u2191 修改時間',
32
36
  sortTitleAsc: '\u2191 標題', sortTitleDesc: '\u2193 標題',
33
37
  ctxNewNoteHere: '在此新建筆記', ctxNewTodoHere: '在此新建待辦',
@@ -45,7 +49,9 @@ var i18nData = {
45
49
  'en_US': {
46
50
  newNotebook: 'New Notebook', newNote: 'New Note', newTodo: 'New To-do',
47
51
  sort: 'Sort', collapseAll: 'Collapse All', expandAll: 'Expand All',
48
- search: 'Search...', sync: 'Synchronise', syncing: 'Syncing...', syncDone: '\u2714 Sync Done', loading: 'Loading...',
52
+ search: 'Search note contents...', searchResultCount: '{count} results found',
53
+ searchNoResult: 'No matching notes found', searching: 'Searching...',
54
+ sync: 'Synchronise', syncing: 'Syncing...', syncDone: '\u2714 Sync Done', loading: 'Loading...',
49
55
  sortUpdatedDesc: '\u2193 Updated', sortUpdatedAsc: '\u2191 Updated',
50
56
  sortTitleAsc: '\u2191 Title', sortTitleDesc: '\u2193 Title',
51
57
  ctxNewNoteHere: 'New Note Here', ctxNewTodoHere: 'New To-do Here',
@@ -60,6 +66,26 @@ var i18nData = {
60
66
  confirmDeleteNote: 'Delete this note?',
61
67
  promptRename: 'Enter new name:', promptMoveNote: 'Enter target notebook name:',
62
68
  },
69
+ 'ja_JP': {
70
+ newNotebook: '\u65B0\u898F\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF', newNote: '\u65B0\u898F\u30CE\u30FC\u30C8', newTodo: '\u65B0\u898F\u30BF\u30B9\u30AF',
71
+ sort: '\u4E26\u3079\u66FF\u3048', collapseAll: '\u3059\u3079\u3066\u6298\u308A\u305F\u305F\u3080', expandAll: '\u3059\u3079\u3066\u5C55\u958B',
72
+ search: '\u30CE\u30FC\u30C8\u5185\u5BB9\u3092\u691C\u7D22...', searchResultCount: '{count} \u4EF6\u306E\u7D50\u679C',
73
+ searchNoResult: '\u4E00\u81F4\u3059\u308B\u30CE\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093', searching: '\u691C\u7D22\u4E2D...',
74
+ sync: '\u540C\u671F', syncing: '\u540C\u671F\u4E2D...', syncDone: '\u2714 \u540C\u671F\u5B8C\u4E86', loading: '\u8AAD\u307F\u8FBC\u307F\u4E2D...',
75
+ sortUpdatedDesc: '\u2193 \u66F4\u65B0\u65E5\u6642', sortUpdatedAsc: '\u2191 \u66F4\u65B0\u65E5\u6642',
76
+ sortTitleAsc: '\u2191 \u30BF\u30A4\u30C8\u30EB', sortTitleDesc: '\u2193 \u30BF\u30A4\u30C8\u30EB',
77
+ ctxNewNoteHere: '\u3053\u3053\u306B\u65B0\u898F\u30CE\u30FC\u30C8', ctxNewTodoHere: '\u3053\u3053\u306B\u65B0\u898F\u30BF\u30B9\u30AF',
78
+ ctxNewSubNotebook: '\u65B0\u898F\u30B5\u30D6\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF', ctxRenameFolder: '\u540D\u524D\u3092\u5909\u66F4',
79
+ ctxExportFolder: '\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF\u3092\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8', ctxDeleteFolder: '\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF\u3092\u524A\u9664',
80
+ ctxOpenNote: '\u30CE\u30FC\u30C8\u3092\u958B\u304F', ctxOpenInNewWindow: '\u65B0\u3057\u3044\u30A6\u30A3\u30F3\u30C9\u30A6\u3067\u958B\u304F',
81
+ ctxCopyLink: 'Markdown\u30EA\u30F3\u30AF\u3092\u30B3\u30D4\u30FC', ctxDuplicateNote: '\u8907\u88FD',
82
+ ctxSwitchNoteType: '\u30CE\u30FC\u30C8/\u30BF\u30B9\u30AF\u5207\u308A\u66FF\u3048', ctxToggleTodo: '\u5B8C\u4E86\u72B6\u614B\u3092\u5207\u308A\u66FF\u3048',
83
+ ctxRenameNote: '\u540D\u524D\u3092\u5909\u66F4', ctxMoveNote: '\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF\u306B\u79FB\u52D5...',
84
+ ctxNoteInfo: '\u30CE\u30FC\u30C8\u30D7\u30ED\u30D1\u30C6\u30A3', ctxDeleteNote: '\u30CE\u30FC\u30C8\u3092\u524A\u9664',
85
+ 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
+ confirmDeleteNote: '\u3053\u306E\u30CE\u30FC\u30C8\u3092\u524A\u9664\u3057\u307E\u3059\u304B\uFF1F',
87
+ promptRename: '\u65B0\u3057\u3044\u540D\u524D\u3092\u5165\u529B\uFF1A', promptMoveNote: '\u79FB\u52D5\u5148\u306E\u30CE\u30FC\u30C8\u30D6\u30C3\u30AF\u540D\uFF1A',
88
+ },
63
89
  };
64
90
 
65
91
  function getI18n(locale) {
@@ -340,6 +366,62 @@ joplin.plugins.register({
340
366
  } else if (msg.name === 'newTodo') {
341
367
  await joplin.commands.execute('newTodo');
342
368
  await refreshPanel();
369
+ } else if (msg.name === 'search') {
370
+ var query = msg.query;
371
+ if (!query || !query.trim()) {
372
+ // Empty query: tell webview to clear search results
373
+ await joplin.views.panels.postMessage(panel, { name: 'searchResults', results: null, query: '' });
374
+ return;
375
+ }
376
+ try {
377
+ var searchResults = [];
378
+ var page = 1;
379
+ var hasMore = true;
380
+ while (hasMore && searchResults.length < 100) {
381
+ var result = await joplin.data.get(['search'], {
382
+ query: query,
383
+ fields: ['id', 'title', 'body', 'parent_id', 'is_todo', 'todo_completed'],
384
+ page: page, limit: 20,
385
+ });
386
+ searchResults = searchResults.concat(result.items);
387
+ hasMore = result.has_more;
388
+ page++;
389
+ }
390
+ // Build folder name lookup
391
+ var folderNameMap = {};
392
+ for (var i = 0; i < allFoldersCache.length; i++) {
393
+ folderNameMap[allFoldersCache[i].id] = allFoldersCache[i].title;
394
+ }
395
+ // Extract snippet around the matched keyword
396
+ var items = [];
397
+ for (var i = 0; i < searchResults.length; i++) {
398
+ var note = searchResults[i];
399
+ var snippet = '';
400
+ var body = note.body || '';
401
+ var lowerBody = body.toLowerCase();
402
+ var lowerQuery = query.toLowerCase();
403
+ var matchIdx = lowerBody.indexOf(lowerQuery);
404
+ if (matchIdx >= 0) {
405
+ var start = Math.max(0, matchIdx - 40);
406
+ var end = Math.min(body.length, matchIdx + lowerQuery.length + 80);
407
+ snippet = (start > 0 ? '...' : '') + body.substring(start, end).replace(/\n/g, ' ') + (end < body.length ? '...' : '');
408
+ } else {
409
+ // Title match - show beginning of body
410
+ snippet = body.substring(0, 120).replace(/\n/g, ' ') + (body.length > 120 ? '...' : '');
411
+ }
412
+ items.push({
413
+ id: note.id,
414
+ title: note.title || '(untitled)',
415
+ is_todo: note.is_todo,
416
+ todo_completed: note.todo_completed,
417
+ snippet: snippet,
418
+ folderName: folderNameMap[note.parent_id] || '',
419
+ });
420
+ }
421
+ await joplin.views.panels.postMessage(panel, { name: 'searchResults', results: items, query: query });
422
+ } catch (err) {
423
+ console.error('Joplin Explorer: search error', err);
424
+ }
343
425
  } else if (msg.name === 'cycleSort') {
344
426
  var sortModes = ['updated_desc', 'updated_asc', 'title_asc', 'title_desc'];
345
427
  var idx = sortModes.indexOf(currentSort);
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 1,
3
3
  "id": "com.github.joplin-explorer",
4
4
  "app_min_version": "2.6.0",
5
- "version": "1.0.1",
5
+ "version": "1.1.0",
6
6
  "name": "Joplin Explorer",
7
7
  "description": "A unified sidebar that displays notebooks and notes together in a single tree view",
8
8
  "author": "user",
Binary file
@@ -273,3 +273,64 @@ html, body {
273
273
  .tree-item.drop-below {
274
274
  box-shadow: 0 2px 0 0 var(--joplin-color2, #4a9cf5);
275
275
  }
276
+
277
+ /* Search results */
278
+ .search-status {
279
+ padding: 8px 12px;
280
+ font-size: 11px;
281
+ color: var(--joplin-color-faded, #888);
282
+ border-bottom: 1px solid var(--joplin-divider-color, #eee);
283
+ }
284
+
285
+ .search-result-item {
286
+ display: flex;
287
+ align-items: flex-start;
288
+ padding: 8px 10px;
289
+ gap: 8px;
290
+ min-height: auto;
291
+ }
292
+
293
+ .search-result-item .icon {
294
+ margin-top: 2px;
295
+ flex-shrink: 0;
296
+ }
297
+
298
+ .search-result-content {
299
+ flex: 1;
300
+ min-width: 0;
301
+ overflow: hidden;
302
+ }
303
+
304
+ .search-result-title {
305
+ font-weight: 600;
306
+ font-size: 12px;
307
+ line-height: 1.4;
308
+ white-space: nowrap;
309
+ overflow: hidden;
310
+ text-overflow: ellipsis;
311
+ }
312
+
313
+ .search-result-folder {
314
+ font-size: 10px;
315
+ color: var(--joplin-color-faded, #999);
316
+ margin-top: 1px;
317
+ }
318
+
319
+ .search-result-snippet {
320
+ font-size: 11px;
321
+ color: var(--joplin-color-faded, #777);
322
+ margin-top: 3px;
323
+ line-height: 1.4;
324
+ display: -webkit-box;
325
+ -webkit-line-clamp: 2;
326
+ -webkit-box-orient: vertical;
327
+ overflow: hidden;
328
+ word-break: break-all;
329
+ }
330
+
331
+ mark.search-highlight {
332
+ background: rgba(255, 213, 0, 0.4);
333
+ color: inherit;
334
+ padding: 0 1px;
335
+ border-radius: 2px;
336
+ }
@@ -213,6 +213,13 @@ webviewApi.onMessage(function(msg) {
213
213
  }
214
214
  }, 2000);
215
215
  }
216
+ } else if (m.name === 'searchResults') {
217
+ if (m.results === null) {
218
+ // Cleared search
219
+ if (_searchMode) exitSearchMode();
220
+ } else {
221
+ renderSearchResults(m.results, m.query);
222
+ }
216
223
  } else if (m.name === 'selectNote') {
217
224
  // Update selection without full re-render
218
225
  document.querySelectorAll('.tree-item.note.selected').forEach(function(el) {
@@ -341,28 +348,93 @@ document.addEventListener('drop', function(e) {
341
348
  });
342
349
  });
343
350
 
344
- // Search filter
351
+ // ======================== Content Search ========================
352
+ var _searchTimer = null;
353
+ var _searchMode = false;
354
+
355
+ function escapeRegex(str) {
356
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
357
+ }
358
+
359
+ function highlightText(text, query) {
360
+ if (!query) return text;
361
+ // Escape HTML first
362
+ var escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
363
+ var regex = new RegExp('(' + escapeRegex(query) + ')', 'gi');
364
+ return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
365
+ }
366
+
367
+ function renderSearchResults(results, query) {
368
+ var container = document.getElementById('tree-container');
369
+ if (!container) return;
370
+
371
+ if (!results || results.length === 0) {
372
+ container.innerHTML = '<div class="search-status">' + T('searchNoResult') + '</div>';
373
+ return;
374
+ }
375
+
376
+ var countText = T('searchResultCount').replace('{count}', results.length);
377
+ var html = '<div class="search-status">' + countText + '</div>';
378
+
379
+ for (var i = 0; i < results.length; i++) {
380
+ var item = results[i];
381
+ var icon = '\uD83D\uDCDD';
382
+ if (item.is_todo) {
383
+ icon = item.todo_completed ? '\u2611' : '\u2610';
384
+ }
385
+ html += '<div class="search-result-item tree-item note" data-id="' + item.id + '" data-type="note">';
386
+ html += '<span class="icon note-icon">' + icon + '</span>';
387
+ html += '<div class="search-result-content">';
388
+ html += '<div class="search-result-title">' + highlightText(item.title, query) + '</div>';
389
+ if (item.folderName) {
390
+ html += '<div class="search-result-folder">\uD83D\uDCC2 ' + item.folderName + '</div>';
391
+ }
392
+ if (item.snippet) {
393
+ html += '<div class="search-result-snippet">' + highlightText(item.snippet, query) + '</div>';
394
+ }
395
+ html += '</div></div>';
396
+ }
397
+
398
+ container.innerHTML = html;
399
+ _searchMode = true;
400
+ }
401
+
402
+ function exitSearchMode() {
403
+ _searchMode = false;
404
+ // Trigger a refresh to restore the tree
405
+ postMsg({ name: 'refresh' });
406
+ }
407
+
345
408
  document.addEventListener('input', function(e) {
346
409
  if (e.target.id !== 'search-input') return;
347
- var query = e.target.value.toLowerCase();
410
+ var query = e.target.value.trim();
348
411
 
349
- document.querySelectorAll('.tree-item.note').forEach(function(item) {
350
- var label = item.querySelector('.label').textContent.toLowerCase();
351
- item.style.display = (label.indexOf(query) >= 0 || !query) ? '' : 'none';
352
- });
412
+ if (_searchTimer) clearTimeout(_searchTimer);
353
413
 
354
- if (query) {
355
- document.querySelectorAll('.children').forEach(function(c) { c.classList.remove('collapsed'); });
356
- document.querySelectorAll('.toggle').forEach(function(t) { t.innerHTML = '\u25BC'; t.classList.add('expanded'); });
414
+ if (!query) {
415
+ if (_searchMode) exitSearchMode();
416
+ return;
357
417
  }
358
418
 
359
- document.querySelectorAll('.children').forEach(function(childrenDiv) {
360
- var folder = childrenDiv.previousElementSibling;
361
- if (!folder || !folder.classList.contains('folder')) return;
362
- if (!query) { folder.style.display = ''; childrenDiv.style.display = ''; return; }
363
- var hasVisible = childrenDiv.querySelector('.tree-item.note:not([style*="display: none"])');
364
- var hasVisibleSub = childrenDiv.querySelector('.tree-item.folder:not([style*="display: none"])');
365
- if (hasVisible || hasVisibleSub) { folder.style.display = ''; childrenDiv.style.display = ''; }
366
- else { folder.style.display = 'none'; childrenDiv.style.display = 'none'; }
367
- });
419
+ // Show "searching..." immediately
420
+ var container = document.getElementById('tree-container');
421
+ if (container) {
422
+ container.innerHTML = '<div class="search-status">' + T('searching') + '</div>';
423
+ }
424
+
425
+ // Debounce: wait 400ms after typing stops
426
+ _searchTimer = setTimeout(function() {
427
+ postMsg({ name: 'search', query: query });
428
+ }, 400);
429
+ });
430
+
431
+ // Handle Escape key to clear search
432
+ document.addEventListener('keydown', function(e) {
433
+ if (e.key === 'Escape') {
434
+ var input = document.getElementById('search-input');
435
+ if (input && input.value) {
436
+ input.value = '';
437
+ if (_searchMode) exitSearchMode();
438
+ }
439
+ }
368
440
  });