plusui-native 0.2.59 → 0.2.62
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/package.json +4 -4
- package/src/index.js +4 -9
- package/templates/base/README.md.template +10 -12
- package/templates/manager.js +61 -18
- package/templates/react/frontend/src/App.tsx +29 -27
- package/templates/react/frontend/src/plusui.ts +305 -70
- package/templates/react/frontend/tsconfig.json +5 -1
- package/templates/react/frontend/vite.config.ts +7 -0
- package/templates/react/main.cpp.template +26 -6
- package/templates/solid/frontend/src/App.tsx +28 -26
- package/templates/solid/frontend/src/plusui.ts +306 -68
- package/templates/solid/frontend/tsconfig.json +5 -1
- package/templates/solid/frontend/vite.config.ts +7 -0
- package/templates/solid/main.cpp.template +26 -6
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// PlusUI — single import entrypoint
|
|
3
|
+
//
|
|
4
|
+
// import plusui from 'plusui';
|
|
5
|
+
//
|
|
6
|
+
// plusui.win.minimize();
|
|
7
|
+
// plusui.fileDrop.onFilesDropped(...);
|
|
8
|
+
// plusui.formatFileSize(bytes);
|
|
9
|
+
//
|
|
10
|
+
// For TypeScript types only:
|
|
11
|
+
// import type { FileInfo } from 'plusui';
|
|
12
|
+
//
|
|
13
|
+
// After running `plusui connect`, custom channel objects are
|
|
14
|
+
// exported from Connections/connections.gen.ts:
|
|
15
|
+
// import { myChannel } from '../Connections/connections.gen';
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
// ─── Bridge bootstrap ────────────────────────────────────────────────────────
|
|
19
|
+
|
|
1
20
|
type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
|
|
2
21
|
type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
|
|
3
22
|
|
|
@@ -25,16 +44,12 @@ function initBridge() {
|
|
|
25
44
|
return new Promise((resolve, reject) => {
|
|
26
45
|
const id = Math.random().toString(36).slice(2, 11);
|
|
27
46
|
const request = JSON.stringify({ id, method, params: args ?? [] });
|
|
28
|
-
|
|
29
47
|
_pending[id] = { resolve, reject };
|
|
30
48
|
|
|
31
49
|
if (typeof w.__native_invoke__ === 'function') {
|
|
32
50
|
w.__native_invoke__(request);
|
|
33
51
|
} else {
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
delete _pending[id];
|
|
36
|
-
resolve(null);
|
|
37
|
-
}, 0);
|
|
52
|
+
setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
setTimeout(() => {
|
|
@@ -48,10 +63,7 @@ function initBridge() {
|
|
|
48
63
|
|
|
49
64
|
w.__response__ = (id: string, result: unknown) => {
|
|
50
65
|
const pending = _pending[id];
|
|
51
|
-
if (pending) {
|
|
52
|
-
pending.resolve(result);
|
|
53
|
-
delete _pending[id];
|
|
54
|
-
}
|
|
66
|
+
if (pending) { pending.resolve(result); delete _pending[id]; }
|
|
55
67
|
};
|
|
56
68
|
|
|
57
69
|
_invoke = w.__invoke__ as InvokeFn;
|
|
@@ -60,97 +72,292 @@ function initBridge() {
|
|
|
60
72
|
async function invoke(method: string, args?: unknown[]) {
|
|
61
73
|
if (!_invoke) {
|
|
62
74
|
initBridge();
|
|
63
|
-
if (!_invoke)
|
|
64
|
-
throw new Error('PlusUI bridge not initialized');
|
|
65
|
-
}
|
|
75
|
+
if (!_invoke) throw new Error('PlusUI bridge not initialized');
|
|
66
76
|
}
|
|
67
|
-
|
|
68
77
|
return _invoke(method, args);
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
initBridge();
|
|
72
81
|
|
|
82
|
+
// ─── Connection (on / emit) ───────────────────────────────────────────────────
|
|
83
|
+
//
|
|
84
|
+
// TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
|
|
85
|
+
//
|
|
86
|
+
// connect.emit('myEvent', { value: 42 }); // TS → C++
|
|
87
|
+
// connect.on('myEvent', (data) => { ... }); // C++ → TS
|
|
88
|
+
//
|
|
89
|
+
// Built-in features use their feature name as a scope:
|
|
90
|
+
// clipboard.on('changed', (data) => { ... }) // 'clipboard.changed'
|
|
91
|
+
// win.on('resized', (data) => { ... }) // 'window.resized'
|
|
92
|
+
//
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
type MessageCallback = (payload: any) => void;
|
|
96
|
+
|
|
97
|
+
class ConnectionClient {
|
|
98
|
+
private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
|
99
|
+
private listeners = new Map<string, Set<MessageCallback>>();
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
const host = globalThis as any;
|
|
103
|
+
host.__plusuiConnectionMessage = (message: unknown) => this.handleIncoming(message);
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
window.addEventListener('plusui:connection:message', (ev: Event) => {
|
|
106
|
+
this.handleIncoming((ev as CustomEvent<unknown>).detail);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private nextId() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
|
|
112
|
+
|
|
113
|
+
private async send(env: { kind: string; id?: string; name: string; payload?: unknown }): Promise<any> {
|
|
114
|
+
const host = globalThis as any;
|
|
115
|
+
if (typeof host.__invoke__ === 'function') return host.__invoke__('connection.dispatch', env);
|
|
116
|
+
if (host.ipc?.postMessage) host.ipc.postMessage(JSON.stringify(env));
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private decode(message: unknown): any | null {
|
|
121
|
+
if (!message) return null;
|
|
122
|
+
if (typeof message === 'string') { try { return JSON.parse(message); } catch { return null; } }
|
|
123
|
+
if (typeof message === 'object') return message;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private handleIncoming(message: unknown): void {
|
|
128
|
+
const env = this.decode(message);
|
|
129
|
+
if (!env) return;
|
|
130
|
+
if ((env.kind === 'result' || env.kind === 'error') && env.id) {
|
|
131
|
+
const entry = this.pending.get(env.id);
|
|
132
|
+
if (!entry) return;
|
|
133
|
+
this.pending.delete(env.id);
|
|
134
|
+
if (env.kind === 'error') entry.reject(new Error(env.error || 'Connection call failed'));
|
|
135
|
+
else entry.resolve(env.payload);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (env.kind === 'event' || env.kind === 'stream' || env.kind === 'publish') {
|
|
139
|
+
const handlers = this.listeners.get(env.name);
|
|
140
|
+
if (handlers) for (const h of handlers) h(env.payload);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
|
|
145
|
+
const id = this.nextId();
|
|
146
|
+
const promise = new Promise<TOut>((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
147
|
+
await this.send({ kind: 'call', id, name, payload });
|
|
148
|
+
return promise;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
152
|
+
void this.send({ kind: 'fire', name, payload });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
156
|
+
const set = this.listeners.get(name) ?? new Set<MessageCallback>();
|
|
157
|
+
set.add(callback as MessageCallback);
|
|
158
|
+
this.listeners.set(name, set);
|
|
159
|
+
return () => {
|
|
160
|
+
const cur = this.listeners.get(name);
|
|
161
|
+
if (!cur) return;
|
|
162
|
+
cur.delete(callback as MessageCallback);
|
|
163
|
+
if (cur.size === 0) this.listeners.delete(name);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
stream<TData = unknown>(name: string) {
|
|
168
|
+
return {
|
|
169
|
+
subscribe: (cb: (payload: TData) => void): (() => void) => {
|
|
170
|
+
void this.send({ kind: 'sub', name });
|
|
171
|
+
const off = this.on<TData>(name, cb);
|
|
172
|
+
return () => { off(); void this.send({ kind: 'unsub', name }); };
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
channel<TData = unknown>(name: string) {
|
|
178
|
+
return {
|
|
179
|
+
subscribe: (cb: (payload: TData) => void): (() => void) => {
|
|
180
|
+
void this.send({ kind: 'sub', name });
|
|
181
|
+
const off = this.on<TData>(name, cb);
|
|
182
|
+
return () => { off(); void this.send({ kind: 'unsub', name }); };
|
|
183
|
+
},
|
|
184
|
+
publish: (payload: TData): void => { void this.send({ kind: 'publish', name, payload }); },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const _client = new ConnectionClient();
|
|
190
|
+
|
|
191
|
+
// ─── FeatureConnect ───────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
export type FeatureConnect = {
|
|
194
|
+
on: <TData = unknown>(name: string, cb: (payload: TData) => void) => (() => void);
|
|
195
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
|
|
196
|
+
call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
|
|
197
|
+
stream: <TData = unknown>(name: string) => { subscribe: (cb: (payload: TData) => void) => (() => void) };
|
|
198
|
+
channel: <TData = unknown>(name: string) => {
|
|
199
|
+
subscribe: (cb: (payload: TData) => void) => (() => void);
|
|
200
|
+
publish: (payload: TData) => void;
|
|
201
|
+
};
|
|
202
|
+
scoped: (scope: string) => FeatureConnect;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function _scopeName(scope: string, name: string): string {
|
|
206
|
+
return name.startsWith(`${scope}.`) ? name : `${scope}.${name}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function _invokeScoped(method: string, payload?: unknown): Promise<unknown> {
|
|
210
|
+
const host = globalThis as any;
|
|
211
|
+
if (typeof host.__invoke__ !== 'function') return undefined;
|
|
212
|
+
return host.__invoke__(method, payload === undefined ? [] : [payload]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function createFeatureConnect(scope: string): FeatureConnect {
|
|
216
|
+
return {
|
|
217
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn) {
|
|
218
|
+
const s = _scopeName(scope, name);
|
|
219
|
+
void _invokeScoped(s, payload);
|
|
220
|
+
_client.fire(s, payload);
|
|
221
|
+
if (typeof window !== 'undefined') {
|
|
222
|
+
window.dispatchEvent(new CustomEvent(`plusui:${s}`, { detail: payload }));
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
|
|
226
|
+
const s = _scopeName(scope, name);
|
|
227
|
+
const off = _client.on<TData>(s, cb);
|
|
228
|
+
if (typeof window === 'undefined') return off;
|
|
229
|
+
const dom = (e: Event) => cb((e as CustomEvent<TData>).detail);
|
|
230
|
+
window.addEventListener(`plusui:${s}`, dom as EventListener);
|
|
231
|
+
return () => { off(); window.removeEventListener(`plusui:${s}`, dom as EventListener); };
|
|
232
|
+
},
|
|
233
|
+
call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
|
|
234
|
+
const s = _scopeName(scope, name);
|
|
235
|
+
const host = globalThis as any;
|
|
236
|
+
if (typeof host.__invoke__ === 'function') return _invokeScoped(s, payload) as Promise<TOut>;
|
|
237
|
+
return _client.call<TOut, TIn>(s, payload);
|
|
238
|
+
},
|
|
239
|
+
stream<TData = unknown>(name: string) { return _client.stream<TData>(_scopeName(scope, name)); },
|
|
240
|
+
channel<TData = unknown>(name: string) { return _client.channel<TData>(_scopeName(scope, name)); },
|
|
241
|
+
scoped: (child: string) => createFeatureConnect(_scopeName(scope, child)),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── connect — custom channels (your app-specific messages) ──────────────────
|
|
246
|
+
export const connect = {
|
|
247
|
+
/** Send a message to C++ backend */
|
|
248
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
249
|
+
_client.fire(name, payload);
|
|
250
|
+
},
|
|
251
|
+
/** Listen for messages from C++ backend. Returns unsubscribe fn. */
|
|
252
|
+
on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
|
|
253
|
+
return _client.on<TData>(name, cb);
|
|
254
|
+
},
|
|
255
|
+
/** Scoped feature connection (auto-prefixes names) */
|
|
256
|
+
feature: createFeatureConnect,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** Advanced: raw connection client — call / stream / channel */
|
|
260
|
+
export const connection = _client;
|
|
261
|
+
|
|
262
|
+
// ─── win — window management ──────────────────────────────────────────────────
|
|
263
|
+
const _winEvents = createFeatureConnect('window');
|
|
264
|
+
|
|
73
265
|
export const win = {
|
|
74
|
-
minimize:
|
|
75
|
-
maximize:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
266
|
+
minimize: async () => invoke('window.minimize', []),
|
|
267
|
+
maximize: async () => invoke('window.maximize', []),
|
|
268
|
+
show: async () => invoke('window.show', []),
|
|
269
|
+
hide: async () => invoke('window.hide', []),
|
|
270
|
+
close: async () => invoke('window.close', []),
|
|
271
|
+
center: async () => invoke('window.center', []),
|
|
272
|
+
setTitle: async (title: string) => invoke('window.setTitle', [title]),
|
|
273
|
+
setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
|
|
274
|
+
setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
|
|
275
|
+
setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
|
|
276
|
+
setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
|
|
277
|
+
setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
|
|
278
|
+
setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
|
|
279
|
+
setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
|
|
280
|
+
getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
|
|
281
|
+
getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
|
|
282
|
+
isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
|
|
283
|
+
isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
|
|
284
|
+
on: _winEvents.on.bind(_winEvents),
|
|
285
|
+
emit: _winEvents.emit.bind(_winEvents),
|
|
80
286
|
};
|
|
81
287
|
|
|
288
|
+
// ─── browser ──────────────────────────────────────────────────────────────────
|
|
289
|
+
const _browserEvents = createFeatureConnect('browser');
|
|
290
|
+
|
|
82
291
|
export const browser = {
|
|
83
|
-
getUrl:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
292
|
+
getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
|
|
293
|
+
navigate: async (url: string) => invoke('browser.navigate', [url]),
|
|
294
|
+
goBack: async () => invoke('browser.goBack', []),
|
|
295
|
+
goForward: async () => invoke('browser.goForward', []),
|
|
296
|
+
reload: async () => invoke('browser.reload', []),
|
|
297
|
+
canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
|
|
88
298
|
canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
|
|
89
299
|
onNavigate: (handler: (url: string) => void) => {
|
|
90
|
-
if (typeof window === 'undefined') {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const eventHandler = (event: Event) => {
|
|
95
|
-
const custom = event as CustomEvent<{ url?: string }>;
|
|
96
|
-
const nextUrl = custom.detail?.url ?? '';
|
|
97
|
-
handler(nextUrl);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
window.addEventListener('plusui:navigate', eventHandler);
|
|
101
|
-
return () => window.removeEventListener('plusui:navigate', eventHandler);
|
|
300
|
+
if (typeof window === 'undefined') return () => {};
|
|
301
|
+
const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
|
|
302
|
+
window.addEventListener('plusui:navigate', h);
|
|
303
|
+
return () => window.removeEventListener('plusui:navigate', h);
|
|
102
304
|
},
|
|
305
|
+
on: _browserEvents.on.bind(_browserEvents),
|
|
306
|
+
emit: _browserEvents.emit.bind(_browserEvents),
|
|
103
307
|
};
|
|
104
308
|
|
|
309
|
+
// ─── router ───────────────────────────────────────────────────────────────────
|
|
105
310
|
export const router = {
|
|
106
|
-
setRoutes: (routes: RouteMap) => {
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
push: async (path: string) => {
|
|
110
|
-
const target = _routes[path] ?? path;
|
|
111
|
-
return invoke('browser.navigate', [target]);
|
|
112
|
-
},
|
|
311
|
+
setRoutes: (routes: RouteMap) => { _routes = routes; },
|
|
312
|
+
push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
|
|
113
313
|
};
|
|
114
314
|
|
|
315
|
+
// ─── app ──────────────────────────────────────────────────────────────────────
|
|
316
|
+
const _appEvents = createFeatureConnect('app');
|
|
317
|
+
|
|
115
318
|
export const app = {
|
|
116
|
-
quit:
|
|
319
|
+
quit: async () => invoke('app.quit', []),
|
|
320
|
+
on: _appEvents.on.bind(_appEvents),
|
|
321
|
+
emit: _appEvents.emit.bind(_appEvents),
|
|
117
322
|
};
|
|
118
323
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
324
|
+
// ─── clipboard ────────────────────────────────────────────────────────────────
|
|
325
|
+
const _clipboardEvents = createFeatureConnect('clipboard');
|
|
326
|
+
|
|
327
|
+
export const clipboard = {
|
|
328
|
+
getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
|
|
329
|
+
setText: async (text: string) => invoke('clipboard.setText', [text]),
|
|
330
|
+
clear: async () => invoke('clipboard.clear', []),
|
|
331
|
+
hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
|
|
332
|
+
on: _clipboardEvents.on.bind(_clipboardEvents),
|
|
333
|
+
emit: _clipboardEvents.emit.bind(_clipboardEvents),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// ─── fileDrop ─────────────────────────────────────────────────────────────────
|
|
337
|
+
export interface FileInfo { path: string; name: string; type: string; size: number; }
|
|
126
338
|
|
|
127
339
|
export const fileDrop = {
|
|
128
340
|
setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
|
|
129
|
-
isEnabled:
|
|
341
|
+
isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
|
|
130
342
|
onFilesDropped: (handler: (files: FileInfo[]) => void) => {
|
|
131
343
|
if (typeof window === 'undefined') return () => {};
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
};
|
|
136
|
-
window.addEventListener('plusui:fileDrop.filesDropped', eventHandler);
|
|
137
|
-
return () => window.removeEventListener('plusui:fileDrop.filesDropped', eventHandler);
|
|
344
|
+
const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
|
|
345
|
+
window.addEventListener('plusui:fileDrop.filesDropped', h);
|
|
346
|
+
return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
|
|
138
347
|
},
|
|
139
348
|
onDragEnter: (handler: () => void) => {
|
|
140
349
|
if (typeof window === 'undefined') return () => {};
|
|
141
|
-
|
|
142
|
-
window.
|
|
143
|
-
return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
350
|
+
window.addEventListener('plusui:fileDrop.dragEnter', handler);
|
|
351
|
+
return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
|
|
144
352
|
},
|
|
145
353
|
onDragLeave: (handler: () => void) => {
|
|
146
354
|
if (typeof window === 'undefined') return () => {};
|
|
147
|
-
|
|
148
|
-
window.
|
|
149
|
-
return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
355
|
+
window.addEventListener('plusui:fileDrop.dragLeave', handler);
|
|
356
|
+
return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
|
|
150
357
|
},
|
|
151
358
|
};
|
|
152
359
|
|
|
153
|
-
//
|
|
360
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
154
361
|
export function formatFileSize(bytes: number): string {
|
|
155
362
|
if (bytes === 0) return '0 Bytes';
|
|
156
363
|
const k = 1024;
|
|
@@ -159,6 +366,37 @@ export function formatFileSize(bytes: number): string {
|
|
|
159
366
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
160
367
|
}
|
|
161
368
|
|
|
162
|
-
export function isImageFile(file: FileInfo): boolean {
|
|
163
|
-
|
|
164
|
-
|
|
369
|
+
export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
|
|
370
|
+
|
|
371
|
+
// ─── Top-level on / emit ─────────────────────────────────────────────────────
|
|
372
|
+
//
|
|
373
|
+
// import plusui from 'plusui';
|
|
374
|
+
//
|
|
375
|
+
// plusui.emit('myEvent', { value: 42 }); // TS → C++
|
|
376
|
+
// plusui.on('myEvent', (data) => { ... }); // C++ → TS
|
|
377
|
+
//
|
|
378
|
+
// plusui.win.minimize();
|
|
379
|
+
// plusui.clipboard.on('changed', (data) => { ... });
|
|
380
|
+
//
|
|
381
|
+
export const on = connect.on.bind(connect) as typeof connect.on;
|
|
382
|
+
export const emit = connect.emit.bind(connect) as typeof connect.emit;
|
|
383
|
+
|
|
384
|
+
// ─── Default export — everything under one roof ───────────────────────────────
|
|
385
|
+
const plusui = {
|
|
386
|
+
/** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
|
|
387
|
+
feature: createFeatureConnect,
|
|
388
|
+
connection,
|
|
389
|
+
win,
|
|
390
|
+
browser,
|
|
391
|
+
router,
|
|
392
|
+
app,
|
|
393
|
+
clipboard,
|
|
394
|
+
fileDrop,
|
|
395
|
+
formatFileSize,
|
|
396
|
+
isImageFile,
|
|
397
|
+
on,
|
|
398
|
+
emit,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
export default plusui;
|
|
402
|
+
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"strict": true,
|
|
16
16
|
"noUnusedLocals": true,
|
|
17
17
|
"noUnusedParameters": true,
|
|
18
|
-
"noFallthroughCasesInSwitch": true
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"baseUrl": ".",
|
|
20
|
+
"paths": {
|
|
21
|
+
"plusui": ["./src/plusui.ts"]
|
|
22
|
+
}
|
|
19
23
|
},
|
|
20
24
|
"include": ["src"],
|
|
21
25
|
"references": [{ "path": "./tsconfig.node.json" }]
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
import solid from 'vite-plugin-solid';
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
3
4
|
|
|
4
5
|
export default defineConfig({
|
|
5
6
|
plugins: [solid()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
// `import plusui from 'plusui'` resolves to your local plusui.ts
|
|
10
|
+
plusui: fileURLToPath(new URL('./src/plusui.ts', import.meta.url)),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
6
13
|
build: {
|
|
7
14
|
outDir: 'dist',
|
|
8
15
|
emptyOutDir: true,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#include <plusui/plusui.hpp> // All-in-one framework header
|
|
2
2
|
#include <iostream>
|
|
3
3
|
#include "generated/assets.h"
|
|
4
|
+
// ── Generated channel bindings (run `plusui connect` to regenerate) ──────────
|
|
5
|
+
#include "Connections/connections.gen.hpp"
|
|
4
6
|
|
|
5
7
|
using namespace plusui;
|
|
6
8
|
|
|
@@ -103,6 +105,14 @@ struct WebGPUConfig {
|
|
|
103
105
|
// ============================================================================
|
|
104
106
|
// MAIN - Application Entry Point
|
|
105
107
|
// ============================================================================
|
|
108
|
+
// ── Connect instance ─────────────────────────────────────────────────────────
|
|
109
|
+
// connect is the bridge between C++ and the frontend.
|
|
110
|
+
// Run `plusui connect` to generate Connections/ from your name.on / name.emit usage.
|
|
111
|
+
// Then declare: Connections ch(connect);
|
|
112
|
+
// and use: ch.myEvent.on([](const json& p) { ... });
|
|
113
|
+
// ch.myEvent.emit({{"value", 42}});
|
|
114
|
+
static Connect connect;
|
|
115
|
+
|
|
106
116
|
int main() {
|
|
107
117
|
// Build the app with configuration
|
|
108
118
|
auto appBuilder = createApp()
|
|
@@ -158,12 +168,14 @@ int main() {
|
|
|
158
168
|
#endif
|
|
159
169
|
|
|
160
170
|
// ========================================
|
|
161
|
-
//
|
|
171
|
+
// CONNECT — bind frontend ↔ backend
|
|
162
172
|
// ========================================
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// });
|
|
166
|
-
//
|
|
173
|
+
// Wires the connect object to this window.
|
|
174
|
+
// Connections ch gives you named channel objects — same API as TypeScript:
|
|
175
|
+
// ch.myEvent.on([](const json& p) { ... }); // receive from frontend
|
|
176
|
+
// ch.myEvent.emit({{"value", 42}}); // send to frontend
|
|
177
|
+
bindConnect(mainWindow, connect);
|
|
178
|
+
Connections ch(connect); // use ch.name.on() / ch.name.emit()
|
|
167
179
|
|
|
168
180
|
// ========================================
|
|
169
181
|
// FILE DROP EVENTS (Native Drag & Drop API)
|
|
@@ -188,7 +200,15 @@ int main() {
|
|
|
188
200
|
// ============================================================================
|
|
189
201
|
// FRONTEND API REFERENCE
|
|
190
202
|
// ============================================================================
|
|
191
|
-
// import
|
|
203
|
+
// import plusui from 'plusui';
|
|
204
|
+
// import { connect, win, clipboard, app, browser, router, fileDrop } from 'plusui';
|
|
205
|
+
//
|
|
206
|
+
// CONNECT (custom channels — same API on both sides):
|
|
207
|
+
// Run `plusui connect` to generate Connections/ from your name.on / name.emit calls.
|
|
208
|
+
// C++: ch.myEvent.on([](const json& p) { ... }); // receive
|
|
209
|
+
// ch.myEvent.emit({{"value", 42}}); // send
|
|
210
|
+
// TS: myEvent.on((data) => { ... }); // receive
|
|
211
|
+
// myEvent.emit({ value: 42 }); // send
|
|
192
212
|
//
|
|
193
213
|
// WINDOW: win.minimize(), win.maximize(), win.close(), win.center(),
|
|
194
214
|
// win.setSize(w, h), win.setPosition(x, y), win.setTitle(str),
|