@talex-touch/utils 1.0.33 → 1.0.35
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/common/file-scan-utils.ts +0 -1
- package/common/storage/constants.ts +1 -0
- package/index.ts +1 -0
- package/market/constants.ts +95 -0
- package/market/index.ts +2 -0
- package/market/types.ts +118 -0
- package/package.json +1 -1
- package/plugin/channel.ts +66 -7
- package/plugin/index.ts +1 -0
- package/plugin/sdk/README.md +5 -1
- package/plugin/sdk/box-sdk.ts +148 -41
- package/plugin/sdk/channel.ts +34 -0
- package/plugin/sdk/division-box.ts +2 -7
- package/plugin/sdk/feature-sdk.ts +3 -7
- package/plugin/sdk/hooks/bridge.ts +3 -1
- package/plugin/sdk/hooks/life-cycle.ts +4 -1
- package/plugin/sdk/storage.ts +2 -1
- package/plugin/widget.ts +25 -0
- package/renderer/storage/storage-subscription.ts +196 -0
- package/types/icon.ts +7 -0
package/index.ts
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { StorageList } from '../common/storage/constants'
|
|
2
|
+
import type {
|
|
3
|
+
MarketProviderDefinition,
|
|
4
|
+
MarketSourcesPayload,
|
|
5
|
+
MarketSourcesStorageInfo,
|
|
6
|
+
MarketProviderTrustLevel,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
export const MARKET_SOURCES_STORAGE_KEY = StorageList.MARKET_SOURCES
|
|
10
|
+
export const MARKET_SOURCES_STORAGE_VERSION = 1
|
|
11
|
+
|
|
12
|
+
function defineProvider(
|
|
13
|
+
provider: Omit<MarketProviderDefinition, 'trustLevel'> & {
|
|
14
|
+
trustLevel?: MarketProviderTrustLevel
|
|
15
|
+
},
|
|
16
|
+
): MarketProviderDefinition {
|
|
17
|
+
return {
|
|
18
|
+
trustLevel: provider.trustLevel ?? 'unverified',
|
|
19
|
+
...provider,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_MARKET_PROVIDERS: MarketProviderDefinition[] = [
|
|
24
|
+
defineProvider({
|
|
25
|
+
id: 'talex-official',
|
|
26
|
+
name: 'Talex Official',
|
|
27
|
+
type: 'nexusStore',
|
|
28
|
+
url: 'https://raw.githubusercontent.com/talex-touch/tuff-official-plugins/main/plugins.json',
|
|
29
|
+
description: '官方插件市场,提供经过审核的核心插件。',
|
|
30
|
+
enabled: true,
|
|
31
|
+
priority: 100,
|
|
32
|
+
trustLevel: 'official',
|
|
33
|
+
readOnly: true,
|
|
34
|
+
config: {
|
|
35
|
+
manifestUrl:
|
|
36
|
+
'https://raw.githubusercontent.com/talex-touch/tuff-official-plugins/main/plugins.json',
|
|
37
|
+
baseUrl: 'https://raw.githubusercontent.com/talex-touch/tuff-official-plugins/main/',
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
defineProvider({
|
|
41
|
+
id: 'github-releases',
|
|
42
|
+
name: 'GitHub Releases',
|
|
43
|
+
type: 'repository',
|
|
44
|
+
description: '从 GitHub 仓库 releases / manifest 中读取插件。',
|
|
45
|
+
enabled: false,
|
|
46
|
+
priority: 80,
|
|
47
|
+
trustLevel: 'unverified',
|
|
48
|
+
config: {
|
|
49
|
+
platform: 'github',
|
|
50
|
+
apiBase: 'https://api.github.com',
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
defineProvider({
|
|
54
|
+
id: 'gitee-repos',
|
|
55
|
+
name: 'Gitee 仓库',
|
|
56
|
+
type: 'repository',
|
|
57
|
+
description: 'Gitee 平台插件仓库,适合国内网络。',
|
|
58
|
+
enabled: false,
|
|
59
|
+
priority: 70,
|
|
60
|
+
trustLevel: 'unverified',
|
|
61
|
+
config: {
|
|
62
|
+
platform: 'gitee',
|
|
63
|
+
apiBase: 'https://gitee.com/api/v5',
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
defineProvider({
|
|
67
|
+
id: 'npm-scope',
|
|
68
|
+
name: 'NPM 包',
|
|
69
|
+
type: 'npmPackage',
|
|
70
|
+
description: '基于 NPM 关键字或 scope 的插件发布渠道。',
|
|
71
|
+
enabled: false,
|
|
72
|
+
priority: 60,
|
|
73
|
+
trustLevel: 'unverified',
|
|
74
|
+
config: {
|
|
75
|
+
registryUrl: 'https://registry.npmjs.org',
|
|
76
|
+
keyword: 'talex-touch-plugin',
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
export const MARKET_SOURCES_STORAGE_INFO: MarketSourcesStorageInfo = {
|
|
82
|
+
storageKey: MARKET_SOURCES_STORAGE_KEY,
|
|
83
|
+
version: MARKET_SOURCES_STORAGE_VERSION,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createDefaultMarketSourcesPayload(): MarketSourcesPayload {
|
|
87
|
+
const clone = typeof structuredClone === 'function'
|
|
88
|
+
? structuredClone(DEFAULT_MARKET_PROVIDERS)
|
|
89
|
+
: JSON.parse(JSON.stringify(DEFAULT_MARKET_PROVIDERS))
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
version: MARKET_SOURCES_STORAGE_VERSION,
|
|
93
|
+
sources: clone,
|
|
94
|
+
}
|
|
95
|
+
}
|
package/market/index.ts
ADDED
package/market/types.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { StorageList } from '../common/storage/constants'
|
|
2
|
+
|
|
3
|
+
export type MarketProviderType = 'repository' | 'nexusStore' | 'npmPackage'
|
|
4
|
+
|
|
5
|
+
export type MarketProviderTrustLevel = 'official' | 'verified' | 'unverified'
|
|
6
|
+
|
|
7
|
+
export interface MarketProviderDefinition {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
type: MarketProviderType
|
|
11
|
+
/**
|
|
12
|
+
* Base URL or identifier for the provider.
|
|
13
|
+
* Individual provider implementations can interpret this differently.
|
|
14
|
+
*/
|
|
15
|
+
url?: string
|
|
16
|
+
/**
|
|
17
|
+
* Additional configuration object for provider specific options.
|
|
18
|
+
*/
|
|
19
|
+
config?: Record<string, any>
|
|
20
|
+
description?: string
|
|
21
|
+
enabled: boolean
|
|
22
|
+
priority: number
|
|
23
|
+
trustLevel?: MarketProviderTrustLevel
|
|
24
|
+
tags?: string[]
|
|
25
|
+
/**
|
|
26
|
+
* Whether this provider should be treated as read-only (no install)
|
|
27
|
+
*/
|
|
28
|
+
readOnly?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MarketSourcesPayload {
|
|
32
|
+
/**
|
|
33
|
+
* Schema version, used for migrations.
|
|
34
|
+
*/
|
|
35
|
+
version: number
|
|
36
|
+
sources: MarketProviderDefinition[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type MarketInstallInstruction =
|
|
40
|
+
| {
|
|
41
|
+
type: 'url'
|
|
42
|
+
url: string
|
|
43
|
+
format?: 'zip' | 'tar' | 'tgz' | 'tpex'
|
|
44
|
+
integrity?: string
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
type: 'npm'
|
|
48
|
+
packageName: string
|
|
49
|
+
version?: string
|
|
50
|
+
registry?: string
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
type: 'git'
|
|
54
|
+
repo: string
|
|
55
|
+
ref?: string
|
|
56
|
+
sparse?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MarketPlugin {
|
|
60
|
+
id: string
|
|
61
|
+
name: string
|
|
62
|
+
version?: string
|
|
63
|
+
description?: string
|
|
64
|
+
category?: string
|
|
65
|
+
tags?: string[]
|
|
66
|
+
author?: string
|
|
67
|
+
icon?: string
|
|
68
|
+
metadata?: Record<string, unknown>
|
|
69
|
+
readmeUrl?: string
|
|
70
|
+
homepage?: string
|
|
71
|
+
downloadUrl?: string
|
|
72
|
+
install?: MarketInstallInstruction
|
|
73
|
+
providerId: string
|
|
74
|
+
providerName: string
|
|
75
|
+
providerType: MarketProviderType
|
|
76
|
+
providerTrustLevel: MarketProviderTrustLevel
|
|
77
|
+
trusted: boolean
|
|
78
|
+
official?: boolean
|
|
79
|
+
timestamp?: number | string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface MarketProviderResultMeta {
|
|
83
|
+
providerId: string
|
|
84
|
+
providerName: string
|
|
85
|
+
providerType: MarketProviderType
|
|
86
|
+
success: boolean
|
|
87
|
+
error?: string
|
|
88
|
+
fetchedAt: number
|
|
89
|
+
itemCount: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MarketProviderListOptions {
|
|
93
|
+
keyword?: string
|
|
94
|
+
force?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MarketHttpRequestOptions {
|
|
98
|
+
url: string
|
|
99
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
100
|
+
headers?: Record<string, string>
|
|
101
|
+
params?: Record<string, any>
|
|
102
|
+
data?: any
|
|
103
|
+
timeout?: number
|
|
104
|
+
responseType?: 'json' | 'text' | 'arraybuffer'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface MarketHttpResponse<T = unknown> {
|
|
108
|
+
status: number
|
|
109
|
+
statusText: string
|
|
110
|
+
headers: Record<string, string>
|
|
111
|
+
data: T
|
|
112
|
+
url: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface MarketSourcesStorageInfo {
|
|
116
|
+
storageKey: StorageList
|
|
117
|
+
version: number
|
|
118
|
+
}
|
package/package.json
CHANGED
package/plugin/channel.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
DataCode,
|
|
11
11
|
} from '../channel'
|
|
12
12
|
|
|
13
|
+
const CHANNEL_DEFAULT_TIMEOUT = 10_000
|
|
14
|
+
|
|
13
15
|
let cachedIpcRenderer: IpcRenderer | null = null
|
|
14
16
|
|
|
15
17
|
// 使用惰性解析避免在打包阶段静态引入 electron
|
|
@@ -197,6 +199,19 @@ class TouchChannel implements ITouchClientChannel {
|
|
|
197
199
|
return true
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
private formatPayloadPreview(payload: unknown): string {
|
|
203
|
+
if (payload === null || payload === undefined)
|
|
204
|
+
return String(payload)
|
|
205
|
+
if (typeof payload === 'string')
|
|
206
|
+
return payload.length > 200 ? `${payload.slice(0, 200)}…` : payload
|
|
207
|
+
try {
|
|
208
|
+
return JSON.stringify(payload)
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return '[unserializable]'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
200
215
|
send(eventName: string, arg: any): Promise<any> {
|
|
201
216
|
const uniqueId = `${new Date().getTime()}#${eventName}@${Math.random().toString(
|
|
202
217
|
12,
|
|
@@ -207,7 +222,7 @@ class TouchChannel implements ITouchClientChannel {
|
|
|
207
222
|
data: arg,
|
|
208
223
|
sync: {
|
|
209
224
|
timeStamp: new Date().getTime(),
|
|
210
|
-
timeout:
|
|
225
|
+
timeout: CHANNEL_DEFAULT_TIMEOUT,
|
|
211
226
|
id: uniqueId,
|
|
212
227
|
},
|
|
213
228
|
name: eventName,
|
|
@@ -218,10 +233,40 @@ class TouchChannel implements ITouchClientChannel {
|
|
|
218
233
|
},
|
|
219
234
|
} as RawStandardChannelData
|
|
220
235
|
|
|
221
|
-
return new Promise((resolve) => {
|
|
222
|
-
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
try {
|
|
238
|
+
this.ipcRenderer.send('@plugin-process-message', data)
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
242
|
+
console.error(
|
|
243
|
+
`[PluginChannel] Failed to send "${eventName}": ${errorMessage}`,
|
|
244
|
+
{ payloadPreview: this.formatPayloadPreview(arg) },
|
|
245
|
+
)
|
|
246
|
+
reject(
|
|
247
|
+
Object.assign(
|
|
248
|
+
new Error(`Failed to send plugin channel message "${eventName}": ${errorMessage}`),
|
|
249
|
+
{ code: 'plugin_channel_send_failed' },
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const timeoutMs = data.sync?.timeout ?? CHANNEL_DEFAULT_TIMEOUT
|
|
256
|
+
const timeoutHandle = setTimeout(() => {
|
|
257
|
+
if (!this.pendingMap.has(uniqueId))
|
|
258
|
+
return
|
|
259
|
+
this.pendingMap.delete(uniqueId)
|
|
260
|
+
const timeoutError = Object.assign(
|
|
261
|
+
new Error(`Plugin channel request "${eventName}" timed out after ${timeoutMs}ms`),
|
|
262
|
+
{ code: 'plugin_channel_timeout' },
|
|
263
|
+
)
|
|
264
|
+
console.warn(timeoutError.message)
|
|
265
|
+
reject(timeoutError)
|
|
266
|
+
}, timeoutMs)
|
|
223
267
|
|
|
224
268
|
this.pendingMap.set(uniqueId, (res: any) => {
|
|
269
|
+
clearTimeout(timeoutHandle)
|
|
225
270
|
this.pendingMap.delete(uniqueId)
|
|
226
271
|
|
|
227
272
|
resolve(res.data)
|
|
@@ -241,12 +286,26 @@ class TouchChannel implements ITouchClientChannel {
|
|
|
241
286
|
},
|
|
242
287
|
} as RawStandardChannelData
|
|
243
288
|
|
|
244
|
-
|
|
289
|
+
try {
|
|
290
|
+
const res = this.__parse_raw_data(
|
|
291
|
+
void 0,
|
|
292
|
+
this.ipcRenderer.sendSync('@plugin-process-message', data),
|
|
293
|
+
)!
|
|
245
294
|
|
|
246
|
-
|
|
247
|
-
|
|
295
|
+
if (res.header.status === 'reply')
|
|
296
|
+
return res.data
|
|
248
297
|
|
|
249
|
-
|
|
298
|
+
return res
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
302
|
+
console.error('[PluginChannel] Failed to sendSync message', {
|
|
303
|
+
eventName,
|
|
304
|
+
error: errorMessage,
|
|
305
|
+
payloadPreview: this.formatPayloadPreview(arg),
|
|
306
|
+
})
|
|
307
|
+
throw new Error(`Failed to sendSync plugin channel message "${eventName}": ${errorMessage}`)
|
|
308
|
+
}
|
|
250
309
|
}
|
|
251
310
|
}
|
|
252
311
|
|
package/plugin/index.ts
CHANGED
package/plugin/sdk/README.md
CHANGED
|
@@ -26,8 +26,10 @@ plugin.box.shrink()
|
|
|
26
26
|
plugin.box.hideInput()
|
|
27
27
|
plugin.box.showInput()
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// 获取与设置输入
|
|
30
30
|
const input = await plugin.box.getInput()
|
|
31
|
+
await plugin.box.setInput('Hello Touch!')
|
|
32
|
+
await plugin.box.clearInput()
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
### 2. FeatureSDK - 搜索结果管理
|
|
@@ -203,6 +205,8 @@ export default {
|
|
|
203
205
|
- `core-box:hide-input` - 隐藏输入框
|
|
204
206
|
- `core-box:show-input` - 显示输入框
|
|
205
207
|
- `core-box:get-input` - 获取当前输入值
|
|
208
|
+
- `core-box:set-input` - 设置输入框内容
|
|
209
|
+
- `core-box:clear-input` - 清空输入框
|
|
206
210
|
- `core-box:input-changed` - 输入变化广播(主进程 → 插件)
|
|
207
211
|
- `core-box:set-input-visibility` - 设置输入框可见性(主进程 → 渲染进程)
|
|
208
212
|
- `core-box:request-input-value` - 请求输入值(主进程 → 渲染进程)
|
package/plugin/sdk/box-sdk.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Box SDK for Plugin Development
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides a unified API for plugins to control the CoreBox window behavior,
|
|
5
5
|
* including visibility, size, input field control, and input value access.
|
|
6
6
|
*/
|
|
7
|
+
import { ensureRendererChannel } from './channel'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Clipboard content type flags for binary combination
|
|
11
|
+
*/
|
|
12
|
+
export enum ClipboardType {
|
|
13
|
+
TEXT = 0b0001,
|
|
14
|
+
IMAGE = 0b0010,
|
|
15
|
+
FILE = 0b0100,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Preset clipboard type combinations
|
|
20
|
+
*/
|
|
21
|
+
export const ClipboardTypePresets = {
|
|
22
|
+
TEXT_ONLY: ClipboardType.TEXT,
|
|
23
|
+
TEXT_AND_IMAGE: ClipboardType.TEXT | ClipboardType.IMAGE,
|
|
24
|
+
ALL: ClipboardType.TEXT | ClipboardType.IMAGE | ClipboardType.FILE,
|
|
25
|
+
} as const
|
|
7
26
|
|
|
8
27
|
/**
|
|
9
28
|
* Expand options for CoreBox window
|
|
@@ -17,18 +36,18 @@ export interface BoxExpandOptions {
|
|
|
17
36
|
|
|
18
37
|
/**
|
|
19
38
|
* Box SDK interface for plugins
|
|
20
|
-
*
|
|
39
|
+
*
|
|
21
40
|
* @example
|
|
22
41
|
* ```typescript
|
|
23
42
|
* // Hide CoreBox
|
|
24
43
|
* plugin.box.hide()
|
|
25
|
-
*
|
|
44
|
+
*
|
|
26
45
|
* // Show CoreBox
|
|
27
46
|
* plugin.box.show()
|
|
28
|
-
*
|
|
47
|
+
*
|
|
29
48
|
* // Expand to show 10 items
|
|
30
49
|
* plugin.box.expand({ length: 10 })
|
|
31
|
-
*
|
|
50
|
+
*
|
|
32
51
|
* // Get current input
|
|
33
52
|
* const input = plugin.box.getInput()
|
|
34
53
|
* ```
|
|
@@ -36,93 +55,142 @@ export interface BoxExpandOptions {
|
|
|
36
55
|
export interface BoxSDK {
|
|
37
56
|
/**
|
|
38
57
|
* Hides the CoreBox window
|
|
39
|
-
*
|
|
58
|
+
*
|
|
40
59
|
* @example
|
|
41
60
|
* ```typescript
|
|
42
61
|
* plugin.box.hide()
|
|
43
62
|
* ```
|
|
44
63
|
*/
|
|
45
|
-
hide()
|
|
64
|
+
hide: () => void
|
|
46
65
|
|
|
47
66
|
/**
|
|
48
67
|
* Shows the CoreBox window
|
|
49
|
-
*
|
|
68
|
+
*
|
|
50
69
|
* @example
|
|
51
70
|
* ```typescript
|
|
52
71
|
* plugin.box.show()
|
|
53
72
|
* ```
|
|
54
73
|
*/
|
|
55
|
-
show()
|
|
74
|
+
show: () => void
|
|
56
75
|
|
|
57
76
|
/**
|
|
58
77
|
* Expands the CoreBox window
|
|
59
|
-
*
|
|
78
|
+
*
|
|
60
79
|
* @param options - Optional expansion configuration
|
|
61
|
-
*
|
|
80
|
+
*
|
|
62
81
|
* @example
|
|
63
82
|
* ```typescript
|
|
64
83
|
* // Expand to show 10 items
|
|
65
84
|
* plugin.box.expand({ length: 10 })
|
|
66
|
-
*
|
|
85
|
+
*
|
|
67
86
|
* // Force maximum expansion
|
|
68
87
|
* plugin.box.expand({ forceMax: true })
|
|
69
|
-
*
|
|
88
|
+
*
|
|
70
89
|
* // Default expansion
|
|
71
90
|
* plugin.box.expand()
|
|
72
91
|
* ```
|
|
73
92
|
*/
|
|
74
|
-
expand(options?: BoxExpandOptions)
|
|
93
|
+
expand: (options?: BoxExpandOptions) => Promise<void>
|
|
75
94
|
|
|
76
95
|
/**
|
|
77
96
|
* Shrinks the CoreBox window to compact size
|
|
78
|
-
*
|
|
97
|
+
*
|
|
79
98
|
* @example
|
|
80
99
|
* ```typescript
|
|
81
100
|
* plugin.box.shrink()
|
|
82
101
|
* ```
|
|
83
102
|
*/
|
|
84
|
-
shrink()
|
|
103
|
+
shrink: () => Promise<void>
|
|
85
104
|
|
|
86
105
|
/**
|
|
87
106
|
* Hides the input field in CoreBox
|
|
88
|
-
*
|
|
107
|
+
*
|
|
89
108
|
* @example
|
|
90
109
|
* ```typescript
|
|
91
110
|
* plugin.box.hideInput()
|
|
92
111
|
* ```
|
|
93
112
|
*/
|
|
94
|
-
hideInput()
|
|
113
|
+
hideInput: () => Promise<void>
|
|
95
114
|
|
|
96
115
|
/**
|
|
97
116
|
* Shows the input field in CoreBox
|
|
98
|
-
*
|
|
117
|
+
*
|
|
99
118
|
* @example
|
|
100
119
|
* ```typescript
|
|
101
120
|
* plugin.box.showInput()
|
|
102
121
|
* ```
|
|
103
122
|
*/
|
|
104
|
-
showInput()
|
|
123
|
+
showInput: () => Promise<void>
|
|
105
124
|
|
|
106
125
|
/**
|
|
107
126
|
* Gets the current input value from CoreBox search field
|
|
108
|
-
*
|
|
127
|
+
*
|
|
109
128
|
* @returns Promise resolving to the current input string
|
|
110
|
-
*
|
|
129
|
+
*
|
|
111
130
|
* @example
|
|
112
131
|
* ```typescript
|
|
113
132
|
* const input = await plugin.box.getInput()
|
|
114
133
|
* console.log('Current input:', input)
|
|
115
134
|
* ```
|
|
116
135
|
*/
|
|
117
|
-
getInput()
|
|
136
|
+
getInput: () => Promise<string>
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Sets the CoreBox search input to the specified value
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* await plugin.box.setInput('hello world')
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
setInput: (value: string) => Promise<void>
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Clears the CoreBox search input
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* await plugin.box.clearInput()
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
clearInput: () => Promise<void>
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Enable input monitoring for attached UI view
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* await plugin.box.allowInput()
|
|
164
|
+
* plugin.channel.regChannel('core-box:input-change', ({ data }) => {
|
|
165
|
+
* console.log('Input changed:', data.input)
|
|
166
|
+
* })
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
allowInput: () => Promise<void>
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Enable clipboard monitoring for specified type combination
|
|
173
|
+
*
|
|
174
|
+
* @param types - Binary combination of ClipboardType flags
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* // Allow text and images
|
|
179
|
+
* await plugin.box.allowClipboard(ClipboardType.TEXT | ClipboardType.IMAGE)
|
|
180
|
+
*
|
|
181
|
+
* // Or use presets
|
|
182
|
+
* await plugin.box.allowClipboard(ClipboardTypePresets.TEXT_AND_IMAGE)
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
allowClipboard: (types: number) => Promise<void>
|
|
118
186
|
}
|
|
119
187
|
|
|
120
188
|
/**
|
|
121
189
|
* Creates a Box SDK instance for plugin use
|
|
122
|
-
*
|
|
190
|
+
*
|
|
123
191
|
* @param channel - The plugin channel bridge for IPC communication
|
|
124
192
|
* @returns Configured Box SDK instance
|
|
125
|
-
*
|
|
193
|
+
*
|
|
126
194
|
* @internal
|
|
127
195
|
*/
|
|
128
196
|
export function createBoxSDK(channel: any): BoxSDK {
|
|
@@ -148,7 +216,8 @@ export function createBoxSDK(channel: any): BoxSDK {
|
|
|
148
216
|
async expand(options?: BoxExpandOptions): Promise<void> {
|
|
149
217
|
try {
|
|
150
218
|
await sendFn('core-box:expand', options || {})
|
|
151
|
-
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
152
221
|
console.error('[Box SDK] Failed to expand CoreBox:', error)
|
|
153
222
|
throw error
|
|
154
223
|
}
|
|
@@ -157,7 +226,8 @@ export function createBoxSDK(channel: any): BoxSDK {
|
|
|
157
226
|
async shrink(): Promise<void> {
|
|
158
227
|
try {
|
|
159
228
|
await sendFn('core-box:expand', { mode: 'collapse' })
|
|
160
|
-
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
161
231
|
console.error('[Box SDK] Failed to shrink CoreBox:', error)
|
|
162
232
|
throw error
|
|
163
233
|
}
|
|
@@ -166,7 +236,8 @@ export function createBoxSDK(channel: any): BoxSDK {
|
|
|
166
236
|
async hideInput(): Promise<void> {
|
|
167
237
|
try {
|
|
168
238
|
await sendFn('core-box:hide-input')
|
|
169
|
-
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
170
241
|
console.error('[Box SDK] Failed to hide input:', error)
|
|
171
242
|
throw error
|
|
172
243
|
}
|
|
@@ -175,7 +246,8 @@ export function createBoxSDK(channel: any): BoxSDK {
|
|
|
175
246
|
async showInput(): Promise<void> {
|
|
176
247
|
try {
|
|
177
248
|
await sendFn('core-box:show-input')
|
|
178
|
-
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
179
251
|
console.error('[Box SDK] Failed to show input:', error)
|
|
180
252
|
throw error
|
|
181
253
|
}
|
|
@@ -185,35 +257,70 @@ export function createBoxSDK(channel: any): BoxSDK {
|
|
|
185
257
|
try {
|
|
186
258
|
const result = await sendFn('core-box:get-input')
|
|
187
259
|
return result?.data?.input || result?.input || ''
|
|
188
|
-
}
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
189
262
|
console.error('[Box SDK] Failed to get input:', error)
|
|
190
263
|
throw error
|
|
191
264
|
}
|
|
192
|
-
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
async setInput(value: string): Promise<void> {
|
|
268
|
+
try {
|
|
269
|
+
await sendFn('core-box:set-input', { value })
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error('[Box SDK] Failed to set input:', error)
|
|
273
|
+
throw error
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async clearInput(): Promise<void> {
|
|
278
|
+
try {
|
|
279
|
+
await sendFn('core-box:clear-input')
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error('[Box SDK] Failed to clear input:', error)
|
|
283
|
+
throw error
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async allowInput(): Promise<void> {
|
|
288
|
+
try {
|
|
289
|
+
await sendFn('core-box:allow-input')
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
console.error('[Box SDK] Failed to enable input monitoring:', error)
|
|
293
|
+
throw error
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
async allowClipboard(types: number): Promise<void> {
|
|
298
|
+
try {
|
|
299
|
+
await sendFn('core-box:allow-clipboard', types)
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
console.error('[Box SDK] Failed to enable clipboard monitoring:', error)
|
|
303
|
+
throw error
|
|
304
|
+
}
|
|
305
|
+
},
|
|
193
306
|
}
|
|
194
307
|
}
|
|
195
308
|
|
|
196
309
|
/**
|
|
197
310
|
* Hook for using Box SDK in plugin context
|
|
198
|
-
*
|
|
311
|
+
*
|
|
199
312
|
* @returns Box SDK instance
|
|
200
|
-
*
|
|
313
|
+
*
|
|
201
314
|
* @example
|
|
202
315
|
* ```typescript
|
|
203
316
|
* const box = useBox()
|
|
204
|
-
*
|
|
317
|
+
*
|
|
205
318
|
* box.hide()
|
|
206
319
|
* box.expand({ length: 10 })
|
|
207
320
|
* const input = await box.getInput()
|
|
208
321
|
* ```
|
|
209
322
|
*/
|
|
210
323
|
export function useBox(): BoxSDK {
|
|
211
|
-
|
|
212
|
-
const channel = window.$channel
|
|
213
|
-
|
|
214
|
-
if (!channel) {
|
|
215
|
-
throw new Error('[Box SDK] Channel not available. Make sure this is called in a plugin context.')
|
|
216
|
-
}
|
|
217
|
-
|
|
324
|
+
const channel = ensureRendererChannel('[Box SDK] Channel not available. Make sure this is called in a plugin context.')
|
|
218
325
|
return createBoxSDK(channel)
|
|
219
326
|
}
|
package/plugin/sdk/channel.ts
CHANGED
|
@@ -4,6 +4,34 @@ import { genChannel } from '../channel'
|
|
|
4
4
|
|
|
5
5
|
const ensureClientChannel = (): ITouchClientChannel => genChannel()
|
|
6
6
|
|
|
7
|
+
const DEFAULT_CHANNEL_ERROR = '[Plugin SDK] Channel not available. Make sure this code runs inside a plugin renderer context.'
|
|
8
|
+
|
|
9
|
+
let cachedWindowChannel: ITouchClientChannel | null = null
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ensures that the renderer-side plugin channel (window.$channel) exists and returns it.
|
|
13
|
+
*
|
|
14
|
+
* @param errorMessage - Optional custom error message when the channel is unavailable
|
|
15
|
+
*/
|
|
16
|
+
export function ensureRendererChannel(errorMessage = DEFAULT_CHANNEL_ERROR): ITouchClientChannel {
|
|
17
|
+
const globalWindow = typeof window === 'undefined' ? undefined : window
|
|
18
|
+
const channel = globalWindow?.$channel ?? cachedWindowChannel
|
|
19
|
+
|
|
20
|
+
if (!channel) {
|
|
21
|
+
throw new Error(errorMessage)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
cachedWindowChannel = channel
|
|
25
|
+
return channel
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convenience hook for accessing window.$channel in plugin renderers.
|
|
30
|
+
*/
|
|
31
|
+
export function useChannel(errorMessage?: string): ITouchClientChannel {
|
|
32
|
+
return ensureRendererChannel(errorMessage)
|
|
33
|
+
}
|
|
34
|
+
|
|
7
35
|
export function createPluginRendererChannel(): IPluginRendererChannel {
|
|
8
36
|
const client = ensureClientChannel()
|
|
9
37
|
|
|
@@ -46,3 +74,9 @@ export function usePluginRendererChannel(): IPluginRendererChannel {
|
|
|
46
74
|
|
|
47
75
|
return cachedRendererChannel
|
|
48
76
|
}
|
|
77
|
+
|
|
78
|
+
declare global {
|
|
79
|
+
interface Window {
|
|
80
|
+
$channel: ITouchClientChannel
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
DivisionBoxState,
|
|
12
12
|
SessionInfo,
|
|
13
13
|
} from '../../types/division-box'
|
|
14
|
+
import { ensureRendererChannel } from './channel'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* State change event handler
|
|
@@ -255,12 +256,6 @@ export function createDivisionBoxSDK(channel: any): DivisionBoxSDK {
|
|
|
255
256
|
* ```
|
|
256
257
|
*/
|
|
257
258
|
export function useDivisionBox(): DivisionBoxSDK {
|
|
258
|
-
|
|
259
|
-
const channel = window.$channel
|
|
260
|
-
|
|
261
|
-
if (!channel) {
|
|
262
|
-
throw new Error('[DivisionBox SDK] Channel not available. Make sure this is called in a plugin context.')
|
|
263
|
-
}
|
|
264
|
-
|
|
259
|
+
const channel = ensureRendererChannel('[DivisionBox SDK] Channel not available. Make sure this is called in a plugin context.')
|
|
265
260
|
return createDivisionBoxSDK(channel)
|
|
266
261
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { TuffItem } from '../../core-box/tuff'
|
|
10
|
+
import { ensureRendererChannel } from './channel'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Input change event handler
|
|
@@ -218,18 +219,13 @@ export function createFeatureSDK(boxItemsAPI: any, channel: any): FeatureSDK {
|
|
|
218
219
|
* ```
|
|
219
220
|
*/
|
|
220
221
|
export function useFeature(): FeatureSDK {
|
|
221
|
-
// @ts-ignore - window.$boxItems
|
|
222
|
+
// @ts-ignore - window.$boxItems is injected by the plugin system
|
|
222
223
|
const boxItemsAPI = window.$boxItems
|
|
223
|
-
|
|
224
|
-
const channel = window.$channel
|
|
224
|
+
const channel = ensureRendererChannel('[Feature SDK] Channel not available. Make sure this is called in a plugin context.')
|
|
225
225
|
|
|
226
226
|
if (!boxItemsAPI) {
|
|
227
227
|
throw new Error('[Feature SDK] boxItems API not available. Make sure this is called in a plugin context.')
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
if (!channel) {
|
|
231
|
-
throw new Error('[Feature SDK] Channel not available. Make sure this is called in a plugin context.')
|
|
232
|
-
}
|
|
233
|
-
|
|
234
230
|
return createFeatureSDK(boxItemsAPI, channel)
|
|
235
231
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BridgeEventForCoreBox } from '../enum/bridge-event'
|
|
2
|
+
import { ensureRendererChannel } from '../channel'
|
|
2
3
|
|
|
3
4
|
export type BridgeEvent = BridgeEventForCoreBox
|
|
4
5
|
|
|
@@ -26,7 +27,8 @@ export function injectBridgeEvent<T>(type: BridgeEvent, hook: BridgeHook<T>) {
|
|
|
26
27
|
|
|
27
28
|
// Only register the channel listener once per event type
|
|
28
29
|
if (hooks.length === 0) {
|
|
29
|
-
|
|
30
|
+
const channel = ensureRendererChannel('[TouchSDK] Bridge channel not available. Make sure hooks run in plugin renderer context.')
|
|
31
|
+
channel.regChannel(type, ({ data }) => {
|
|
30
32
|
console.debug(`[TouchSDK] ${type} event received: `, data)
|
|
31
33
|
// When the event is received, call all registered hooks for this type
|
|
32
34
|
const registeredHooks = __hooks[type]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ensureRendererChannel } from '../channel'
|
|
2
|
+
|
|
1
3
|
export enum LifecycleHooks {
|
|
2
4
|
ENABLE = 'en',
|
|
3
5
|
DISABLE = 'di',
|
|
@@ -22,7 +24,8 @@ export function injectHook(type: LifecycleHooks, hook: Function, processFunc = (
|
|
|
22
24
|
const hooks: Array<Function> = __hooks[type] || (__hooks[type] = [])
|
|
23
25
|
|
|
24
26
|
if (hooks.length === 0) {
|
|
25
|
-
|
|
27
|
+
const channel = ensureRendererChannel('[Lifecycle Hook] Channel not available. Make sure hooks run in plugin renderer context.')
|
|
28
|
+
channel.regChannel(`@lifecycle:${type}`, (obj: any) => {
|
|
26
29
|
processFunc(obj)
|
|
27
30
|
|
|
28
31
|
// @ts-ignore
|
package/plugin/sdk/storage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FileDetails, StorageStats, StorageTreeNode } from '../../types/storage'
|
|
2
|
+
import { ensureRendererChannel } from './channel'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Get the storage for the current plugin.
|
|
@@ -15,7 +16,7 @@ export function usePluginStorage() {
|
|
|
15
16
|
throw new Error('[Plugin SDK] Cannot determine plugin name. Make sure this is called in a plugin context.')
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const channel =
|
|
19
|
+
const channel = ensureRendererChannel('[Plugin Storage] Channel not available. Make sure this is called in a plugin context.')
|
|
19
20
|
|
|
20
21
|
return {
|
|
21
22
|
/**
|
package/plugin/widget.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const DEFAULT_WIDGET_RENDERERS = {
|
|
2
|
+
CORE_PREVIEW_CARD: 'core-preview-card',
|
|
3
|
+
CORE_INTELLIGENCE_ANSWER: 'core-intelligence-answer',
|
|
4
|
+
} as const
|
|
5
|
+
|
|
6
|
+
const values = Object.values(DEFAULT_WIDGET_RENDERERS)
|
|
7
|
+
export const DEFAULT_WIDGET_RENDERER_IDS = new Set<string>(values)
|
|
8
|
+
|
|
9
|
+
export function isDefaultWidgetRenderer(id: string | undefined): boolean {
|
|
10
|
+
return Boolean(id) && DEFAULT_WIDGET_RENDERER_IDS.has(id!)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WidgetRegistrationPayload {
|
|
14
|
+
widgetId: string
|
|
15
|
+
pluginName: string
|
|
16
|
+
featureId: string
|
|
17
|
+
filePath: string
|
|
18
|
+
code: string
|
|
19
|
+
styles: string
|
|
20
|
+
hash: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function makeWidgetId(pluginName: string, featureId: string): string {
|
|
24
|
+
return `${pluginName}::${featureId}`
|
|
25
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { IStorageChannel } from './base-storage'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Storage subscription callback type
|
|
5
|
+
*/
|
|
6
|
+
export type StorageSubscriptionCallback = (data: any) => void
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Storage subscription manager for renderer process
|
|
10
|
+
* Provides easy subscription to storage updates via channel events
|
|
11
|
+
*/
|
|
12
|
+
class StorageSubscriptionManager {
|
|
13
|
+
private channel: IStorageChannel | null = null
|
|
14
|
+
private subscribers = new Map<string, Set<StorageSubscriptionCallback>>()
|
|
15
|
+
private channelListenerRegistered = false
|
|
16
|
+
private pendingUpdates = new Map<string, NodeJS.Timeout>()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the subscription manager with a channel
|
|
20
|
+
*/
|
|
21
|
+
init(channel: IStorageChannel): void {
|
|
22
|
+
this.channel = channel
|
|
23
|
+
|
|
24
|
+
if (!this.channelListenerRegistered) {
|
|
25
|
+
// Listen to storage:update events from main process
|
|
26
|
+
this.channel.regChannel('storage:update', ({ data }) => {
|
|
27
|
+
const { name } = data as { name: string }
|
|
28
|
+
this.handleStorageUpdate(name)
|
|
29
|
+
})
|
|
30
|
+
this.channelListenerRegistered = true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to storage changes for a specific config
|
|
36
|
+
* @param configName - The configuration file name (e.g., 'app-setting.ini')
|
|
37
|
+
* @param callback - Callback function to receive updates
|
|
38
|
+
* @returns Unsubscribe function
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const unsubscribe = subscribeStorage('app-setting.ini', (data) => {
|
|
43
|
+
* console.log('Config updated:', data)
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
46
|
+
* // Later:
|
|
47
|
+
* unsubscribe()
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
subscribe(configName: string, callback: StorageSubscriptionCallback): () => void {
|
|
51
|
+
if (!this.subscribers.has(configName)) {
|
|
52
|
+
this.subscribers.set(configName, new Set())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.subscribers.get(configName)!.add(callback)
|
|
56
|
+
|
|
57
|
+
// Immediately load and call with current data
|
|
58
|
+
if (this.channel) {
|
|
59
|
+
const currentData = this.channel.sendSync('storage:get', configName)
|
|
60
|
+
if (currentData) {
|
|
61
|
+
try {
|
|
62
|
+
callback(currentData)
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(`[StorageSubscription] Callback error for "${configName}":`, error)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Return unsubscribe function
|
|
71
|
+
return () => {
|
|
72
|
+
this.unsubscribe(configName, callback)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Unsubscribe from storage changes
|
|
78
|
+
* @param configName - The configuration file name
|
|
79
|
+
* @param callback - The same callback function used in subscribe
|
|
80
|
+
*/
|
|
81
|
+
unsubscribe(configName: string, callback: StorageSubscriptionCallback): void {
|
|
82
|
+
const callbacks = this.subscribers.get(configName)
|
|
83
|
+
if (callbacks) {
|
|
84
|
+
callbacks.delete(callback)
|
|
85
|
+
if (callbacks.size === 0) {
|
|
86
|
+
this.subscribers.delete(configName)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle storage update events from main process
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
private async handleStorageUpdate(configName: string): Promise<void> {
|
|
96
|
+
const callbacks = this.subscribers.get(configName)
|
|
97
|
+
if (!callbacks || callbacks.size === 0) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!this.channel) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Debounce updates to avoid excessive IPC and callback invocations
|
|
106
|
+
const existing = this.pendingUpdates.get(configName)
|
|
107
|
+
if (existing) {
|
|
108
|
+
clearTimeout(existing)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const timer = setTimeout(async () => {
|
|
112
|
+
// Fetch latest data
|
|
113
|
+
const data = await this.channel!.send('storage:get', configName)
|
|
114
|
+
if (!data) {
|
|
115
|
+
this.pendingUpdates.delete(configName)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Notify all subscribers
|
|
120
|
+
callbacks.forEach((callback) => {
|
|
121
|
+
try {
|
|
122
|
+
callback(data)
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error(`[StorageSubscription] Callback error for "${configName}":`, error)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
this.pendingUpdates.delete(configName)
|
|
130
|
+
}, 50) // 50ms debounce window
|
|
131
|
+
|
|
132
|
+
this.pendingUpdates.set(configName, timer)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the number of active subscriptions for a config
|
|
137
|
+
* @param configName - The configuration file name
|
|
138
|
+
* @returns Number of active callbacks subscribed to this config
|
|
139
|
+
*/
|
|
140
|
+
getSubscriberCount(configName: string): number {
|
|
141
|
+
return this.subscribers.get(configName)?.size ?? 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clear all subscriptions
|
|
146
|
+
*/
|
|
147
|
+
clear(): void {
|
|
148
|
+
this.subscribers.clear()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Global singleton instance
|
|
153
|
+
const subscriptionManager = new StorageSubscriptionManager()
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Initialize storage subscription system with channel
|
|
157
|
+
* Must be called before using subscribeStorage
|
|
158
|
+
*
|
|
159
|
+
* @param channel - The storage channel
|
|
160
|
+
*/
|
|
161
|
+
export function initStorageSubscription(channel: IStorageChannel): void {
|
|
162
|
+
subscriptionManager.init(channel)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Subscribe to storage configuration changes
|
|
167
|
+
*
|
|
168
|
+
* @param configName - Configuration file name (e.g., 'app-setting.ini')
|
|
169
|
+
* @param callback - Callback function that receives updated data
|
|
170
|
+
* @returns Unsubscribe function
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* import { subscribeStorage } from '@talex-touch/utils/renderer/storage/storage-subscription'
|
|
175
|
+
*
|
|
176
|
+
* const unsubscribe = subscribeStorage('app-setting.ini', (data) => {
|
|
177
|
+
* console.log('Settings updated:', data)
|
|
178
|
+
* })
|
|
179
|
+
*
|
|
180
|
+
* // Clean up when no longer needed
|
|
181
|
+
* unsubscribe()
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export function subscribeStorage(
|
|
185
|
+
configName: string,
|
|
186
|
+
callback: StorageSubscriptionCallback,
|
|
187
|
+
): () => void {
|
|
188
|
+
return subscriptionManager.subscribe(configName, callback)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get subscription manager instance (for debugging)
|
|
193
|
+
*/
|
|
194
|
+
export function getSubscriptionManager(): StorageSubscriptionManager {
|
|
195
|
+
return subscriptionManager
|
|
196
|
+
}
|
package/types/icon.ts
CHANGED
|
@@ -37,6 +37,13 @@ export interface ITuffIcon {
|
|
|
37
37
|
/** Icon value */
|
|
38
38
|
value: string
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Icon Colorful (Only for URL type)
|
|
42
|
+
* @desc This prop defines whether a URL icon should be rendered in colorful mode.
|
|
43
|
+
* It is only applicable when the icon type is 'url'.
|
|
44
|
+
*/
|
|
45
|
+
colorful?: boolean
|
|
46
|
+
|
|
40
47
|
/** Icon status (optional) */
|
|
41
48
|
status?: TuffIconStatus
|
|
42
49
|
|