@talex-touch/utils 1.0.13 → 1.0.15

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.
Files changed (41) hide show
  1. package/base/index.ts +181 -181
  2. package/channel/index.ts +108 -99
  3. package/common/index.ts +2 -39
  4. package/common/storage/constants.ts +3 -0
  5. package/common/storage/entity/app-settings.ts +47 -0
  6. package/common/storage/entity/index.ts +1 -0
  7. package/common/storage/index.ts +3 -0
  8. package/common/utils.ts +160 -0
  9. package/core-box/README.md +218 -0
  10. package/core-box/index.ts +7 -0
  11. package/core-box/search.ts +536 -0
  12. package/core-box/types.ts +384 -0
  13. package/electron/download-manager.ts +118 -0
  14. package/{common → electron}/env-tool.ts +56 -56
  15. package/electron/touch-core.ts +167 -0
  16. package/electron/window.ts +71 -0
  17. package/eventbus/index.ts +86 -87
  18. package/index.ts +5 -0
  19. package/package.json +55 -30
  20. package/permission/index.ts +48 -48
  21. package/plugin/channel.ts +203 -193
  22. package/plugin/index.ts +216 -121
  23. package/plugin/log/logger-manager.ts +60 -0
  24. package/plugin/log/logger.ts +75 -0
  25. package/plugin/log/types.ts +27 -0
  26. package/plugin/preload.ts +39 -39
  27. package/plugin/sdk/common.ts +27 -27
  28. package/plugin/sdk/hooks/life-cycle.ts +95 -95
  29. package/plugin/sdk/index.ts +18 -13
  30. package/plugin/sdk/service/index.ts +29 -29
  31. package/plugin/sdk/types.ts +578 -0
  32. package/plugin/sdk/window/index.ts +40 -40
  33. package/renderer/index.ts +2 -0
  34. package/renderer/ref.ts +54 -54
  35. package/renderer/slots.ts +124 -0
  36. package/renderer/storage/app-settings.ts +34 -0
  37. package/renderer/storage/base-storage.ts +335 -0
  38. package/renderer/storage/index.ts +1 -0
  39. package/search/types.ts +726 -0
  40. package/service/index.ts +67 -67
  41. package/service/protocol/index.ts +77 -77
package/renderer/ref.ts CHANGED
@@ -1,55 +1,55 @@
1
- import { computed, customRef } from 'vue'
2
-
3
- export function useModelWrapper(props: any, emit: any, name = 'modelValue') {
4
- return computed({
5
- get: () => props[name],
6
- set: (value) => emit(`update:${name}`, value)
7
- })
8
- }
9
-
10
- export function throttleRef(value: any, time: number) {
11
-
12
- let ts = 0
13
-
14
- return customRef((track, trigger) => {
15
- return {
16
- get() {
17
- track()
18
- return value
19
- },
20
- set(newValue) {
21
-
22
- if( new Date().getTime() - ts < time ) return
23
-
24
- value = newValue
25
- track()
26
- trigger()
27
- ts = new Date().getTime()
28
- }
29
- }
30
- })
31
-
32
- }
33
-
34
- export function debounceRef(value: any, delay: number) {
35
-
36
- let timer: any
37
-
38
- return customRef((track, trigger) => {
39
- return {
40
- get() {
41
- track()
42
- return value
43
- },
44
- set(newValue) {
45
- clearTimeout(timer)
46
- timer = setTimeout(() => {
47
- value = newValue
48
- track()
49
- trigger()
50
- }, delay)
51
- }
52
- }
53
- })
54
-
1
+ import { computed, customRef } from 'vue'
2
+
3
+ export function useModelWrapper(props: any, emit: any, name = 'modelValue') {
4
+ return computed({
5
+ get: () => props[name],
6
+ set: (value) => emit(`update:${name}`, value)
7
+ })
8
+ }
9
+
10
+ export function throttleRef(value: any, time: number) {
11
+
12
+ let ts = 0
13
+
14
+ return customRef((track, trigger) => {
15
+ return {
16
+ get() {
17
+ track()
18
+ return value
19
+ },
20
+ set(newValue) {
21
+
22
+ if( new Date().getTime() - ts < time ) return
23
+
24
+ value = newValue
25
+ track()
26
+ trigger()
27
+ ts = new Date().getTime()
28
+ }
29
+ }
30
+ })
31
+
32
+ }
33
+
34
+ export function debounceRef(value: any, delay: number) {
35
+
36
+ let timer: any
37
+
38
+ return customRef((track, trigger) => {
39
+ return {
40
+ get() {
41
+ track()
42
+ return value
43
+ },
44
+ set(newValue) {
45
+ clearTimeout(timer)
46
+ timer = setTimeout(() => {
47
+ value = newValue
48
+ track()
49
+ trigger()
50
+ }, delay)
51
+ }
52
+ }
53
+ })
54
+
55
55
  }
@@ -0,0 +1,124 @@
1
+ import { VNode } from 'vue';
2
+
3
+ type SlotSelector = string | string[] | ((name: string) => boolean);
4
+ type VNodePredicate = (node: VNode) => boolean;
5
+
6
+ /**
7
+ * Normalizes a slot input into a flat array of VNodes.
8
+ *
9
+ * @param input - Slot input, which can be a function, VNode, or array of VNodes.
10
+ * @returns A flat array of VNodes.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Normalize a mixed slot manually (VNode | VNode[] | () => VNode[])
15
+ * const vnodes = normalizeSlot(this.$slots.default);
16
+ * ```
17
+ */
18
+ export function normalizeSlot(input: unknown): VNode[] {
19
+ if (!input) return [];
20
+
21
+ if (typeof input === 'function') {
22
+ return normalizeSlot(input());
23
+ }
24
+
25
+ if (Array.isArray(input)) {
26
+ return input.flatMap(normalizeSlot);
27
+ }
28
+
29
+ return [input as VNode];
30
+ }
31
+
32
+ /**
33
+ * Recursively flattens a list of VNodes and filters them using an optional predicate.
34
+ *
35
+ * @param nodes - The VNodes to flatten and filter.
36
+ * @param predicate - Optional function to filter VNodes.
37
+ * @returns A flat array of VNodes that satisfy the predicate.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // Flatten children of a given vnode array, filtering by custom type
42
+ * const flat = flattenVNodes(vnodes, vnode => typeof vnode.type === 'object');
43
+ * ```
44
+ */
45
+ export function flattenVNodes(
46
+ nodes: VNode[],
47
+ predicate?: VNodePredicate
48
+ ): VNode[] {
49
+ const result: VNode[] = [];
50
+
51
+ for (const node of nodes) {
52
+ if (!node) continue;
53
+
54
+ if (predicate?.(node)) {
55
+ result.push(node);
56
+ } else if (node.children) {
57
+ const children = normalizeSlot(node.children);
58
+ result.push(...flattenVNodes(children, predicate));
59
+ }
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ /**
66
+ * Extracts and flattens VNodes from one or more named slots,
67
+ * optionally filtered by a VNode predicate.
68
+ *
69
+ * @param slots - The slots object (e.g., this.$slots).
70
+ * @param slotSelector - Slot name(s) or a function to select slot names.
71
+ * @param predicate - Optional function to filter VNodes.
72
+ * @returns A flat array of matching VNodes from the selected slots.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * // 1. Extract from default slot and keep only nodes with name === 'MyComponent'
77
+ * const result1 = extractFromSlots(this.$slots, 'default', vnode => vnode.type?.name === 'MyComponent');
78
+ *
79
+ * // 2. Extract all VNodes from 'header' and 'footer' slots
80
+ * const result2 = extractFromSlots(this.$slots, ['header', 'footer']);
81
+ *
82
+ * // 3. Extract all VNodes from slots whose names start with 'section-'
83
+ * const result3 = extractFromSlots(this.$slots, name => name.startsWith('section-'));
84
+ * ```
85
+ */
86
+ export function extractFromSlots(
87
+ slots: Record<string, unknown>,
88
+ slotSelector: SlotSelector = 'default',
89
+ predicate?: VNodePredicate
90
+ ): VNode[] {
91
+ const selectedSlotNames = resolveSlotNames(slots, slotSelector);
92
+
93
+ const vnodes = selectedSlotNames.flatMap((name) =>
94
+ normalizeSlot(slots[name])
95
+ );
96
+
97
+ return flattenVNodes(vnodes, predicate);
98
+ }
99
+
100
+ /**
101
+ * Resolves slot names from the selector input.
102
+ *
103
+ * @param slots - The slots object.
104
+ * @param selector - A string, string array, or function.
105
+ * @returns An array of matched slot names.
106
+ */
107
+ function resolveSlotNames(
108
+ slots: Record<string, unknown>,
109
+ selector: SlotSelector
110
+ ): string[] {
111
+ if (typeof selector === 'string') {
112
+ return slots[selector] ? [selector] : [];
113
+ }
114
+
115
+ if (Array.isArray(selector)) {
116
+ return selector.filter((name) => !!slots[name]);
117
+ }
118
+
119
+ if (typeof selector === 'function') {
120
+ return Object.keys(slots).filter(selector);
121
+ }
122
+
123
+ return [];
124
+ }
@@ -0,0 +1,34 @@
1
+ import { TouchStorage } from '.';
2
+ import { appSettingOriginData, StorageList, type AppSetting } from '../..';
3
+
4
+ /**
5
+ * Application settings storage manager
6
+ *
7
+ * Manages application configuration using `TouchStorage`, providing reactive data
8
+ * and automatic persistence.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { appSettings } from './app-settings-storage';
13
+ *
14
+ * // Read a setting
15
+ * const isAutoStart = appSettings.data.autoStart;
16
+ *
17
+ * // Modify a setting (auto-saved)
18
+ * appSettings.data.autoStart = true;
19
+ * ```
20
+ */
21
+ class AppSettingsStorage extends TouchStorage<AppSetting> {
22
+ /**
23
+ * Initializes a new instance of the AppSettingsStorage class
24
+ */
25
+ constructor() {
26
+ super(StorageList.APP_SETTING, JSON.parse(JSON.stringify(appSettingOriginData)));
27
+ this.setAutoSave(true);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Global instance of the application settings
33
+ */
34
+ export const appSettings = new AppSettingsStorage();
@@ -0,0 +1,335 @@
1
+ import {
2
+ reactive,
3
+ watch,
4
+ type UnwrapNestedRefs,
5
+ type WatchHandle,
6
+ } from 'vue';
7
+ import { useDebounceFn } from '@vueuse/core'
8
+ import type { ITouchClientChannel } from '../../channel';
9
+
10
+ /**
11
+ * Interface representing the external communication channel.
12
+ * Must be initialized before any `TouchStorage` instance is used.
13
+ */
14
+ export interface IStorageChannel extends ITouchClientChannel {
15
+ /**
16
+ * Asynchronous send interface
17
+ * @param event Event name
18
+ * @param payload Event payload
19
+ */
20
+ send(event: string, payload: unknown): Promise<unknown>;
21
+
22
+ /**
23
+ * Synchronous send interface
24
+ * @param event Event name
25
+ * @param payload Event payload
26
+ */
27
+ sendSync(event: string, payload: unknown): unknown;
28
+ }
29
+
30
+ let channel: IStorageChannel | null = null;
31
+
32
+ /**
33
+ * Initializes the global channel for communication.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { initStorageChannel } from './TouchStorage';
38
+ * import { ipcRenderer } from 'electron';
39
+ *
40
+ * initStorageChannel({
41
+ * send: ipcRenderer.invoke.bind(ipcRenderer),
42
+ * sendSync: ipcRenderer.sendSync.bind(ipcRenderer),
43
+ * });
44
+ * ```
45
+ */
46
+ export function initStorageChannel(c: IStorageChannel): void {
47
+ channel = c;
48
+ }
49
+
50
+ /**
51
+ * Global registry of storage instances.
52
+ */
53
+ export const storages = new Map<string, TouchStorage<any>>();
54
+
55
+ /**
56
+ * A reactive storage utility with optional auto-save and update subscriptions.
57
+ *
58
+ * @template T Shape of the stored data.
59
+ */
60
+ export class TouchStorage<T extends object> {
61
+ readonly #qualifiedName: string;
62
+ #autoSave = false;
63
+ #autoSaveStopHandle?: WatchHandle;
64
+ #assigning = false;
65
+ readonly originalData: T;
66
+ private readonly _onUpdate: Array<() => void> = [];
67
+
68
+ /**
69
+ * The reactive data exposed to users.
70
+ */
71
+ public data: UnwrapNestedRefs<T>;
72
+
73
+ /**
74
+ * Creates a new reactive storage instance.
75
+ *
76
+ * @param qName Globally unique name for the instance
77
+ * @param initData Initial data to populate the storage
78
+ * @param onUpdate Optional callback when data is updated
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const settings = new TouchStorage('settings', { darkMode: false });
83
+ * ```
84
+ */
85
+ constructor(qName: string, initData: T, onUpdate?: () => void) {
86
+ if (storages.has(qName)) {
87
+ throw new Error(`Storage "${qName}" already exists`);
88
+ }
89
+ if (!channel) {
90
+ throw new Error(
91
+ 'TouchStorage: channel is not initialized. Please call initStorageChannel(...) before using.'
92
+ );
93
+ }
94
+
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()
101
+
102
+ if (onUpdate) this._onUpdate.push(onUpdate);
103
+
104
+ channel.regChannel('storage:update', ({ data }) => {
105
+ const { name } = data!
106
+
107
+ if (name === qName) {
108
+ this.loadFromRemote()
109
+ }
110
+ })
111
+
112
+ storages.set(qName, this);
113
+ }
114
+
115
+ /**
116
+ * Returns the unique identifier of this storage.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * console.log(userStore.getQualifiedName()); // "user"
121
+ * ```
122
+ */
123
+ getQualifiedName(): string {
124
+ return this.#qualifiedName;
125
+ }
126
+
127
+ /**
128
+ * Checks whether auto-save is currently enabled.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * if (store.isAutoSave()) console.log("Auto-save is on!");
133
+ * ```
134
+ */
135
+ isAutoSave(): boolean {
136
+ return this.#autoSave;
137
+ }
138
+
139
+ /**
140
+ * Saves the current data to remote storage.
141
+ *
142
+ * @param options Optional configuration
143
+ * @param options.force Force save even if data is being assigned
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * await store.saveToRemote();
148
+ * ```
149
+ */
150
+ saveToRemote = useDebounceFn(async (options?: { force?: boolean }): Promise<void> => {
151
+ if (!channel) {
152
+ throw new Error("TouchStorage: channel not initialized");
153
+ }
154
+
155
+ if (this.#assigning && !options?.force) {
156
+ console.debug("[Storage] Skip saveToRemote for", this.getQualifiedName());
157
+ return;
158
+ }
159
+
160
+ console.debug("Storage saveToRemote triggered", this.getQualifiedName());
161
+
162
+ await channel.send('storage:save', {
163
+ key: this.#qualifiedName,
164
+ content: JSON.stringify(this.data),
165
+ clear: false,
166
+ });
167
+ }, 300);
168
+
169
+ /**
170
+ * Enables or disables auto-saving.
171
+ *
172
+ * @param autoSave Whether to enable auto-saving
173
+ * @returns The current instance for chaining
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * store.setAutoSave(true);
178
+ * ```
179
+ */
180
+ setAutoSave(autoSave: boolean): this {
181
+ this.#autoSave = autoSave;
182
+
183
+ this.#autoSaveStopHandle?.();
184
+
185
+ if (autoSave) {
186
+ this.#autoSaveStopHandle = watch(
187
+ this.data,
188
+ () => {
189
+ if (this.#assigning) {
190
+ console.debug("[Storage] Skip auto-save watch handle for", this.getQualifiedName());
191
+ return;
192
+ }
193
+
194
+ this._onUpdate.forEach((fn) => {
195
+ try {
196
+ fn();
197
+ } catch (e) {
198
+ console.error(`[TouchStorage] onUpdate error in "${this.#qualifiedName}":`, e);
199
+ }
200
+ });
201
+
202
+ this.saveToRemote();
203
+ },
204
+ { deep: true, immediate: true },
205
+ );
206
+ }
207
+
208
+ return this;
209
+ }
210
+
211
+ /**
212
+ * Registers a callback that runs when data changes (only triggered in auto-save mode).
213
+ *
214
+ * @param fn Callback function
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * store.onUpdate(() => {
219
+ * console.log('Data changed');
220
+ * });
221
+ * ```
222
+ */
223
+ onUpdate(fn: () => void): void {
224
+ this._onUpdate.push(fn);
225
+ }
226
+
227
+ /**
228
+ * Removes a previously registered update callback.
229
+ *
230
+ * @param fn The same callback used in `onUpdate`
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * const cb = () => console.log("Change!");
235
+ * store.onUpdate(cb);
236
+ * store.offUpdate(cb);
237
+ * ```
238
+ */
239
+ offUpdate(fn: () => void): void {
240
+ const index = this._onUpdate.indexOf(fn);
241
+ if (index !== -1) {
242
+ this._onUpdate.splice(index, 1);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Internal method to assign new values and trigger update events. (Debounced)
248
+ *
249
+ * @param newData Partial update data
250
+ * @param stopWatch Whether to stop the watcher after assignment
251
+ */
252
+ assignDataDebounced = useDebounceFn(this.assignData.bind(this), 100)
253
+
254
+ /**
255
+ * Internal method to assign new values and trigger update events.
256
+ *
257
+ * @param newData Partial update data
258
+ * @param stopWatch Whether to stop the watcher during assignment
259
+ */
260
+ private assignData(newData: Partial<T>, stopWatch: boolean = true): void {
261
+ if (stopWatch && this.#autoSave) {
262
+ this.#assigning = true;
263
+ console.debug(`[Storage] Stop auto-save watch handle for ${this.getQualifiedName()}`);
264
+ }
265
+
266
+ Object.assign(this.data, newData);
267
+ console.debug(`[Storage] Assign data to ${this.getQualifiedName()}`);
268
+
269
+ if (stopWatch && this.#autoSave) {
270
+ setTimeout(() => {
271
+ this.#assigning = false;
272
+ console.debug(`[Storage] Resume auto-save watch handle for ${this.getQualifiedName()}`);
273
+ }, 0);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Applies new data to the current storage instance. Use with caution.
279
+ *
280
+ * @param data Partial object to merge into current data
281
+ * @returns The current instance for chaining
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * store.applyData({ theme: 'dark' });
286
+ * ```
287
+ */
288
+ applyData(data: Partial<T>): this {
289
+ this.assignData(data);
290
+ return this;
291
+ }
292
+
293
+ /**
294
+ * Reloads data from remote storage and applies it.
295
+ *
296
+ * @returns The current instance
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * await store.reloadFromRemote();
301
+ * ```
302
+ */
303
+ async reloadFromRemote(): Promise<this> {
304
+ if (!channel) {
305
+ throw new Error("TouchStorage: channel not initialized");
306
+ }
307
+
308
+ const result = await channel.send('storage:reload', this.#qualifiedName);
309
+ const parsed = result ? (result as Partial<T>) : {};
310
+ this.assignData(parsed, true);
311
+
312
+ return this;
313
+ }
314
+ /**
315
+ * Loads data from remote storage and applies it.
316
+ *
317
+ * @returns The current instance
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * store.loadFromRemote();
322
+ * ```
323
+ */
324
+ loadFromRemote(): this {
325
+ if (!channel) {
326
+ throw new Error("TouchStorage: channel not initialized");
327
+ }
328
+
329
+ const result = channel.sendSync('storage:get', this.#qualifiedName)
330
+ const parsed = result ? (result as Partial<T>) : {};
331
+ this.assignData(parsed, true);
332
+
333
+ return this;
334
+ }
335
+ }
@@ -0,0 +1 @@
1
+ export * from './base-storage'