@talex-touch/utils 1.0.30 → 1.0.31
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/animation/window-node.ts +202 -0
- package/auth/useAuthState.ts +1 -1
- package/auth/useClerkConfig.ts +2 -2
- package/channel/index.ts +6 -0
- package/common/file-scan-utils.ts +4 -1
- package/common/storage/entity/app-settings.ts +14 -0
- package/common/utils/file.ts +4 -1
- package/common/utils/polling.ts +51 -3
- package/core-box/builder/tuff-builder.ts +1 -1
- package/core-box/tuff/tuff-dsl.ts +106 -2
- package/index.ts +1 -1
- package/package.json +1 -1
- package/plugin/index.ts +19 -4
- package/plugin/preload.ts +5 -2
- package/plugin/sdk/index.ts +1 -6
- package/plugin/sdk/types.ts +21 -1
- package/renderer/index.ts +1 -0
- package/renderer/storage/base-storage.ts +52 -12
- package/search/types.ts +3 -4
- package/types/icon.ts +2 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { TalexTouch } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Window animation controller return type
|
|
5
|
+
*/
|
|
6
|
+
export interface WindowAnimationController {
|
|
7
|
+
/**
|
|
8
|
+
* Update window height with animation
|
|
9
|
+
* @param newHeight - The new height to animate to
|
|
10
|
+
* @param duration - Animation duration in seconds (default: 0.5)
|
|
11
|
+
* @returns Promise that resolves to true if animation completed successfully, false if interrupted
|
|
12
|
+
*/
|
|
13
|
+
updateHeight: (newHeight: number, duration?: number) => Promise<boolean>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cancel current animation
|
|
17
|
+
* @returns Promise that resolves to true if there was an animation to cancel, false otherwise
|
|
18
|
+
*/
|
|
19
|
+
cancel: () => Promise<boolean>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Toggle window visibility
|
|
23
|
+
* @param visible - Optional parameter to explicitly set visibility state
|
|
24
|
+
* @returns Promise that resolves to true if operation completed successfully, false otherwise
|
|
25
|
+
*/
|
|
26
|
+
toggleWindow: (visible?: boolean) => Promise<boolean>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Change current window instance
|
|
30
|
+
* @param newWindow - New TouchWindow instance
|
|
31
|
+
*/
|
|
32
|
+
changeWindow: (newWindow: TalexTouch.ITouchWindow) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tracks the state of an animation
|
|
37
|
+
*/
|
|
38
|
+
interface AnimationState {
|
|
39
|
+
intervalId: NodeJS.Timeout | null
|
|
40
|
+
completed: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Simple easing function for smooth animation
|
|
45
|
+
*/
|
|
46
|
+
function easeInOutCubic(t: number): number {
|
|
47
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Use native Node.js animation for window (main process only)
|
|
52
|
+
* @param window - TouchWindow instance (optional, can be set later with changeWindow)
|
|
53
|
+
* @returns WindowAnimationController with updateHeight, cancel, toggleWindow, and changeWindow methods
|
|
54
|
+
*/
|
|
55
|
+
export function useWindowAnimation(window?: TalexTouch.ITouchWindow): WindowAnimationController {
|
|
56
|
+
// Store current window reference inside the function scope
|
|
57
|
+
let currentWindow: TalexTouch.ITouchWindow | null = window || null
|
|
58
|
+
|
|
59
|
+
const animationState: AnimationState = {
|
|
60
|
+
intervalId: null,
|
|
61
|
+
completed: false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if current window is valid
|
|
66
|
+
* @returns true if window is valid, false otherwise
|
|
67
|
+
*/
|
|
68
|
+
const isWindowValid = (): boolean => {
|
|
69
|
+
return (
|
|
70
|
+
currentWindow !== null &&
|
|
71
|
+
currentWindow.window !== null &&
|
|
72
|
+
!currentWindow.window.isDestroyed()
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get current window with validation
|
|
78
|
+
* @returns current window or throws error if invalid
|
|
79
|
+
*/
|
|
80
|
+
const getCurrentWindow = (): TalexTouch.ITouchWindow => {
|
|
81
|
+
if (!isWindowValid()) {
|
|
82
|
+
throw new Error('Window is not valid or has been destroyed')
|
|
83
|
+
}
|
|
84
|
+
return currentWindow!
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const updateHeight = async (newHeight: number, duration: number = 0.5): Promise<boolean> => {
|
|
88
|
+
try {
|
|
89
|
+
const window = getCurrentWindow()
|
|
90
|
+
|
|
91
|
+
// Cancel any existing animation
|
|
92
|
+
if (animationState.intervalId) {
|
|
93
|
+
clearInterval(animationState.intervalId)
|
|
94
|
+
animationState.intervalId = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Reset state for new animation
|
|
98
|
+
animationState.completed = false
|
|
99
|
+
|
|
100
|
+
const browserWindow = window.window
|
|
101
|
+
const [currentWidth, currentHeight] = browserWindow.getSize()
|
|
102
|
+
const [x, y] = browserWindow.getPosition()
|
|
103
|
+
|
|
104
|
+
const startHeight = currentHeight
|
|
105
|
+
const heightDelta = newHeight - startHeight
|
|
106
|
+
const startTime = Date.now()
|
|
107
|
+
const durationMs = duration * 1000
|
|
108
|
+
|
|
109
|
+
return new Promise<boolean>((resolve) => {
|
|
110
|
+
animationState.intervalId = setInterval(() => {
|
|
111
|
+
// Check if window is still valid
|
|
112
|
+
if (!isWindowValid()) {
|
|
113
|
+
if (animationState.intervalId) {
|
|
114
|
+
clearInterval(animationState.intervalId)
|
|
115
|
+
animationState.intervalId = null
|
|
116
|
+
}
|
|
117
|
+
resolve(false)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const elapsed = Date.now() - startTime
|
|
122
|
+
const progress = Math.min(elapsed / durationMs, 1)
|
|
123
|
+
const easedProgress = easeInOutCubic(progress)
|
|
124
|
+
const animatedHeight = Math.round(startHeight + heightDelta * easedProgress)
|
|
125
|
+
|
|
126
|
+
browserWindow.setSize(currentWidth, animatedHeight)
|
|
127
|
+
browserWindow.setPosition(x, y)
|
|
128
|
+
|
|
129
|
+
if (progress >= 1) {
|
|
130
|
+
if (animationState.intervalId) {
|
|
131
|
+
clearInterval(animationState.intervalId)
|
|
132
|
+
animationState.intervalId = null
|
|
133
|
+
}
|
|
134
|
+
animationState.completed = true
|
|
135
|
+
resolve(true)
|
|
136
|
+
}
|
|
137
|
+
}, 16) // ~60fps
|
|
138
|
+
})
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Error in updateHeight:', error)
|
|
141
|
+
return Promise.resolve(false)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cancel = async (): Promise<boolean> => {
|
|
146
|
+
if (animationState.intervalId) {
|
|
147
|
+
clearInterval(animationState.intervalId)
|
|
148
|
+
animationState.intervalId = null
|
|
149
|
+
return Promise.resolve(true)
|
|
150
|
+
}
|
|
151
|
+
return Promise.resolve(false)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const toggleWindow = async (visible?: boolean): Promise<boolean> => {
|
|
155
|
+
try {
|
|
156
|
+
const window = getCurrentWindow()
|
|
157
|
+
const browserWindow = window.window
|
|
158
|
+
|
|
159
|
+
// Determine target visibility state
|
|
160
|
+
const targetVisible = visible !== undefined ? visible : !browserWindow.isVisible()
|
|
161
|
+
|
|
162
|
+
if (targetVisible) {
|
|
163
|
+
// Show window
|
|
164
|
+
browserWindow.show()
|
|
165
|
+
} else {
|
|
166
|
+
// Hide window
|
|
167
|
+
if (process.platform === 'darwin') {
|
|
168
|
+
// On macOS, we can simply hide the window
|
|
169
|
+
browserWindow.hide()
|
|
170
|
+
} else {
|
|
171
|
+
// On other platforms, move window far off-screen before hiding
|
|
172
|
+
browserWindow.setPosition(-100000, -100000)
|
|
173
|
+
browserWindow.hide()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Promise.resolve(true)
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error in toggleWindow:', error)
|
|
180
|
+
return Promise.resolve(false)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const changeWindow = (newWindow: TalexTouch.ITouchWindow): void => {
|
|
185
|
+
// Cancel any ongoing animation
|
|
186
|
+
if (animationState.intervalId) {
|
|
187
|
+
clearInterval(animationState.intervalId)
|
|
188
|
+
animationState.intervalId = null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Set new window
|
|
192
|
+
currentWindow = newWindow
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
updateHeight,
|
|
197
|
+
cancel,
|
|
198
|
+
toggleWindow,
|
|
199
|
+
changeWindow
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
package/auth/useAuthState.ts
CHANGED
package/auth/useClerkConfig.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ClerkConfig } from "./clerk-types"
|
|
2
2
|
|
|
3
|
-
const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
|
|
4
|
-
const clerkDomain = import.meta.env.VITE_CLERK_DOMAIN
|
|
3
|
+
const clerkPublishableKey = (import.meta.env as any).VITE_CLERK_PUBLISHABLE_KEY
|
|
4
|
+
const clerkDomain = (import.meta.env as any).VITE_CLERK_DOMAIN
|
|
5
5
|
|
|
6
6
|
if (!clerkPublishableKey?.length) {
|
|
7
7
|
throw new Error('VITE_CLERK_PUBLISHABLE_KEY is not set')
|
package/channel/index.ts
CHANGED
|
@@ -175,3 +175,9 @@ export interface StandardChannelData extends RawStandardChannelData {
|
|
|
175
175
|
export type IChannelData = any //boolean | number | string | null | undefined | {
|
|
176
176
|
// [prop: string]: any
|
|
177
177
|
// }
|
|
178
|
+
|
|
179
|
+
// Default export for Vite compatibility (only values, not types)
|
|
180
|
+
export default {
|
|
181
|
+
ChannelType,
|
|
182
|
+
DataCode
|
|
183
|
+
}
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* @version 1.0.0
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
|
+
const path = typeof window === 'undefined'
|
|
12
|
+
? require('path')
|
|
13
|
+
: require('path-browserify')
|
|
11
14
|
import {
|
|
12
15
|
type FileScanOptions,
|
|
13
16
|
DEFAULT_SCAN_OPTIONS,
|
|
@@ -39,6 +39,20 @@ const _appSettingOriginData = {
|
|
|
39
39
|
searchEngine: {
|
|
40
40
|
logsEnabled: false,
|
|
41
41
|
},
|
|
42
|
+
window: {
|
|
43
|
+
closeToTray: true,
|
|
44
|
+
startMinimized: false,
|
|
45
|
+
startSilent: false,
|
|
46
|
+
},
|
|
47
|
+
setup: {
|
|
48
|
+
accessibility: false,
|
|
49
|
+
notifications: false,
|
|
50
|
+
autoStart: false,
|
|
51
|
+
showTray: true,
|
|
52
|
+
adminPrivileges: false,
|
|
53
|
+
hideDock: false,
|
|
54
|
+
},
|
|
55
|
+
layout: 'simple',
|
|
42
56
|
};
|
|
43
57
|
|
|
44
58
|
export const appSettingOriginData = Object.freeze(_appSettingOriginData)
|
package/common/utils/file.ts
CHANGED
package/common/utils/polling.ts
CHANGED
|
@@ -19,6 +19,7 @@ export class PollingService {
|
|
|
19
19
|
private tasks = new Map<string, PollingTask>();
|
|
20
20
|
private timerId: NodeJS.Timeout | null = null;
|
|
21
21
|
private isRunning = false;
|
|
22
|
+
private quitListenerCleanup?: () => void;
|
|
22
23
|
|
|
23
24
|
private constructor() {
|
|
24
25
|
// Private constructor to enforce singleton pattern
|
|
@@ -111,17 +112,53 @@ export class PollingService {
|
|
|
111
112
|
*/
|
|
112
113
|
public start(): void {
|
|
113
114
|
if (this.isRunning) {
|
|
115
|
+
console.warn('[PollingService] Already running, skipping start.');
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
this.isRunning = true;
|
|
117
|
-
console.
|
|
119
|
+
console.log('[PollingService] Polling service started');
|
|
120
|
+
this._setupQuitListener();
|
|
118
121
|
this._reschedule();
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Sets up Electron app quit listener if running in Electron environment
|
|
126
|
+
* Uses lazy resolution to avoid hard dependency on Electron
|
|
127
|
+
*/
|
|
128
|
+
private _setupQuitListener(): void {
|
|
129
|
+
// Check if we're in Electron environment
|
|
130
|
+
try {
|
|
131
|
+
// Use dynamic require to avoid hard dependency on Electron
|
|
132
|
+
// Similar to the approach used in packages/utils/plugin/channel.ts
|
|
133
|
+
const electron = (globalThis as any)?.electron ??
|
|
134
|
+
(typeof require !== 'undefined' ? require('electron') : null);
|
|
135
|
+
|
|
136
|
+
if (electron?.app) {
|
|
137
|
+
const app = electron.app;
|
|
138
|
+
|
|
139
|
+
// Listen to before-quit event
|
|
140
|
+
const quitHandler = () => {
|
|
141
|
+
this.stop('app quit');
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
app.on('before-quit', quitHandler);
|
|
145
|
+
|
|
146
|
+
// Store cleanup function
|
|
147
|
+
this.quitListenerCleanup = () => {
|
|
148
|
+
app.removeListener('before-quit', quitHandler);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// Not in Electron environment or Electron not available
|
|
153
|
+
// This is fine, just skip the quit listener setup
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
121
157
|
/**
|
|
122
158
|
* Stops the polling service and clears all scheduled tasks.
|
|
159
|
+
* @param reason - Optional reason for stopping the service (for logging purposes)
|
|
123
160
|
*/
|
|
124
|
-
public stop(): void {
|
|
161
|
+
public stop(reason?: string): void {
|
|
125
162
|
if (!this.isRunning) {
|
|
126
163
|
return;
|
|
127
164
|
}
|
|
@@ -130,7 +167,18 @@ export class PollingService {
|
|
|
130
167
|
clearTimeout(this.timerId);
|
|
131
168
|
this.timerId = null;
|
|
132
169
|
}
|
|
133
|
-
|
|
170
|
+
|
|
171
|
+
// Clean up quit listener
|
|
172
|
+
if (this.quitListenerCleanup) {
|
|
173
|
+
this.quitListenerCleanup();
|
|
174
|
+
this.quitListenerCleanup = undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (reason) {
|
|
178
|
+
console.log(`[PollingService] Stopping polling service: ${reason}`);
|
|
179
|
+
} else {
|
|
180
|
+
console.log('[PollingService] Polling service stopped');
|
|
181
|
+
}
|
|
134
182
|
}
|
|
135
183
|
|
|
136
184
|
private _reschedule(): void {
|
|
@@ -972,7 +972,7 @@ class TuffUtils {
|
|
|
972
972
|
* @param type - 图标类型
|
|
973
973
|
* @returns {TuffIcon} 创建的图标对象
|
|
974
974
|
*/
|
|
975
|
-
static createIcon(value: string, type: 'emoji' | 'url' | '
|
|
975
|
+
static createIcon(value: string, type: 'emoji' | 'url' | 'file' = 'emoji'): TuffIcon {
|
|
976
976
|
return {
|
|
977
977
|
type,
|
|
978
978
|
value
|
|
@@ -1041,18 +1041,88 @@ export interface TuffDisplayAction extends Omit<TuffAction, 'payload'> {
|
|
|
1041
1041
|
|
|
1042
1042
|
// ==================== 工具类型 ====================
|
|
1043
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* 查询输入类型枚举
|
|
1046
|
+
*
|
|
1047
|
+
* @description
|
|
1048
|
+
* 定义系统支持的所有输入类型。
|
|
1049
|
+
* 用于插件、providers 和 features 声明其支持的输入类型。
|
|
1050
|
+
*/
|
|
1051
|
+
export enum TuffInputType {
|
|
1052
|
+
/** 纯文本输入 */
|
|
1053
|
+
Text = 'text',
|
|
1054
|
+
/** 图像输入(data URL 格式) */
|
|
1055
|
+
Image = 'image',
|
|
1056
|
+
/** 文件输入(文件路径数组) */
|
|
1057
|
+
Files = 'files',
|
|
1058
|
+
/** 富文本输入(HTML 格式) */
|
|
1059
|
+
Html = 'html'
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* 查询输入项
|
|
1064
|
+
*
|
|
1065
|
+
* @description
|
|
1066
|
+
* 定义查询中除文本外的多种类型输入(如剪贴板数据)。
|
|
1067
|
+
* 用于支持图像、文件、富文本等多媒体输入。
|
|
1068
|
+
*/
|
|
1069
|
+
export interface TuffQueryInput {
|
|
1070
|
+
/**
|
|
1071
|
+
* 输入类型
|
|
1072
|
+
* @description 定义输入数据的类型
|
|
1073
|
+
* @required
|
|
1074
|
+
*/
|
|
1075
|
+
type: TuffInputType;
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* 输入内容
|
|
1079
|
+
* @description 根据类型不同包含不同格式的数据:
|
|
1080
|
+
* - text: 纯文本字符串
|
|
1081
|
+
* - image: data URL 格式的图像数据
|
|
1082
|
+
* - files: JSON 序列化的文件路径数组
|
|
1083
|
+
* - html: HTML 格式的富文本
|
|
1084
|
+
* @required
|
|
1085
|
+
*/
|
|
1086
|
+
content: string;
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* 原始内容
|
|
1090
|
+
* @description 可选的原始格式内容,如富文本的 HTML 源码
|
|
1091
|
+
*/
|
|
1092
|
+
rawContent?: string;
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* 缩略图
|
|
1096
|
+
* @description 图像的缩略图 data URL(用于预览)
|
|
1097
|
+
*/
|
|
1098
|
+
thumbnail?: string;
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* 元数据
|
|
1102
|
+
* @description 附加的元数据信息
|
|
1103
|
+
*/
|
|
1104
|
+
metadata?: Record<string, any>;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1044
1107
|
/**
|
|
1045
1108
|
* 搜索查询结构
|
|
1046
1109
|
*
|
|
1047
1110
|
* @description
|
|
1048
1111
|
* 定义搜索请求的参数和过滤条件。
|
|
1049
1112
|
* 系统根据这些参数执行搜索并返回匹配结果。
|
|
1113
|
+
*
|
|
1114
|
+
* **重要区分**:
|
|
1115
|
+
* - `text`: 用户在输入框中主动输入的查询文本
|
|
1116
|
+
* - `inputs`: 来自剪贴板或其他来源的附加输入数据(图像、文件、富文本等)
|
|
1050
1117
|
*/
|
|
1051
1118
|
export interface TuffQuery {
|
|
1052
1119
|
/**
|
|
1053
|
-
*
|
|
1054
|
-
* @description
|
|
1120
|
+
* 用户输入的查询文本
|
|
1121
|
+
* @description 这是用户在搜索框中主动输入的文本,不包括剪贴板内容
|
|
1055
1122
|
* @required
|
|
1123
|
+
*
|
|
1124
|
+
* @example
|
|
1125
|
+
* 用户输入 "translate" → text = "translate"
|
|
1056
1126
|
*/
|
|
1057
1127
|
text: string;
|
|
1058
1128
|
|
|
@@ -1062,6 +1132,31 @@ export interface TuffQuery {
|
|
|
1062
1132
|
*/
|
|
1063
1133
|
type?: 'text' | 'voice' | 'image';
|
|
1064
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* 多类型输入(附加数据)
|
|
1137
|
+
* @description 除了用户输入的文本外的其他输入数据(如剪贴板中的图像、文件、富文本等)
|
|
1138
|
+
*
|
|
1139
|
+
* **与 text 的区别**:
|
|
1140
|
+
* - `text`: 用户主动输入,总是存在
|
|
1141
|
+
* - `inputs`: 系统自动检测的附加数据,可能为空
|
|
1142
|
+
*
|
|
1143
|
+
* 当用户触发 feature 时,系统会自动检测剪贴板并填充此字段。
|
|
1144
|
+
*
|
|
1145
|
+
* @example
|
|
1146
|
+
* 场景 1: 用户输入 "translate" + 剪贴板有图片
|
|
1147
|
+
* text: "translate"
|
|
1148
|
+
* inputs: [{ type: 'image', content: 'data:image/png;base64,...' }]
|
|
1149
|
+
*
|
|
1150
|
+
* 场景 2: 用户输入 "compress" + 剪贴板有文件
|
|
1151
|
+
* text: "compress"
|
|
1152
|
+
* inputs: [{ type: 'files', content: '["/path/to/file"]' }]
|
|
1153
|
+
*
|
|
1154
|
+
* 场景 3: 用户输入 "format" + 剪贴板有富文本
|
|
1155
|
+
* text: "format"
|
|
1156
|
+
* inputs: [{ type: 'html', content: 'plain text', rawContent: '<p>html</p>' }]
|
|
1157
|
+
*/
|
|
1158
|
+
inputs?: TuffQueryInput[];
|
|
1159
|
+
|
|
1065
1160
|
/**
|
|
1066
1161
|
* 过滤条件
|
|
1067
1162
|
* @description 限制搜索范围的过滤器
|
|
@@ -1237,6 +1332,15 @@ export interface ISearchProvider<C> {
|
|
|
1237
1332
|
*/
|
|
1238
1333
|
readonly icon?: any
|
|
1239
1334
|
|
|
1335
|
+
/**
|
|
1336
|
+
* Supported input types
|
|
1337
|
+
* @description Declares which types of inputs this provider can handle.
|
|
1338
|
+
* If not specified, defaults to [TuffInputType.Text] only.
|
|
1339
|
+
* When query contains non-text inputs, only providers supporting those types will be searched.
|
|
1340
|
+
* @example [TuffInputType.Text, TuffInputType.Image, TuffInputType.Files]
|
|
1341
|
+
*/
|
|
1342
|
+
readonly supportedInputTypes?: TuffInputType[]
|
|
1343
|
+
|
|
1240
1344
|
/**
|
|
1241
1345
|
* Core search method (PULL mode).
|
|
1242
1346
|
* The engine calls this method to get results from the provider.
|
package/index.ts
CHANGED
package/package.json
CHANGED
package/plugin/index.ts
CHANGED
|
@@ -150,6 +150,15 @@ export interface IPluginFeature {
|
|
|
150
150
|
* Default is 0
|
|
151
151
|
*/
|
|
152
152
|
priority?: number
|
|
153
|
+
/**
|
|
154
|
+
* Accepted input types for this feature
|
|
155
|
+
* @description Declares which types of inputs this feature can accept and process.
|
|
156
|
+
* If not specified, defaults to ['text'] only (backward compatible).
|
|
157
|
+
* When query contains inputs, only features accepting those input types will be shown.
|
|
158
|
+
* @example ['text', 'image'] - Feature accepts both text and images
|
|
159
|
+
* @example ['image', 'files'] - Feature only accepts images and files (no text-only queries)
|
|
160
|
+
*/
|
|
161
|
+
acceptedInputTypes?: Array<'text' | 'image' | 'files' | 'html'>
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
export type IFeatureInteraction = {
|
|
@@ -187,11 +196,14 @@ export interface IFeatureLifeCycle {
|
|
|
187
196
|
/**
|
|
188
197
|
* Called when a feature is triggered via a matching command.
|
|
189
198
|
* @param id - Feature ID
|
|
190
|
-
* @param data - The triggering payload
|
|
199
|
+
* @param data - The triggering payload. Can be:
|
|
200
|
+
* - string: Plain text query (backward compatible)
|
|
201
|
+
* - TuffQuery object: Complete query with text and optional inputs array containing clipboard data (images, files, HTML)
|
|
191
202
|
* @param feature - The full feature definition
|
|
192
203
|
* @param signal - An AbortSignal to cancel the operation
|
|
204
|
+
* @returns If returns false, the feature will not enter activation state (e.g., just opens browser and exits)
|
|
193
205
|
*/
|
|
194
|
-
onFeatureTriggered(id: string, data: any, feature: IPluginFeature, signal?: AbortSignal): void
|
|
206
|
+
onFeatureTriggered(id: string, data: any, feature: IPluginFeature, signal?: AbortSignal): boolean | void
|
|
195
207
|
|
|
196
208
|
/**
|
|
197
209
|
* Called when user input changes within this feature’s input box.
|
|
@@ -253,10 +265,13 @@ export interface ITargetFeatureLifeCycle {
|
|
|
253
265
|
|
|
254
266
|
/**
|
|
255
267
|
* Called when the feature is triggered via a matching command.
|
|
256
|
-
* @param data - The triggering payload
|
|
268
|
+
* @param data - The triggering payload. Can be:
|
|
269
|
+
* - string: Plain text query (backward compatible)
|
|
270
|
+
* - TuffQuery object: Complete query with text and optional inputs array containing clipboard data (images, files, HTML)
|
|
257
271
|
* @param feature - The full feature definition
|
|
272
|
+
* @returns If returns false, the feature will not enter activation state (e.g., just opens browser and exits)
|
|
258
273
|
*/
|
|
259
|
-
onFeatureTriggered(data: any, feature: IPluginFeature): void
|
|
274
|
+
onFeatureTriggered(data: any, feature: IPluginFeature): boolean | void
|
|
260
275
|
|
|
261
276
|
/**
|
|
262
277
|
* Called when user input changes within this feature’s input box.
|
package/plugin/preload.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ITouchClientChannel } from '../channel'
|
|
2
|
+
import type { ITouchSDK } from './sdk/index'
|
|
3
|
+
// Import SDK for side effects (initializes hooks)
|
|
2
4
|
import './sdk/index'
|
|
3
5
|
|
|
4
|
-
// window type
|
|
6
|
+
// window type - includes both plugin preload types and SDK types
|
|
5
7
|
declare global {
|
|
6
|
-
|
|
8
|
+
interface Window {
|
|
7
9
|
$plugin: {
|
|
8
10
|
name: string
|
|
9
11
|
path: Object
|
|
@@ -13,6 +15,7 @@ declare global {
|
|
|
13
15
|
$config: {
|
|
14
16
|
themeStyle: any
|
|
15
17
|
}
|
|
18
|
+
$touchSDK: ITouchSDK
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
|
package/plugin/sdk/index.ts
CHANGED
|
@@ -5,12 +5,7 @@ export interface ITouchSDK {
|
|
|
5
5
|
__hooks: {}
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
declare global {
|
|
10
|
-
export interface Window {
|
|
11
|
-
$touchSDK: ITouchSDK
|
|
12
|
-
}
|
|
13
|
-
}
|
|
8
|
+
// Note: Window.$touchSDK is declared in ../preload.ts to avoid duplicate declarations
|
|
14
9
|
|
|
15
10
|
export * from './types'
|
|
16
11
|
export * from './window/index'
|
package/plugin/sdk/types.ts
CHANGED
|
@@ -518,9 +518,29 @@ export interface IPluginLifecycle {
|
|
|
518
518
|
/**
|
|
519
519
|
* Called when a plugin feature is triggered
|
|
520
520
|
* @param featureId - The ID of the triggered feature
|
|
521
|
-
* @param query - The search query or input data
|
|
521
|
+
* @param query - The search query or input data. Can be:
|
|
522
|
+
* - string: Plain text query (backward compatible)
|
|
523
|
+
* - TuffQuery object: Complete query with text and optional inputs array
|
|
524
|
+
* - query.text: The text query string
|
|
525
|
+
* - query.inputs: Array of TuffQueryInput objects (images, files, HTML)
|
|
522
526
|
* @param feature - The feature configuration object
|
|
523
527
|
* @returns Promise or void
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* onFeatureTriggered(featureId, query, feature) {
|
|
531
|
+
* if (typeof query === 'string') {
|
|
532
|
+
* // Backward compatible: plain text query
|
|
533
|
+
* console.log('Text query:', query)
|
|
534
|
+
* } else {
|
|
535
|
+
* // New: complete query object
|
|
536
|
+
* console.log('Text:', query.text)
|
|
537
|
+
* const imageInput = query.inputs?.find(i => i.type === 'image')
|
|
538
|
+
* if (imageInput) {
|
|
539
|
+
* console.log('Image data:', imageInput.content)
|
|
540
|
+
* }
|
|
541
|
+
* }
|
|
542
|
+
* }
|
|
543
|
+
* ```
|
|
524
544
|
*/
|
|
525
545
|
onFeatureTriggered(featureId: string, query: any, feature: any): Promise<void> | void;
|
|
526
546
|
|
package/renderer/index.ts
CHANGED
|
@@ -29,8 +29,14 @@ export interface IStorageChannel extends ITouchClientChannel {
|
|
|
29
29
|
|
|
30
30
|
let channel: IStorageChannel | null = null;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Queue of initialization callbacks waiting for channel initialization
|
|
34
|
+
*/
|
|
35
|
+
const pendingInitializations: Array<() => void> = [];
|
|
36
|
+
|
|
32
37
|
/**
|
|
33
38
|
* Initializes the global channel for communication.
|
|
39
|
+
* Processes all pending storage initializations after initialization.
|
|
34
40
|
*
|
|
35
41
|
* @example
|
|
36
42
|
* ```ts
|
|
@@ -45,6 +51,14 @@ let channel: IStorageChannel | null = null;
|
|
|
45
51
|
*/
|
|
46
52
|
export function initStorageChannel(c: IStorageChannel): void {
|
|
47
53
|
channel = c;
|
|
54
|
+
|
|
55
|
+
// Process all pending storage initializations
|
|
56
|
+
for (const initFn of pendingInitializations) {
|
|
57
|
+
initFn();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Clear the queue
|
|
61
|
+
pendingInitializations.length = 0;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -64,6 +78,7 @@ export class TouchStorage<T extends object> {
|
|
|
64
78
|
#assigning = false;
|
|
65
79
|
readonly originalData: T;
|
|
66
80
|
private readonly _onUpdate: Array<() => void> = [];
|
|
81
|
+
#channelInitialized = false;
|
|
67
82
|
|
|
68
83
|
/**
|
|
69
84
|
* The reactive data exposed to users.
|
|
@@ -72,6 +87,7 @@ export class TouchStorage<T extends object> {
|
|
|
72
87
|
|
|
73
88
|
/**
|
|
74
89
|
* Creates a new reactive storage instance.
|
|
90
|
+
* If channel is not initialized, the instance will be queued for initialization.
|
|
75
91
|
*
|
|
76
92
|
* @param qName Globally unique name for the instance
|
|
77
93
|
* @param initData Initial data to populate the storage
|
|
@@ -86,30 +102,52 @@ export class TouchStorage<T extends object> {
|
|
|
86
102
|
if (storages.has(qName)) {
|
|
87
103
|
throw new Error(`Storage "${qName}" already exists`);
|
|
88
104
|
}
|
|
105
|
+
|
|
106
|
+
this.#qualifiedName = qName;
|
|
107
|
+
this.originalData = initData;
|
|
108
|
+
this.data = reactive({ ...initData }) as UnwrapNestedRefs<T>;
|
|
109
|
+
|
|
110
|
+
if (onUpdate) this._onUpdate.push(onUpdate);
|
|
111
|
+
|
|
112
|
+
// Register to storages map immediately
|
|
113
|
+
storages.set(qName, this);
|
|
114
|
+
|
|
115
|
+
// Initialize channel-dependent operations
|
|
116
|
+
if (channel) {
|
|
117
|
+
this.#initializeChannel();
|
|
118
|
+
} else {
|
|
119
|
+
// Queue initialization callback for later
|
|
120
|
+
pendingInitializations.push(() => this.#initializeChannel());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Initialize channel-dependent operations
|
|
126
|
+
*/
|
|
127
|
+
#initializeChannel(): void {
|
|
128
|
+
if (this.#channelInitialized) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
89
132
|
if (!channel) {
|
|
90
133
|
throw new Error(
|
|
91
134
|
'TouchStorage: channel is not initialized. Please call initStorageChannel(...) before using.'
|
|
92
135
|
);
|
|
93
136
|
}
|
|
94
137
|
|
|
95
|
-
this.#
|
|
96
|
-
this.originalData = initData;
|
|
97
|
-
|
|
98
|
-
// const stored = (channel.sendSync('storage:get', qName) as Partial<T>) || {};
|
|
99
|
-
this.data = reactive({ ...initData }) as UnwrapNestedRefs<T>;
|
|
100
|
-
this.loadFromRemote()
|
|
138
|
+
this.#channelInitialized = true;
|
|
101
139
|
|
|
102
|
-
|
|
140
|
+
// Load data from remote
|
|
141
|
+
this.loadFromRemote();
|
|
103
142
|
|
|
143
|
+
// Register update listener
|
|
104
144
|
channel.regChannel('storage:update', ({ data }) => {
|
|
105
145
|
const { name } = data!
|
|
106
146
|
|
|
107
|
-
if (name ===
|
|
147
|
+
if (name === this.#qualifiedName) {
|
|
108
148
|
this.loadFromRemote()
|
|
109
149
|
}
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
storages.set(qName, this);
|
|
150
|
+
});
|
|
113
151
|
}
|
|
114
152
|
|
|
115
153
|
/**
|
|
@@ -313,6 +351,7 @@ export class TouchStorage<T extends object> {
|
|
|
313
351
|
}
|
|
314
352
|
/**
|
|
315
353
|
* Loads data from remote storage and applies it.
|
|
354
|
+
* If channel is not initialized yet, this method will do nothing.
|
|
316
355
|
*
|
|
317
356
|
* @returns The current instance
|
|
318
357
|
*
|
|
@@ -323,7 +362,8 @@ export class TouchStorage<T extends object> {
|
|
|
323
362
|
*/
|
|
324
363
|
loadFromRemote(): this {
|
|
325
364
|
if (!channel) {
|
|
326
|
-
|
|
365
|
+
// Channel not initialized yet, data will be loaded when channel is ready
|
|
366
|
+
return this;
|
|
327
367
|
}
|
|
328
368
|
|
|
329
369
|
const result = channel.sendSync('storage:get', this.#qualifiedName)
|
package/search/types.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { IFeatureCommand } from '../plugin';
|
|
20
|
-
import type { ITuffIcon } from '../types/icon';
|
|
20
|
+
import type { ITuffIcon, TuffIconType } from '../types/icon';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Search Result Item Interface
|
|
@@ -591,9 +591,8 @@ export function createDataItem(options: {
|
|
|
591
591
|
name,
|
|
592
592
|
desc,
|
|
593
593
|
icon: {
|
|
594
|
-
type: iconType,
|
|
595
|
-
value: iconValue
|
|
596
|
-
init: async () => {} // Required by ITuffIcon interface
|
|
594
|
+
type: (iconType === 'remix' || iconType === 'base64' ? 'url' : (iconType === 'file' || iconType === 'emoji' ? iconType : 'emoji')) as TuffIconType,
|
|
595
|
+
value: iconValue
|
|
597
596
|
},
|
|
598
597
|
push: false, // Data items don't support push mode
|
|
599
598
|
names: [name], // Include name in searchable names
|
package/types/icon.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* - emoji: Emoji characters (e.g., "🚀")
|
|
12
12
|
* - url: Remote URL (http/https) or Data URL (data:image/...)
|
|
13
13
|
* - file: Local file path (relative to plugin root directory)
|
|
14
|
+
* - class: Class name (e.g., "i-ri-rocket-line")
|
|
14
15
|
*/
|
|
15
|
-
export type TuffIconType = 'emoji' | 'url' | 'file'
|
|
16
|
+
export type TuffIconType = 'emoji' | 'url' | 'file' | 'class'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Icon status enumeration
|