joplin-plugin-explorer 1.0.0 → 1.0.2
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 +1 -2
- package/publish/index.js +516 -1
- package/publish/manifest.json +1 -1
- package/publish/plugin.jpl +0 -0
- package/publish/webview/panel.css +275 -108
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joplin-plugin-explorer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dist": "webpack --env production",
|
|
13
|
-
"prepare": "npm run dist",
|
|
14
13
|
"dev": "webpack --watch"
|
|
15
14
|
},
|
|
16
15
|
"keywords": ["joplin-plugin", "joplin", "sidebar", "explorer", "tree-view", "notebooks"],
|
package/publish/index.js
CHANGED
|
@@ -1 +1,516 @@
|
|
|
1
|
-
|
|
1
|
+
/* Notes In List Plugin - notebooks and notes in a single tree view */
|
|
2
|
+
|
|
3
|
+
/* ======================== i18n ======================== */
|
|
4
|
+
var i18nData = {
|
|
5
|
+
'zh_CN': {
|
|
6
|
+
newNotebook: '新建笔记本', newNote: '新建笔记', newTodo: '新建待办',
|
|
7
|
+
sort: '排序', collapseAll: '全部折叠', expandAll: '全部展开',
|
|
8
|
+
search: '搜索...', sync: '同步', syncing: '同步中...', syncDone: '\u2714 同步完成', loading: '加载中...',
|
|
9
|
+
sortUpdatedDesc: '\u2193 修改时间', sortUpdatedAsc: '\u2191 修改时间',
|
|
10
|
+
sortTitleAsc: '\u2191 标题', sortTitleDesc: '\u2193 标题',
|
|
11
|
+
// Folder context menu
|
|
12
|
+
ctxNewNoteHere: '在此新建笔记', ctxNewTodoHere: '在此新建待办',
|
|
13
|
+
ctxNewSubNotebook: '新建子笔记本', ctxRenameFolder: '重命名',
|
|
14
|
+
ctxExportFolder: '导出笔记本', ctxDeleteFolder: '删除笔记本',
|
|
15
|
+
// Note context menu
|
|
16
|
+
ctxOpenNote: '打开笔记', ctxOpenInNewWindow: '在新窗口中打开',
|
|
17
|
+
ctxCopyLink: '复制 Markdown 链接', ctxDuplicateNote: '复制副本',
|
|
18
|
+
ctxSwitchNoteType: '笔记/待办 切换', ctxToggleTodo: '切换完成状态',
|
|
19
|
+
ctxRenameNote: '重命名', ctxMoveNote: '移动到笔记本...',
|
|
20
|
+
ctxNoteInfo: '笔记属性', ctxDeleteNote: '删除笔记',
|
|
21
|
+
// Confirm dialogs
|
|
22
|
+
confirmDeleteFolder: '确定删除此笔记本及其所有内容吗?',
|
|
23
|
+
confirmDeleteNote: '确定删除此笔记吗?',
|
|
24
|
+
promptRename: '请输入新名称:',
|
|
25
|
+
promptMoveNote: '请输入目标笔记本名称:',
|
|
26
|
+
},
|
|
27
|
+
'zh_TW': {
|
|
28
|
+
newNotebook: '新建筆記本', newNote: '新建筆記', newTodo: '新建待辦',
|
|
29
|
+
sort: '排序', collapseAll: '全部摺疊', expandAll: '全部展開',
|
|
30
|
+
search: '搜尋...', sync: '同步', syncing: '同步中...', syncDone: '\u2714 同步完成', loading: '載入中...',
|
|
31
|
+
sortUpdatedDesc: '\u2193 修改時間', sortUpdatedAsc: '\u2191 修改時間',
|
|
32
|
+
sortTitleAsc: '\u2191 標題', sortTitleDesc: '\u2193 標題',
|
|
33
|
+
ctxNewNoteHere: '在此新建筆記', ctxNewTodoHere: '在此新建待辦',
|
|
34
|
+
ctxNewSubNotebook: '新建子筆記本', ctxRenameFolder: '重新命名',
|
|
35
|
+
ctxExportFolder: '匯出筆記本', ctxDeleteFolder: '刪除筆記本',
|
|
36
|
+
ctxOpenNote: '開啟筆記', ctxOpenInNewWindow: '在新視窗中開啟',
|
|
37
|
+
ctxCopyLink: '複製 Markdown 連結', ctxDuplicateNote: '複製副本',
|
|
38
|
+
ctxSwitchNoteType: '筆記/待辦 切換', ctxToggleTodo: '切換完成狀態',
|
|
39
|
+
ctxRenameNote: '重新命名', ctxMoveNote: '移動到筆記本...',
|
|
40
|
+
ctxNoteInfo: '筆記屬性', ctxDeleteNote: '刪除筆記',
|
|
41
|
+
confirmDeleteFolder: '確定刪除此筆記本及其所有內容嗎?',
|
|
42
|
+
confirmDeleteNote: '確定刪除此筆記嗎?',
|
|
43
|
+
promptRename: '請輸入新名稱:', promptMoveNote: '請輸入目標筆記本名稱:',
|
|
44
|
+
},
|
|
45
|
+
'en_US': {
|
|
46
|
+
newNotebook: 'New Notebook', newNote: 'New Note', newTodo: 'New To-do',
|
|
47
|
+
sort: 'Sort', collapseAll: 'Collapse All', expandAll: 'Expand All',
|
|
48
|
+
search: 'Search...', sync: 'Synchronise', syncing: 'Syncing...', syncDone: '\u2714 Sync Done', loading: 'Loading...',
|
|
49
|
+
sortUpdatedDesc: '\u2193 Updated', sortUpdatedAsc: '\u2191 Updated',
|
|
50
|
+
sortTitleAsc: '\u2191 Title', sortTitleDesc: '\u2193 Title',
|
|
51
|
+
ctxNewNoteHere: 'New Note Here', ctxNewTodoHere: 'New To-do Here',
|
|
52
|
+
ctxNewSubNotebook: 'New Sub-notebook', ctxRenameFolder: 'Rename',
|
|
53
|
+
ctxExportFolder: 'Export Notebook', ctxDeleteFolder: 'Delete Notebook',
|
|
54
|
+
ctxOpenNote: 'Open Note', ctxOpenInNewWindow: 'Open in New Window',
|
|
55
|
+
ctxCopyLink: 'Copy Markdown Link', ctxDuplicateNote: 'Duplicate',
|
|
56
|
+
ctxSwitchNoteType: 'Switch Note/To-do', ctxToggleTodo: 'Toggle Completed',
|
|
57
|
+
ctxRenameNote: 'Rename', ctxMoveNote: 'Move to Notebook...',
|
|
58
|
+
ctxNoteInfo: 'Note Properties', ctxDeleteNote: 'Delete Note',
|
|
59
|
+
confirmDeleteFolder: 'Delete this notebook and all its contents?',
|
|
60
|
+
confirmDeleteNote: 'Delete this note?',
|
|
61
|
+
promptRename: 'Enter new name:', promptMoveNote: 'Enter target notebook name:',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function getI18n(locale) {
|
|
66
|
+
return i18nData[locale] || i18nData[locale.split('_')[0]] || i18nData['en_US'];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ======================== Data helpers ======================== */
|
|
70
|
+
function escapeHtml(str) {
|
|
71
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function getAllFolders() {
|
|
75
|
+
var folders = [];
|
|
76
|
+
var page = 1;
|
|
77
|
+
var hasMore = true;
|
|
78
|
+
while (hasMore) {
|
|
79
|
+
var result = await joplin.data.get(['folders'], {
|
|
80
|
+
fields: ['id', 'title', 'parent_id', 'icon'],
|
|
81
|
+
page: page, limit: 100,
|
|
82
|
+
});
|
|
83
|
+
folders = folders.concat(result.items);
|
|
84
|
+
hasMore = result.has_more;
|
|
85
|
+
page++;
|
|
86
|
+
}
|
|
87
|
+
return folders;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getNotesInFolder(folderId) {
|
|
91
|
+
var notes = [];
|
|
92
|
+
var page = 1;
|
|
93
|
+
var hasMore = true;
|
|
94
|
+
while (hasMore) {
|
|
95
|
+
var result = await joplin.data.get(['folders', folderId, 'notes'], {
|
|
96
|
+
fields: ['id', 'title', 'parent_id', 'is_todo', 'todo_completed', 'updated_time', 'user_updated_time'],
|
|
97
|
+
page: page, limit: 100,
|
|
98
|
+
order_by: 'user_updated_time', order_dir: 'DESC',
|
|
99
|
+
});
|
|
100
|
+
notes = notes.concat(result.items);
|
|
101
|
+
hasMore = result.has_more;
|
|
102
|
+
page++;
|
|
103
|
+
}
|
|
104
|
+
return notes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildTree(folders, notesByFolder) {
|
|
108
|
+
var folderMap = {};
|
|
109
|
+
for (var i = 0; i < folders.length; i++) {
|
|
110
|
+
var f = folders[i];
|
|
111
|
+
folderMap[f.id] = {
|
|
112
|
+
type: 'folder', id: f.id, title: f.title,
|
|
113
|
+
parent_id: f.parent_id, icon: f.icon, note_count: 0, children: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
var folderIds = Object.keys(notesByFolder);
|
|
117
|
+
for (var i = 0; i < folderIds.length; i++) {
|
|
118
|
+
var fid = folderIds[i];
|
|
119
|
+
var folder = folderMap[fid];
|
|
120
|
+
if (folder) {
|
|
121
|
+
var notes = notesByFolder[fid];
|
|
122
|
+
folder.note_count = notes.length;
|
|
123
|
+
for (var j = 0; j < notes.length; j++) {
|
|
124
|
+
var n = notes[j];
|
|
125
|
+
folder.children.push({
|
|
126
|
+
type: 'note', id: n.id, title: n.title || '(untitled)',
|
|
127
|
+
is_todo: n.is_todo, todo_completed: n.todo_completed,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
var roots = [];
|
|
133
|
+
for (var i = 0; i < folders.length; i++) {
|
|
134
|
+
var f = folders[i];
|
|
135
|
+
var node = folderMap[f.id];
|
|
136
|
+
if (f.parent_id && folderMap[f.parent_id]) {
|
|
137
|
+
folderMap[f.parent_id].children.unshift(node);
|
|
138
|
+
} else {
|
|
139
|
+
roots.push(node);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Recursively compute total note count including sub-folders
|
|
144
|
+
function calcTotalCount(node) {
|
|
145
|
+
var total = 0;
|
|
146
|
+
if (!node.children) return 0;
|
|
147
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
148
|
+
var child = node.children[i];
|
|
149
|
+
if (child.type === 'note') {
|
|
150
|
+
total++;
|
|
151
|
+
} else if (child.type === 'folder') {
|
|
152
|
+
calcTotalCount(child);
|
|
153
|
+
total += child.total_count;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
node.total_count = total + (node.note_count || 0) - (node.note_count || 0);
|
|
157
|
+
// note_count is direct notes only (already counted in children), total_count is all
|
|
158
|
+
node.total_count = total;
|
|
159
|
+
return total;
|
|
160
|
+
}
|
|
161
|
+
for (var i = 0; i < roots.length; i++) {
|
|
162
|
+
calcTotalCount(roots[i]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return roots;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getFolderIcon(node) {
|
|
169
|
+
var iconData = node.icon;
|
|
170
|
+
if (iconData && typeof iconData === 'string') {
|
|
171
|
+
try { iconData = JSON.parse(iconData); } catch(e) { iconData = null; }
|
|
172
|
+
}
|
|
173
|
+
if (iconData && iconData.emoji) return iconData.emoji;
|
|
174
|
+
return '\uD83D\uDCC2';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderTreeHtml(nodes, selectedNoteId, collapsedSet, level) {
|
|
178
|
+
level = level || 0;
|
|
179
|
+
var html = '';
|
|
180
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
181
|
+
var node = nodes[i];
|
|
182
|
+
var indent = level * 18;
|
|
183
|
+
if (node.type === 'folder') {
|
|
184
|
+
var count = node.total_count || node.note_count || 0;
|
|
185
|
+
var isCollapsed = collapsedSet[node.id];
|
|
186
|
+
var arrowChar = isCollapsed ? '\u25B6' : '\u25BC';
|
|
187
|
+
var toggleClass = isCollapsed ? 'toggle' : 'toggle expanded';
|
|
188
|
+
html += '<div class="tree-item folder" style="padding-left:' + indent + 'px" data-id="' + node.id + '" data-type="folder">';
|
|
189
|
+
html += '<span class="' + toggleClass + '">' + arrowChar + '</span>';
|
|
190
|
+
html += '<span class="icon folder-icon">' + getFolderIcon(node) + '</span>';
|
|
191
|
+
html += '<span class="label">' + escapeHtml(node.title) + '</span>';
|
|
192
|
+
html += '<span class="count">' + count + '</span>';
|
|
193
|
+
html += '</div>';
|
|
194
|
+
html += '<div class="children' + (isCollapsed ? ' collapsed' : '') + '" data-folder-id="' + node.id + '">';
|
|
195
|
+
if (node.children) {
|
|
196
|
+
html += renderTreeHtml(node.children, selectedNoteId, collapsedSet, level + 1);
|
|
197
|
+
}
|
|
198
|
+
html += '</div>';
|
|
199
|
+
} else {
|
|
200
|
+
var selected = node.id === selectedNoteId ? ' selected' : '';
|
|
201
|
+
var icon = '\uD83D\uDCDD';
|
|
202
|
+
if (node.is_todo) {
|
|
203
|
+
icon = node.todo_completed ? '\u2611' : '\u2610';
|
|
204
|
+
}
|
|
205
|
+
html += '<div class="tree-item note' + selected + '" style="padding-left:' + indent + 'px" data-id="' + node.id + '" data-type="note">';
|
|
206
|
+
html += '<span class="icon note-icon">' + icon + '</span>';
|
|
207
|
+
html += '<span class="label">' + escapeHtml(node.title) + '</span>';
|
|
208
|
+
html += '</div>';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return html;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ======================== Plugin ======================== */
|
|
215
|
+
joplin.plugins.register({
|
|
216
|
+
onStart: async function () {
|
|
217
|
+
// Get locale
|
|
218
|
+
var locale = await joplin.settings.globalValue('locale') || 'en_US';
|
|
219
|
+
var t = getI18n(locale);
|
|
220
|
+
|
|
221
|
+
var panel = await joplin.views.panels.create('notesInListPanel');
|
|
222
|
+
await joplin.views.panels.addScript(panel, 'webview/panel.css');
|
|
223
|
+
await joplin.views.panels.addScript(panel, 'webview/panel.js');
|
|
224
|
+
await joplin.views.panels.setHtml(panel, '<div id="notes-in-list-root"><p style="padding:12px;">' + t.loading + '</p></div>');
|
|
225
|
+
await joplin.views.panels.show(panel, true);
|
|
226
|
+
|
|
227
|
+
var selectedNoteId = '';
|
|
228
|
+
var collapsedFolders = {};
|
|
229
|
+
var currentSort = 'updated_desc';
|
|
230
|
+
var allFoldersCache = [];
|
|
231
|
+
var isFirstLoad = true;
|
|
232
|
+
|
|
233
|
+
function sortNotes(notes, sortMode) {
|
|
234
|
+
var sorted = notes.slice();
|
|
235
|
+
switch (sortMode) {
|
|
236
|
+
case 'title_asc': sorted.sort(function(a, b) { return (a.title || '').localeCompare(b.title || ''); }); break;
|
|
237
|
+
case 'title_desc': sorted.sort(function(a, b) { return (b.title || '').localeCompare(a.title || ''); }); break;
|
|
238
|
+
case 'updated_asc': sorted.sort(function(a, b) { return (a.user_updated_time || 0) - (b.user_updated_time || 0); }); break;
|
|
239
|
+
default: sorted.sort(function(a, b) { return (b.user_updated_time || 0) - (a.user_updated_time || 0); }); break;
|
|
240
|
+
}
|
|
241
|
+
return sorted;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function refreshPanel() {
|
|
245
|
+
try {
|
|
246
|
+
var folders = await getAllFolders();
|
|
247
|
+
allFoldersCache = folders;
|
|
248
|
+
|
|
249
|
+
// Default: all folders collapsed, then expand path to current note
|
|
250
|
+
if (isFirstLoad) {
|
|
251
|
+
for (var fi = 0; fi < folders.length; fi++) {
|
|
252
|
+
collapsedFolders[folders[fi].id] = true;
|
|
253
|
+
}
|
|
254
|
+
// Expand the path to the currently selected note
|
|
255
|
+
var currentNote = await joplin.workspace.selectedNote();
|
|
256
|
+
if (currentNote) {
|
|
257
|
+
selectedNoteId = currentNote.id;
|
|
258
|
+
var parentId = currentNote.parent_id;
|
|
259
|
+
while (parentId) {
|
|
260
|
+
delete collapsedFolders[parentId];
|
|
261
|
+
var parentFolder = null;
|
|
262
|
+
for (var pi = 0; pi < folders.length; pi++) {
|
|
263
|
+
if (folders[pi].id === parentId) { parentFolder = folders[pi]; break; }
|
|
264
|
+
}
|
|
265
|
+
parentId = parentFolder ? parentFolder.parent_id : null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
isFirstLoad = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var notesByFolder = {};
|
|
272
|
+
|
|
273
|
+
var batchSize = 10;
|
|
274
|
+
for (var i = 0; i < folders.length; i += batchSize) {
|
|
275
|
+
var batch = folders.slice(i, i + batchSize);
|
|
276
|
+
var promises = [];
|
|
277
|
+
for (var j = 0; j < batch.length; j++) promises.push(getNotesInFolder(batch[j].id));
|
|
278
|
+
var results = await Promise.all(promises);
|
|
279
|
+
for (var j = 0; j < batch.length; j++) notesByFolder[batch[j].id] = sortNotes(results[j], currentSort);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
var tree = buildTree(folders, notesByFolder);
|
|
283
|
+
var treeHtml = renderTreeHtml(tree, selectedNoteId, collapsedFolders);
|
|
284
|
+
|
|
285
|
+
var sortLabels = {
|
|
286
|
+
'updated_desc': t.sortUpdatedDesc, 'updated_asc': t.sortUpdatedAsc,
|
|
287
|
+
'title_asc': t.sortTitleAsc, 'title_desc': t.sortTitleDesc,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Pass i18n via data attribute (inline scripts may not execute in webview)
|
|
291
|
+
var i18nJson = escapeHtml(JSON.stringify(t));
|
|
292
|
+
|
|
293
|
+
var html = '<div id="notes-in-list-root" data-i18n="' + i18nJson + '">'
|
|
294
|
+
+ ' <div class="toolbar">'
|
|
295
|
+
+ ' <button id="btn-new-notebook" title="' + t.newNotebook + '">\uD83D\uDCC1+</button>'
|
|
296
|
+
+ ' <button id="btn-new-note" title="' + t.newNote + '">\uD83D\uDCDD+</button>'
|
|
297
|
+
+ ' <button id="btn-new-todo" title="' + t.newTodo + '">\u2610+</button>'
|
|
298
|
+
+ ' <button id="btn-sort" title="' + t.sort + '">' + sortLabels[currentSort] + '</button>'
|
|
299
|
+
+ ' <button id="btn-collapse-all" title="' + t.collapseAll + '">\u25B2</button>'
|
|
300
|
+
+ ' </div>'
|
|
301
|
+
+ ' <div class="search-bar">'
|
|
302
|
+
+ ' <input id="search-input" type="text" placeholder="\uD83D\uDD0D ' + t.search + '" />'
|
|
303
|
+
+ ' </div>'
|
|
304
|
+
+ ' <div id="tree-container">' + treeHtml + '</div>'
|
|
305
|
+
+ ' <div class="bottom-bar">'
|
|
306
|
+
+ ' <button id="btn-sync" title="' + t.sync + '">\uD83D\uDD04 ' + t.sync + '</button>'
|
|
307
|
+
+ ' </div>'
|
|
308
|
+
+ '</div>';
|
|
309
|
+
|
|
310
|
+
await joplin.views.panels.setHtml(panel, html);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.error('Notes In List: refresh error', err);
|
|
313
|
+
await joplin.views.panels.setHtml(panel, '<div style="padding:12px;color:red;">Error: ' + escapeHtml(String(err)) + '</div>');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await joplin.views.panels.onMessage(panel, async function(msg) {
|
|
318
|
+
if (msg.name === 'openNote') {
|
|
319
|
+
selectedNoteId = msg.id;
|
|
320
|
+
await joplin.commands.execute('openNote', msg.id);
|
|
321
|
+
} else if (msg.name === 'refresh') {
|
|
322
|
+
await refreshPanel();
|
|
323
|
+
} else if (msg.name === 'toggleFolder') {
|
|
324
|
+
if (collapsedFolders[msg.id]) { delete collapsedFolders[msg.id]; }
|
|
325
|
+
else { collapsedFolders[msg.id] = true; }
|
|
326
|
+
await refreshPanel();
|
|
327
|
+
} else if (msg.name === 'collapseAll') {
|
|
328
|
+
var folders = await getAllFolders();
|
|
329
|
+
for (var i = 0; i < folders.length; i++) collapsedFolders[folders[i].id] = true;
|
|
330
|
+
await refreshPanel();
|
|
331
|
+
} else if (msg.name === 'expandAll') {
|
|
332
|
+
collapsedFolders = {};
|
|
333
|
+
await refreshPanel();
|
|
334
|
+
} else if (msg.name === 'newNotebook') {
|
|
335
|
+
await joplin.commands.execute('newFolder');
|
|
336
|
+
await refreshPanel();
|
|
337
|
+
} else if (msg.name === 'newNote') {
|
|
338
|
+
await joplin.commands.execute('newNote');
|
|
339
|
+
await refreshPanel();
|
|
340
|
+
} else if (msg.name === 'newTodo') {
|
|
341
|
+
await joplin.commands.execute('newTodo');
|
|
342
|
+
await refreshPanel();
|
|
343
|
+
} else if (msg.name === 'cycleSort') {
|
|
344
|
+
var sortModes = ['updated_desc', 'updated_asc', 'title_asc', 'title_desc'];
|
|
345
|
+
var idx = sortModes.indexOf(currentSort);
|
|
346
|
+
currentSort = sortModes[(idx + 1) % sortModes.length];
|
|
347
|
+
await refreshPanel();
|
|
348
|
+
} else if (msg.name === 'sync') {
|
|
349
|
+
await joplin.views.panels.postMessage(panel, { name: 'syncState', state: 'syncing' });
|
|
350
|
+
try {
|
|
351
|
+
await joplin.commands.execute('synchronize');
|
|
352
|
+
} catch(e) {
|
|
353
|
+
console.error('Joplin Explorer: sync error', e);
|
|
354
|
+
}
|
|
355
|
+
// Keep "syncing" visible for a few seconds, then show "done" briefly
|
|
356
|
+
await new Promise(function(r) { setTimeout(r, 3000); });
|
|
357
|
+
await joplin.views.panels.postMessage(panel, { name: 'syncState', state: 'done' });
|
|
358
|
+
await refreshPanel();
|
|
359
|
+
} else if (msg.name === 'contextMenu') {
|
|
360
|
+
var action = msg.action;
|
|
361
|
+
var id = msg.id;
|
|
362
|
+
var itemType = msg.itemType;
|
|
363
|
+
try {
|
|
364
|
+
if (itemType === 'folder') {
|
|
365
|
+
switch (action) {
|
|
366
|
+
case 'newNote':
|
|
367
|
+
await joplin.data.post(['folders', id, 'notes'], null, { title: '' });
|
|
368
|
+
break;
|
|
369
|
+
case 'newTodo':
|
|
370
|
+
await joplin.data.post(['folders', id, 'notes'], null, { title: '', is_todo: 1 });
|
|
371
|
+
break;
|
|
372
|
+
case 'newSubNotebook':
|
|
373
|
+
await joplin.data.post(['folders'], null, { title: t.newNotebook, parent_id: id });
|
|
374
|
+
break;
|
|
375
|
+
case 'deleteFolder':
|
|
376
|
+
await joplin.data.delete(['folders', id]);
|
|
377
|
+
break;
|
|
378
|
+
case 'renameFolder':
|
|
379
|
+
if (msg.newTitle) await joplin.data.put(['folders', id], null, { title: msg.newTitle });
|
|
380
|
+
break;
|
|
381
|
+
case 'exportFolder':
|
|
382
|
+
await joplin.commands.execute('exportFolders', [id]);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
} else if (itemType === 'note') {
|
|
386
|
+
switch (action) {
|
|
387
|
+
case 'openNote':
|
|
388
|
+
await joplin.commands.execute('openNote', id);
|
|
389
|
+
break;
|
|
390
|
+
case 'openInNewWindow':
|
|
391
|
+
await joplin.commands.execute('openNoteInNewWindow', id);
|
|
392
|
+
break;
|
|
393
|
+
case 'copyLink':
|
|
394
|
+
var linkNote = await joplin.data.get(['notes', id], { fields: ['id', 'title'] });
|
|
395
|
+
var mdLink = '[' + linkNote.title + '](:/' + linkNote.id + ')';
|
|
396
|
+
await joplin.clipboard.writeText(mdLink);
|
|
397
|
+
break;
|
|
398
|
+
case 'duplicateNote':
|
|
399
|
+
var srcNote = await joplin.data.get(['notes', id], { fields: ['title', 'body', 'parent_id', 'is_todo'] });
|
|
400
|
+
await joplin.data.post(['notes'], null, {
|
|
401
|
+
title: srcNote.title + ' (copy)', body: srcNote.body,
|
|
402
|
+
parent_id: srcNote.parent_id, is_todo: srcNote.is_todo,
|
|
403
|
+
});
|
|
404
|
+
break;
|
|
405
|
+
case 'switchNoteType':
|
|
406
|
+
var sn = await joplin.data.get(['notes', id], { fields: ['is_todo'] });
|
|
407
|
+
await joplin.data.put(['notes', id], null, { is_todo: sn.is_todo ? 0 : 1 });
|
|
408
|
+
break;
|
|
409
|
+
case 'toggleTodo':
|
|
410
|
+
var tn = await joplin.data.get(['notes', id], { fields: ['is_todo', 'todo_completed'] });
|
|
411
|
+
if (tn.is_todo) {
|
|
412
|
+
await joplin.data.put(['notes', id], null, { todo_completed: tn.todo_completed ? 0 : Date.now() });
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case 'renameNote':
|
|
416
|
+
if (msg.newTitle) await joplin.data.put(['notes', id], null, { title: msg.newTitle });
|
|
417
|
+
break;
|
|
418
|
+
case 'moveNote':
|
|
419
|
+
if (msg.targetFolderName) {
|
|
420
|
+
var targetFolder = null;
|
|
421
|
+
for (var i = 0; i < allFoldersCache.length; i++) {
|
|
422
|
+
if (allFoldersCache[i].title === msg.targetFolderName) {
|
|
423
|
+
targetFolder = allFoldersCache[i]; break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (targetFolder) {
|
|
427
|
+
await joplin.data.put(['notes', id], null, { parent_id: targetFolder.id });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
case 'noteInfo':
|
|
432
|
+
var info = await joplin.data.get(['notes', id], { fields: ['id', 'title', 'created_time', 'updated_time', 'is_todo', 'parent_id'] });
|
|
433
|
+
var parentTitle = '';
|
|
434
|
+
for (var i = 0; i < allFoldersCache.length; i++) {
|
|
435
|
+
if (allFoldersCache[i].id === info.parent_id) { parentTitle = allFoldersCache[i].title; break; }
|
|
436
|
+
}
|
|
437
|
+
var infoText = 'ID: ' + info.id
|
|
438
|
+
+ '\nTitle: ' + info.title
|
|
439
|
+
+ '\nNotebook: ' + parentTitle
|
|
440
|
+
+ '\nCreated: ' + new Date(info.created_time).toLocaleString()
|
|
441
|
+
+ '\nUpdated: ' + new Date(info.updated_time).toLocaleString()
|
|
442
|
+
+ '\nType: ' + (info.is_todo ? 'To-do' : 'Note');
|
|
443
|
+
// Send info back to webview to display
|
|
444
|
+
return { name: 'showInfo', text: infoText };
|
|
445
|
+
case 'deleteNote':
|
|
446
|
+
await joplin.data.delete(['notes', id]);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
await refreshPanel();
|
|
451
|
+
} catch (err) {
|
|
452
|
+
console.error('Notes In List: context menu error', err);
|
|
453
|
+
}
|
|
454
|
+
} else if (msg.name === 'dragDrop') {
|
|
455
|
+
try {
|
|
456
|
+
var dragId = msg.dragId;
|
|
457
|
+
var dragType = msg.dragType;
|
|
458
|
+
var targetId = msg.targetId;
|
|
459
|
+
var position = msg.position; // 'into', 'above', 'below'
|
|
460
|
+
|
|
461
|
+
if (dragType === 'note') {
|
|
462
|
+
// Move note to target folder
|
|
463
|
+
if (msg.position === 'into') {
|
|
464
|
+
// Find which folder the target belongs to
|
|
465
|
+
var targetFolderId = targetId;
|
|
466
|
+
// If target is a note, find its parent folder
|
|
467
|
+
if (msg.dragType === 'note') {
|
|
468
|
+
// Check if targetId is a folder
|
|
469
|
+
var isFolder = false;
|
|
470
|
+
for (var i = 0; i < allFoldersCache.length; i++) {
|
|
471
|
+
if (allFoldersCache[i].id === targetId) { isFolder = true; break; }
|
|
472
|
+
}
|
|
473
|
+
if (!isFolder) {
|
|
474
|
+
// Target is a note, get its parent_id
|
|
475
|
+
var targetNote = await joplin.data.get(['notes', targetId], { fields: ['parent_id'] });
|
|
476
|
+
targetFolderId = targetNote.parent_id;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
await joplin.data.put(['notes', dragId], null, { parent_id: targetFolderId });
|
|
480
|
+
}
|
|
481
|
+
} else if (dragType === 'folder') {
|
|
482
|
+
if (position === 'into') {
|
|
483
|
+
// Move folder as child of target folder
|
|
484
|
+
if (dragId !== targetId) {
|
|
485
|
+
await joplin.data.put(['folders', dragId], null, { parent_id: targetId });
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
// Move folder to same parent as target folder
|
|
489
|
+
var targetFolder = null;
|
|
490
|
+
for (var i = 0; i < allFoldersCache.length; i++) {
|
|
491
|
+
if (allFoldersCache[i].id === targetId) { targetFolder = allFoldersCache[i]; break; }
|
|
492
|
+
}
|
|
493
|
+
if (targetFolder) {
|
|
494
|
+
await joplin.data.put(['folders', dragId], null, { parent_id: targetFolder.parent_id || '' });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
await refreshPanel();
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.error('Notes In List: drag drop error', err);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
await joplin.workspace.onNoteSelectionChange(async function() {
|
|
506
|
+
var note = await joplin.workspace.selectedNote();
|
|
507
|
+
if (note && note.id !== selectedNoteId) {
|
|
508
|
+
selectedNoteId = note.id;
|
|
509
|
+
// Only send selection update to webview, don't re-render everything
|
|
510
|
+
await joplin.views.panels.postMessage(panel, { name: 'selectNote', id: note.id });
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
await refreshPanel();
|
|
515
|
+
},
|
|
516
|
+
});
|
package/publish/manifest.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.0.2",
|
|
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
|
|
@@ -1,108 +1,275 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.toolbar
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
html, body {
|
|
2
|
+
height: 100%;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#notes-in-list-root {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
10
|
+
font-size: 13px;
|
|
11
|
+
color: var(--joplin-color);
|
|
12
|
+
background-color: var(--joplin-background-color);
|
|
13
|
+
height: 100vh;
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
user-select: none;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Toolbar */
|
|
21
|
+
.toolbar {
|
|
22
|
+
display: flex;
|
|
23
|
+
gap: 4px;
|
|
24
|
+
padding: 6px 8px;
|
|
25
|
+
border-bottom: 1px solid var(--joplin-divider-color, #ddd);
|
|
26
|
+
align-items: center;
|
|
27
|
+
flex-shrink: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.toolbar button {
|
|
31
|
+
background: var(--joplin-background-color3, #f0f0f0);
|
|
32
|
+
border: 1px solid var(--joplin-divider-color, #ccc);
|
|
33
|
+
border-radius: 4px;
|
|
34
|
+
padding: 4px 8px;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
color: var(--joplin-color);
|
|
37
|
+
font-size: 12px;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
flex-shrink: 0;
|
|
40
|
+
line-height: 1.2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.toolbar button:hover {
|
|
44
|
+
background: var(--joplin-background-color-hover3, #e0e0e0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.toolbar button:active {
|
|
48
|
+
opacity: 0.7;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#btn-sort {
|
|
52
|
+
min-width: 76px;
|
|
53
|
+
text-align: center;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Search bar */
|
|
57
|
+
.search-bar {
|
|
58
|
+
padding: 4px 8px;
|
|
59
|
+
border-bottom: 1px solid var(--joplin-divider-color, #ddd);
|
|
60
|
+
flex-shrink: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#search-input {
|
|
64
|
+
width: 100%;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
padding: 5px 8px;
|
|
67
|
+
border: 1px solid var(--joplin-divider-color, #ccc);
|
|
68
|
+
border-radius: 4px;
|
|
69
|
+
background: var(--joplin-background-color, #fff);
|
|
70
|
+
color: var(--joplin-color);
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
outline: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#search-input:focus {
|
|
76
|
+
border-color: var(--joplin-color2, #4a9cf5);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Tree container */
|
|
80
|
+
#tree-container {
|
|
81
|
+
flex: 1;
|
|
82
|
+
overflow-y: scroll;
|
|
83
|
+
padding: 4px 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#tree-container::-webkit-scrollbar {
|
|
87
|
+
width: 8px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#tree-container::-webkit-scrollbar-track {
|
|
91
|
+
background: transparent;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#tree-container::-webkit-scrollbar-thumb {
|
|
95
|
+
background: var(--joplin-color-faded, #888);
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#tree-container::-webkit-scrollbar-thumb:hover {
|
|
100
|
+
background: var(--joplin-color, #555);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Tree items */
|
|
104
|
+
.tree-item {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
padding: 3px 8px;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
gap: 5px;
|
|
110
|
+
border-radius: 3px;
|
|
111
|
+
margin: 1px 4px;
|
|
112
|
+
min-height: 24px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.tree-item:hover {
|
|
116
|
+
background: var(--joplin-background-color-hover3, rgba(100, 100, 100, 0.1));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.tree-item.note.selected {
|
|
120
|
+
background: var(--joplin-selected-color, rgba(74, 156, 245, 0.2));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Toggle arrow */
|
|
124
|
+
.toggle {
|
|
125
|
+
width: 14px;
|
|
126
|
+
text-align: center;
|
|
127
|
+
font-size: 9px;
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
color: var(--joplin-color-faded, #888);
|
|
130
|
+
transition: transform 0.15s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Icons */
|
|
134
|
+
.icon {
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
font-size: 14px;
|
|
137
|
+
width: 18px;
|
|
138
|
+
text-align: center;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Label */
|
|
142
|
+
.label {
|
|
143
|
+
flex: 1;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
text-overflow: ellipsis;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Note count badge */
|
|
150
|
+
.count {
|
|
151
|
+
color: var(--joplin-color-faded, #888);
|
|
152
|
+
font-size: 10px;
|
|
153
|
+
flex-shrink: 0;
|
|
154
|
+
background: var(--joplin-background-color3, rgba(100,100,100,0.1));
|
|
155
|
+
border-radius: 8px;
|
|
156
|
+
padding: 0 5px;
|
|
157
|
+
min-width: 16px;
|
|
158
|
+
text-align: center;
|
|
159
|
+
line-height: 16px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Collapsed children */
|
|
163
|
+
.children.collapsed {
|
|
164
|
+
display: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Folder styling */
|
|
168
|
+
.folder .label {
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Note styling */
|
|
173
|
+
.note .label {
|
|
174
|
+
font-weight: normal;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.note .icon {
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Bottom bar (sync) */
|
|
182
|
+
.bottom-bar {
|
|
183
|
+
display: flex;
|
|
184
|
+
padding: 6px 8px;
|
|
185
|
+
border-top: 1px solid var(--joplin-divider-color, #ddd);
|
|
186
|
+
flex-shrink: 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.bottom-bar button {
|
|
190
|
+
flex: 1;
|
|
191
|
+
background: var(--joplin-background-color3, #f0f0f0);
|
|
192
|
+
border: 1px solid var(--joplin-divider-color, #ccc);
|
|
193
|
+
border-radius: 4px;
|
|
194
|
+
padding: 5px 10px;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
color: var(--joplin-color);
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.bottom-bar button:hover {
|
|
201
|
+
background: var(--joplin-background-color-hover3, #e0e0e0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.bottom-bar button:active {
|
|
205
|
+
opacity: 0.7;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.bottom-bar button:disabled,
|
|
209
|
+
.bottom-bar button.syncing {
|
|
210
|
+
opacity: 0.5;
|
|
211
|
+
cursor: not-allowed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.bottom-bar button.sync-done {
|
|
215
|
+
color: #27ae60;
|
|
216
|
+
opacity: 1;
|
|
217
|
+
cursor: default;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Context menu */
|
|
221
|
+
.context-menu {
|
|
222
|
+
position: fixed;
|
|
223
|
+
background: var(--joplin-background-color, #fff);
|
|
224
|
+
border: 1px solid var(--joplin-divider-color, #ccc);
|
|
225
|
+
border-radius: 6px;
|
|
226
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
227
|
+
padding: 4px 0;
|
|
228
|
+
min-width: 160px;
|
|
229
|
+
z-index: 9999;
|
|
230
|
+
font-size: 12px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.ctx-item {
|
|
234
|
+
padding: 6px 14px;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
color: var(--joplin-color);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.ctx-item:hover {
|
|
240
|
+
background: var(--joplin-background-color-hover3, rgba(100, 100, 100, 0.1));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.ctx-danger {
|
|
244
|
+
color: #e74c3c;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.ctx-danger:hover {
|
|
248
|
+
background: rgba(231, 76, 60, 0.1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.ctx-sep {
|
|
252
|
+
height: 1px;
|
|
253
|
+
background: var(--joplin-divider-color, #ddd);
|
|
254
|
+
margin: 4px 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Drag & Drop */
|
|
258
|
+
.tree-item.dragging {
|
|
259
|
+
opacity: 0.4;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.tree-item.drop-target {
|
|
263
|
+
background: var(--joplin-selected-color, rgba(74, 156, 245, 0.25));
|
|
264
|
+
outline: 2px solid var(--joplin-color2, #4a9cf5);
|
|
265
|
+
outline-offset: -2px;
|
|
266
|
+
border-radius: 3px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.tree-item.drop-above {
|
|
270
|
+
box-shadow: 0 -2px 0 0 var(--joplin-color2, #4a9cf5);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.tree-item.drop-below {
|
|
274
|
+
box-shadow: 0 2px 0 0 var(--joplin-color2, #4a9cf5);
|
|
275
|
+
}
|