@lenorin/dsh-tauri-launcher 1.0.7 → 1.0.8
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/launcher/bin/dsh-launcher.exe +0 -0
- package/lib/client.js +353 -232
- package/package.json +1 -1
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
* 在设置面板注册「桌面启动」分区:桌面端启动开关(含快速确认与乐观反馈)、
|
|
4
4
|
* 添加快捷方式按钮、关闭确认弹窗、诊断信息(仅出错时显示)。
|
|
5
5
|
* 与宿主通信走 /api/dsh-tauri-launcher/*(同源 fetch,仅回环)。
|
|
6
|
+
*
|
|
7
|
+
* 除设置分区外,本文件还承载两个与桌面端外壳(Tauri 主窗口)协作的功能,
|
|
8
|
+
* 仅在页面被 iframe 内嵌(桌面端)时启用:
|
|
9
|
+
* 1. 标题栏主题中继:把 DSH 主题方案与真实生效的 token 发给外壳(installThemeRelay);
|
|
10
|
+
* 2. 会话导航栈 + 系统主题跟随(installSessionNav / installSystemThemeFollow)。
|
|
11
|
+
*
|
|
12
|
+
* 模块级结构(2026-09 阶段一拆分,见优化报告 v2):
|
|
13
|
+
* - createSessionNavStack:应用层会话导航栈(纯逻辑,无 DOM/服务依赖,可单测)
|
|
14
|
+
* - retryLadder:服务就绪重试梯子(theme/change 事件、sessions 服务可能后于插件就绪)
|
|
15
|
+
* - installDesktopSection / installThemeRelay / installSystemThemeFollow / installSessionNav
|
|
6
16
|
*/
|
|
7
17
|
window.__ModuleLoader__.load({
|
|
8
18
|
id: '@lenorin/dsh-tauri-launcher',
|
|
@@ -114,12 +124,153 @@ body [class*='centerCol'] {
|
|
|
114
124
|
},
|
|
115
125
|
}
|
|
116
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 服务就绪重试梯子:目标服务(theme/change 事件、sessions)可能在插件
|
|
129
|
+
* 之后才就绪,且 preference 等于默认值时不会发变化事件——用短重试保证
|
|
130
|
+
* 初始状态一定送达(成功或达到 maxTries 后停止)。返回清理函数。
|
|
131
|
+
*/
|
|
132
|
+
function retryLadder(attempt, { immediate = false, intervalMs = 500, maxTries = 12 } = {}) {
|
|
133
|
+
let done = false
|
|
134
|
+
let tries = 0
|
|
135
|
+
const run = () => {
|
|
136
|
+
if (done) return
|
|
137
|
+
try {
|
|
138
|
+
if (attempt()) done = true
|
|
139
|
+
} catch { /* 服务未就绪,下一轮重试 */ }
|
|
140
|
+
}
|
|
141
|
+
if (immediate) run()
|
|
142
|
+
const timer = setInterval(() => {
|
|
143
|
+
tries += 1
|
|
144
|
+
if (done || tries >= maxTries) {
|
|
145
|
+
clearInterval(timer)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
run()
|
|
149
|
+
}, intervalMs)
|
|
150
|
+
return () => clearInterval(timer)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 应用层会话导航栈(纯逻辑,无 DOM / DSH 服务依赖,可单测)。
|
|
155
|
+
* DSH 是 React SPA:切会话不产生浏览器历史条目,history.back() 无效,
|
|
156
|
+
* 前进/后退等价于在「最近访问会话」双栈上移动(对标浏览器语义):
|
|
157
|
+
* - 非跳转来源的 current 变化 → 上一个会话压 backStack、清空 forwardStack;
|
|
158
|
+
* - goBack/goForward 弹栈跳转,当前会话压对向栈;栈条目 { id, address }
|
|
159
|
+
* (address 供 catalog 子会话 openSubagent);上限 cap、连续重复跳过;
|
|
160
|
+
* - 跳转前置 pendingJump 锁,防止自身触发的变化被当作用户切换重复压栈,
|
|
161
|
+
* 超时(jumpTimeoutMs)兜底解锁,由调用方传入 onUnlock 回调同步最新状态。
|
|
162
|
+
*/
|
|
163
|
+
function createSessionNavStack({ cap = 50, jumpTimeoutMs = 1500 } = {}) {
|
|
164
|
+
let backStack = []
|
|
165
|
+
let forwardStack = []
|
|
166
|
+
let currentId = undefined
|
|
167
|
+
let currentAddress = undefined
|
|
168
|
+
let pendingJump = null
|
|
169
|
+
let jumpTimer = null
|
|
170
|
+
|
|
171
|
+
const pushStack = (stack, entry) => {
|
|
172
|
+
const top = stack[stack.length - 1]
|
|
173
|
+
if (top && top.id === entry.id && top.address === entry.address) return
|
|
174
|
+
stack.push(entry)
|
|
175
|
+
if (stack.length > cap) stack.shift()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const clearJumpTimer = () => {
|
|
179
|
+
if (jumpTimer) {
|
|
180
|
+
clearTimeout(jumpTimer)
|
|
181
|
+
jumpTimer = null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
/** 前进/后退两个方向当前是否可走(供外壳置灰按钮)。 */
|
|
187
|
+
status: () => ({ back: backStack.length > 0, forward: forwardStack.length > 0 }),
|
|
188
|
+
/** 以快照初始化当前会话(订阅前先取一次快照;跳转解锁后同步最新状态)。 */
|
|
189
|
+
init: (id, address) => {
|
|
190
|
+
currentId = id
|
|
191
|
+
currentAddress = address
|
|
192
|
+
},
|
|
193
|
+
/** 跳转锁是否生效中。 */
|
|
194
|
+
isPending: () => pendingJump !== null,
|
|
195
|
+
/**
|
|
196
|
+
* 非跳转来源的 current 变化。返回 true 表示状态对外可见变化(调用方应上报)。
|
|
197
|
+
* 规则:current 变 undefined 只清 address;同 id 只更新 address(不压栈);
|
|
198
|
+
* 真正切换才压 backStack 并清空 forwardStack。
|
|
199
|
+
*/
|
|
200
|
+
onExternalChange(snap) {
|
|
201
|
+
if (snap.id === undefined) {
|
|
202
|
+
currentAddress = undefined
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
if (snap.id === currentId) {
|
|
206
|
+
if (snap.address !== undefined) currentAddress = snap.address
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
if (currentId !== undefined) {
|
|
210
|
+
pushStack(backStack, { id: currentId, address: currentAddress })
|
|
211
|
+
forwardStack = []
|
|
212
|
+
}
|
|
213
|
+
currentId = snap.id
|
|
214
|
+
currentAddress = snap.address
|
|
215
|
+
return true
|
|
216
|
+
},
|
|
217
|
+
/**
|
|
218
|
+
* 跳转锁生效期间的变化。返回 true 表示跳转已被确认(命中目标)或被
|
|
219
|
+
* 用户的新切换取代(二者都应解锁并同步状态后上报)。
|
|
220
|
+
*/
|
|
221
|
+
onPendingChange(snap) {
|
|
222
|
+
if (snap.id === pendingJump.id || (snap.id !== undefined && snap.id !== currentId)) {
|
|
223
|
+
clearJumpTimer()
|
|
224
|
+
pendingJump = null
|
|
225
|
+
currentId = snap.id
|
|
226
|
+
currentAddress = snap.address
|
|
227
|
+
return true
|
|
228
|
+
}
|
|
229
|
+
return false
|
|
230
|
+
},
|
|
231
|
+
/** 后退:弹出目标(当前会话压 forwardStack)。无可退时返回 null。 */
|
|
232
|
+
goBack() {
|
|
233
|
+
if (backStack.length === 0) return null
|
|
234
|
+
const target = backStack.pop()
|
|
235
|
+
if (currentId !== undefined) pushStack(forwardStack, { id: currentId, address: currentAddress })
|
|
236
|
+
return target
|
|
237
|
+
},
|
|
238
|
+
/** 前进:对称于 goBack。 */
|
|
239
|
+
goForward() {
|
|
240
|
+
if (forwardStack.length === 0) return null
|
|
241
|
+
const target = forwardStack.pop()
|
|
242
|
+
if (currentId !== undefined) pushStack(backStack, { id: currentId, address: currentAddress })
|
|
243
|
+
return target
|
|
244
|
+
},
|
|
245
|
+
/**
|
|
246
|
+
* 准备一次跳转:置 pendingJump 锁、同步 current,并启动超时兜底 timer。
|
|
247
|
+
* 超时先解锁再回调 onUnlock(调用方应读最新快照并上报);跳转动作抛出
|
|
248
|
+
* 异常时调用 abortJump 回滚锁(timer 保留:到期仍会同步一次状态)。
|
|
249
|
+
*/
|
|
250
|
+
jumpTo(entry, onUnlock) {
|
|
251
|
+
pendingJump = { id: entry.id, address: entry.address }
|
|
252
|
+
currentId = entry.id
|
|
253
|
+
currentAddress = entry.address
|
|
254
|
+
clearJumpTimer()
|
|
255
|
+
jumpTimer = setTimeout(() => {
|
|
256
|
+
pendingJump = null
|
|
257
|
+
onUnlock()
|
|
258
|
+
}, jumpTimeoutMs)
|
|
259
|
+
},
|
|
260
|
+
/** 跳转动作抛出异常时回滚锁。 */
|
|
261
|
+
abortJump: () => {
|
|
262
|
+
pendingJump = null
|
|
263
|
+
},
|
|
264
|
+
/** 释放 timer(插件卸载时)。 */
|
|
265
|
+
dispose: clearJumpTimer,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
117
269
|
exports.inject = ['slots']
|
|
118
270
|
|
|
119
|
-
|
|
120
|
-
const slots = ctx.get('slots')
|
|
121
|
-
if (slots === undefined) return
|
|
271
|
+
/* ---------- 设置分区「桌面启动」 ---------- */
|
|
122
272
|
|
|
273
|
+
function installDesktopSection(ctx, slots) {
|
|
123
274
|
const styleEl = document.createElement('style')
|
|
124
275
|
styleEl.dataset.plugin = '@lenorin/dsh-tauri-launcher'
|
|
125
276
|
styleEl.textContent = CSS
|
|
@@ -269,14 +420,19 @@ body [class*='centerCol'] {
|
|
|
269
420
|
{ name: 'settings.section', id: 'desktop-launch', order: 30, label: '桌面启动' },
|
|
270
421
|
() => React.createElement(DesktopSection, null),
|
|
271
422
|
))
|
|
423
|
+
}
|
|
272
424
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
425
|
+
/* ---------- 标题栏主题中继(DSH → 外壳) ---------- */
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 桌面端把 DSH 嵌在 iframe 里;这里读取官方主题服务解析出的浅/深方案,
|
|
429
|
+
* 并把 DSH 页面真实生效的设计 token(计算样式)发给启动器外壳,
|
|
430
|
+
* 让标题栏与 DSH 配色逐像素一致。仅当本页面被嵌入(iframe)时才有意义,
|
|
431
|
+
* 普通浏览器标签(window.parent === window)直接跳过。
|
|
432
|
+
* 注意:内置主题的快照 tokens 是空表,真实颜色在 CSS 变量里
|
|
433
|
+
* (body[data-ds-dark-theme] 切换),必须经 getComputedStyle 取。
|
|
434
|
+
*/
|
|
435
|
+
function installThemeRelay(ctx) {
|
|
280
436
|
const readVar = (name) => {
|
|
281
437
|
try {
|
|
282
438
|
const style = getComputedStyle(document.body)
|
|
@@ -319,255 +475,220 @@ body [class*='centerCol'] {
|
|
|
319
475
|
}
|
|
320
476
|
themeSent = true
|
|
321
477
|
window.parent.postMessage(payload, '*')
|
|
478
|
+
// 返回 true 供重试梯子判定“已送达”,停止后续重试(与历史 themeSent 语义一致)
|
|
479
|
+
return true
|
|
322
480
|
}
|
|
323
481
|
|
|
324
482
|
try {
|
|
325
483
|
ctx.on('theme/change', postTheme)
|
|
326
484
|
} catch { /* 事件不可用时依赖下面的重试梯子 */ }
|
|
327
485
|
|
|
328
|
-
// 主题服务可能在本插件之后才就绪,且 preference 等于默认值时不会发
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (themeSent || themeTries >= 12) {
|
|
334
|
-
clearInterval(themeTimer)
|
|
335
|
-
return
|
|
336
|
-
}
|
|
337
|
-
postTheme()
|
|
338
|
-
}, 500)
|
|
339
|
-
ctx.effect(() => () => clearInterval(themeTimer), 'dsh-tauri-launcher: theme relay timer')
|
|
340
|
-
|
|
341
|
-
// ---------- 会话导航(应用层会话栈,等价浏览器前进/后退) ----------
|
|
342
|
-
// DSH 是 React SPA:切会话不产生浏览器历史条目,history.back() 无效。
|
|
343
|
-
// 这里维护"最近访问会话"栈:非本插件发起的 current 变化 → 上一个会话压
|
|
344
|
-
// backStack、清空 forwardStack;◀ 弹 backStack 跳转(当前压 forwardStack),
|
|
345
|
-
// ▶ 反向。跳转前设 pendingJump 锁,防止自身触发的变化被重复压栈,超时兜底解锁。
|
|
346
|
-
// sessions 服务存在就绪时序:一次性 ctx.get 可能拿到 undefined(静默失效),
|
|
347
|
-
// 故用重试梯子初始化;外壳每 3 秒 ping 一次,状态周期性回报实现自愈。
|
|
348
|
-
if (window.parent !== window) {
|
|
349
|
-
let navDispose = null
|
|
350
|
-
let navReady = false
|
|
486
|
+
// 主题服务可能在本插件之后才就绪,且 preference 等于默认值时不会发
|
|
487
|
+
// theme/change:用重试梯子保证初始状态一定送达(成功或 6 秒后停止)。
|
|
488
|
+
const disposeTimer = retryLadder(postTheme, {})
|
|
489
|
+
ctx.effect(() => disposeTimer, 'dsh-tauri-launcher: theme relay timer')
|
|
490
|
+
}
|
|
351
491
|
|
|
352
|
-
|
|
353
|
-
|
|
492
|
+
/* ---------- 桌面端系统主题跟随(外壳 → DSH) ---------- */
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* WebView2 页面里的 prefers-color-scheme 只有宿主显式设置
|
|
496
|
+
* PreferredColorScheme 才会随 Windows 变化,而 tauri/wry 未暴露该能力
|
|
497
|
+
* (wry#806),DSH 的 `system` 偏好会被冻结在启动时的取值。启动器轮询
|
|
498
|
+
* 到系统主题变化后经外壳 postMessage 转发到这里:偏好为 `system` 时把
|
|
499
|
+
* ThemeRuntime 持有的 MediaQueryList.matches(冻结值)覆盖为真实系统值
|
|
500
|
+
* 并触发重发布——偏好保持「跟随系统」不变,界面随真实系统主题切换;
|
|
501
|
+
* 用户手动选了固定主题时不干预。
|
|
502
|
+
* ⚠️ 兼容层 hack:依赖 ThemeRuntime 的 media/publish 运行时成员(无真
|
|
503
|
+
* 私有化),外壳升级若重命名则静默退化为现状(跟随失效,无副作用)。
|
|
504
|
+
*/
|
|
505
|
+
function installSystemThemeFollow(ctx, parentOrigin) {
|
|
506
|
+
const applySystemTheme = (scheme) => {
|
|
354
507
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!theme || !(theme.media instanceof MediaQueryList)) return
|
|
372
|
-
if (typeof theme.publish !== 'function') return
|
|
373
|
-
const snap = typeof theme.getTheme === 'function' ? theme.getTheme() : null
|
|
374
|
-
if (!snap || snap.preference !== 'system') return
|
|
375
|
-
if (snap.active && snap.active.colorScheme === scheme) return
|
|
376
|
-
const desc = Object.getOwnPropertyDescriptor(MediaQueryList.prototype, 'matches')
|
|
377
|
-
if (!desc || typeof desc.get !== 'function') return
|
|
378
|
-
const wantDark = scheme === 'dark'
|
|
379
|
-
Object.defineProperty(theme.media, 'matches', {
|
|
380
|
-
configurable: true,
|
|
381
|
-
get() { return wantDark }
|
|
382
|
-
})
|
|
383
|
-
theme.publish()
|
|
384
|
-
} catch { /* 主题服务异常:忽略,等待下次通知 */ }
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const onSystemThemeMessage = (event) => {
|
|
388
|
-
const data = event.data || {}
|
|
389
|
-
if (event.source !== window.parent) return
|
|
390
|
-
if (event.origin !== parentOrigin) return
|
|
391
|
-
const scheme = data.__tbSystemTheme
|
|
392
|
-
if (scheme === 'light' || scheme === 'dark') applySystemTheme(scheme)
|
|
393
|
-
}
|
|
394
|
-
window.addEventListener('message', onSystemThemeMessage)
|
|
395
|
-
ctx.effect(() => () => window.removeEventListener('message', onSystemThemeMessage), 'dsh-tauri-launcher: system theme follow')
|
|
508
|
+
const theme = ctx.get('theme')
|
|
509
|
+
if (!theme || !(theme.media instanceof MediaQueryList)) return
|
|
510
|
+
if (typeof theme.publish !== 'function') return
|
|
511
|
+
const snap = typeof theme.getTheme === 'function' ? theme.getTheme() : null
|
|
512
|
+
if (!snap || snap.preference !== 'system') return
|
|
513
|
+
if (snap.active && snap.active.colorScheme === scheme) return
|
|
514
|
+
const desc = Object.getOwnPropertyDescriptor(MediaQueryList.prototype, 'matches')
|
|
515
|
+
if (!desc || typeof desc.get !== 'function') return
|
|
516
|
+
const wantDark = scheme === 'dark'
|
|
517
|
+
Object.defineProperty(theme.media, 'matches', {
|
|
518
|
+
configurable: true,
|
|
519
|
+
get() { return wantDark }
|
|
520
|
+
})
|
|
521
|
+
theme.publish()
|
|
522
|
+
} catch { /* 主题服务异常:忽略,等待下次通知 */ }
|
|
523
|
+
}
|
|
396
524
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
525
|
+
const onSystemThemeMessage = (event) => {
|
|
526
|
+
const data = event.data || {}
|
|
527
|
+
if (event.source !== window.parent) return
|
|
528
|
+
if (event.origin !== parentOrigin) return
|
|
529
|
+
const scheme = data.__tbSystemTheme
|
|
530
|
+
if (scheme === 'light' || scheme === 'dark') applySystemTheme(scheme)
|
|
531
|
+
}
|
|
532
|
+
window.addEventListener('message', onSystemThemeMessage)
|
|
533
|
+
ctx.effect(() => () => window.removeEventListener('message', onSystemThemeMessage), 'dsh-tauri-launcher: system theme follow')
|
|
534
|
+
}
|
|
403
535
|
|
|
404
|
-
|
|
405
|
-
let backStack = []
|
|
406
|
-
let forwardStack = []
|
|
407
|
-
let currentId = undefined
|
|
408
|
-
let currentAddress = undefined
|
|
409
|
-
let pendingJump = null
|
|
410
|
-
let jumpTimer = null
|
|
411
|
-
|
|
412
|
-
const snapCurrent = () => {
|
|
413
|
-
const snap = store.getSnapshot()
|
|
414
|
-
return { id: snap.current, address: snap.currentAddress }
|
|
415
|
-
}
|
|
536
|
+
/* ---------- 会话导航(应用层会话栈,等价浏览器前进/后退) ---------- */
|
|
416
537
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
window.parent.postMessage(payload, '*')
|
|
424
|
-
} catch { /* 忽略 */ }
|
|
425
|
-
}
|
|
538
|
+
/**
|
|
539
|
+
* sessions 服务存在就绪时序:一次性 ctx.get 可能拿到 undefined(静默失效),
|
|
540
|
+
* 故用重试梯子初始化;外壳每 3 秒 ping 一次,状态周期性回报实现自愈。
|
|
541
|
+
*/
|
|
542
|
+
function installSessionNav(ctx, parentOrigin) {
|
|
543
|
+
let navDispose = null
|
|
426
544
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
545
|
+
const initNav = () => {
|
|
546
|
+
const sessions = ctx.get('sessions')
|
|
547
|
+
const store = sessions && sessions.list
|
|
548
|
+
if (!store || typeof store.subscribe !== 'function') {
|
|
549
|
+
return false
|
|
550
|
+
}
|
|
433
551
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
clearTimeout(jumpTimer)
|
|
439
|
-
jumpTimer = setTimeout(() => {
|
|
440
|
-
pendingJump = null
|
|
441
|
-
const snap = snapCurrent()
|
|
442
|
-
currentId = snap.id
|
|
443
|
-
currentAddress = snap.address
|
|
444
|
-
sendStatus()
|
|
445
|
-
}, 1500)
|
|
446
|
-
try {
|
|
447
|
-
if (entry.address) sessions.openSubagent(entry.address)
|
|
448
|
-
else sessions.open(entry.id)
|
|
449
|
-
} catch {
|
|
450
|
-
pendingJump = null
|
|
451
|
-
}
|
|
452
|
-
}
|
|
552
|
+
const snapCurrent = () => {
|
|
553
|
+
const snap = store.getSnapshot()
|
|
554
|
+
return { id: snap.current, address: snap.currentAddress }
|
|
555
|
+
}
|
|
453
556
|
|
|
454
|
-
|
|
455
|
-
if (backStack.length === 0) return
|
|
456
|
-
const target = backStack.pop()
|
|
457
|
-
if (currentId !== undefined) pushStack(forwardStack, { id: currentId, address: currentAddress })
|
|
458
|
-
sendStatus()
|
|
459
|
-
jumpTo(target)
|
|
460
|
-
}
|
|
557
|
+
const nav = createSessionNavStack()
|
|
461
558
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
559
|
+
const sendStatus = () => {
|
|
560
|
+
if (window.parent === window) return
|
|
561
|
+
const payload = { __tbNavStatus: { back: nav.status().back, forward: nav.status().forward } }
|
|
562
|
+
try {
|
|
563
|
+
// 用 '*' 投递:WebView2 下对 http://tauri.localhost 虚拟主机的
|
|
564
|
+
// 精确 targetOrigin 匹配有丢弃嫌疑;外壳侧会校验 event.origin。
|
|
565
|
+
window.parent.postMessage(payload, '*')
|
|
566
|
+
} catch { /* 忽略 */ }
|
|
567
|
+
}
|
|
469
568
|
|
|
470
|
-
|
|
569
|
+
const jumpTo = (entry) => {
|
|
570
|
+
nav.jumpTo(entry, () => {
|
|
571
|
+
// 超时兜底解锁:读最新快照同步当前会话并上报。
|
|
471
572
|
const snap = snapCurrent()
|
|
472
|
-
|
|
473
|
-
if (snap.id === pendingJump.id || snap.id !== undefined && snap.id !== currentId) {
|
|
474
|
-
pendingJump = null
|
|
475
|
-
clearTimeout(jumpTimer)
|
|
476
|
-
currentId = snap.id
|
|
477
|
-
currentAddress = snap.address
|
|
478
|
-
sendStatus()
|
|
479
|
-
}
|
|
480
|
-
return
|
|
481
|
-
}
|
|
482
|
-
if (snap.id === undefined) {
|
|
483
|
-
currentAddress = undefined
|
|
484
|
-
sendStatus()
|
|
485
|
-
return
|
|
486
|
-
}
|
|
487
|
-
if (snap.id === currentId) {
|
|
488
|
-
if (snap.address !== undefined) currentAddress = snap.address
|
|
489
|
-
return
|
|
490
|
-
}
|
|
491
|
-
if (currentId !== undefined) {
|
|
492
|
-
pushStack(backStack, { id: currentId, address: currentAddress })
|
|
493
|
-
forwardStack = []
|
|
494
|
-
}
|
|
495
|
-
currentId = snap.id
|
|
496
|
-
currentAddress = snap.address
|
|
573
|
+
nav.init(snap.id, snap.address)
|
|
497
574
|
sendStatus()
|
|
575
|
+
})
|
|
576
|
+
try {
|
|
577
|
+
if (entry.address) sessions.openSubagent(entry.address)
|
|
578
|
+
else sessions.open(entry.id)
|
|
579
|
+
} catch {
|
|
580
|
+
nav.abortJump()
|
|
498
581
|
}
|
|
582
|
+
}
|
|
499
583
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
else if (dir === 'forward') goForward()
|
|
507
|
-
else if (dir === 'ping') sendStatus()
|
|
508
|
-
}
|
|
509
|
-
window.addEventListener('message', onNavMessage)
|
|
510
|
-
|
|
511
|
-
const onKeydown = (event) => {
|
|
512
|
-
if (!event.altKey || event.ctrlKey || event.shiftKey) return
|
|
513
|
-
if (event.key === 'ArrowLeft') {
|
|
514
|
-
event.preventDefault()
|
|
515
|
-
goBack()
|
|
516
|
-
} else if (event.key === 'ArrowRight') {
|
|
517
|
-
event.preventDefault()
|
|
518
|
-
goForward()
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
window.addEventListener('keydown', onKeydown, true)
|
|
522
|
-
|
|
523
|
-
// 鼠标侧键:DOM button 3 = 后退(XButton1)、4 = 前进(XButton2)
|
|
524
|
-
const onMouseup = (event) => {
|
|
525
|
-
if (event.button === 3) {
|
|
526
|
-
event.preventDefault()
|
|
527
|
-
goBack()
|
|
528
|
-
} else if (event.button === 4) {
|
|
529
|
-
event.preventDefault()
|
|
530
|
-
goForward()
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
window.addEventListener('mouseup', onMouseup, true)
|
|
584
|
+
const goBack = () => {
|
|
585
|
+
const target = nav.goBack()
|
|
586
|
+
if (!target) return
|
|
587
|
+
sendStatus()
|
|
588
|
+
jumpTo(target)
|
|
589
|
+
}
|
|
534
590
|
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
currentAddress = init.address
|
|
591
|
+
const goForward = () => {
|
|
592
|
+
const target = nav.goForward()
|
|
593
|
+
if (!target) return
|
|
539
594
|
sendStatus()
|
|
595
|
+
jumpTo(target)
|
|
596
|
+
}
|
|
540
597
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
window.removeEventListener('mouseup', onMouseup, true)
|
|
598
|
+
const onListChange = () => {
|
|
599
|
+
const snap = snapCurrent()
|
|
600
|
+
if (nav.isPending()) {
|
|
601
|
+
if (nav.onPendingChange(snap)) sendStatus()
|
|
602
|
+
return
|
|
547
603
|
}
|
|
548
|
-
|
|
604
|
+
if (nav.onExternalChange(snap)) sendStatus()
|
|
549
605
|
}
|
|
550
606
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
607
|
+
const onNavMessage = (event) => {
|
|
608
|
+
const data = event.data || {}
|
|
609
|
+
if (event.source !== window.parent) return
|
|
610
|
+
if (event.origin !== parentOrigin) return
|
|
611
|
+
const dir = data.__tbNav
|
|
612
|
+
if (dir === 'back') goBack()
|
|
613
|
+
else if (dir === 'forward') goForward()
|
|
614
|
+
else if (dir === 'ping') sendStatus()
|
|
615
|
+
}
|
|
616
|
+
window.addEventListener('message', onNavMessage)
|
|
617
|
+
|
|
618
|
+
const onKeydown = (event) => {
|
|
619
|
+
if (!event.altKey || event.ctrlKey || event.shiftKey) return
|
|
620
|
+
if (event.key === 'ArrowLeft') {
|
|
621
|
+
event.preventDefault()
|
|
622
|
+
goBack()
|
|
623
|
+
} else if (event.key === 'ArrowRight') {
|
|
624
|
+
event.preventDefault()
|
|
625
|
+
goForward()
|
|
561
626
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
627
|
+
}
|
|
628
|
+
window.addEventListener('keydown', onKeydown, true)
|
|
629
|
+
|
|
630
|
+
// 鼠标侧键:DOM button 3 = 后退(XButton1)、4 = 前进(XButton2)
|
|
631
|
+
const onMouseup = (event) => {
|
|
632
|
+
if (event.button === 3) {
|
|
633
|
+
event.preventDefault()
|
|
634
|
+
goBack()
|
|
635
|
+
} else if (event.button === 4) {
|
|
636
|
+
event.preventDefault()
|
|
637
|
+
goForward()
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
window.addEventListener('mouseup', onMouseup, true)
|
|
641
|
+
|
|
642
|
+
const unsubscribe = store.subscribe(onListChange)
|
|
643
|
+
const init = snapCurrent()
|
|
644
|
+
nav.init(init.id, init.address)
|
|
645
|
+
sendStatus()
|
|
646
|
+
|
|
647
|
+
navDispose = () => {
|
|
648
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
649
|
+
nav.dispose()
|
|
650
|
+
window.removeEventListener('message', onNavMessage)
|
|
651
|
+
window.removeEventListener('keydown', onKeydown, true)
|
|
652
|
+
window.removeEventListener('mouseup', onMouseup, true)
|
|
653
|
+
}
|
|
654
|
+
return true
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 立即尝试一次,再走重试梯子(服务可能在之后才就绪)
|
|
658
|
+
const disposeTimer = retryLadder(initNav, { immediate: true })
|
|
659
|
+
|
|
660
|
+
ctx.effect(() => () => {
|
|
661
|
+
disposeTimer()
|
|
662
|
+
if (navDispose) navDispose()
|
|
663
|
+
}, 'dsh-tauri-launcher: session nav')
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* 父窗口 origin:跨源读不到 window.parent.location,用 document.referrer
|
|
668
|
+
* 推导(兜底 http://tauri.localhost)。
|
|
669
|
+
*/
|
|
670
|
+
function deriveParentOrigin() {
|
|
671
|
+
try {
|
|
672
|
+
if (document.referrer) return new URL(document.referrer).origin
|
|
673
|
+
} catch { /* 兜底 http://tauri.localhost */ }
|
|
674
|
+
return 'http://tauri.localhost'
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
exports.apply = function (ctx) {
|
|
678
|
+
const slots = ctx.get('slots')
|
|
679
|
+
if (slots === undefined) return
|
|
680
|
+
|
|
681
|
+
installDesktopSection(ctx, slots)
|
|
682
|
+
|
|
683
|
+
// 主题中继内部自带 window.parent 检查(浏览器直开自动不启用)。
|
|
684
|
+
installThemeRelay(ctx)
|
|
685
|
+
|
|
686
|
+
// 以下功能仅在 iframe 内嵌(桌面端)时有意义;浏览器直开时
|
|
687
|
+
// Chromium 原生跟随系统主题、且无标题栏,直接跳过。
|
|
688
|
+
if (window.parent !== window) {
|
|
689
|
+
const parentOrigin = deriveParentOrigin()
|
|
690
|
+
installSystemThemeFollow(ctx, parentOrigin)
|
|
691
|
+
installSessionNav(ctx, parentOrigin)
|
|
571
692
|
}
|
|
572
693
|
}
|
|
573
694
|
|