plusui-native 0.2.59 → 0.2.61
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 +3 -1
- 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 +3 -1
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plusui-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.61",
|
|
4
4
|
"description": "PlusUI CLI - Build C++ desktop apps modern UI ",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"semver": "^7.6.0",
|
|
28
28
|
"which": "^4.0.0",
|
|
29
29
|
"execa": "^8.0.1",
|
|
30
|
-
"plusui-native-builder": "^0.1.
|
|
31
|
-
"plusui-native-connect": "^0.1.
|
|
30
|
+
"plusui-native-builder": "^0.1.60",
|
|
31
|
+
"plusui-native-connect": "^0.1.60"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"plusui-native-connect": "^0.1.
|
|
34
|
+
"plusui-native-connect": "^0.1.60"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { mkdir, readFile, stat, rm, readdir, writeFile, copyFile } from 'fs/promises';
|
|
4
4
|
import { existsSync, watch, statSync, mkdirSync } from 'fs';
|
|
@@ -400,8 +400,8 @@ async function updatePlusUIPackages() {
|
|
|
400
400
|
function getAppBindgenPaths() {
|
|
401
401
|
return {
|
|
402
402
|
featuresDir: join(process.cwd(), 'src', 'features'),
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
// Connections/ is now at the project root — shared by C++ and TS
|
|
404
|
+
outputDir: join(process.cwd(), 'Connections'),
|
|
405
405
|
};
|
|
406
406
|
}
|
|
407
407
|
|
|
@@ -1089,14 +1089,12 @@ async function runBindgen(providedArgs = null, options = {}) {
|
|
|
1089
1089
|
|
|
1090
1090
|
let usedDefaultAppMode = false;
|
|
1091
1091
|
let defaultOutputDir = null;
|
|
1092
|
-
let defaultFrontendOutputDir = null;
|
|
1093
1092
|
|
|
1094
1093
|
if (bindgenArgs.length === 0) {
|
|
1095
|
-
const { outputDir: appOutputDir
|
|
1094
|
+
const { outputDir: appOutputDir } = getAppBindgenPaths();
|
|
1096
1095
|
bindgenArgs = [process.cwd(), appOutputDir];
|
|
1097
1096
|
usedDefaultAppMode = true;
|
|
1098
1097
|
defaultOutputDir = appOutputDir;
|
|
1099
|
-
defaultFrontendOutputDir = frontendOutputDir;
|
|
1100
1098
|
log(`Project mode: ${process.cwd()} -> ${appOutputDir}`, 'dim');
|
|
1101
1099
|
}
|
|
1102
1100
|
|
|
@@ -1110,9 +1108,6 @@ async function runBindgen(providedArgs = null, options = {}) {
|
|
|
1110
1108
|
proc.on('close', async (code) => {
|
|
1111
1109
|
if (code === 0) {
|
|
1112
1110
|
try {
|
|
1113
|
-
if (usedDefaultAppMode && defaultOutputDir && defaultFrontendOutputDir) {
|
|
1114
|
-
await syncGeneratedTsBindings(defaultOutputDir, defaultFrontendOutputDir);
|
|
1115
|
-
}
|
|
1116
1111
|
log('\nBindgen complete!', 'green');
|
|
1117
1112
|
resolve();
|
|
1118
1113
|
} catch (syncErr) {
|
|
@@ -40,23 +40,19 @@ npm run connect
|
|
|
40
40
|
|
|
41
41
|
Default connect generator paths:
|
|
42
42
|
- Input: project root scan (frontend + backend files)
|
|
43
|
-
- Output: `
|
|
43
|
+
- Output: `Connections/` (at project root)
|
|
44
44
|
|
|
45
45
|
Generated structure:
|
|
46
|
-
- `
|
|
47
|
-
- `
|
|
48
|
-
- `
|
|
46
|
+
- `Connections/connections.gen.ts` — typed TS channel exports (regenerated)
|
|
47
|
+
- `Connections/connections.gen.hpp` — C++ `Connections` struct (regenerated)
|
|
48
|
+
- `Connections/connect.manifest.json` — detected channel list (regenerated)
|
|
49
|
+
|
|
50
|
+
All three files are always regenerated — do not manually edit them.
|
|
49
51
|
|
|
50
52
|
Scan extensions:
|
|
51
53
|
- `WEB_IO`: `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.html`
|
|
52
54
|
- `CPP_IO`: `.h`, `.hpp`, `.hh`, `.hxx`, `.cpp`, `.cc`, `.cxx`
|
|
53
55
|
|
|
54
|
-
Custom binding kinds detected:
|
|
55
|
-
- `method`
|
|
56
|
-
- `service`
|
|
57
|
-
- `stream`
|
|
58
|
-
- `event`
|
|
59
|
-
|
|
60
56
|
`plusui connect` scans your project structure and does not require a schema file.
|
|
61
57
|
|
|
62
58
|
You can also pass custom paths:
|
|
@@ -163,10 +159,12 @@ just help # Show all commands
|
|
|
163
159
|
|
|
164
160
|
## Calling C++ from TypeScript
|
|
165
161
|
|
|
166
|
-
|
|
162
|
+
All features and custom channels are available via a single import:
|
|
167
163
|
|
|
168
164
|
```tsx
|
|
169
|
-
import
|
|
165
|
+
import plusui from 'plusui';
|
|
166
|
+
// or named imports:
|
|
167
|
+
import { connect, win, clipboard, app } from 'plusui';
|
|
170
168
|
|
|
171
169
|
// Window controls
|
|
172
170
|
await win.minimize();
|
package/templates/manager.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { EnvironmentDoctor } from '../src/doctor/index.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { spawn } from 'child_process';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = dirname(__filename);
|
|
@@ -24,6 +25,58 @@ export class TemplateManager {
|
|
|
24
25
|
return this.cliPackageJson.version;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
async loadCliPackageJson() {
|
|
29
|
+
if (!this.cliPackageJson) {
|
|
30
|
+
const packageJsonPath = join(this.templatesDir, '..', 'package.json');
|
|
31
|
+
const content = await readFile(packageJsonPath, 'utf8');
|
|
32
|
+
this.cliPackageJson = JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
return this.cliPackageJson;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getLatestPublishedVersion(packageName) {
|
|
38
|
+
try {
|
|
39
|
+
const version = execSync(`npm view ${packageName} version`, {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
42
|
+
}).trim();
|
|
43
|
+
return version || null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toCompatibleRange(version) {
|
|
50
|
+
if (!version) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return version.startsWith('^') || version.startsWith('~') ? version : `^${version}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async resolveTemplatePackageVersions() {
|
|
57
|
+
const cliPkg = await this.loadCliPackageJson();
|
|
58
|
+
const cliVersion = cliPkg.version;
|
|
59
|
+
const [major, minor] = cliVersion.split('.');
|
|
60
|
+
|
|
61
|
+
const fallbackCliRange = `^${major}.${minor}.0`;
|
|
62
|
+
const fallbackToolsRange = `^${major}.1.0`;
|
|
63
|
+
|
|
64
|
+
const publishedCli = this.getLatestPublishedVersion('plusui-native');
|
|
65
|
+
const publishedCore = this.getLatestPublishedVersion('plusui-native-core');
|
|
66
|
+
const publishedBuilder = this.getLatestPublishedVersion('plusui-native-builder');
|
|
67
|
+
const publishedConnect = this.getLatestPublishedVersion('plusui-native-connect');
|
|
68
|
+
|
|
69
|
+
const cliDependencyBuilder = cliPkg.dependencies?.['plusui-native-builder'] || null;
|
|
70
|
+
const cliDependencyConnect = cliPkg.dependencies?.['plusui-native-connect'] || null;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
cli: this.toCompatibleRange(publishedCli) || fallbackCliRange,
|
|
74
|
+
core: this.toCompatibleRange(publishedCore) || fallbackToolsRange,
|
|
75
|
+
builder: this.toCompatibleRange(publishedBuilder) || cliDependencyBuilder || fallbackToolsRange,
|
|
76
|
+
connect: this.toCompatibleRange(publishedConnect) || cliDependencyConnect || fallbackToolsRange
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
27
80
|
async create(projectName, options = {}) {
|
|
28
81
|
const template = options.template || 'react';
|
|
29
82
|
const templatePath = join(this.templatesDir, template);
|
|
@@ -53,32 +106,22 @@ export class TemplateManager {
|
|
|
53
106
|
|
|
54
107
|
await mkdir(projectPath, { recursive: true });
|
|
55
108
|
|
|
56
|
-
// 3.
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
// Use version ranges that match latest published versions
|
|
60
|
-
// For CLI (plusui-native): Use major.minor.0 to get latest compatible
|
|
61
|
-
const [major, minor, patch] = cliVersion.split('.');
|
|
62
|
-
const cliVersionRange = `^${major}.${minor}.0`;
|
|
63
|
-
|
|
64
|
-
// Core version follows 0.1.x pattern when CLI is 0.2.x
|
|
65
|
-
const coreVersionRange = `^${major}.1.0`;
|
|
66
|
-
|
|
67
|
-
// Builder and connect generator also follow 0.1.x pattern
|
|
68
|
-
const builderVersionRange = `^${major}.1.0`;
|
|
69
|
-
const connectVersionRange = `^${major}.1.0`;
|
|
109
|
+
// 3. Resolve package versions for generated app dependencies
|
|
110
|
+
const resolvedVersions = await this.resolveTemplatePackageVersions();
|
|
70
111
|
|
|
71
112
|
// 4. Prepare template variables
|
|
72
113
|
const variables = {
|
|
73
114
|
PROJECT_NAME: projectName,
|
|
74
115
|
PROJECT_NAME_LOWER: projectName.toLowerCase(),
|
|
75
116
|
PROJECT_VERSION: '0.1.0',
|
|
76
|
-
PLUSUI_CLI_VERSION:
|
|
77
|
-
PLUSUI_CORE_VERSION:
|
|
78
|
-
PLUSUI_BUILDER_VERSION:
|
|
79
|
-
PLUSUI_CONNECT_VERSION:
|
|
117
|
+
PLUSUI_CLI_VERSION: resolvedVersions.cli,
|
|
118
|
+
PLUSUI_CORE_VERSION: resolvedVersions.core,
|
|
119
|
+
PLUSUI_BUILDER_VERSION: resolvedVersions.builder,
|
|
120
|
+
PLUSUI_CONNECT_VERSION: resolvedVersions.connect
|
|
80
121
|
};
|
|
81
122
|
|
|
123
|
+
console.log(chalk.dim(`Using versions: core=${variables.PLUSUI_CORE_VERSION}, builder=${variables.PLUSUI_BUILDER_VERSION}, connect=${variables.PLUSUI_CONNECT_VERSION}`));
|
|
124
|
+
|
|
82
125
|
// 5. Copy template files
|
|
83
126
|
await this.copyTemplate(templatePath, projectPath, variables);
|
|
84
127
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
-
import { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from '
|
|
2
|
+
import { win, browser, router, app, fileDrop, formatFileSize, 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 = {
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// PlusUI — single import entrypoint
|
|
3
|
+
//
|
|
4
|
+
// import plusui from 'plusui';
|
|
5
|
+
// import { connect, win, clipboard, app } from 'plusui';
|
|
6
|
+
//
|
|
7
|
+
// Everything your app needs — built-in features AND your own
|
|
8
|
+
// custom connect channels — is available from this one import.
|
|
9
|
+
//
|
|
10
|
+
// After running `plusui connect`, Connections/connections.gen.ts
|
|
11
|
+
// is auto-imported here so all discovered channels become typed.
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
// ─── Bridge bootstrap ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
1
16
|
type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
|
|
2
17
|
type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
|
|
3
18
|
|
|
@@ -25,16 +40,12 @@ function initBridge() {
|
|
|
25
40
|
return new Promise((resolve, reject) => {
|
|
26
41
|
const id = Math.random().toString(36).slice(2, 11);
|
|
27
42
|
const request = JSON.stringify({ id, method, params: args ?? [] });
|
|
28
|
-
|
|
29
43
|
_pending[id] = { resolve, reject };
|
|
30
44
|
|
|
31
45
|
if (typeof w.__native_invoke__ === 'function') {
|
|
32
46
|
w.__native_invoke__(request);
|
|
33
47
|
} else {
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
delete _pending[id];
|
|
36
|
-
resolve(null);
|
|
37
|
-
}, 0);
|
|
48
|
+
setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
setTimeout(() => {
|
|
@@ -48,10 +59,7 @@ function initBridge() {
|
|
|
48
59
|
|
|
49
60
|
w.__response__ = (id: string, result: unknown) => {
|
|
50
61
|
const pending = _pending[id];
|
|
51
|
-
if (pending) {
|
|
52
|
-
pending.resolve(result);
|
|
53
|
-
delete _pending[id];
|
|
54
|
-
}
|
|
62
|
+
if (pending) { pending.resolve(result); delete _pending[id]; }
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
_invoke = w.__invoke__ as InvokeFn;
|
|
@@ -60,99 +68,292 @@ function initBridge() {
|
|
|
60
68
|
async function invoke(method: string, args?: unknown[]) {
|
|
61
69
|
if (!_invoke) {
|
|
62
70
|
initBridge();
|
|
63
|
-
if (!_invoke)
|
|
64
|
-
throw new Error('PlusUI bridge not initialized');
|
|
65
|
-
}
|
|
71
|
+
if (!_invoke) throw new Error('PlusUI bridge not initialized');
|
|
66
72
|
}
|
|
67
|
-
|
|
68
73
|
return _invoke(method, args);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
initBridge();
|
|
72
77
|
|
|
78
|
+
// ─── Connection (on / emit) ───────────────────────────────────────────────────
|
|
79
|
+
//
|
|
80
|
+
// TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
|
|
81
|
+
//
|
|
82
|
+
// connect.emit('myEvent', { value: 42 }); // TS → C++
|
|
83
|
+
// connect.on('myEvent', (data) => { ... }); // C++ → TS
|
|
84
|
+
//
|
|
85
|
+
// Built-in features use their feature name as a scope:
|
|
86
|
+
// clipboard.on('changed', (data) => { ... }) // 'clipboard.changed'
|
|
87
|
+
// win.on('resized', (data) => { ... }) // 'window.resized'
|
|
88
|
+
//
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
type MessageCallback = (payload: any) => void;
|
|
92
|
+
|
|
93
|
+
class ConnectionClient {
|
|
94
|
+
private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
|
95
|
+
private listeners = new Map<string, Set<MessageCallback>>();
|
|
96
|
+
|
|
97
|
+
constructor() {
|
|
98
|
+
const host = globalThis as any;
|
|
99
|
+
host.__plusuiConnectionMessage = (message: unknown) => this.handleIncoming(message);
|
|
100
|
+
if (typeof window !== 'undefined') {
|
|
101
|
+
window.addEventListener('plusui:connection:message', (ev: Event) => {
|
|
102
|
+
this.handleIncoming((ev as CustomEvent<unknown>).detail);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private nextId() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
|
|
108
|
+
|
|
109
|
+
private async send(env: { kind: string; id?: string; name: string; payload?: unknown }): Promise<any> {
|
|
110
|
+
const host = globalThis as any;
|
|
111
|
+
if (typeof host.__invoke__ === 'function') return host.__invoke__('connection.dispatch', env);
|
|
112
|
+
if (host.ipc?.postMessage) host.ipc.postMessage(JSON.stringify(env));
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private decode(message: unknown): any | null {
|
|
117
|
+
if (!message) return null;
|
|
118
|
+
if (typeof message === 'string') { try { return JSON.parse(message); } catch { return null; } }
|
|
119
|
+
if (typeof message === 'object') return message;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private handleIncoming(message: unknown): void {
|
|
124
|
+
const env = this.decode(message);
|
|
125
|
+
if (!env) return;
|
|
126
|
+
if ((env.kind === 'result' || env.kind === 'error') && env.id) {
|
|
127
|
+
const entry = this.pending.get(env.id);
|
|
128
|
+
if (!entry) return;
|
|
129
|
+
this.pending.delete(env.id);
|
|
130
|
+
if (env.kind === 'error') entry.reject(new Error(env.error || 'Connection call failed'));
|
|
131
|
+
else entry.resolve(env.payload);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (env.kind === 'event' || env.kind === 'stream' || env.kind === 'publish') {
|
|
135
|
+
const handlers = this.listeners.get(env.name);
|
|
136
|
+
if (handlers) for (const h of handlers) h(env.payload);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
|
|
141
|
+
const id = this.nextId();
|
|
142
|
+
const promise = new Promise<TOut>((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
143
|
+
await this.send({ kind: 'call', id, name, payload });
|
|
144
|
+
return promise;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
148
|
+
void this.send({ kind: 'fire', name, payload });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
152
|
+
const set = this.listeners.get(name) ?? new Set<MessageCallback>();
|
|
153
|
+
set.add(callback as MessageCallback);
|
|
154
|
+
this.listeners.set(name, set);
|
|
155
|
+
return () => {
|
|
156
|
+
const cur = this.listeners.get(name);
|
|
157
|
+
if (!cur) return;
|
|
158
|
+
cur.delete(callback as MessageCallback);
|
|
159
|
+
if (cur.size === 0) this.listeners.delete(name);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
stream<TData = unknown>(name: string) {
|
|
164
|
+
return {
|
|
165
|
+
subscribe: (cb: (payload: TData) => void): (() => void) => {
|
|
166
|
+
void this.send({ kind: 'sub', name });
|
|
167
|
+
const off = this.on<TData>(name, cb);
|
|
168
|
+
return () => { off(); void this.send({ kind: 'unsub', name }); };
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
channel<TData = unknown>(name: string) {
|
|
174
|
+
return {
|
|
175
|
+
subscribe: (cb: (payload: TData) => void): (() => void) => {
|
|
176
|
+
void this.send({ kind: 'sub', name });
|
|
177
|
+
const off = this.on<TData>(name, cb);
|
|
178
|
+
return () => { off(); void this.send({ kind: 'unsub', name }); };
|
|
179
|
+
},
|
|
180
|
+
publish: (payload: TData): void => { void this.send({ kind: 'publish', name, payload }); },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const _client = new ConnectionClient();
|
|
186
|
+
|
|
187
|
+
// ─── FeatureConnect ───────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export type FeatureConnect = {
|
|
190
|
+
on: <TData = unknown>(name: string, cb: (payload: TData) => void) => (() => void);
|
|
191
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
|
|
192
|
+
call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
|
|
193
|
+
stream: <TData = unknown>(name: string) => { subscribe: (cb: (payload: TData) => void) => (() => void) };
|
|
194
|
+
channel: <TData = unknown>(name: string) => {
|
|
195
|
+
subscribe: (cb: (payload: TData) => void) => (() => void);
|
|
196
|
+
publish: (payload: TData) => void;
|
|
197
|
+
};
|
|
198
|
+
scoped: (scope: string) => FeatureConnect;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function _scopeName(scope: string, name: string): string {
|
|
202
|
+
return name.startsWith(`${scope}.`) ? name : `${scope}.${name}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function _invokeScoped(method: string, payload?: unknown): Promise<unknown> {
|
|
206
|
+
const host = globalThis as any;
|
|
207
|
+
if (typeof host.__invoke__ !== 'function') return undefined;
|
|
208
|
+
return host.__invoke__(method, payload === undefined ? [] : [payload]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createFeatureConnect(scope: string): FeatureConnect {
|
|
212
|
+
return {
|
|
213
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn) {
|
|
214
|
+
const s = _scopeName(scope, name);
|
|
215
|
+
void _invokeScoped(s, payload);
|
|
216
|
+
_client.fire(s, payload);
|
|
217
|
+
if (typeof window !== 'undefined') {
|
|
218
|
+
window.dispatchEvent(new CustomEvent(`plusui:${s}`, { detail: payload }));
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
|
|
222
|
+
const s = _scopeName(scope, name);
|
|
223
|
+
const off = _client.on<TData>(s, cb);
|
|
224
|
+
if (typeof window === 'undefined') return off;
|
|
225
|
+
const dom = (e: Event) => cb((e as CustomEvent<TData>).detail);
|
|
226
|
+
window.addEventListener(`plusui:${s}`, dom as EventListener);
|
|
227
|
+
return () => { off(); window.removeEventListener(`plusui:${s}`, dom as EventListener); };
|
|
228
|
+
},
|
|
229
|
+
call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
|
|
230
|
+
const s = _scopeName(scope, name);
|
|
231
|
+
const host = globalThis as any;
|
|
232
|
+
if (typeof host.__invoke__ === 'function') return _invokeScoped(s, payload) as Promise<TOut>;
|
|
233
|
+
return _client.call<TOut, TIn>(s, payload);
|
|
234
|
+
},
|
|
235
|
+
stream<TData = unknown>(name: string) { return _client.stream<TData>(_scopeName(scope, name)); },
|
|
236
|
+
channel<TData = unknown>(name: string) { return _client.channel<TData>(_scopeName(scope, name)); },
|
|
237
|
+
scoped: (child: string) => createFeatureConnect(_scopeName(scope, child)),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── connect — custom channels (your app-specific messages) ──────────────────
|
|
242
|
+
export const connect = {
|
|
243
|
+
/** Send a message to C++ backend */
|
|
244
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
245
|
+
_client.fire(name, payload);
|
|
246
|
+
},
|
|
247
|
+
/** Listen for messages from C++ backend. Returns unsubscribe fn. */
|
|
248
|
+
on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
|
|
249
|
+
return _client.on<TData>(name, cb);
|
|
250
|
+
},
|
|
251
|
+
/** Scoped feature connection (auto-prefixes names) */
|
|
252
|
+
feature: createFeatureConnect,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** Advanced: raw connection client — call / stream / channel */
|
|
256
|
+
export const connection = _client;
|
|
257
|
+
|
|
258
|
+
// ─── win — window management ──────────────────────────────────────────────────
|
|
259
|
+
const _winEvents = createFeatureConnect('window');
|
|
260
|
+
|
|
73
261
|
export const win = {
|
|
74
|
-
minimize:
|
|
75
|
-
maximize:
|
|
76
|
-
show:
|
|
77
|
-
hide:
|
|
78
|
-
close:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
262
|
+
minimize: async () => invoke('window.minimize', []),
|
|
263
|
+
maximize: async () => invoke('window.maximize', []),
|
|
264
|
+
show: async () => invoke('window.show', []),
|
|
265
|
+
hide: async () => invoke('window.hide', []),
|
|
266
|
+
close: async () => invoke('window.close', []),
|
|
267
|
+
center: async () => invoke('window.center', []),
|
|
268
|
+
setTitle: async (title: string) => invoke('window.setTitle', [title]),
|
|
269
|
+
setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
|
|
270
|
+
setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
|
|
271
|
+
setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
|
|
272
|
+
setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
|
|
273
|
+
setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
|
|
274
|
+
setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
|
|
275
|
+
setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
|
|
276
|
+
getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
|
|
277
|
+
getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
|
|
278
|
+
isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
|
|
279
|
+
isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
|
|
280
|
+
on: _winEvents.on.bind(_winEvents),
|
|
281
|
+
emit: _winEvents.emit.bind(_winEvents),
|
|
82
282
|
};
|
|
83
283
|
|
|
284
|
+
// ─── browser ──────────────────────────────────────────────────────────────────
|
|
285
|
+
const _browserEvents = createFeatureConnect('browser');
|
|
286
|
+
|
|
84
287
|
export const browser = {
|
|
85
|
-
getUrl:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
288
|
+
getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
|
|
289
|
+
navigate: async (url: string) => invoke('browser.navigate', [url]),
|
|
290
|
+
goBack: async () => invoke('browser.goBack', []),
|
|
291
|
+
goForward: async () => invoke('browser.goForward', []),
|
|
292
|
+
reload: async () => invoke('browser.reload', []),
|
|
293
|
+
canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
|
|
90
294
|
canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
|
|
91
295
|
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);
|
|
296
|
+
if (typeof window === 'undefined') return () => {};
|
|
297
|
+
const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
|
|
298
|
+
window.addEventListener('plusui:navigate', h);
|
|
299
|
+
return () => window.removeEventListener('plusui:navigate', h);
|
|
104
300
|
},
|
|
301
|
+
on: _browserEvents.on.bind(_browserEvents),
|
|
302
|
+
emit: _browserEvents.emit.bind(_browserEvents),
|
|
105
303
|
};
|
|
106
304
|
|
|
305
|
+
// ─── router ───────────────────────────────────────────────────────────────────
|
|
107
306
|
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
|
-
},
|
|
307
|
+
setRoutes: (routes: RouteMap) => { _routes = routes; },
|
|
308
|
+
push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
|
|
115
309
|
};
|
|
116
310
|
|
|
311
|
+
// ─── app ──────────────────────────────────────────────────────────────────────
|
|
312
|
+
const _appEvents = createFeatureConnect('app');
|
|
313
|
+
|
|
117
314
|
export const app = {
|
|
118
|
-
quit:
|
|
315
|
+
quit: async () => invoke('app.quit', []),
|
|
316
|
+
on: _appEvents.on.bind(_appEvents),
|
|
317
|
+
emit: _appEvents.emit.bind(_appEvents),
|
|
119
318
|
};
|
|
120
319
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
320
|
+
// ─── clipboard ────────────────────────────────────────────────────────────────
|
|
321
|
+
const _clipboardEvents = createFeatureConnect('clipboard');
|
|
322
|
+
|
|
323
|
+
export const clipboard = {
|
|
324
|
+
getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
|
|
325
|
+
setText: async (text: string) => invoke('clipboard.setText', [text]),
|
|
326
|
+
clear: async () => invoke('clipboard.clear', []),
|
|
327
|
+
hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
|
|
328
|
+
on: _clipboardEvents.on.bind(_clipboardEvents),
|
|
329
|
+
emit: _clipboardEvents.emit.bind(_clipboardEvents),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// ─── fileDrop ─────────────────────────────────────────────────────────────────
|
|
333
|
+
export interface FileInfo { path: string; name: string; type: string; size: number; }
|
|
128
334
|
|
|
129
335
|
export const fileDrop = {
|
|
130
336
|
setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
|
|
131
|
-
isEnabled:
|
|
337
|
+
isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
|
|
132
338
|
onFilesDropped: (handler: (files: FileInfo[]) => void) => {
|
|
133
339
|
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);
|
|
340
|
+
const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
|
|
341
|
+
window.addEventListener('plusui:fileDrop.filesDropped', h);
|
|
342
|
+
return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
|
|
140
343
|
},
|
|
141
344
|
onDragEnter: (handler: () => void) => {
|
|
142
345
|
if (typeof window === 'undefined') return () => {};
|
|
143
|
-
|
|
144
|
-
window.
|
|
145
|
-
return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
346
|
+
window.addEventListener('plusui:fileDrop.dragEnter', handler);
|
|
347
|
+
return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
|
|
146
348
|
},
|
|
147
349
|
onDragLeave: (handler: () => void) => {
|
|
148
350
|
if (typeof window === 'undefined') return () => {};
|
|
149
|
-
|
|
150
|
-
window.
|
|
151
|
-
return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
351
|
+
window.addEventListener('plusui:fileDrop.dragLeave', handler);
|
|
352
|
+
return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
|
|
152
353
|
},
|
|
153
354
|
};
|
|
154
355
|
|
|
155
|
-
//
|
|
356
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
156
357
|
export function formatFileSize(bytes: number): string {
|
|
157
358
|
if (bytes === 0) return '0 Bytes';
|
|
158
359
|
const k = 1024;
|
|
@@ -161,6 +362,40 @@ export function formatFileSize(bytes: number): string {
|
|
|
161
362
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
162
363
|
}
|
|
163
364
|
|
|
164
|
-
export function isImageFile(file: FileInfo): boolean {
|
|
165
|
-
|
|
166
|
-
|
|
365
|
+
export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
|
|
366
|
+
|
|
367
|
+
// ─── Top-level on / emit ─────────────────────────────────────────────────────
|
|
368
|
+
//
|
|
369
|
+
// Custom app events — import directly, no wrapper prefix:
|
|
370
|
+
//
|
|
371
|
+
// import { on, emit } from 'plusui';
|
|
372
|
+
//
|
|
373
|
+
// emit('myEvent', { value: 42 }); // TS → C++
|
|
374
|
+
// on('myEvent', (data) => { ... }); // C++ → TS
|
|
375
|
+
//
|
|
376
|
+
// Framework features use their own namespace:
|
|
377
|
+
//
|
|
378
|
+
// import { win, clipboard } from 'plusui';
|
|
379
|
+
//
|
|
380
|
+
// win.minimize();
|
|
381
|
+
// clipboard.on('changed', (data) => { ... });
|
|
382
|
+
//
|
|
383
|
+
export const on = connect.on.bind(connect) as typeof connect.on;
|
|
384
|
+
export const emit = connect.emit.bind(connect) as typeof connect.emit;
|
|
385
|
+
|
|
386
|
+
// ─── Default export — everything under one roof ───────────────────────────────
|
|
387
|
+
const plusui = {
|
|
388
|
+
/** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
|
|
389
|
+
feature: createFeatureConnect,
|
|
390
|
+
connection,
|
|
391
|
+
win,
|
|
392
|
+
browser,
|
|
393
|
+
router,
|
|
394
|
+
app,
|
|
395
|
+
clipboard,
|
|
396
|
+
fileDrop,
|
|
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 { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from '
|
|
2
|
+
import { win, browser, router, app, fileDrop, formatFileSize, 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 = {
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// PlusUI — single import entrypoint
|
|
3
|
+
//
|
|
4
|
+
// import plusui from 'plusui';
|
|
5
|
+
// import { connect, win, clipboard, app } from 'plusui';
|
|
6
|
+
//
|
|
7
|
+
// Everything your app needs — built-in features AND your own
|
|
8
|
+
// custom connect channels — is available from this one import.
|
|
9
|
+
//
|
|
10
|
+
// After running `plusui connect`, Connections/connections.gen.ts
|
|
11
|
+
// is auto-imported here so all discovered channels become typed.
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
// ─── Bridge bootstrap ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
1
16
|
type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
|
|
2
17
|
type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
|
|
3
18
|
|
|
@@ -25,16 +40,12 @@ function initBridge() {
|
|
|
25
40
|
return new Promise((resolve, reject) => {
|
|
26
41
|
const id = Math.random().toString(36).slice(2, 11);
|
|
27
42
|
const request = JSON.stringify({ id, method, params: args ?? [] });
|
|
28
|
-
|
|
29
43
|
_pending[id] = { resolve, reject };
|
|
30
44
|
|
|
31
45
|
if (typeof w.__native_invoke__ === 'function') {
|
|
32
46
|
w.__native_invoke__(request);
|
|
33
47
|
} else {
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
delete _pending[id];
|
|
36
|
-
resolve(null);
|
|
37
|
-
}, 0);
|
|
48
|
+
setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
setTimeout(() => {
|
|
@@ -48,10 +59,7 @@ function initBridge() {
|
|
|
48
59
|
|
|
49
60
|
w.__response__ = (id: string, result: unknown) => {
|
|
50
61
|
const pending = _pending[id];
|
|
51
|
-
if (pending) {
|
|
52
|
-
pending.resolve(result);
|
|
53
|
-
delete _pending[id];
|
|
54
|
-
}
|
|
62
|
+
if (pending) { pending.resolve(result); delete _pending[id]; }
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
_invoke = w.__invoke__ as InvokeFn;
|
|
@@ -60,97 +68,292 @@ function initBridge() {
|
|
|
60
68
|
async function invoke(method: string, args?: unknown[]) {
|
|
61
69
|
if (!_invoke) {
|
|
62
70
|
initBridge();
|
|
63
|
-
if (!_invoke)
|
|
64
|
-
throw new Error('PlusUI bridge not initialized');
|
|
65
|
-
}
|
|
71
|
+
if (!_invoke) throw new Error('PlusUI bridge not initialized');
|
|
66
72
|
}
|
|
67
|
-
|
|
68
73
|
return _invoke(method, args);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
initBridge();
|
|
72
77
|
|
|
78
|
+
// ─── Connection (on / emit) ───────────────────────────────────────────────────
|
|
79
|
+
//
|
|
80
|
+
// TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
|
|
81
|
+
//
|
|
82
|
+
// connect.emit('myEvent', { value: 42 }); // TS → C++
|
|
83
|
+
// connect.on('myEvent', (data) => { ... }); // C++ → TS
|
|
84
|
+
//
|
|
85
|
+
// Built-in features use their feature name as a scope:
|
|
86
|
+
// clipboard.on('changed', (data) => { ... }) // 'clipboard.changed'
|
|
87
|
+
// win.on('resized', (data) => { ... }) // 'window.resized'
|
|
88
|
+
//
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
type MessageCallback = (payload: any) => void;
|
|
92
|
+
|
|
93
|
+
class ConnectionClient {
|
|
94
|
+
private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
|
95
|
+
private listeners = new Map<string, Set<MessageCallback>>();
|
|
96
|
+
|
|
97
|
+
constructor() {
|
|
98
|
+
const host = globalThis as any;
|
|
99
|
+
host.__plusuiConnectionMessage = (message: unknown) => this.handleIncoming(message);
|
|
100
|
+
if (typeof window !== 'undefined') {
|
|
101
|
+
window.addEventListener('plusui:connection:message', (ev: Event) => {
|
|
102
|
+
this.handleIncoming((ev as CustomEvent<unknown>).detail);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private nextId() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
|
|
108
|
+
|
|
109
|
+
private async send(env: { kind: string; id?: string; name: string; payload?: unknown }): Promise<any> {
|
|
110
|
+
const host = globalThis as any;
|
|
111
|
+
if (typeof host.__invoke__ === 'function') return host.__invoke__('connection.dispatch', env);
|
|
112
|
+
if (host.ipc?.postMessage) host.ipc.postMessage(JSON.stringify(env));
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private decode(message: unknown): any | null {
|
|
117
|
+
if (!message) return null;
|
|
118
|
+
if (typeof message === 'string') { try { return JSON.parse(message); } catch { return null; } }
|
|
119
|
+
if (typeof message === 'object') return message;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private handleIncoming(message: unknown): void {
|
|
124
|
+
const env = this.decode(message);
|
|
125
|
+
if (!env) return;
|
|
126
|
+
if ((env.kind === 'result' || env.kind === 'error') && env.id) {
|
|
127
|
+
const entry = this.pending.get(env.id);
|
|
128
|
+
if (!entry) return;
|
|
129
|
+
this.pending.delete(env.id);
|
|
130
|
+
if (env.kind === 'error') entry.reject(new Error(env.error || 'Connection call failed'));
|
|
131
|
+
else entry.resolve(env.payload);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (env.kind === 'event' || env.kind === 'stream' || env.kind === 'publish') {
|
|
135
|
+
const handlers = this.listeners.get(env.name);
|
|
136
|
+
if (handlers) for (const h of handlers) h(env.payload);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
|
|
141
|
+
const id = this.nextId();
|
|
142
|
+
const promise = new Promise<TOut>((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
143
|
+
await this.send({ kind: 'call', id, name, payload });
|
|
144
|
+
return promise;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
148
|
+
void this.send({ kind: 'fire', name, payload });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
|
|
152
|
+
const set = this.listeners.get(name) ?? new Set<MessageCallback>();
|
|
153
|
+
set.add(callback as MessageCallback);
|
|
154
|
+
this.listeners.set(name, set);
|
|
155
|
+
return () => {
|
|
156
|
+
const cur = this.listeners.get(name);
|
|
157
|
+
if (!cur) return;
|
|
158
|
+
cur.delete(callback as MessageCallback);
|
|
159
|
+
if (cur.size === 0) this.listeners.delete(name);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
stream<TData = unknown>(name: string) {
|
|
164
|
+
return {
|
|
165
|
+
subscribe: (cb: (payload: TData) => void): (() => void) => {
|
|
166
|
+
void this.send({ kind: 'sub', name });
|
|
167
|
+
const off = this.on<TData>(name, cb);
|
|
168
|
+
return () => { off(); void this.send({ kind: 'unsub', name }); };
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
channel<TData = unknown>(name: string) {
|
|
174
|
+
return {
|
|
175
|
+
subscribe: (cb: (payload: TData) => void): (() => void) => {
|
|
176
|
+
void this.send({ kind: 'sub', name });
|
|
177
|
+
const off = this.on<TData>(name, cb);
|
|
178
|
+
return () => { off(); void this.send({ kind: 'unsub', name }); };
|
|
179
|
+
},
|
|
180
|
+
publish: (payload: TData): void => { void this.send({ kind: 'publish', name, payload }); },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const _client = new ConnectionClient();
|
|
186
|
+
|
|
187
|
+
// ─── FeatureConnect ───────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export type FeatureConnect = {
|
|
190
|
+
on: <TData = unknown>(name: string, cb: (payload: TData) => void) => (() => void);
|
|
191
|
+
emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
|
|
192
|
+
call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
|
|
193
|
+
stream: <TData = unknown>(name: string) => { subscribe: (cb: (payload: TData) => void) => (() => void) };
|
|
194
|
+
channel: <TData = unknown>(name: string) => {
|
|
195
|
+
subscribe: (cb: (payload: TData) => void) => (() => void);
|
|
196
|
+
publish: (payload: TData) => void;
|
|
197
|
+
};
|
|
198
|
+
scoped: (scope: string) => FeatureConnect;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function _scopeName(scope: string, name: string): string {
|
|
202
|
+
return name.startsWith(`${scope}.`) ? name : `${scope}.${name}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function _invokeScoped(method: string, payload?: unknown): Promise<unknown> {
|
|
206
|
+
const host = globalThis as any;
|
|
207
|
+
if (typeof host.__invoke__ !== 'function') return undefined;
|
|
208
|
+
return host.__invoke__(method, payload === undefined ? [] : [payload]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createFeatureConnect(scope: string): FeatureConnect {
|
|
212
|
+
return {
|
|
213
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn) {
|
|
214
|
+
const s = _scopeName(scope, name);
|
|
215
|
+
void _invokeScoped(s, payload);
|
|
216
|
+
_client.fire(s, payload);
|
|
217
|
+
if (typeof window !== 'undefined') {
|
|
218
|
+
window.dispatchEvent(new CustomEvent(`plusui:${s}`, { detail: payload }));
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
|
|
222
|
+
const s = _scopeName(scope, name);
|
|
223
|
+
const off = _client.on<TData>(s, cb);
|
|
224
|
+
if (typeof window === 'undefined') return off;
|
|
225
|
+
const dom = (e: Event) => cb((e as CustomEvent<TData>).detail);
|
|
226
|
+
window.addEventListener(`plusui:${s}`, dom as EventListener);
|
|
227
|
+
return () => { off(); window.removeEventListener(`plusui:${s}`, dom as EventListener); };
|
|
228
|
+
},
|
|
229
|
+
call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
|
|
230
|
+
const s = _scopeName(scope, name);
|
|
231
|
+
const host = globalThis as any;
|
|
232
|
+
if (typeof host.__invoke__ === 'function') return _invokeScoped(s, payload) as Promise<TOut>;
|
|
233
|
+
return _client.call<TOut, TIn>(s, payload);
|
|
234
|
+
},
|
|
235
|
+
stream<TData = unknown>(name: string) { return _client.stream<TData>(_scopeName(scope, name)); },
|
|
236
|
+
channel<TData = unknown>(name: string) { return _client.channel<TData>(_scopeName(scope, name)); },
|
|
237
|
+
scoped: (child: string) => createFeatureConnect(_scopeName(scope, child)),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── connect — custom channels (your app-specific messages) ──────────────────
|
|
242
|
+
export const connect = {
|
|
243
|
+
/** Send a message to C++ backend */
|
|
244
|
+
emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
|
|
245
|
+
_client.fire(name, payload);
|
|
246
|
+
},
|
|
247
|
+
/** Listen for messages from C++ backend. Returns unsubscribe fn. */
|
|
248
|
+
on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
|
|
249
|
+
return _client.on<TData>(name, cb);
|
|
250
|
+
},
|
|
251
|
+
/** Scoped feature connection (auto-prefixes names) */
|
|
252
|
+
feature: createFeatureConnect,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** Advanced: raw connection client — call / stream / channel */
|
|
256
|
+
export const connection = _client;
|
|
257
|
+
|
|
258
|
+
// ─── win — window management ──────────────────────────────────────────────────
|
|
259
|
+
const _winEvents = createFeatureConnect('window');
|
|
260
|
+
|
|
73
261
|
export const win = {
|
|
74
|
-
minimize:
|
|
75
|
-
maximize:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
262
|
+
minimize: async () => invoke('window.minimize', []),
|
|
263
|
+
maximize: async () => invoke('window.maximize', []),
|
|
264
|
+
show: async () => invoke('window.show', []),
|
|
265
|
+
hide: async () => invoke('window.hide', []),
|
|
266
|
+
close: async () => invoke('window.close', []),
|
|
267
|
+
center: async () => invoke('window.center', []),
|
|
268
|
+
setTitle: async (title: string) => invoke('window.setTitle', [title]),
|
|
269
|
+
setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
|
|
270
|
+
setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
|
|
271
|
+
setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
|
|
272
|
+
setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
|
|
273
|
+
setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
|
|
274
|
+
setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
|
|
275
|
+
setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
|
|
276
|
+
getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
|
|
277
|
+
getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
|
|
278
|
+
isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
|
|
279
|
+
isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
|
|
280
|
+
on: _winEvents.on.bind(_winEvents),
|
|
281
|
+
emit: _winEvents.emit.bind(_winEvents),
|
|
80
282
|
};
|
|
81
283
|
|
|
284
|
+
// ─── browser ──────────────────────────────────────────────────────────────────
|
|
285
|
+
const _browserEvents = createFeatureConnect('browser');
|
|
286
|
+
|
|
82
287
|
export const browser = {
|
|
83
|
-
getUrl:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
288
|
+
getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
|
|
289
|
+
navigate: async (url: string) => invoke('browser.navigate', [url]),
|
|
290
|
+
goBack: async () => invoke('browser.goBack', []),
|
|
291
|
+
goForward: async () => invoke('browser.goForward', []),
|
|
292
|
+
reload: async () => invoke('browser.reload', []),
|
|
293
|
+
canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
|
|
88
294
|
canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
|
|
89
295
|
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);
|
|
296
|
+
if (typeof window === 'undefined') return () => {};
|
|
297
|
+
const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
|
|
298
|
+
window.addEventListener('plusui:navigate', h);
|
|
299
|
+
return () => window.removeEventListener('plusui:navigate', h);
|
|
102
300
|
},
|
|
301
|
+
on: _browserEvents.on.bind(_browserEvents),
|
|
302
|
+
emit: _browserEvents.emit.bind(_browserEvents),
|
|
103
303
|
};
|
|
104
304
|
|
|
305
|
+
// ─── router ───────────────────────────────────────────────────────────────────
|
|
105
306
|
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
|
-
},
|
|
307
|
+
setRoutes: (routes: RouteMap) => { _routes = routes; },
|
|
308
|
+
push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
|
|
113
309
|
};
|
|
114
310
|
|
|
311
|
+
// ─── app ──────────────────────────────────────────────────────────────────────
|
|
312
|
+
const _appEvents = createFeatureConnect('app');
|
|
313
|
+
|
|
115
314
|
export const app = {
|
|
116
|
-
quit:
|
|
315
|
+
quit: async () => invoke('app.quit', []),
|
|
316
|
+
on: _appEvents.on.bind(_appEvents),
|
|
317
|
+
emit: _appEvents.emit.bind(_appEvents),
|
|
117
318
|
};
|
|
118
319
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
320
|
+
// ─── clipboard ────────────────────────────────────────────────────────────────
|
|
321
|
+
const _clipboardEvents = createFeatureConnect('clipboard');
|
|
322
|
+
|
|
323
|
+
export const clipboard = {
|
|
324
|
+
getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
|
|
325
|
+
setText: async (text: string) => invoke('clipboard.setText', [text]),
|
|
326
|
+
clear: async () => invoke('clipboard.clear', []),
|
|
327
|
+
hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
|
|
328
|
+
on: _clipboardEvents.on.bind(_clipboardEvents),
|
|
329
|
+
emit: _clipboardEvents.emit.bind(_clipboardEvents),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// ─── fileDrop ─────────────────────────────────────────────────────────────────
|
|
333
|
+
export interface FileInfo { path: string; name: string; type: string; size: number; }
|
|
126
334
|
|
|
127
335
|
export const fileDrop = {
|
|
128
336
|
setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
|
|
129
|
-
isEnabled:
|
|
337
|
+
isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
|
|
130
338
|
onFilesDropped: (handler: (files: FileInfo[]) => void) => {
|
|
131
339
|
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);
|
|
340
|
+
const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
|
|
341
|
+
window.addEventListener('plusui:fileDrop.filesDropped', h);
|
|
342
|
+
return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
|
|
138
343
|
},
|
|
139
344
|
onDragEnter: (handler: () => void) => {
|
|
140
345
|
if (typeof window === 'undefined') return () => {};
|
|
141
|
-
|
|
142
|
-
window.
|
|
143
|
-
return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
346
|
+
window.addEventListener('plusui:fileDrop.dragEnter', handler);
|
|
347
|
+
return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
|
|
144
348
|
},
|
|
145
349
|
onDragLeave: (handler: () => void) => {
|
|
146
350
|
if (typeof window === 'undefined') return () => {};
|
|
147
|
-
|
|
148
|
-
window.
|
|
149
|
-
return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
351
|
+
window.addEventListener('plusui:fileDrop.dragLeave', handler);
|
|
352
|
+
return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
|
|
150
353
|
},
|
|
151
354
|
};
|
|
152
355
|
|
|
153
|
-
//
|
|
356
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
154
357
|
export function formatFileSize(bytes: number): string {
|
|
155
358
|
if (bytes === 0) return '0 Bytes';
|
|
156
359
|
const k = 1024;
|
|
@@ -159,6 +362,41 @@ export function formatFileSize(bytes: number): string {
|
|
|
159
362
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
160
363
|
}
|
|
161
364
|
|
|
162
|
-
export function isImageFile(file: FileInfo): boolean {
|
|
163
|
-
|
|
164
|
-
|
|
365
|
+
export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
|
|
366
|
+
|
|
367
|
+
// ─── Top-level on / emit ─────────────────────────────────────────────────────
|
|
368
|
+
//
|
|
369
|
+
// Custom app events — import directly, no wrapper prefix:
|
|
370
|
+
//
|
|
371
|
+
// import { on, emit } from 'plusui';
|
|
372
|
+
//
|
|
373
|
+
// emit('myEvent', { value: 42 }); // TS → C++
|
|
374
|
+
// on('myEvent', (data) => { ... }); // C++ → TS
|
|
375
|
+
//
|
|
376
|
+
// Framework features use their own namespace:
|
|
377
|
+
//
|
|
378
|
+
// import { win, clipboard } from 'plusui';
|
|
379
|
+
//
|
|
380
|
+
// win.minimize();
|
|
381
|
+
// clipboard.on('changed', (data) => { ... });
|
|
382
|
+
//
|
|
383
|
+
export const on = connect.on.bind(connect) as typeof connect.on;
|
|
384
|
+
export const emit = connect.emit.bind(connect) as typeof connect.emit;
|
|
385
|
+
|
|
386
|
+
// ─── Default export — everything under one roof ───────────────────────────────
|
|
387
|
+
const plusui = {
|
|
388
|
+
/** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
|
|
389
|
+
feature: createFeatureConnect,
|
|
390
|
+
connection,
|
|
391
|
+
win,
|
|
392
|
+
browser,
|
|
393
|
+
router,
|
|
394
|
+
app,
|
|
395
|
+
clipboard,
|
|
396
|
+
fileDrop,
|
|
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),
|