@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.
@@ -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
+
@@ -1,5 +1,5 @@
1
1
  import { createGlobalState } from '@vueuse/core'
2
- import { shallowReactive } from 'vue'
2
+ import { shallowReactive, computed } from 'vue'
3
3
  import { ClerkAuthState, CurrentUser } from './clerk-types'
4
4
 
5
5
  export const useAuthState = createGlobalState(() => {
@@ -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
- import path from 'path'
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)
@@ -1,4 +1,7 @@
1
- import path from 'path-browserify'
1
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
2
+ const path = typeof window === 'undefined'
3
+ ? require('path')
4
+ : require('path-browserify')
2
5
 
3
6
  /**
4
7
  * Enum for various file types.
@@ -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.debug('[PollingService] Service started.');
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
- console.log('[PollingService] Service stopped.');
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' | 'base64' | 'component' = 'emoji'): TuffIcon {
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
@@ -1,4 +1,4 @@
1
- export * from './auth'
1
+ // export * from './auth' // Renderer-only, use @talex-touch/utils/renderer instead
2
2
  export * from './base'
3
3
  export * from './common'
4
4
  export * from './plugin'
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "module": "./index.ts",
6
6
  "license": "MPL-2.0",
7
7
  "private": false,
8
- "version": "1.0.30",
8
+ "version": "1.0.31",
9
9
  "scripts": {
10
10
  "publish": "npm publish --access public"
11
11
  },
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
- export interface Window {
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
 
@@ -5,12 +5,7 @@ export interface ITouchSDK {
5
5
  __hooks: {}
6
6
  }
7
7
 
8
- // window type
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'
@@ -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
@@ -3,3 +3,4 @@ export * from './storage'
3
3
  export * from './touch-sdk'
4
4
  export * from './touch-sdk/utils'
5
5
  export * from './hooks'
6
+ export * from '../auth' // Re-export auth for renderer
@@ -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.#qualifiedName = qName;
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
- if (onUpdate) this._onUpdate.push(onUpdate);
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 === qName) {
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
- throw new Error("TouchStorage: channel not initialized");
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