mn-rails-core 0.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/LICENSE +21 -0
- package/dist/index.d.ts +679 -0
- package/dist/index.js +1645 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1645 @@
|
|
|
1
|
+
// src/MN.ts
|
|
2
|
+
import { MN as BaseMN7 } from "marginnote";
|
|
3
|
+
|
|
4
|
+
// src/db/NoteDatabase.ts
|
|
5
|
+
var NoteDatabase = class {
|
|
6
|
+
db;
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.db = db ?? Database.sharedInstance();
|
|
9
|
+
}
|
|
10
|
+
getNoteById(noteId) {
|
|
11
|
+
try {
|
|
12
|
+
const note = this.db.getNoteById(noteId);
|
|
13
|
+
return note ?? null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
getNotebookById(notebookId) {
|
|
19
|
+
try {
|
|
20
|
+
const topic = this.db.getNotebookById(notebookId);
|
|
21
|
+
return topic ?? null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
getDocumentById(md5) {
|
|
27
|
+
try {
|
|
28
|
+
const book = this.db.getDocumentById(md5);
|
|
29
|
+
return book ?? null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
getMediaByHash(hash) {
|
|
35
|
+
try {
|
|
36
|
+
const media = this.db.getMediaByHash(hash);
|
|
37
|
+
return media ?? null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 获取所有笔记。若提供 notebookId 则返回该笔记本的笔记,否则聚合所有笔记本的笔记。
|
|
44
|
+
*/
|
|
45
|
+
getAllNotes(notebookId) {
|
|
46
|
+
try {
|
|
47
|
+
if (notebookId) {
|
|
48
|
+
const topic = this.db.getNotebookById(notebookId);
|
|
49
|
+
return topic?.notes ?? [];
|
|
50
|
+
}
|
|
51
|
+
const notebooks = this.db.allNotebooks();
|
|
52
|
+
return notebooks.flatMap((n) => n.notes ?? []);
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
createNote(title, notebookId, docMd5) {
|
|
58
|
+
try {
|
|
59
|
+
const notebook = this.getNotebookById(notebookId);
|
|
60
|
+
if (!notebook) return null;
|
|
61
|
+
const doc = (docMd5 ? this.getDocumentById(docMd5) : null) ?? (notebook.documents?.[0] ?? (notebook.mainDocMd5 ? this.getDocumentById(notebook.mainDocMd5) : null));
|
|
62
|
+
if (!doc) return null;
|
|
63
|
+
const note = Note.createWithTitleNotebookDocument(title, notebook, doc);
|
|
64
|
+
if (note) {
|
|
65
|
+
note.noteTitle = title;
|
|
66
|
+
return note;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
updateNote(noteId, updates) {
|
|
74
|
+
try {
|
|
75
|
+
const note = this.getNoteById(noteId);
|
|
76
|
+
if (!note) return false;
|
|
77
|
+
if (updates.noteTitle !== void 0) note.noteTitle = updates.noteTitle;
|
|
78
|
+
if (updates.excerptText !== void 0) note.excerptText = updates.excerptText;
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
deleteNote(noteId) {
|
|
85
|
+
try {
|
|
86
|
+
this.db.deleteBookNote(noteId);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/config/Config.ts
|
|
95
|
+
import { MN as BaseMN } from "marginnote";
|
|
96
|
+
var Config = class {
|
|
97
|
+
prefix;
|
|
98
|
+
constructor(prefix) {
|
|
99
|
+
this.prefix = prefix;
|
|
100
|
+
}
|
|
101
|
+
get(key, defaultValue) {
|
|
102
|
+
try {
|
|
103
|
+
const fullKey = `${this.prefix}.${key}`;
|
|
104
|
+
const value = NSUserDefaults.standardUserDefaults().objectForKey(fullKey);
|
|
105
|
+
return value !== null && value !== void 0 ? value : defaultValue;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
BaseMN.log(`Config get failed: ${key}`, "error");
|
|
108
|
+
return defaultValue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
set(key, value) {
|
|
112
|
+
try {
|
|
113
|
+
const fullKey = `${this.prefix}.${key}`;
|
|
114
|
+
NSUserDefaults.standardUserDefaults().setObjectForKey(value, fullKey);
|
|
115
|
+
return NSUserDefaults.standardUserDefaults().synchronize();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
BaseMN.log(`Config set failed: ${key}`, "error");
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
remove(key) {
|
|
122
|
+
try {
|
|
123
|
+
const fullKey = `${this.prefix}.${key}`;
|
|
124
|
+
NSUserDefaults.standardUserDefaults().removeObjectForKey(fullKey);
|
|
125
|
+
return NSUserDefaults.standardUserDefaults().synchronize();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
BaseMN.log(`Config remove failed: ${key}`, "error");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
getAll() {
|
|
132
|
+
try {
|
|
133
|
+
const defaults = NSUserDefaults.standardUserDefaults();
|
|
134
|
+
const allKeys = defaults.dictionaryRepresentation();
|
|
135
|
+
const result = {};
|
|
136
|
+
for (const key in allKeys) {
|
|
137
|
+
if (key.startsWith(this.prefix + ".")) {
|
|
138
|
+
const shortKey = key.substring(this.prefix.length + 1);
|
|
139
|
+
result[shortKey] = allKeys[key];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
BaseMN.log("Config getAll failed", "error");
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
clear() {
|
|
149
|
+
try {
|
|
150
|
+
const all = this.getAll();
|
|
151
|
+
for (const key in all) this.remove(key);
|
|
152
|
+
return true;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
BaseMN.log("Config clear failed", "error");
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// src/state/StateManager.ts
|
|
161
|
+
import { reactive, watch, effect, stop } from "@vue/reactivity";
|
|
162
|
+
|
|
163
|
+
// src/bridge/MessageBridge.ts
|
|
164
|
+
import { MN as BaseMN3 } from "marginnote";
|
|
165
|
+
|
|
166
|
+
// src/bridge/Bridge.ts
|
|
167
|
+
import { MN as BaseMN2 } from "marginnote";
|
|
168
|
+
var BridgeManager = class {
|
|
169
|
+
controllers = /* @__PURE__ */ new Map();
|
|
170
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
171
|
+
/**
|
|
172
|
+
* 注册 Controller 的 actions
|
|
173
|
+
*/
|
|
174
|
+
register(controllerName, actions) {
|
|
175
|
+
this.controllers.set(controllerName, actions);
|
|
176
|
+
BaseMN2.log(`Bridge: Registered controller "${controllerName}"`, "bridge");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 注销 Controller
|
|
180
|
+
*/
|
|
181
|
+
unregister(controllerName) {
|
|
182
|
+
this.controllers.delete(controllerName);
|
|
183
|
+
BaseMN2.log(`Bridge: Unregistered controller "${controllerName}"`, "bridge");
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 调用 action
|
|
187
|
+
*/
|
|
188
|
+
async call(controllerName, actionName, ...args) {
|
|
189
|
+
const controller = this.controllers.get(controllerName);
|
|
190
|
+
if (!controller) {
|
|
191
|
+
throw new Error(`Controller "${controllerName}" not found`);
|
|
192
|
+
}
|
|
193
|
+
const action = controller[actionName];
|
|
194
|
+
if (!action || typeof action !== "function") {
|
|
195
|
+
throw new Error(`Action "${actionName}" not found in controller "${controllerName}"`);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const result = await action(...args);
|
|
199
|
+
return result;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
BaseMN2.error(`Bridge call error: ${error}`, "bridge");
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 处理来自 WebView 的消息
|
|
207
|
+
*/
|
|
208
|
+
async handleMessage(message) {
|
|
209
|
+
try {
|
|
210
|
+
const result = await this.call(message.controller, message.action, ...message.args);
|
|
211
|
+
return {
|
|
212
|
+
id: message.id,
|
|
213
|
+
success: true,
|
|
214
|
+
data: result
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
id: message.id,
|
|
219
|
+
success: false,
|
|
220
|
+
error: error instanceof Error ? error.message : String(error)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 获取所有已注册的 Controller 名称
|
|
226
|
+
*/
|
|
227
|
+
getControllerNames() {
|
|
228
|
+
return Array.from(this.controllers.keys());
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 获取 Controller 的所有 action 名称
|
|
232
|
+
*/
|
|
233
|
+
getActionNames(controllerName) {
|
|
234
|
+
const controller = this.controllers.get(controllerName);
|
|
235
|
+
if (!controller) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
return Object.keys(controller).filter((key) => typeof controller[key] === "function");
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var bridgeManagerInstance = null;
|
|
242
|
+
function getBridgeManager() {
|
|
243
|
+
if (!bridgeManagerInstance) {
|
|
244
|
+
bridgeManagerInstance = new BridgeManager();
|
|
245
|
+
bridgeManagerInstance.register("__MN_RAILS_INTERNAL__", {
|
|
246
|
+
__stateUpdate__: async (payload) => {
|
|
247
|
+
const defaultState = getState("__default__");
|
|
248
|
+
if (defaultState) {
|
|
249
|
+
defaultState.syncFromWebView("__webview__", payload.state);
|
|
250
|
+
return { success: true };
|
|
251
|
+
}
|
|
252
|
+
return { success: false, error: "Default state not found" };
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return bridgeManagerInstance;
|
|
257
|
+
}
|
|
258
|
+
var bridge = new Proxy({}, {
|
|
259
|
+
get(target, controllerName) {
|
|
260
|
+
return new Proxy({}, {
|
|
261
|
+
get(target2, actionName) {
|
|
262
|
+
return async (...args) => {
|
|
263
|
+
const manager = getBridgeManager();
|
|
264
|
+
return manager.call(controllerName, actionName, ...args);
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// src/bridge/MessageBridge.ts
|
|
272
|
+
var MessageBridge = class {
|
|
273
|
+
webViews = /* @__PURE__ */ new Map();
|
|
274
|
+
// WebView 实例映射
|
|
275
|
+
/**
|
|
276
|
+
* 注册 WebView
|
|
277
|
+
*/
|
|
278
|
+
registerWebView(webViewId, webView) {
|
|
279
|
+
this.webViews.set(webViewId, webView);
|
|
280
|
+
this.injectBridgeScript(webViewId, webView);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 注销 WebView
|
|
284
|
+
*/
|
|
285
|
+
unregisterWebView(webViewId) {
|
|
286
|
+
this.webViews.delete(webViewId);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 注入 bridge 脚本到 WebView
|
|
290
|
+
*/
|
|
291
|
+
injectBridgeScript(webViewId, webView) {
|
|
292
|
+
const bridgeScript = `
|
|
293
|
+
(function() {
|
|
294
|
+
if (window.__MN_RAILS_BRIDGE__) return;
|
|
295
|
+
|
|
296
|
+
window.__MN_RAILS_BRIDGE__ = {
|
|
297
|
+
call: function(controller, action, ...args) {
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
const message = {
|
|
300
|
+
id: Date.now() + '-' + Math.random(),
|
|
301
|
+
controller: controller,
|
|
302
|
+
action: action,
|
|
303
|
+
args: args
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// \u53D1\u9001\u6D88\u606F\u5230\u539F\u751F\u7AEF
|
|
307
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {
|
|
308
|
+
window.webkit.messageHandlers.bridge.postMessage(message);
|
|
309
|
+
} else {
|
|
310
|
+
// \u964D\u7EA7\u65B9\u6848\uFF1A\u4F7F\u7528 prompt
|
|
311
|
+
const result = prompt('__MN_RAILS_BRIDGE__', JSON.stringify(message));
|
|
312
|
+
if (result) {
|
|
313
|
+
try {
|
|
314
|
+
const response = JSON.parse(result);
|
|
315
|
+
if (response.success) {
|
|
316
|
+
resolve(response.data);
|
|
317
|
+
} else {
|
|
318
|
+
reject(new Error(response.error));
|
|
319
|
+
}
|
|
320
|
+
} catch (e) {
|
|
321
|
+
reject(e);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
reject(new Error('Bridge call failed'));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// \u5BFC\u51FA bridge \u5BF9\u8C61
|
|
332
|
+
window.bridge = new Proxy({}, {
|
|
333
|
+
get(target, controllerName) {
|
|
334
|
+
return new Proxy({}, {
|
|
335
|
+
get(target, actionName) {
|
|
336
|
+
return function(...args) {
|
|
337
|
+
return window.__MN_RAILS_BRIDGE__.call(controllerName, actionName, ...args);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// \u7C7B\u578B\u5B9A\u4E49\u4F1A\u5728 WebViewManager \u751F\u6210 HTML \u65F6\u901A\u8FC7 script \u6807\u7B7E\u6CE8\u5165
|
|
345
|
+
// \u8FD9\u91CC\u4E0D\u9700\u8981\u989D\u5916\u5904\u7406
|
|
346
|
+
|
|
347
|
+
// \u72B6\u6001\u7BA1\u7406\u76F8\u5173
|
|
348
|
+
// \u521D\u59CB\u5316\u72B6\u6001\u5B58\u50A8\uFF08\u4F7F\u7528\u7B80\u5355\u7684\u54CD\u5E94\u5F0F\u5B9E\u73B0\uFF09
|
|
349
|
+
window.__MN_RAILS_STATE__ = window.__MN_RAILS_STATE__ || {};
|
|
350
|
+
|
|
351
|
+
// \u72B6\u6001\u66F4\u65B0\u51FD\u6570\uFF08\u7B80\u5355\u7684\u54CD\u5E94\u5F0F\u5B9E\u73B0\uFF0C\u4E0D\u4F9D\u8D56 Vue\uFF09
|
|
352
|
+
var isSyncingFromNative = false; // \u9632\u6B62\u5FAA\u73AF\u540C\u6B65
|
|
353
|
+
|
|
354
|
+
function makeReactive(obj) {
|
|
355
|
+
const listeners = new Map();
|
|
356
|
+
const proxy = new Proxy(obj, {
|
|
357
|
+
set(target, key, value) {
|
|
358
|
+
const oldValue = target[key];
|
|
359
|
+
target[key] = value;
|
|
360
|
+
// \u89E6\u53D1\u76D1\u542C\u5668
|
|
361
|
+
if (listeners.has(key)) {
|
|
362
|
+
listeners.get(key).forEach(fn => {
|
|
363
|
+
try {
|
|
364
|
+
fn(value, oldValue, key);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.error('State listener error:', e);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
// \u81EA\u52A8\u540C\u6B65\u5230\u539F\u751F\u7AEF\uFF08\u5982\u679C\u503C\u53D1\u751F\u53D8\u5316\u4E14\u4E0D\u662F\u4ECE\u539F\u751F\u7AEF\u540C\u6B65\u7684\uFF09
|
|
371
|
+
if (oldValue !== value && !isSyncingFromNative && window.__MN_RAILS_STATE_UPDATE__) {
|
|
372
|
+
try {
|
|
373
|
+
window.__MN_RAILS_STATE_UPDATE__({ [key]: value });
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.error('Failed to sync state to native:', e);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
},
|
|
380
|
+
get(target, key) {
|
|
381
|
+
return target[key];
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
proxy._listeners = listeners;
|
|
385
|
+
return proxy;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// \u521B\u5EFA\u54CD\u5E94\u5F0F\u72B6\u6001\u5BF9\u8C61
|
|
389
|
+
if (!window.__MN_RAILS_STATE_REACTIVE__) {
|
|
390
|
+
window.__MN_RAILS_STATE_REACTIVE__ = makeReactive({});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// \u72B6\u6001\u66F4\u65B0\u5904\u7406\u5668
|
|
394
|
+
window.__MN_RAILS_STATE_HANDLER__ = function(message) {
|
|
395
|
+
if (message.type === 'state-update' && message.payload) {
|
|
396
|
+
const newState = message.payload.state;
|
|
397
|
+
const timestamp = message.payload.timestamp;
|
|
398
|
+
|
|
399
|
+
// \u8BBE\u7F6E\u6807\u5FD7\uFF0C\u9632\u6B62\u89E6\u53D1\u540C\u6B65\u56DE\u539F\u751F\u7AEF
|
|
400
|
+
isSyncingFromNative = true;
|
|
401
|
+
try {
|
|
402
|
+
// \u66F4\u65B0\u72B6\u6001\u5BF9\u8C61
|
|
403
|
+
Object.keys(newState).forEach(key => {
|
|
404
|
+
window.__MN_RAILS_STATE_REACTIVE__[key] = newState[key];
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// \u540C\u6B65\u5230\u975E\u54CD\u5E94\u5F0F\u526F\u672C
|
|
408
|
+
Object.assign(window.__MN_RAILS_STATE__, newState);
|
|
409
|
+
|
|
410
|
+
// \u89E6\u53D1\u5168\u5C40\u72B6\u6001\u66F4\u65B0\u4E8B\u4EF6
|
|
411
|
+
if (window.dispatchEvent) {
|
|
412
|
+
window.dispatchEvent(new CustomEvent('__mn_rails_state_update__', {
|
|
413
|
+
detail: { state: newState, timestamp: timestamp }
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
} finally {
|
|
417
|
+
// \u5EF6\u8FDF\u91CD\u7F6E\u6807\u5FD7
|
|
418
|
+
setTimeout(function() {
|
|
419
|
+
isSyncingFromNative = false;
|
|
420
|
+
}, 0);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// \u72B6\u6001\u66F4\u65B0\u56DE\u4F20\u51FD\u6570
|
|
426
|
+
window.__MN_RAILS_STATE_UPDATE__ = function(stateUpdate) {
|
|
427
|
+
const message = {
|
|
428
|
+
type: 'state-update-from-webview',
|
|
429
|
+
payload: {
|
|
430
|
+
state: stateUpdate,
|
|
431
|
+
timestamp: Date.now()
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// \u901A\u8FC7 bridge \u53D1\u9001\u6D88\u606F\u5230\u539F\u751F\u7AEF
|
|
436
|
+
if (window.__MN_RAILS_BRIDGE__) {
|
|
437
|
+
// \u4F7F\u7528\u4E00\u4E2A\u7279\u6B8A\u7684 controller \u548C action \u6765\u4F20\u9012\u72B6\u6001\u66F4\u65B0
|
|
438
|
+
window.__MN_RAILS_BRIDGE__.call('__MN_RAILS_INTERNAL__', '__stateUpdate__', message.payload)
|
|
439
|
+
.catch(err => console.error('Failed to sync state to native:', err));
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// \u6D88\u606F\u5904\u7406\u5668\uFF08\u5904\u7406\u6765\u81EA\u539F\u751F\u7AEF\u7684\u6D88\u606F\uFF09
|
|
444
|
+
window.__MN_RAILS_MESSAGE_HANDLER__ = function(message) {
|
|
445
|
+
if (message.type === 'state-update') {
|
|
446
|
+
window.__MN_RAILS_STATE_HANDLER__(message);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// useState Hook \u5B9E\u73B0
|
|
451
|
+
window.useState = function(key, defaultValue, onChange) {
|
|
452
|
+
// \u786E\u4FDD\u72B6\u6001\u5BF9\u8C61\u5B58\u5728
|
|
453
|
+
if (!window.__MN_RAILS_STATE_REACTIVE__) {
|
|
454
|
+
window.__MN_RAILS_STATE_REACTIVE__ = makeReactive({});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const state = window.__MN_RAILS_STATE_REACTIVE__;
|
|
458
|
+
|
|
459
|
+
// \u5982\u679C\u72B6\u6001\u4E0D\u5B58\u5728\u4E14\u6709\u9ED8\u8BA4\u503C\uFF0C\u521D\u59CB\u5316\u72B6\u6001
|
|
460
|
+
if (!(key in state) && defaultValue !== undefined) {
|
|
461
|
+
state[key] = defaultValue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// \u6CE8\u518C\u76D1\u542C\u5668\uFF08\u5982\u679C\u6709 onChange \u56DE\u8C03\uFF09
|
|
465
|
+
let unsubscribe = null;
|
|
466
|
+
if (typeof onChange === 'function') {
|
|
467
|
+
const listener = function(newValue, oldValue, changedKey) {
|
|
468
|
+
if (changedKey === key) {
|
|
469
|
+
onChange(newValue, oldValue);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// \u786E\u4FDD _listeners \u5B58\u5728
|
|
474
|
+
if (!state._listeners) {
|
|
475
|
+
state._listeners = new Map();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// \u6CE8\u518C\u76D1\u542C\u5668
|
|
479
|
+
if (!state._listeners.has(key)) {
|
|
480
|
+
state._listeners.set(key, []);
|
|
481
|
+
}
|
|
482
|
+
state._listeners.get(key).push(listener);
|
|
483
|
+
|
|
484
|
+
// \u8FD4\u56DE\u53D6\u6D88\u76D1\u542C\u7684\u51FD\u6570
|
|
485
|
+
unsubscribe = function() {
|
|
486
|
+
if (state._listeners) {
|
|
487
|
+
const listeners = state._listeners.get(key);
|
|
488
|
+
if (listeners) {
|
|
489
|
+
const index = listeners.indexOf(listener);
|
|
490
|
+
if (index > -1) {
|
|
491
|
+
listeners.splice(index, 1);
|
|
492
|
+
}
|
|
493
|
+
if (listeners.length === 0) {
|
|
494
|
+
state._listeners.delete(key);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Getter \u51FD\u6570
|
|
502
|
+
const getter = function() {
|
|
503
|
+
return state[key];
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Setter \u51FD\u6570
|
|
507
|
+
const setter = function(valueOrUpdater) {
|
|
508
|
+
const currentValue = state[key];
|
|
509
|
+
let newValue;
|
|
510
|
+
|
|
511
|
+
// \u652F\u6301\u51FD\u6570\u5F62\u5F0F\u7684\u66F4\u65B0\uFF1Asetter(prev => newValue)
|
|
512
|
+
if (typeof valueOrUpdater === 'function') {
|
|
513
|
+
newValue = valueOrUpdater(currentValue);
|
|
514
|
+
} else {
|
|
515
|
+
newValue = valueOrUpdater;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// \u66F4\u65B0\u72B6\u6001\uFF08\u4F1A\u81EA\u52A8\u89E6\u53D1\u76D1\u542C\u5668\u548C\u540C\u6B65\uFF09
|
|
519
|
+
state[key] = newValue;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// \u8FD4\u56DE [getter, setter] \u6216 [getter, setter, unsubscribe]
|
|
523
|
+
if (unsubscribe) {
|
|
524
|
+
return [getter, setter, unsubscribe];
|
|
525
|
+
}
|
|
526
|
+
return [getter, setter];
|
|
527
|
+
};
|
|
528
|
+
})();
|
|
529
|
+
`;
|
|
530
|
+
try {
|
|
531
|
+
if (webView && typeof webView.evaluateJavaScript === "function") {
|
|
532
|
+
webView.evaluateJavaScript(bridgeScript);
|
|
533
|
+
}
|
|
534
|
+
} catch (error) {
|
|
535
|
+
BaseMN3.error(`Failed to inject bridge script: ${error}`, "webview");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* 处理来自 WebView 的消息
|
|
540
|
+
*/
|
|
541
|
+
async handleWebViewMessage(webViewId, message) {
|
|
542
|
+
const manager = getBridgeManager();
|
|
543
|
+
return manager.handleMessage(message);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* 发送消息到 WebView
|
|
547
|
+
*/
|
|
548
|
+
sendToWebView(webViewId, type, payload) {
|
|
549
|
+
const webView = this.webViews.get(webViewId);
|
|
550
|
+
if (!webView) {
|
|
551
|
+
BaseMN3.error(`WebView "${webViewId}" not found`, "webview");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const script = `
|
|
555
|
+
if (window.__MN_RAILS_MESSAGE_HANDLER__) {
|
|
556
|
+
window.__MN_RAILS_MESSAGE_HANDLER__(${JSON.stringify({ type, payload })});
|
|
557
|
+
}
|
|
558
|
+
`;
|
|
559
|
+
try {
|
|
560
|
+
if (webView && typeof webView.evaluateJavaScript === "function") {
|
|
561
|
+
webView.evaluateJavaScript(script);
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
BaseMN3.error(`Failed to send message to WebView: ${error}`, "webview");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var messageBridgeInstance = null;
|
|
569
|
+
function getMessageBridge() {
|
|
570
|
+
if (!messageBridgeInstance) {
|
|
571
|
+
messageBridgeInstance = new MessageBridge();
|
|
572
|
+
}
|
|
573
|
+
return messageBridgeInstance;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/state/StateManager.ts
|
|
577
|
+
var StateManager = class {
|
|
578
|
+
state;
|
|
579
|
+
watchHandles = /* @__PURE__ */ new Map();
|
|
580
|
+
webViewWatchHandles = [];
|
|
581
|
+
registeredWebViews = /* @__PURE__ */ new Set();
|
|
582
|
+
autoSyncHandle = null;
|
|
583
|
+
isSyncing = false;
|
|
584
|
+
// 防止循环同步
|
|
585
|
+
constructor(initialState) {
|
|
586
|
+
this.state = reactive({ ...initialState });
|
|
587
|
+
this.setupAutoSync();
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 获取状态值
|
|
591
|
+
*/
|
|
592
|
+
get(key) {
|
|
593
|
+
return this.state[key];
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* 设置状态值
|
|
597
|
+
*/
|
|
598
|
+
set(key, value) {
|
|
599
|
+
this.state[key] = value;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* 获取整个状态对象
|
|
603
|
+
*/
|
|
604
|
+
getState() {
|
|
605
|
+
return this.state;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* 监听状态变化
|
|
609
|
+
*/
|
|
610
|
+
watch(key, listener, options) {
|
|
611
|
+
const keyStr = key;
|
|
612
|
+
const stopHandle = watch(
|
|
613
|
+
() => this.state[key],
|
|
614
|
+
(newValue, oldValue) => {
|
|
615
|
+
try {
|
|
616
|
+
listener(newValue, oldValue, keyStr);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
MN.error(`State listener error for key "${keyStr}": ${error}`, "state");
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
immediate: options?.immediate ?? false,
|
|
623
|
+
deep: options?.deep ?? true
|
|
624
|
+
}
|
|
625
|
+
);
|
|
626
|
+
if (!this.watchHandles.has(keyStr)) {
|
|
627
|
+
this.watchHandles.set(keyStr, []);
|
|
628
|
+
}
|
|
629
|
+
this.watchHandles.get(keyStr).push(stopHandle);
|
|
630
|
+
return () => {
|
|
631
|
+
stopHandle();
|
|
632
|
+
const handles = this.watchHandles.get(keyStr);
|
|
633
|
+
if (handles) {
|
|
634
|
+
const index = handles.indexOf(stopHandle);
|
|
635
|
+
if (index > -1) {
|
|
636
|
+
handles.splice(index, 1);
|
|
637
|
+
}
|
|
638
|
+
if (handles.length === 0) {
|
|
639
|
+
this.watchHandles.delete(keyStr);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* 监听整个状态变化
|
|
646
|
+
*/
|
|
647
|
+
watchAll(listener) {
|
|
648
|
+
const runner = effect(() => {
|
|
649
|
+
try {
|
|
650
|
+
const currentState = { ...this.state };
|
|
651
|
+
listener(currentState);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
MN.error(`WebView state listener error: ${error}`, "state");
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
this.webViewWatchHandles.push(runner);
|
|
657
|
+
return () => {
|
|
658
|
+
stop(runner);
|
|
659
|
+
const idx = this.webViewWatchHandles.indexOf(runner);
|
|
660
|
+
if (idx > -1) this.webViewWatchHandles.splice(idx, 1);
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* 同步状态到 WebView
|
|
665
|
+
*/
|
|
666
|
+
syncToWebView(webViewId, state) {
|
|
667
|
+
const messageBridge = getMessageBridge();
|
|
668
|
+
const stateToSync = state || this.getState();
|
|
669
|
+
messageBridge.sendToWebView(webViewId, "state-update", {
|
|
670
|
+
state: stateToSync,
|
|
671
|
+
timestamp: Date.now()
|
|
672
|
+
});
|
|
673
|
+
MN.log(`State synced to WebView: ${webViewId}`, "state");
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* 从 WebView 同步状态
|
|
677
|
+
*/
|
|
678
|
+
syncFromWebView(webViewId, state) {
|
|
679
|
+
this.isSyncing = true;
|
|
680
|
+
try {
|
|
681
|
+
Object.keys(state).forEach((key) => {
|
|
682
|
+
if (key in this.state) {
|
|
683
|
+
this.state[key] = state[key];
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
MN.log(`State synced from WebView: ${webViewId}`, "state");
|
|
687
|
+
} finally {
|
|
688
|
+
setTimeout(() => {
|
|
689
|
+
this.isSyncing = false;
|
|
690
|
+
}, 0);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* 注册需要同步状态的 WebView
|
|
695
|
+
*/
|
|
696
|
+
registerWebView(webViewId) {
|
|
697
|
+
if (this.registeredWebViews.has(webViewId)) {
|
|
698
|
+
MN.log(`WebView "${webViewId}" already registered`, "state");
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
this.registeredWebViews.add(webViewId);
|
|
702
|
+
this.syncInitialState(webViewId);
|
|
703
|
+
MN.log(`WebView "${webViewId}" registered for state sync`, "state");
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* 取消注册 WebView
|
|
707
|
+
*/
|
|
708
|
+
unregisterWebView(webViewId) {
|
|
709
|
+
if (this.registeredWebViews.delete(webViewId)) {
|
|
710
|
+
MN.log(`WebView "${webViewId}" unregistered from state sync`, "state");
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* 同步初始状态到新注册的 WebView
|
|
715
|
+
*/
|
|
716
|
+
syncInitialState(webViewId) {
|
|
717
|
+
this.syncToWebView(webViewId);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* 设置自动同步监听
|
|
721
|
+
*/
|
|
722
|
+
setupAutoSync() {
|
|
723
|
+
this.autoSyncHandle = this.watchAll((state) => {
|
|
724
|
+
if (this.isSyncing) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
this.registeredWebViews.forEach((webViewId) => {
|
|
728
|
+
this.syncToWebView(webViewId, state);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* 清理资源
|
|
734
|
+
*/
|
|
735
|
+
destroy() {
|
|
736
|
+
if (this.autoSyncHandle) {
|
|
737
|
+
this.autoSyncHandle();
|
|
738
|
+
this.autoSyncHandle = null;
|
|
739
|
+
}
|
|
740
|
+
this.watchHandles.forEach((handles) => {
|
|
741
|
+
handles.forEach((handle) => handle());
|
|
742
|
+
});
|
|
743
|
+
this.watchHandles.clear();
|
|
744
|
+
this.webViewWatchHandles.forEach((handle) => stop(handle));
|
|
745
|
+
this.webViewWatchHandles = [];
|
|
746
|
+
this.registeredWebViews.clear();
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
var stateManagers = /* @__PURE__ */ new Map();
|
|
750
|
+
function defineState(name, initialState) {
|
|
751
|
+
const existing = stateManagers.get(name);
|
|
752
|
+
if (existing) {
|
|
753
|
+
MN.log(`State "${name}" already exists, returning existing instance`, "state");
|
|
754
|
+
return existing;
|
|
755
|
+
}
|
|
756
|
+
const manager = new StateManager(initialState);
|
|
757
|
+
stateManagers.set(name, manager);
|
|
758
|
+
MN.log(`State "${name}" defined`, "state");
|
|
759
|
+
return manager;
|
|
760
|
+
}
|
|
761
|
+
function getState(name) {
|
|
762
|
+
const m = stateManagers.get(name);
|
|
763
|
+
return m == null ? void 0 : m;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/webui/WebViewManager.ts
|
|
767
|
+
import { MN as BaseMN5 } from "marginnote";
|
|
768
|
+
|
|
769
|
+
// src/utils/file.ts
|
|
770
|
+
import { MN as BaseMN4 } from "marginnote";
|
|
771
|
+
function readFile(path) {
|
|
772
|
+
try {
|
|
773
|
+
const data = NSFileManager.defaultManager().contentsAtPath?.(path);
|
|
774
|
+
return data ? String(data) : null;
|
|
775
|
+
} catch (error) {
|
|
776
|
+
BaseMN4.log(`Read file failed: ${path}`, "error");
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function fileExists(path) {
|
|
781
|
+
try {
|
|
782
|
+
const manager = NSFileManager.defaultManager();
|
|
783
|
+
return manager.fileExistsAtPath(path);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function createDirectory(path, createIntermediates = true) {
|
|
789
|
+
try {
|
|
790
|
+
const manager = NSFileManager.defaultManager();
|
|
791
|
+
const fn = manager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError;
|
|
792
|
+
if (typeof fn === "function") {
|
|
793
|
+
return fn(path, createIntermediates, {}, null);
|
|
794
|
+
}
|
|
795
|
+
return manager.createDirectoryAtPathAttributes(path, {});
|
|
796
|
+
} catch (error) {
|
|
797
|
+
BaseMN4.log(`Create directory failed: ${path}`, "error");
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function getAddonPath() {
|
|
802
|
+
try {
|
|
803
|
+
const addon = BaseMN4.currentAddon;
|
|
804
|
+
if (addon) {
|
|
805
|
+
if (addon.path) {
|
|
806
|
+
return addon.path;
|
|
807
|
+
}
|
|
808
|
+
if (addon.dir) {
|
|
809
|
+
return addon.dir;
|
|
810
|
+
}
|
|
811
|
+
if (addon.bundlePath) {
|
|
812
|
+
return addon.bundlePath;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
} catch (error) {
|
|
817
|
+
BaseMN4.log(`Failed to get addon path: ${error}`, "error");
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function getViewsPath() {
|
|
822
|
+
const addonPath = getAddonPath();
|
|
823
|
+
if (!addonPath) {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
const srcViewsPath = `${addonPath}/src/views`;
|
|
827
|
+
if (fileExists(srcViewsPath)) {
|
|
828
|
+
return srcViewsPath;
|
|
829
|
+
}
|
|
830
|
+
const viewsPath = `${addonPath}/views`;
|
|
831
|
+
if (fileExists(viewsPath)) {
|
|
832
|
+
return viewsPath;
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/webui/WebViewManager.ts
|
|
838
|
+
var WebViewManager = class {
|
|
839
|
+
webViews = /* @__PURE__ */ new Map();
|
|
840
|
+
messageBridge = getMessageBridge();
|
|
841
|
+
componentCache = /* @__PURE__ */ new Map();
|
|
842
|
+
// 组件缓存
|
|
843
|
+
/**
|
|
844
|
+
* 渲染 WebView
|
|
845
|
+
*/
|
|
846
|
+
async render(component, props, frame) {
|
|
847
|
+
try {
|
|
848
|
+
const webViewId = `webview-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
849
|
+
const defaultFrame = frame || { x: 0, y: 0, width: 400, height: 600 };
|
|
850
|
+
const webViewFrame = {
|
|
851
|
+
x: defaultFrame.x,
|
|
852
|
+
y: defaultFrame.y,
|
|
853
|
+
width: defaultFrame.width,
|
|
854
|
+
height: defaultFrame.height
|
|
855
|
+
};
|
|
856
|
+
const webView = new UIWebView(webViewFrame);
|
|
857
|
+
const html = this.generateHTML(component, props);
|
|
858
|
+
webView.loadHTMLStringBaseURL(html, null);
|
|
859
|
+
this.messageBridge.registerWebView(webViewId, webView);
|
|
860
|
+
const defaultState = getState("__default__");
|
|
861
|
+
if (defaultState) {
|
|
862
|
+
defaultState.registerWebView(webViewId);
|
|
863
|
+
}
|
|
864
|
+
const instance = {
|
|
865
|
+
id: webViewId,
|
|
866
|
+
webView,
|
|
867
|
+
component,
|
|
868
|
+
props
|
|
869
|
+
};
|
|
870
|
+
this.webViews.set(webViewId, instance);
|
|
871
|
+
BaseMN5.log(`WebView rendered: ${webViewId}`, "webview");
|
|
872
|
+
return instance;
|
|
873
|
+
} catch (error) {
|
|
874
|
+
BaseMN5.error(`Failed to render WebView: ${error}`, "webview");
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* 加载组件文件
|
|
880
|
+
* 只允许加载编译后的文件(.js)或 HTML 文件(最终产物)
|
|
881
|
+
* 禁止运行时解析 .vue 或 .jsx 源文件
|
|
882
|
+
*/
|
|
883
|
+
loadComponent(componentName) {
|
|
884
|
+
const viewsPath = getViewsPath();
|
|
885
|
+
if (!viewsPath) {
|
|
886
|
+
BaseMN5.error(`Views path not found, component loading disabled`, "webview");
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
const compiledJsPath = `${viewsPath}/${componentName}.js`;
|
|
890
|
+
if (fileExists(compiledJsPath)) {
|
|
891
|
+
const content = readFile(compiledJsPath);
|
|
892
|
+
if (content) {
|
|
893
|
+
BaseMN5.log(`Component loaded (compiled): ${componentName}.js`, "webview");
|
|
894
|
+
return { content, type: "js" };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const htmlPath = `${viewsPath}/${componentName}.html`;
|
|
898
|
+
if (fileExists(htmlPath)) {
|
|
899
|
+
const content = readFile(htmlPath);
|
|
900
|
+
if (content) {
|
|
901
|
+
BaseMN5.log(`Component loaded: ${componentName}.html`, "webview");
|
|
902
|
+
return { content, type: "html" };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const vuePath = `${viewsPath}/${componentName}.vue`;
|
|
906
|
+
const jsxPath = `${viewsPath}/${componentName}.jsx`;
|
|
907
|
+
if (fileExists(vuePath) || fileExists(jsxPath)) {
|
|
908
|
+
const sourceExt = fileExists(vuePath) ? ".vue" : ".jsx";
|
|
909
|
+
BaseMN5.error(
|
|
910
|
+
`Component "${componentName}" source file (${sourceExt}) found but compiled file (.js) not found. Please run "mn-rails build" or "mn-rails dev" to compile the component.`,
|
|
911
|
+
"webview"
|
|
912
|
+
);
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
BaseMN5.error(
|
|
916
|
+
`Component "${componentName}" not found. Expected compiled file: ${compiledJsPath} or HTML file: ${htmlPath}. If you have a .vue or .jsx source file, please run "mn-rails build" or "mn-rails dev" to compile it.`,
|
|
917
|
+
"webview"
|
|
918
|
+
);
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* 解析组件内容
|
|
923
|
+
* 只支持 HTML 和 JS 文件(最终产物)
|
|
924
|
+
* 禁止运行时解析 .vue 或 .jsx 源文件
|
|
925
|
+
*/
|
|
926
|
+
parseComponentContent(content, type) {
|
|
927
|
+
if (type === "html") {
|
|
928
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
929
|
+
const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/);
|
|
930
|
+
let template = content;
|
|
931
|
+
if (scriptMatch) {
|
|
932
|
+
template = template.replace(/<script[^>]*>[\s\S]*?<\/script>/g, "");
|
|
933
|
+
}
|
|
934
|
+
if (styleMatch) {
|
|
935
|
+
template = template.replace(/<style[^>]*>[\s\S]*?<\/style>/g, "");
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
template: template.trim(),
|
|
939
|
+
script: scriptMatch ? scriptMatch[1].trim() : void 0,
|
|
940
|
+
style: styleMatch ? styleMatch[1].trim() : void 0
|
|
941
|
+
};
|
|
942
|
+
} else {
|
|
943
|
+
return {
|
|
944
|
+
script: content
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* 加载类型定义文件
|
|
950
|
+
*/
|
|
951
|
+
loadTypeDefinitions() {
|
|
952
|
+
const addonPath = getAddonPath();
|
|
953
|
+
if (!addonPath) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
const possiblePaths = [
|
|
957
|
+
`${addonPath}/bridge-types.js`,
|
|
958
|
+
`${addonPath}/dist/bridge-types.js`,
|
|
959
|
+
`${addonPath}/views/bridge-types.js`
|
|
960
|
+
];
|
|
961
|
+
for (const typePath of possiblePaths) {
|
|
962
|
+
if (fileExists(typePath)) {
|
|
963
|
+
const content = readFile(typePath);
|
|
964
|
+
if (content) {
|
|
965
|
+
BaseMN5.log(`Type definitions loaded from: ${typePath}`, "webview");
|
|
966
|
+
return content;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* 生成 HTML 内容
|
|
974
|
+
*/
|
|
975
|
+
generateHTML(component, props) {
|
|
976
|
+
const propsJson = props ? JSON.stringify(props) : "{}";
|
|
977
|
+
const componentData = this.loadComponent(component);
|
|
978
|
+
let templateContent = "";
|
|
979
|
+
let scriptContent = "";
|
|
980
|
+
let styleContent = "";
|
|
981
|
+
if (componentData) {
|
|
982
|
+
const parsed = this.parseComponentContent(componentData.content, componentData.type);
|
|
983
|
+
templateContent = parsed.template || "";
|
|
984
|
+
scriptContent = parsed.script || "";
|
|
985
|
+
styleContent = parsed.style || "";
|
|
986
|
+
} else {
|
|
987
|
+
templateContent = `
|
|
988
|
+
<h1>MN Rails WebView</h1>
|
|
989
|
+
<p>Component: ${component}</p>
|
|
990
|
+
<p>Props: ${propsJson}</p>
|
|
991
|
+
<p>Bridge is available: <span id="bridge-status">Checking...</span></p>
|
|
992
|
+
`;
|
|
993
|
+
scriptContent = `
|
|
994
|
+
// \u7B49\u5F85 bridge \u548C useState \u521D\u59CB\u5316
|
|
995
|
+
setTimeout(() => {
|
|
996
|
+
const statusEl = document.getElementById('bridge-status');
|
|
997
|
+
if (window.bridge) {
|
|
998
|
+
statusEl.textContent = 'Available';
|
|
999
|
+
statusEl.style.color = 'green';
|
|
1000
|
+
} else {
|
|
1001
|
+
statusEl.textContent = 'Not available';
|
|
1002
|
+
statusEl.style.color = 'red';
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// \u68C0\u67E5 useState \u662F\u5426\u53EF\u7528
|
|
1006
|
+
if (typeof window.useState === 'function') {
|
|
1007
|
+
console.log('useState hook is available');
|
|
1008
|
+
}
|
|
1009
|
+
}, 100);
|
|
1010
|
+
`;
|
|
1011
|
+
}
|
|
1012
|
+
const typeDefinitions = this.loadTypeDefinitions();
|
|
1013
|
+
const typeScriptTag = typeDefinitions ? `<script>${typeDefinitions}</script>` : "";
|
|
1014
|
+
return `
|
|
1015
|
+
<!DOCTYPE html>
|
|
1016
|
+
<html>
|
|
1017
|
+
<head>
|
|
1018
|
+
<meta charset="UTF-8">
|
|
1019
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1020
|
+
<title>MN Rails WebView - ${component}</title>
|
|
1021
|
+
<style>
|
|
1022
|
+
body {
|
|
1023
|
+
margin: 0;
|
|
1024
|
+
padding: 16px;
|
|
1025
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1026
|
+
}
|
|
1027
|
+
${styleContent}
|
|
1028
|
+
</style>
|
|
1029
|
+
${typeScriptTag}
|
|
1030
|
+
</head>
|
|
1031
|
+
<body>
|
|
1032
|
+
<div id="app">
|
|
1033
|
+
${templateContent}
|
|
1034
|
+
</div>
|
|
1035
|
+
<script>
|
|
1036
|
+
// \u4F20\u9012 props \u5230\u7EC4\u4EF6
|
|
1037
|
+
window.__MN_RAILS_PROPS__ = ${propsJson};
|
|
1038
|
+
|
|
1039
|
+
${scriptContent}
|
|
1040
|
+
</script>
|
|
1041
|
+
</body>
|
|
1042
|
+
</html>
|
|
1043
|
+
`.trim();
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* 销毁 WebView
|
|
1047
|
+
*/
|
|
1048
|
+
destroy(webViewId) {
|
|
1049
|
+
const instance = this.webViews.get(webViewId);
|
|
1050
|
+
if (!instance) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
const defaultState = getState("__default__");
|
|
1054
|
+
if (defaultState) {
|
|
1055
|
+
defaultState.unregisterWebView(webViewId);
|
|
1056
|
+
}
|
|
1057
|
+
this.messageBridge.unregisterWebView(webViewId);
|
|
1058
|
+
this.webViews.delete(webViewId);
|
|
1059
|
+
BaseMN5.log(`WebView destroyed: ${webViewId}`, "webview");
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* 获取 WebView 实例
|
|
1064
|
+
*/
|
|
1065
|
+
get(webViewId) {
|
|
1066
|
+
return this.webViews.get(webViewId);
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* 获取所有 WebView 实例
|
|
1070
|
+
*/
|
|
1071
|
+
getAll() {
|
|
1072
|
+
return Array.from(this.webViews.values());
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* 更新 WebView 的 props
|
|
1076
|
+
*/
|
|
1077
|
+
updateProps(webViewId, props) {
|
|
1078
|
+
const instance = this.webViews.get(webViewId);
|
|
1079
|
+
if (!instance) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
instance.props = { ...instance.props, ...props };
|
|
1083
|
+
this.messageBridge.sendToWebView(webViewId, "props-update", props);
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
var webViewManagerInstance = null;
|
|
1088
|
+
function getWebViewManager() {
|
|
1089
|
+
if (!webViewManagerInstance) {
|
|
1090
|
+
webViewManagerInstance = new WebViewManager();
|
|
1091
|
+
}
|
|
1092
|
+
return webViewManagerInstance;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/nativeui/NativeUIManager.ts
|
|
1096
|
+
import { MN as BaseMN6 } from "marginnote";
|
|
1097
|
+
var UIButtonType = globalThis.UIButtonType;
|
|
1098
|
+
var UIControlEvents = globalThis.UIControlEvents;
|
|
1099
|
+
var NativeUIManager = class {
|
|
1100
|
+
elements = /* @__PURE__ */ new Map();
|
|
1101
|
+
createButton(config) {
|
|
1102
|
+
try {
|
|
1103
|
+
const elementId = `button-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1104
|
+
const button = UIButton.buttonWithType(UIButtonType?.system ?? 0);
|
|
1105
|
+
button.setTitleForState(config.title, 0);
|
|
1106
|
+
const color = this.getColorForStyle(config.style || "default");
|
|
1107
|
+
button.setTitleColorForState(color, 0);
|
|
1108
|
+
const frame = config.frame || { x: 0, y: 0, width: 100, height: 44 };
|
|
1109
|
+
button.frame = { x: frame.x, y: frame.y, width: frame.width, height: frame.height };
|
|
1110
|
+
if (config.action) {
|
|
1111
|
+
button.addTargetActionForControlEvents(
|
|
1112
|
+
{ handleAction: config.action },
|
|
1113
|
+
"handleAction",
|
|
1114
|
+
UIControlEvents?.TouchUpInside ?? 64
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
const element = { id: elementId, element: button, type: "button" };
|
|
1118
|
+
this.elements.set(elementId, element);
|
|
1119
|
+
BaseMN6.log(`Button created: ${elementId}`, "nativeui");
|
|
1120
|
+
return element;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
BaseMN6.error(`Failed to create button: ${error}`, "nativeui");
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
createLabel(config) {
|
|
1127
|
+
try {
|
|
1128
|
+
const elementId = `label-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1129
|
+
const frame = config.frame || { x: 0, y: 0, width: 200, height: 44 };
|
|
1130
|
+
const labelFrame = { x: frame.x, y: frame.y, width: frame.width, height: frame.height };
|
|
1131
|
+
const label = new UILabel(labelFrame);
|
|
1132
|
+
label.text = config.text;
|
|
1133
|
+
label.textColor = config.color ?? UIColor.blackColor();
|
|
1134
|
+
const element = { id: elementId, element: label, type: "label" };
|
|
1135
|
+
this.elements.set(elementId, element);
|
|
1136
|
+
BaseMN6.log(`Label created: ${elementId}`, "nativeui");
|
|
1137
|
+
return element;
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
BaseMN6.error(`Failed to create label: ${error}`, "nativeui");
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
getColorForStyle(style) {
|
|
1144
|
+
switch (style) {
|
|
1145
|
+
case "primary":
|
|
1146
|
+
return UIColor.systemBlueColor?.() ?? UIColor.blueColor();
|
|
1147
|
+
case "danger":
|
|
1148
|
+
return UIColor.redColor();
|
|
1149
|
+
default:
|
|
1150
|
+
return UIColor.blackColor();
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
destroy(elementId) {
|
|
1154
|
+
const element = this.elements.get(elementId);
|
|
1155
|
+
if (!element) return false;
|
|
1156
|
+
this.elements.delete(elementId);
|
|
1157
|
+
BaseMN6.log(`UI element destroyed: ${elementId}`, "nativeui");
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
get(elementId) {
|
|
1161
|
+
return this.elements.get(elementId);
|
|
1162
|
+
}
|
|
1163
|
+
getAll() {
|
|
1164
|
+
return Array.from(this.elements.values());
|
|
1165
|
+
}
|
|
1166
|
+
clear() {
|
|
1167
|
+
this.elements.clear();
|
|
1168
|
+
BaseMN6.log("All UI elements cleared", "nativeui");
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
var nativeUIManagerInstance = null;
|
|
1172
|
+
function getNativeUIManager() {
|
|
1173
|
+
if (!nativeUIManagerInstance) {
|
|
1174
|
+
nativeUIManagerInstance = new NativeUIManager();
|
|
1175
|
+
}
|
|
1176
|
+
return nativeUIManagerInstance;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/MN.ts
|
|
1180
|
+
var _defaultStateName = "__default__";
|
|
1181
|
+
var _dbInstance = null;
|
|
1182
|
+
var _configInstance = null;
|
|
1183
|
+
function getDb() {
|
|
1184
|
+
if (!_dbInstance) _dbInstance = new NoteDatabase(BaseMN7.db);
|
|
1185
|
+
return _dbInstance;
|
|
1186
|
+
}
|
|
1187
|
+
function getConfig() {
|
|
1188
|
+
if (!_configInstance) _configInstance = new Config(`mn-rails.${BaseMN7.currentAddon.key}`);
|
|
1189
|
+
return _configInstance;
|
|
1190
|
+
}
|
|
1191
|
+
var _MN = new Proxy(BaseMN7, {
|
|
1192
|
+
get(target, prop) {
|
|
1193
|
+
switch (prop) {
|
|
1194
|
+
case "db":
|
|
1195
|
+
return getDb();
|
|
1196
|
+
case "config":
|
|
1197
|
+
return getConfig();
|
|
1198
|
+
case "webUI": {
|
|
1199
|
+
const mgr = getWebViewManager();
|
|
1200
|
+
return {
|
|
1201
|
+
render: (c, p, f) => mgr.render(c, p, f),
|
|
1202
|
+
destroy: (id) => mgr.destroy(id),
|
|
1203
|
+
get: (id) => mgr.get(id),
|
|
1204
|
+
updateProps: (id, p) => mgr.updateProps(id, p)
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
case "nativeUI": {
|
|
1208
|
+
const mgr = getNativeUIManager();
|
|
1209
|
+
return {
|
|
1210
|
+
createButton: (config) => mgr.createButton(config),
|
|
1211
|
+
createLabel: (config) => mgr.createLabel(config),
|
|
1212
|
+
destroy: (id) => mgr.destroy(id),
|
|
1213
|
+
get: (id) => mgr.get(id),
|
|
1214
|
+
clear: () => mgr.clear()
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
case "defineState":
|
|
1218
|
+
return function(definition, name) {
|
|
1219
|
+
return defineState(name ?? _defaultStateName, definition);
|
|
1220
|
+
};
|
|
1221
|
+
case "getState":
|
|
1222
|
+
return function(name) {
|
|
1223
|
+
return getState(name ?? _defaultStateName);
|
|
1224
|
+
};
|
|
1225
|
+
case "state":
|
|
1226
|
+
return getState(_defaultStateName);
|
|
1227
|
+
case "currentNotebookId":
|
|
1228
|
+
return target.currnetNotebookId;
|
|
1229
|
+
case "currentDocMd5":
|
|
1230
|
+
return target.currentDocmd5;
|
|
1231
|
+
default:
|
|
1232
|
+
return target[prop];
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
var MN = _MN;
|
|
1237
|
+
|
|
1238
|
+
// src/lifecycle/LifecycleManager.ts
|
|
1239
|
+
var defineLifecycleHandlers = globalThis.defineLifecycleHandlers;
|
|
1240
|
+
var LifecycleManager = class {
|
|
1241
|
+
app;
|
|
1242
|
+
constructor(app) {
|
|
1243
|
+
this.app = app;
|
|
1244
|
+
this.register();
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* 注册生命周期钩子
|
|
1248
|
+
*/
|
|
1249
|
+
register() {
|
|
1250
|
+
if (typeof defineLifecycleHandlers !== "function") {
|
|
1251
|
+
console.warn("defineLifecycleHandlers is not available. Lifecycle hooks will not work.");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
defineLifecycleHandlers({
|
|
1255
|
+
instanceMethods: {
|
|
1256
|
+
// 笔记本打开时
|
|
1257
|
+
notebookWillOpen: (notebookid) => {
|
|
1258
|
+
this.app.onNotebookChanged?.(notebookid);
|
|
1259
|
+
},
|
|
1260
|
+
// 笔记本关闭时
|
|
1261
|
+
notebookWillClose: (notebookid) => {
|
|
1262
|
+
this.app.onNotebookClosing?.(notebookid);
|
|
1263
|
+
},
|
|
1264
|
+
// 文档打开时
|
|
1265
|
+
documentDidOpen: (docmd5) => {
|
|
1266
|
+
this.app.onDocumentOpened?.(docmd5);
|
|
1267
|
+
},
|
|
1268
|
+
// 文档关闭时
|
|
1269
|
+
documentWillClose: (docmd5) => {
|
|
1270
|
+
this.app.onDocumentClosing?.(docmd5);
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
classMethods: {
|
|
1274
|
+
// 插件连接时(相当于 onLaunch)
|
|
1275
|
+
addonDidConnect: () => {
|
|
1276
|
+
this.app.onLaunch?.();
|
|
1277
|
+
},
|
|
1278
|
+
// 插件断开连接时(相当于 onExit)
|
|
1279
|
+
addonWillDisconnect: () => {
|
|
1280
|
+
this.app.onExit?.();
|
|
1281
|
+
},
|
|
1282
|
+
// 应用进入后台
|
|
1283
|
+
applicationDidEnterBackground: () => {
|
|
1284
|
+
},
|
|
1285
|
+
// 应用进入前台
|
|
1286
|
+
applicationWillEnterForeground: () => {
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 手动触发笔记选中事件
|
|
1293
|
+
*
|
|
1294
|
+
* MarginNote 没有提供"笔记选中"的原生生命周期钩子,因此需要开发者手动监听 MarginNote 的笔记选择相关 API,
|
|
1295
|
+
* 并在检测到笔记选中时调用此方法。
|
|
1296
|
+
*
|
|
1297
|
+
* 调用此方法会触发 App 的 `onNoteSelected` 钩子。
|
|
1298
|
+
*
|
|
1299
|
+
* 典型用法:
|
|
1300
|
+
* 1. 在 App 的 `onLaunch` 中注册 MarginNote 的笔记选择监听器(如通过 `MN.notificationCenter` 或其他 API)
|
|
1301
|
+
* 2. 在监听器回调中调用 `this.lifecycle.triggerNoteSelected(note)`
|
|
1302
|
+
* 3. 框架会自动调用 App 的 `onNoteSelected` 方法
|
|
1303
|
+
*
|
|
1304
|
+
* @param note - 被选中的笔记对象
|
|
1305
|
+
*
|
|
1306
|
+
* @example
|
|
1307
|
+
* ```typescript
|
|
1308
|
+
* class MyPlugin extends App {
|
|
1309
|
+
* onLaunch() {
|
|
1310
|
+
* // 注册笔记选择监听
|
|
1311
|
+
* MN.notificationCenter.addObserver('noteSelected', (note) => {
|
|
1312
|
+
* this.lifecycle?.triggerNoteSelected(note);
|
|
1313
|
+
* });
|
|
1314
|
+
* }
|
|
1315
|
+
*
|
|
1316
|
+
* onNoteSelected(note) {
|
|
1317
|
+
* console.log('Note selected:', note);
|
|
1318
|
+
* }
|
|
1319
|
+
* }
|
|
1320
|
+
* ```
|
|
1321
|
+
*/
|
|
1322
|
+
triggerNoteSelected(note) {
|
|
1323
|
+
this.app.onNoteSelected?.(note);
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* 手动触发配置文件切换事件
|
|
1327
|
+
*
|
|
1328
|
+
* MarginNote 没有提供"配置文件切换"的原生生命周期钩子,因此需要开发者手动监听 MarginNote 的配置切换相关 API,
|
|
1329
|
+
* 并在检测到配置切换时调用此方法。
|
|
1330
|
+
*
|
|
1331
|
+
* 调用此方法会触发 App 的 `onProfileChanged` 钩子。
|
|
1332
|
+
*
|
|
1333
|
+
* 典型用法:
|
|
1334
|
+
* 1. 在 App 的 `onLaunch` 中注册 MarginNote 的配置切换监听器(如通过 `MN.notificationCenter` 或其他 API)
|
|
1335
|
+
* 2. 在监听器回调中调用 `this.lifecycle.triggerProfileChanged(profileId)`
|
|
1336
|
+
* 3. 框架会自动调用 App 的 `onProfileChanged` 方法
|
|
1337
|
+
*
|
|
1338
|
+
* @param profileId - 新的配置文件 ID
|
|
1339
|
+
*
|
|
1340
|
+
* @example
|
|
1341
|
+
* ```typescript
|
|
1342
|
+
* class MyPlugin extends App {
|
|
1343
|
+
* onLaunch() {
|
|
1344
|
+
* // 注册配置切换监听
|
|
1345
|
+
* MN.notificationCenter.addObserver('profileChanged', (profileId) => {
|
|
1346
|
+
* this.lifecycle?.triggerProfileChanged(profileId);
|
|
1347
|
+
* });
|
|
1348
|
+
* }
|
|
1349
|
+
*
|
|
1350
|
+
* onProfileChanged(profileId) {
|
|
1351
|
+
* console.log('Profile changed to:', profileId);
|
|
1352
|
+
* }
|
|
1353
|
+
* }
|
|
1354
|
+
* ```
|
|
1355
|
+
*/
|
|
1356
|
+
triggerProfileChanged(profileId) {
|
|
1357
|
+
this.app.onProfileChanged?.(profileId);
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
// src/App.ts
|
|
1362
|
+
var App = class {
|
|
1363
|
+
lifecycleManager;
|
|
1364
|
+
constructor() {
|
|
1365
|
+
this.lifecycleManager = new LifecycleManager(this);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* 获取生命周期管理器(用于手动触发事件)
|
|
1369
|
+
*
|
|
1370
|
+
* 返回 LifecycleManager 实例,用于在 App 子类中手动触发某些生命周期事件。
|
|
1371
|
+
*
|
|
1372
|
+
* 典型用法:
|
|
1373
|
+
* - 在 `onLaunch` 中注册 MarginNote 的监听器(如笔记选择、配置切换等)
|
|
1374
|
+
* - 在监听器回调中调用 `this.lifecycle.triggerNoteSelected(note)` 或 `this.lifecycle.triggerProfileChanged(profileId)`
|
|
1375
|
+
* - 这些调用会触发对应的 `onNoteSelected` 或 `onProfileChanged` 钩子
|
|
1376
|
+
*
|
|
1377
|
+
* @example
|
|
1378
|
+
* ```typescript
|
|
1379
|
+
* class MyPlugin extends App {
|
|
1380
|
+
* onLaunch() {
|
|
1381
|
+
* // 注册笔记选择监听
|
|
1382
|
+
* MN.notificationCenter.addObserver('noteSelected', (note) => {
|
|
1383
|
+
* this.lifecycle?.triggerNoteSelected(note);
|
|
1384
|
+
* });
|
|
1385
|
+
* }
|
|
1386
|
+
* }
|
|
1387
|
+
* ```
|
|
1388
|
+
*/
|
|
1389
|
+
get lifecycle() {
|
|
1390
|
+
return this.lifecycleManager;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// src/Controller.ts
|
|
1395
|
+
var Controller = class {
|
|
1396
|
+
controllerName;
|
|
1397
|
+
constructor(name) {
|
|
1398
|
+
this.controllerName = name || this.constructor.name;
|
|
1399
|
+
this.registerActions();
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Controller 销毁时调用
|
|
1403
|
+
*/
|
|
1404
|
+
onDestroy() {
|
|
1405
|
+
this.unregisterActions();
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* 定义供 Web 端调用的 actions
|
|
1409
|
+
* 子类应该重写此属性以提供类型安全的 API
|
|
1410
|
+
*/
|
|
1411
|
+
actions = {};
|
|
1412
|
+
/**
|
|
1413
|
+
* 注册 actions 到 Bridge
|
|
1414
|
+
*/
|
|
1415
|
+
registerActions() {
|
|
1416
|
+
if (Object.keys(this.actions).length > 0) {
|
|
1417
|
+
const manager = getBridgeManager();
|
|
1418
|
+
manager.register(this.controllerName, this.actions);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* 注销 actions
|
|
1423
|
+
*/
|
|
1424
|
+
unregisterActions() {
|
|
1425
|
+
const manager = getBridgeManager();
|
|
1426
|
+
manager.unregister(this.controllerName);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* 更新 actions(当 actions 发生变化时调用)
|
|
1430
|
+
*/
|
|
1431
|
+
updateActions() {
|
|
1432
|
+
this.unregisterActions();
|
|
1433
|
+
this.registerActions();
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// src/bridge/web-bridge.ts
|
|
1438
|
+
function createWebBridge() {
|
|
1439
|
+
const w = typeof window !== "undefined" ? window : void 0;
|
|
1440
|
+
if (w?.bridge) return w.bridge;
|
|
1441
|
+
return new Proxy({}, {
|
|
1442
|
+
get(_target, controllerName) {
|
|
1443
|
+
return new Proxy({}, {
|
|
1444
|
+
get(_t, actionName) {
|
|
1445
|
+
return async (...args) => {
|
|
1446
|
+
const g = typeof window !== "undefined" ? window : void 0;
|
|
1447
|
+
if (g?.__MN_RAILS_BRIDGE__) {
|
|
1448
|
+
return g.__MN_RAILS_BRIDGE__.call(controllerName, actionName, ...args);
|
|
1449
|
+
}
|
|
1450
|
+
throw new Error("Bridge not available");
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/utils/dialog.ts
|
|
1459
|
+
var UIAlertViewStyle = globalThis.UIAlertViewStyle;
|
|
1460
|
+
var _lang = typeof globalThis.lang !== "undefined" ? globalThis.lang : { sure: "OK", cancel: "Cancel", make_your_choice: "Make your choice" };
|
|
1461
|
+
function alert(message) {
|
|
1462
|
+
try {
|
|
1463
|
+
MN.app.alert(message);
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
MN.log(`Alert failed: ${error}`, "error");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function popup(options) {
|
|
1469
|
+
const {
|
|
1470
|
+
title = MN.currentAddon.title,
|
|
1471
|
+
message,
|
|
1472
|
+
type = UIAlertViewStyle?.Default ?? 0,
|
|
1473
|
+
buttons = [_lang.sure],
|
|
1474
|
+
canCancel = true,
|
|
1475
|
+
multiLine = false
|
|
1476
|
+
} = options;
|
|
1477
|
+
return new Promise((resolve) => {
|
|
1478
|
+
try {
|
|
1479
|
+
if (canCancel) {
|
|
1480
|
+
UIAlertView.showWithTitleMessageStyleCancelButtonTitleOtherButtonTitlesTapBlock(
|
|
1481
|
+
title,
|
|
1482
|
+
message,
|
|
1483
|
+
type,
|
|
1484
|
+
_lang.cancel,
|
|
1485
|
+
multiLine ? buttons.map((k) => byteSlice(k.replace(/\n/g, ""), 0, 40)) : buttons,
|
|
1486
|
+
(alert2, buttonIndex) => {
|
|
1487
|
+
resolve({
|
|
1488
|
+
inputContent: type === (UIAlertViewStyle?.Default ?? 0) ? void 0 : alert2.textFieldAtIndex(0)?.text,
|
|
1489
|
+
buttonIndex: buttonIndex - 1
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
);
|
|
1493
|
+
} else {
|
|
1494
|
+
UIAlertView.showWithTitleMessageStyleCancelButtonTitleOtherButtonTitlesTapBlock(
|
|
1495
|
+
title,
|
|
1496
|
+
message,
|
|
1497
|
+
type,
|
|
1498
|
+
multiLine ? byteSlice(buttons[0].replace(/\n/g, ""), 0, 40) : buttons[0],
|
|
1499
|
+
multiLine ? buttons.slice(1).map((k) => byteSlice(k.replace(/\n/g, ""), 0, 40)) : buttons.slice(1),
|
|
1500
|
+
(alert2, buttonIndex) => {
|
|
1501
|
+
resolve({
|
|
1502
|
+
inputContent: type === (UIAlertViewStyle?.Default ?? 0) ? void 0 : alert2.textFieldAtIndex(0)?.text,
|
|
1503
|
+
buttonIndex
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
MN.log(`Popup failed: ${error}`, "error");
|
|
1510
|
+
resolve({ buttonIndex: -1 });
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
async function confirm(title = MN.currentAddon.title, message = "") {
|
|
1515
|
+
const { buttonIndex } = await popup({
|
|
1516
|
+
title,
|
|
1517
|
+
message,
|
|
1518
|
+
buttons: [_lang.sure],
|
|
1519
|
+
multiLine: false,
|
|
1520
|
+
canCancel: true
|
|
1521
|
+
});
|
|
1522
|
+
return buttonIndex === 0;
|
|
1523
|
+
}
|
|
1524
|
+
async function select(options, title = MN.currentAddon.title, message = _lang.make_your_choice, canCancel = false) {
|
|
1525
|
+
const { buttonIndex } = await popup({
|
|
1526
|
+
title,
|
|
1527
|
+
message,
|
|
1528
|
+
buttons: options,
|
|
1529
|
+
multiLine: true,
|
|
1530
|
+
canCancel
|
|
1531
|
+
});
|
|
1532
|
+
if (buttonIndex === -1) {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
value: options[buttonIndex],
|
|
1537
|
+
index: buttonIndex
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function byteLength(text) {
|
|
1541
|
+
return Array.from(text).reduce((acc, cur) => {
|
|
1542
|
+
acc += cur.charCodeAt(0) > 255 ? 2 : 1;
|
|
1543
|
+
return acc;
|
|
1544
|
+
}, 0);
|
|
1545
|
+
}
|
|
1546
|
+
function byteSlice(text, begin, ...rest) {
|
|
1547
|
+
const res = [begin, ...rest].map((k) => {
|
|
1548
|
+
if (k < 0) k = byteLength(text) + k;
|
|
1549
|
+
if (k === 0 || k === 1) return k;
|
|
1550
|
+
return Array.from(text).reduce(
|
|
1551
|
+
(acc, cur, index) => {
|
|
1552
|
+
const byte = acc.byte + (cur.charCodeAt(0) > 255 ? 2 : 1);
|
|
1553
|
+
if (!acc.enough && byte <= k) {
|
|
1554
|
+
acc = { index, byte, enough: false };
|
|
1555
|
+
} else {
|
|
1556
|
+
if (!acc.enough) {
|
|
1557
|
+
acc.enough = true;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return acc;
|
|
1561
|
+
},
|
|
1562
|
+
{ index: 0, byte: 0, enough: false }
|
|
1563
|
+
).index + 1;
|
|
1564
|
+
});
|
|
1565
|
+
return text.slice(res[0], res[1]);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/utils/note.ts
|
|
1569
|
+
function getNoteAllText(note) {
|
|
1570
|
+
const parts = [];
|
|
1571
|
+
if (note.noteTitle) {
|
|
1572
|
+
parts.push(note.noteTitle);
|
|
1573
|
+
}
|
|
1574
|
+
if (note.excerptText) {
|
|
1575
|
+
parts.push(note.excerptText);
|
|
1576
|
+
}
|
|
1577
|
+
if (note.comments && note.comments.length > 0) {
|
|
1578
|
+
note.comments.forEach((comment) => {
|
|
1579
|
+
if (comment.type === "TextNote" && comment.text) {
|
|
1580
|
+
parts.push(comment.text);
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
return parts.join("\n\n");
|
|
1585
|
+
}
|
|
1586
|
+
function isNoteExist(note) {
|
|
1587
|
+
if (typeof note === "string") {
|
|
1588
|
+
return MN.db.getNoteById(note) !== null;
|
|
1589
|
+
} else {
|
|
1590
|
+
return MN.db.getNoteById(note.noteId) !== null;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
function getLinkedNotes(note) {
|
|
1594
|
+
const linkedNotes = [];
|
|
1595
|
+
if (note.linkedNotes && note.linkedNotes.length > 0) {
|
|
1596
|
+
note.linkedNotes.forEach((link) => {
|
|
1597
|
+
const linkedNote = MN.db.getNoteById(link.noteid);
|
|
1598
|
+
if (linkedNote) {
|
|
1599
|
+
linkedNotes.push(linkedNote);
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
return linkedNotes;
|
|
1604
|
+
}
|
|
1605
|
+
function getChildNotes(note) {
|
|
1606
|
+
return note.childNotes || [];
|
|
1607
|
+
}
|
|
1608
|
+
function getParentNote(note) {
|
|
1609
|
+
return note.parentNote || null;
|
|
1610
|
+
}
|
|
1611
|
+
export {
|
|
1612
|
+
App,
|
|
1613
|
+
BridgeManager,
|
|
1614
|
+
Config,
|
|
1615
|
+
Controller,
|
|
1616
|
+
LifecycleManager,
|
|
1617
|
+
MN,
|
|
1618
|
+
MessageBridge,
|
|
1619
|
+
NativeUIManager,
|
|
1620
|
+
NoteDatabase,
|
|
1621
|
+
StateManager,
|
|
1622
|
+
WebViewManager,
|
|
1623
|
+
alert,
|
|
1624
|
+
bridge,
|
|
1625
|
+
confirm,
|
|
1626
|
+
createDirectory,
|
|
1627
|
+
createWebBridge,
|
|
1628
|
+
defineState,
|
|
1629
|
+
fileExists,
|
|
1630
|
+
getAddonPath,
|
|
1631
|
+
getBridgeManager,
|
|
1632
|
+
getChildNotes,
|
|
1633
|
+
getLinkedNotes,
|
|
1634
|
+
getMessageBridge,
|
|
1635
|
+
getNativeUIManager,
|
|
1636
|
+
getNoteAllText,
|
|
1637
|
+
getParentNote,
|
|
1638
|
+
getState,
|
|
1639
|
+
getViewsPath,
|
|
1640
|
+
getWebViewManager,
|
|
1641
|
+
isNoteExist,
|
|
1642
|
+
popup,
|
|
1643
|
+
readFile,
|
|
1644
|
+
select
|
|
1645
|
+
};
|