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,99 +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
|
-
show:
|
|
77
|
-
hide:
|
|
78
|
-
close:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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),
|
|
82
286
|
};
|
|
83
287
|
|
|
288
|
+
// ─── browser ──────────────────────────────────────────────────────────────────
|
|
289
|
+
const _browserEvents = createFeatureConnect('browser');
|
|
290
|
+
|
|
84
291
|
export const browser = {
|
|
85
|
-
getUrl:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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>,
|
|
90
298
|
canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
|
|
91
299
|
onNavigate: (handler: (url: string) => void) => {
|
|
92
|
-
if (typeof window === 'undefined') {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const eventHandler = (event: Event) => {
|
|
97
|
-
const custom = event as CustomEvent<{ url?: string }>;
|
|
98
|
-
const nextUrl = custom.detail?.url ?? '';
|
|
99
|
-
handler(nextUrl);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
window.addEventListener('plusui:navigate', eventHandler);
|
|
103
|
-
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);
|
|
104
304
|
},
|
|
305
|
+
on: _browserEvents.on.bind(_browserEvents),
|
|
306
|
+
emit: _browserEvents.emit.bind(_browserEvents),
|
|
105
307
|
};
|
|
106
308
|
|
|
309
|
+
// ─── router ───────────────────────────────────────────────────────────────────
|
|
107
310
|
export const router = {
|
|
108
|
-
setRoutes: (routes: RouteMap) => {
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
push: async (path: string) => {
|
|
112
|
-
const target = _routes[path] ?? path;
|
|
113
|
-
return invoke('browser.navigate', [target]);
|
|
114
|
-
},
|
|
311
|
+
setRoutes: (routes: RouteMap) => { _routes = routes; },
|
|
312
|
+
push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
|
|
115
313
|
};
|
|
116
314
|
|
|
315
|
+
// ─── app ──────────────────────────────────────────────────────────────────────
|
|
316
|
+
const _appEvents = createFeatureConnect('app');
|
|
317
|
+
|
|
117
318
|
export const app = {
|
|
118
|
-
quit:
|
|
319
|
+
quit: async () => invoke('app.quit', []),
|
|
320
|
+
on: _appEvents.on.bind(_appEvents),
|
|
321
|
+
emit: _appEvents.emit.bind(_appEvents),
|
|
119
322
|
};
|
|
120
323
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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; }
|
|
128
338
|
|
|
129
339
|
export const fileDrop = {
|
|
130
340
|
setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
|
|
131
|
-
isEnabled:
|
|
341
|
+
isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
|
|
132
342
|
onFilesDropped: (handler: (files: FileInfo[]) => void) => {
|
|
133
343
|
if (typeof window === 'undefined') return () => {};
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
138
|
-
window.addEventListener('plusui:fileDrop.filesDropped', eventHandler);
|
|
139
|
-
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);
|
|
140
347
|
},
|
|
141
348
|
onDragEnter: (handler: () => void) => {
|
|
142
349
|
if (typeof window === 'undefined') return () => {};
|
|
143
|
-
|
|
144
|
-
window.
|
|
145
|
-
return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
350
|
+
window.addEventListener('plusui:fileDrop.dragEnter', handler);
|
|
351
|
+
return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
|
|
146
352
|
},
|
|
147
353
|
onDragLeave: (handler: () => void) => {
|
|
148
354
|
if (typeof window === 'undefined') return () => {};
|
|
149
|
-
|
|
150
|
-
window.
|
|
151
|
-
return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
355
|
+
window.addEventListener('plusui:fileDrop.dragLeave', handler);
|
|
356
|
+
return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
|
|
152
357
|
},
|
|
153
358
|
};
|
|
154
359
|
|
|
155
|
-
//
|
|
360
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
156
361
|
export function formatFileSize(bytes: number): string {
|
|
157
362
|
if (bytes === 0) return '0 Bytes';
|
|
158
363
|
const k = 1024;
|
|
@@ -161,6 +366,36 @@ export function formatFileSize(bytes: number): string {
|
|
|
161
366
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
162
367
|
}
|
|
163
368
|
|
|
164
|
-
export function isImageFile(file: FileInfo): boolean {
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
"strict": true,
|
|
15
15
|
"noUnusedLocals": true,
|
|
16
16
|
"noUnusedParameters": true,
|
|
17
|
-
"noFallthroughCasesInSwitch": true
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": {
|
|
20
|
+
"plusui": ["./src/plusui.ts"]
|
|
21
|
+
}
|
|
18
22
|
},
|
|
19
23
|
"include": ["src"],
|
|
20
24
|
"references": [{ "path": "./tsconfig.node.json" }]
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
3
4
|
|
|
4
5
|
export default defineConfig({
|
|
5
6
|
plugins: [react()],
|
|
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
|
|
|
@@ -104,6 +106,14 @@ struct WebGPUConfig {
|
|
|
104
106
|
// ============================================================================
|
|
105
107
|
// MAIN - Application Entry Point
|
|
106
108
|
// ============================================================================
|
|
109
|
+
// ── Connect instance ─────────────────────────────────────────────────────────
|
|
110
|
+
// connect is the bridge between C++ and the frontend.
|
|
111
|
+
// Run `plusui connect` to generate Connections/ from your name.on / name.emit usage.
|
|
112
|
+
// Then declare: Connections ch(connect);
|
|
113
|
+
// and use: ch.myEvent.on([](const json& p) { ... });
|
|
114
|
+
// ch.myEvent.emit({{"value", 42}});
|
|
115
|
+
static Connect connect;
|
|
116
|
+
|
|
107
117
|
int main() {
|
|
108
118
|
// Build the app with configuration
|
|
109
119
|
auto appBuilder = createApp()
|
|
@@ -164,12 +174,14 @@ int main() {
|
|
|
164
174
|
#endif
|
|
165
175
|
|
|
166
176
|
// ========================================
|
|
167
|
-
//
|
|
177
|
+
// CONNECT — bind frontend ↔ backend
|
|
168
178
|
// ========================================
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// });
|
|
172
|
-
//
|
|
179
|
+
// Wires the connect object to this window.
|
|
180
|
+
// Connections ch gives you named channel objects — same API as TypeScript:
|
|
181
|
+
// ch.myEvent.on([](const json& p) { ... }); // receive from frontend
|
|
182
|
+
// ch.myEvent.emit({{"value", 42}}); // send to frontend
|
|
183
|
+
bindConnect(mainWindow, connect);
|
|
184
|
+
Connections ch(connect); // use ch.name.on() / ch.name.emit()
|
|
173
185
|
|
|
174
186
|
// ========================================
|
|
175
187
|
// FILE DROP EVENTS (Native Drag & Drop API)
|
|
@@ -194,7 +206,15 @@ int main() {
|
|
|
194
206
|
// ============================================================================
|
|
195
207
|
// FRONTEND API REFERENCE
|
|
196
208
|
// ============================================================================
|
|
197
|
-
// import
|
|
209
|
+
// import plusui from 'plusui';
|
|
210
|
+
// import { connect, win, clipboard, app, browser, router, fileDrop } from 'plusui';
|
|
211
|
+
//
|
|
212
|
+
// CONNECT (custom channels — same API on both sides):
|
|
213
|
+
// Run `plusui connect` to generate Connections/ from your name.on / name.emit calls.
|
|
214
|
+
// C++: ch.myEvent.on([](const json& p) { ... }); // receive
|
|
215
|
+
// ch.myEvent.emit({{"value", 42}}); // send
|
|
216
|
+
// TS: myEvent.on((data) => { ... }); // receive
|
|
217
|
+
// myEvent.emit({ value: 42 }); // send
|
|
198
218
|
//
|
|
199
219
|
// WINDOW: win.minimize(), win.maximize(), win.close(), win.center(),
|
|
200
220
|
// win.setSize(w, h), win.setPosition(x, y), win.setTitle(str),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSignal, onMount, onCleanup, Show, For } from 'solid-js';
|
|
2
|
-
import
|
|
2
|
+
import plusui, { type FileInfo } from 'plusui';
|
|
3
|
+
// Custom channels (generated by `plusui connect`) — import what you use:
|
|
4
|
+
// import { greeting, download } from '../Connections/connections.gen';
|
|
3
5
|
|
|
4
6
|
// Define routes for your app (optional - for SPA routing)
|
|
5
7
|
const routes = {
|
|
@@ -23,32 +25,32 @@ function App() {
|
|
|
23
25
|
|
|
24
26
|
onMount(() => {
|
|
25
27
|
// Setup routes
|
|
26
|
-
router.setRoutes(routes);
|
|
28
|
+
plusui.router.setRoutes(routes);
|
|
27
29
|
|
|
28
30
|
// Listen for navigation changes
|
|
29
|
-
browser.onNavigate((url) => {
|
|
31
|
+
plusui.browser.onNavigate((url) => {
|
|
30
32
|
setCurrentUrl(url);
|
|
31
|
-
browser.canGoBack().then(setCanGoBack);
|
|
32
|
-
browser.canGoForward().then(setCanGoForward);
|
|
33
|
+
plusui.browser.canGoBack().then(setCanGoBack);
|
|
34
|
+
plusui.browser.canGoForward().then(setCanGoForward);
|
|
33
35
|
});
|
|
34
36
|
|
|
35
37
|
// Get initial state
|
|
36
|
-
browser.getUrl().then(setCurrentUrl);
|
|
37
|
-
browser.canGoBack().then(setCanGoBack);
|
|
38
|
-
browser.canGoForward().then(setCanGoForward);
|
|
38
|
+
plusui.browser.getUrl().then(setCurrentUrl);
|
|
39
|
+
plusui.browser.canGoBack().then(setCanGoBack);
|
|
40
|
+
plusui.browser.canGoForward().then(setCanGoForward);
|
|
39
41
|
|
|
40
42
|
// Setup FileDrop listeners
|
|
41
|
-
const unsubDrop = fileDrop.onFilesDropped((droppedFiles) => {
|
|
43
|
+
const unsubDrop = plusui.fileDrop.onFilesDropped((droppedFiles) => {
|
|
42
44
|
console.log('Files dropped:', droppedFiles);
|
|
43
45
|
setFiles(prev => [...prev, ...droppedFiles]);
|
|
44
46
|
setIsDragging(false);
|
|
45
47
|
});
|
|
46
48
|
|
|
47
|
-
const unsubEnter = fileDrop.onDragEnter(() => {
|
|
49
|
+
const unsubEnter = plusui.fileDrop.onDragEnter(() => {
|
|
48
50
|
setIsDragging(true);
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
const unsubLeave = fileDrop.onDragLeave(() => {
|
|
53
|
+
const unsubLeave = plusui.fileDrop.onDragLeave(() => {
|
|
52
54
|
setIsDragging(false);
|
|
53
55
|
});
|
|
54
56
|
|
|
@@ -59,29 +61,29 @@ function App() {
|
|
|
59
61
|
});
|
|
60
62
|
});
|
|
61
63
|
|
|
62
|
-
const handleMinimize = async () => await win.minimize();
|
|
63
|
-
const handleMaximize = async () => await win.maximize();
|
|
64
|
-
const handleClose = async () => await win.close();
|
|
64
|
+
const handleMinimize = async () => await plusui.win.minimize();
|
|
65
|
+
const handleMaximize = async () => await plusui.win.maximize();
|
|
66
|
+
const handleClose = async () => await plusui.win.close();
|
|
65
67
|
const handleGetSize = async () => {
|
|
66
|
-
const size = await win.getSize();
|
|
68
|
+
const size = await plusui.win.getSize();
|
|
67
69
|
setWindowSize(size);
|
|
68
70
|
};
|
|
69
71
|
const handleGetPosition = async () => {
|
|
70
|
-
const pos = await win.getPosition();
|
|
72
|
+
const pos = await plusui.win.getPosition();
|
|
71
73
|
setWindowPos(pos);
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
// Browser navigation
|
|
75
|
-
const handleGoBack = async () => await browser.goBack();
|
|
76
|
-
const handleGoForward = async () => await browser.goForward();
|
|
77
|
-
const handleReload = async () => await browser.reload();
|
|
77
|
+
const handleGoBack = async () => await plusui.browser.goBack();
|
|
78
|
+
const handleGoForward = async () => await plusui.browser.goForward();
|
|
79
|
+
const handleReload = async () => await plusui.browser.reload();
|
|
78
80
|
|
|
79
81
|
// Router navigation
|
|
80
|
-
const handleGoHome = async () => await router.push('/');
|
|
81
|
-
const handleGoSettings = async () => await router.push('/settings');
|
|
82
|
+
const handleGoHome = async () => await plusui.router.push('/');
|
|
83
|
+
const handleGoSettings = async () => await plusui.router.push('/settings');
|
|
82
84
|
|
|
83
85
|
// App control
|
|
84
|
-
const handleQuit = async () => await app.quit();
|
|
86
|
+
const handleQuit = async () => await plusui.app.quit();
|
|
85
87
|
|
|
86
88
|
return (
|
|
87
89
|
<div class="app">
|
|
@@ -112,8 +114,8 @@ function App() {
|
|
|
112
114
|
<h2>Window Position</h2>
|
|
113
115
|
<div class="button-group">
|
|
114
116
|
<button onClick={handleGetPosition} class="button">Get Position</button>
|
|
115
|
-
<button onClick={() => win.setPosition(100, 100)} class="button">Move Left</button>
|
|
116
|
-
<button onClick={() => win.setPosition(800, 100)} class="button">Move Right</button>
|
|
117
|
+
<button onClick={() => plusui.win.setPosition(100, 100)} class="button">Move Left</button>
|
|
118
|
+
<button onClick={() => plusui.win.setPosition(800, 100)} class="button">Move Right</button>
|
|
117
119
|
</div>
|
|
118
120
|
<Show when={windowPos().x !== 0}>
|
|
119
121
|
<p class="info-text">Position: {windowPos().x}, {windowPos().y}</p>
|
|
@@ -137,7 +139,7 @@ function App() {
|
|
|
137
139
|
<button onClick={handleGoSettings} class="button">Settings</button>
|
|
138
140
|
</div>
|
|
139
141
|
<p style={{ 'font-size': '0.85em', color: '#666', 'margin-top': '10px' }}>
|
|
140
|
-
Define routes with <code>router.setRoutes({'{ ... }'})</code> then navigate with <code>router.push('/path')</code>
|
|
142
|
+
Define routes with <code>plusui.router.setRoutes({'{ ... }'})</code> then navigate with <code>plusui.router.push('/path')</code>
|
|
141
143
|
</p>
|
|
142
144
|
</div>
|
|
143
145
|
|
|
@@ -196,7 +198,7 @@ function App() {
|
|
|
196
198
|
<div class="filedrop-file-info">
|
|
197
199
|
<div class="filedrop-file-name">{file.name}</div>
|
|
198
200
|
<div class="filedrop-file-meta">
|
|
199
|
-
{formatFileSize(file.size)} • {file.type}
|
|
201
|
+
{plusui.formatFileSize(file.size)} • {file.type}
|
|
200
202
|
</div>
|
|
201
203
|
</div>
|
|
202
204
|
<button
|