joplin-plugin-explorer 1.0.2 → 1.1.1

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.2",
3
+ "version": "1.1.1",
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) {
@@ -302,6 +328,7 @@ joplin.plugins.register({
302
328
  + ' <input id="search-input" type="text" placeholder="\uD83D\uDD0D ' + t.search + '" />'
303
329
  + ' </div>'
304
330
  + ' <div id="tree-container">' + treeHtml + '</div>'
331
+ + ' <div id="search-results" style="display:none;"></div>'
305
332
  + ' <div class="bottom-bar">'
306
333
  + ' <button id="btn-sync" title="' + t.sync + '">\uD83D\uDD04 ' + t.sync + '</button>'
307
334
  + ' </div>'
@@ -340,6 +367,62 @@ joplin.plugins.register({
340
367
  } else if (msg.name === 'newTodo') {
341
368
  await joplin.commands.execute('newTodo');
342
369
  await refreshPanel();
370
+ } else if (msg.name === 'search') {
371
+ var query = msg.query;
372
+ if (!query || !query.trim()) {
373
+ // Empty query: tell webview to clear search results
374
+ await joplin.views.panels.postMessage(panel, { name: 'searchResults', results: null, query: '', searchId: msg.searchId });
375
+ return;
376
+ }
377
+ try {
378
+ var searchResults = [];
379
+ var page = 1;
380
+ var hasMore = true;
381
+ while (hasMore && searchResults.length < 100) {
382
+ var result = await joplin.data.get(['search'], {
383
+ query: query,
384
+ fields: ['id', 'title', 'body', 'parent_id', 'is_todo', 'todo_completed'],
385
+ page: page, limit: 20,
386
+ });
387
+ searchResults = searchResults.concat(result.items);
388
+ hasMore = result.has_more;
389
+ page++;
390
+ }
391
+ // Build folder name lookup
392
+ var folderNameMap = {};
393
+ for (var i = 0; i < allFoldersCache.length; i++) {
394
+ folderNameMap[allFoldersCache[i].id] = allFoldersCache[i].title;
395
+ }
396
+ // Extract snippet around the matched keyword
397
+ var items = [];
398
+ for (var i = 0; i < searchResults.length; i++) {
399
+ var note = searchResults[i];
400
+ var snippet = '';
401
+ var body = note.body || '';
402
+ var lowerBody = body.toLowerCase();
403
+ var lowerQuery = query.toLowerCase();
404
+ var matchIdx = lowerBody.indexOf(lowerQuery);
405
+ if (matchIdx >= 0) {
406
+ var start = Math.max(0, matchIdx - 40);
407
+ var end = Math.min(body.length, matchIdx + lowerQuery.length + 80);
408
+ snippet = (start > 0 ? '...' : '') + body.substring(start, end).replace(/\n/g, ' ') + (end < body.length ? '...' : '');
409
+ } else {
410
+ // Title match - show beginning of body
411
+ snippet = body.substring(0, 120).replace(/\n/g, ' ') + (body.length > 120 ? '...' : '');
412
+ }
413
+ items.push({
414
+ id: note.id,
415
+ title: note.title || '(untitled)',
416
+ is_todo: note.is_todo,
417
+ todo_completed: note.todo_completed,
418
+ snippet: snippet,
419
+ folderName: folderNameMap[note.parent_id] || '',
420
+ });
421
+ }
422
+ await joplin.views.panels.postMessage(panel, { name: 'searchResults', results: items, query: query, searchId: msg.searchId });
423
+ } catch (err) {
424
+ console.error('Joplin Explorer: search error', err);
425
+ }
343
426
  } else if (msg.name === 'cycleSort') {
344
427
  var sortModes = ['updated_desc', 'updated_asc', 'title_asc', 'title_desc'];
345
428
  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.2",
5
+ "version": "1.1.1",
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,83 @@ 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
+ flex: 1;
279
+ overflow-y: scroll;
280
+ padding: 4px 0;
281
+ }
282
+
283
+ #search-results::-webkit-scrollbar {
284
+ width: 8px;
285
+ }
286
+
287
+ #search-results::-webkit-scrollbar-track {
288
+ background: transparent;
289
+ }
290
+
291
+ #search-results::-webkit-scrollbar-thumb {
292
+ background: var(--joplin-color-faded, #888);
293
+ border-radius: 4px;
294
+ }
295
+
296
+ /* Search results */
297
+ .search-status {
298
+ padding: 8px 12px;
299
+ font-size: 11px;
300
+ color: var(--joplin-color-faded, #888);
301
+ border-bottom: 1px solid var(--joplin-divider-color, #eee);
302
+ }
303
+
304
+ .search-result-item {
305
+ display: flex;
306
+ align-items: flex-start;
307
+ padding: 8px 10px;
308
+ gap: 8px;
309
+ min-height: auto;
310
+ }
311
+
312
+ .search-result-item .icon {
313
+ margin-top: 2px;
314
+ flex-shrink: 0;
315
+ }
316
+
317
+ .search-result-content {
318
+ flex: 1;
319
+ min-width: 0;
320
+ overflow: hidden;
321
+ }
322
+
323
+ .search-result-title {
324
+ font-weight: 600;
325
+ font-size: 12px;
326
+ line-height: 1.4;
327
+ white-space: nowrap;
328
+ overflow: hidden;
329
+ text-overflow: ellipsis;
330
+ }
331
+
332
+ .search-result-folder {
333
+ font-size: 10px;
334
+ color: var(--joplin-color-faded, #999);
335
+ margin-top: 1px;
336
+ }
337
+
338
+ .search-result-snippet {
339
+ font-size: 11px;
340
+ color: var(--joplin-color-faded, #777);
341
+ margin-top: 3px;
342
+ line-height: 1.4;
343
+ display: -webkit-box;
344
+ -webkit-line-clamp: 2;
345
+ -webkit-box-orient: vertical;
346
+ overflow: hidden;
347
+ word-break: break-all;
348
+ }
349
+
350
+ mark.search-highlight {
351
+ background: rgba(255, 213, 0, 0.4);
352
+ color: inherit;
353
+ padding: 0 1px;
354
+ border-radius: 2px;
355
+ }
@@ -21,7 +21,6 @@ var _observer = new MutationObserver(function() {
21
21
  if (!container) return;
22
22
 
23
23
  if (_isFirstRender) {
24
- // First render: scroll to selected note
25
24
  _isFirstRender = false;
26
25
  var selected = container.querySelector('.tree-item.note.selected');
27
26
  if (selected) {
@@ -30,7 +29,6 @@ var _observer = new MutationObserver(function() {
30
29
  }, 30);
31
30
  }
32
31
  } else {
33
- // Subsequent renders: restore previous scroll position
34
32
  container.scrollTop = _savedScrollTop;
35
33
  }
36
34
  });
@@ -213,6 +211,14 @@ webviewApi.onMessage(function(msg) {
213
211
  }
214
212
  }, 2000);
215
213
  }
214
+ } else if (m.name === 'searchResults') {
215
+ // Ignore stale search results
216
+ if (m.searchId !== undefined && m.searchId !== _searchId) return;
217
+ if (m.results === null) {
218
+ if (_searchMode) exitSearchMode();
219
+ } else {
220
+ renderSearchResults(m.results, m.query);
221
+ }
216
222
  } else if (m.name === 'selectNote') {
217
223
  // Update selection without full re-render
218
224
  document.querySelectorAll('.tree-item.note.selected').forEach(function(el) {
@@ -341,28 +347,105 @@ document.addEventListener('drop', function(e) {
341
347
  });
342
348
  });
343
349
 
344
- // Search filter
350
+ // ======================== Content Search ========================
351
+ var _searchTimer = null;
352
+ var _searchMode = false;
353
+ var _searchId = 0;
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 showSearchContainer(show) {
368
+ var tree = document.getElementById('tree-container');
369
+ var search = document.getElementById('search-results');
370
+ if (tree) tree.style.display = show ? 'none' : '';
371
+ if (search) search.style.display = show ? '' : 'none';
372
+ }
373
+
374
+ function renderSearchResults(results, query) {
375
+ var container = document.getElementById('search-results');
376
+ if (!container) return;
377
+
378
+ showSearchContainer(true);
379
+
380
+ if (!results || results.length === 0) {
381
+ container.innerHTML = '<div class="search-status">' + T('searchNoResult') + '</div>';
382
+ return;
383
+ }
384
+
385
+ var countText = T('searchResultCount').replace('{count}', results.length);
386
+ var html = '<div class="search-status">' + countText + '</div>';
387
+
388
+ for (var i = 0; i < results.length; i++) {
389
+ var item = results[i];
390
+ var icon = '\uD83D\uDCDD';
391
+ if (item.is_todo) {
392
+ icon = item.todo_completed ? '\u2611' : '\u2610';
393
+ }
394
+ html += '<div class="search-result-item tree-item note" data-id="' + item.id + '" data-type="note">';
395
+ html += '<span class="icon note-icon">' + icon + '</span>';
396
+ html += '<div class="search-result-content">';
397
+ html += '<div class="search-result-title">' + highlightText(item.title, query) + '</div>';
398
+ if (item.folderName) {
399
+ html += '<div class="search-result-folder">\uD83D\uDCC2 ' + item.folderName + '</div>';
400
+ }
401
+ if (item.snippet) {
402
+ html += '<div class="search-result-snippet">' + highlightText(item.snippet, query) + '</div>';
403
+ }
404
+ html += '</div></div>';
405
+ }
406
+
407
+ container.innerHTML = html;
408
+ _searchMode = true;
409
+ }
410
+
411
+ function exitSearchMode() {
412
+ _searchMode = false;
413
+ showSearchContainer(false);
414
+ }
415
+
345
416
  document.addEventListener('input', function(e) {
346
417
  if (e.target.id !== 'search-input') return;
347
- var query = e.target.value.toLowerCase();
418
+ var query = e.target.value.trim();
348
419
 
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
- });
420
+ if (_searchTimer) clearTimeout(_searchTimer);
421
+ _searchId++;
422
+ var currentSearchId = _searchId;
353
423
 
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'); });
424
+ if (!query) {
425
+ if (_searchMode) exitSearchMode();
426
+ return;
357
427
  }
358
428
 
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
- });
429
+ _searchMode = true;
430
+ var searchContainer = document.getElementById('search-results');
431
+ if (searchContainer) {
432
+ searchContainer.innerHTML = '<div class="search-status">' + T('searching') + '</div>';
433
+ }
434
+ showSearchContainer(true);
435
+
436
+ // Debounce: wait 400ms after typing stops
437
+ _searchTimer = setTimeout(function() {
438
+ postMsg({ name: 'search', query: query, searchId: currentSearchId });
439
+ }, 400);
440
+ });
441
+
442
+ // Handle Escape key to clear search
443
+ document.addEventListener('keydown', function(e) {
444
+ if (e.key === 'Escape') {
445
+ var input = document.getElementById('search-input');
446
+ if (input && input.value) {
447
+ input.value = '';
448
+ if (_searchMode) exitSearchMode();
449
+ }
450
+ }
368
451
  });