plusui-native 0.2.65 → 0.2.68
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/doctor/index.js +9 -0
- package/src/doctor/installers/windows.js +32 -5
- package/src/index.js +1 -1
- package/templates/react/frontend/src/App.tsx +7 -7
- package/templates/react/frontend/src/plusui.ts +535 -99
- package/templates/react/frontend/tsconfig.json +15 -8
- package/templates/react/frontend/vite.config.ts +3 -3
- package/templates/react/main.cpp.template +18 -3
- package/templates/solid/frontend/tsconfig.json +14 -7
- package/templates/solid/frontend/vite.config.ts +2 -3
- package/templates/solid/main.cpp.template +18 -3
- package/templates/solid/frontend/src/plusui.ts +0 -402
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"useDefineForClassFields": true,
|
|
5
|
-
"lib": [
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2020",
|
|
7
|
+
"DOM",
|
|
8
|
+
"DOM.Iterable"
|
|
9
|
+
],
|
|
6
10
|
"module": "ESNext",
|
|
7
11
|
"skipLibCheck": true,
|
|
8
12
|
"moduleResolution": "bundler",
|
|
@@ -15,11 +19,14 @@
|
|
|
15
19
|
"noUnusedLocals": true,
|
|
16
20
|
"noUnusedParameters": true,
|
|
17
21
|
"noFallthroughCasesInSwitch": true,
|
|
18
|
-
"baseUrl": "."
|
|
19
|
-
"paths": {
|
|
20
|
-
"plusui": ["./src/plusui.ts"]
|
|
21
|
-
}
|
|
22
|
+
"baseUrl": "."
|
|
22
23
|
},
|
|
23
|
-
"include": [
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
"include": [
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"references": [
|
|
28
|
+
{
|
|
29
|
+
"path": "./tsconfig.node.json"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
|
-
import { fileURLToPath, URL } from 'node:url';
|
|
4
3
|
|
|
5
4
|
export default defineConfig({
|
|
6
5
|
plugins: [react()],
|
|
7
6
|
resolve: {
|
|
8
7
|
alias: {
|
|
9
|
-
// `import plusui from 'plusui'` resolves to
|
|
10
|
-
plusui:
|
|
8
|
+
// `import plusui from 'plusui'` resolves to the installed plusui-native-core package
|
|
9
|
+
plusui: 'plusui-native-core',
|
|
11
10
|
},
|
|
12
11
|
},
|
|
13
12
|
build: {
|
|
@@ -19,3 +18,4 @@ export default defineConfig({
|
|
|
19
18
|
strictPort: true,
|
|
20
19
|
},
|
|
21
20
|
});
|
|
21
|
+
|
|
@@ -70,6 +70,13 @@ struct WindowConfig {
|
|
|
70
70
|
|
|
71
71
|
// --- WebView ---
|
|
72
72
|
bool scrollbars = false; // Show scrollbars in webview (false = hidden)
|
|
73
|
+
|
|
74
|
+
// --- Routing ---
|
|
75
|
+
// The frontend route this window opens on.
|
|
76
|
+
// Use "/" for root, "/settings" for settings page, etc.
|
|
77
|
+
// Works with any frontend router (React Router, TanStack, Solid Router).
|
|
78
|
+
std::string route = "/"; // Starting route (e.g. "/", "/settings", "/profile")
|
|
79
|
+
int devServerPort = 5173; // Vite dev server port
|
|
73
80
|
} windowConfig;
|
|
74
81
|
|
|
75
82
|
// ----------------------------------------------------------------------------
|
|
@@ -165,12 +172,20 @@ int main() {
|
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
// ========================================
|
|
168
|
-
// NAVIGATION
|
|
175
|
+
// NAVIGATION — Load Frontend Route
|
|
169
176
|
// ========================================
|
|
177
|
+
// The window loads the route from windowConfig.route.
|
|
178
|
+
// In dev mode: http://localhost:{port}{route}
|
|
179
|
+
// In production: file://frontend/dist/index.html#{route}
|
|
170
180
|
#ifdef PLUSUI_DEV_MODE
|
|
171
|
-
|
|
181
|
+
std::string url = "http://localhost:" + std::to_string(windowConfig.devServerPort) + windowConfig.route;
|
|
182
|
+
mainWindow.navigate(url);
|
|
172
183
|
#else
|
|
173
|
-
|
|
184
|
+
if (windowConfig.route == "/") {
|
|
185
|
+
mainWindow.loadFile("frontend/dist/index.html");
|
|
186
|
+
} else {
|
|
187
|
+
mainWindow.loadFile("frontend/dist/index.html#" + windowConfig.route);
|
|
188
|
+
}
|
|
174
189
|
#endif
|
|
175
190
|
|
|
176
191
|
// ========================================
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"useDefineForClassFields": true,
|
|
5
|
-
"lib": [
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2020",
|
|
7
|
+
"DOM",
|
|
8
|
+
"DOM.Iterable"
|
|
9
|
+
],
|
|
6
10
|
"module": "ESNext",
|
|
7
11
|
"skipLibCheck": true,
|
|
8
12
|
"moduleResolution": "bundler",
|
|
@@ -16,11 +20,14 @@
|
|
|
16
20
|
"noUnusedLocals": true,
|
|
17
21
|
"noUnusedParameters": true,
|
|
18
22
|
"noFallthroughCasesInSwitch": true,
|
|
19
|
-
"baseUrl": "."
|
|
20
|
-
"paths": {
|
|
21
|
-
"plusui": ["./src/plusui.ts"]
|
|
22
|
-
}
|
|
23
|
+
"baseUrl": "."
|
|
23
24
|
},
|
|
24
|
-
"include": [
|
|
25
|
-
|
|
25
|
+
"include": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"references": [
|
|
29
|
+
{
|
|
30
|
+
"path": "./tsconfig.node.json"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
26
33
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
import solid from 'vite-plugin-solid';
|
|
3
|
-
import { fileURLToPath, URL } from 'node:url';
|
|
4
3
|
|
|
5
4
|
export default defineConfig({
|
|
6
5
|
plugins: [solid()],
|
|
7
6
|
resolve: {
|
|
8
7
|
alias: {
|
|
9
|
-
// `import plusui from 'plusui'` resolves to
|
|
10
|
-
plusui:
|
|
8
|
+
// `import plusui from 'plusui'` resolves to the installed plusui-native-core package
|
|
9
|
+
plusui: 'plusui-native-core',
|
|
11
10
|
},
|
|
12
11
|
},
|
|
13
12
|
build: {
|
|
@@ -69,6 +69,13 @@ struct WindowConfig {
|
|
|
69
69
|
|
|
70
70
|
// --- WebView ---
|
|
71
71
|
bool scrollbars = false; // Show scrollbars in webview (false = hidden)
|
|
72
|
+
|
|
73
|
+
// --- Routing ---
|
|
74
|
+
// The frontend route this window opens on.
|
|
75
|
+
// Use "/" for root, "/settings" for settings page, etc.
|
|
76
|
+
// Works with any frontend router (Solid Router, TanStack, etc.).
|
|
77
|
+
std::string route = "/"; // Starting route (e.g. "/", "/settings", "/profile")
|
|
78
|
+
int devServerPort = 5173; // Vite dev server port
|
|
72
79
|
} windowConfig;
|
|
73
80
|
|
|
74
81
|
// ----------------------------------------------------------------------------
|
|
@@ -159,12 +166,20 @@ int main() {
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
// ========================================
|
|
162
|
-
// NAVIGATION
|
|
169
|
+
// NAVIGATION — Load Frontend Route
|
|
163
170
|
// ========================================
|
|
171
|
+
// The window loads the route from windowConfig.route.
|
|
172
|
+
// In dev mode: http://localhost:{port}{route}
|
|
173
|
+
// In production: file://frontend/dist/index.html#{route}
|
|
164
174
|
#ifdef PLUSUI_DEV_MODE
|
|
165
|
-
|
|
175
|
+
std::string url = "http://localhost:" + std::to_string(windowConfig.devServerPort) + windowConfig.route;
|
|
176
|
+
mainWindow.navigate(url);
|
|
166
177
|
#else
|
|
167
|
-
|
|
178
|
+
if (windowConfig.route == "/") {
|
|
179
|
+
mainWindow.loadFile("frontend/dist/index.html");
|
|
180
|
+
} else {
|
|
181
|
+
mainWindow.loadFile("frontend/dist/index.html#" + windowConfig.route);
|
|
182
|
+
}
|
|
168
183
|
#endif
|
|
169
184
|
|
|
170
185
|
// ========================================
|
|
@@ -1,402 +0,0 @@
|
|
|
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
|
-
|
|
20
|
-
type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
|
|
21
|
-
type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
|
|
22
|
-
|
|
23
|
-
type WindowSize = { width: number; height: number };
|
|
24
|
-
type WindowPosition = { x: number; y: number };
|
|
25
|
-
type RouteMap = Record<string, string>;
|
|
26
|
-
|
|
27
|
-
let _invoke: InvokeFn | null = null;
|
|
28
|
-
let _pending: PendingMap = {};
|
|
29
|
-
let _routes: RouteMap = {};
|
|
30
|
-
|
|
31
|
-
function initBridge() {
|
|
32
|
-
if (typeof window === 'undefined') return;
|
|
33
|
-
|
|
34
|
-
const w = window as any;
|
|
35
|
-
if (typeof w.__invoke__ === 'function') {
|
|
36
|
-
_invoke = w.__invoke__ as InvokeFn;
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
_pending = {};
|
|
41
|
-
w.__pending__ = _pending;
|
|
42
|
-
|
|
43
|
-
w.__invoke__ = (method: string, args?: unknown[]): Promise<unknown> => {
|
|
44
|
-
return new Promise((resolve, reject) => {
|
|
45
|
-
const id = Math.random().toString(36).slice(2, 11);
|
|
46
|
-
const request = JSON.stringify({ id, method, params: args ?? [] });
|
|
47
|
-
_pending[id] = { resolve, reject };
|
|
48
|
-
|
|
49
|
-
if (typeof w.__native_invoke__ === 'function') {
|
|
50
|
-
w.__native_invoke__(request);
|
|
51
|
-
} else {
|
|
52
|
-
setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
if (_pending[id]) {
|
|
57
|
-
delete _pending[id];
|
|
58
|
-
reject(new Error(`${method} timed out`));
|
|
59
|
-
}
|
|
60
|
-
}, 30000);
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
w.__response__ = (id: string, result: unknown) => {
|
|
65
|
-
const pending = _pending[id];
|
|
66
|
-
if (pending) { pending.resolve(result); delete _pending[id]; }
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
_invoke = w.__invoke__ as InvokeFn;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function invoke(method: string, args?: unknown[]) {
|
|
73
|
-
if (!_invoke) {
|
|
74
|
-
initBridge();
|
|
75
|
-
if (!_invoke) throw new Error('PlusUI bridge not initialized');
|
|
76
|
-
}
|
|
77
|
-
return _invoke(method, args);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
initBridge();
|
|
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
|
-
|
|
265
|
-
export const win = {
|
|
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),
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// ─── browser ──────────────────────────────────────────────────────────────────
|
|
289
|
-
const _browserEvents = createFeatureConnect('browser');
|
|
290
|
-
|
|
291
|
-
export const browser = {
|
|
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>,
|
|
298
|
-
canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
|
|
299
|
-
onNavigate: (handler: (url: string) => void) => {
|
|
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);
|
|
304
|
-
},
|
|
305
|
-
on: _browserEvents.on.bind(_browserEvents),
|
|
306
|
-
emit: _browserEvents.emit.bind(_browserEvents),
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
// ─── router ───────────────────────────────────────────────────────────────────
|
|
310
|
-
export const router = {
|
|
311
|
-
setRoutes: (routes: RouteMap) => { _routes = routes; },
|
|
312
|
-
push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
// ─── app ──────────────────────────────────────────────────────────────────────
|
|
316
|
-
const _appEvents = createFeatureConnect('app');
|
|
317
|
-
|
|
318
|
-
export const app = {
|
|
319
|
-
quit: async () => invoke('app.quit', []),
|
|
320
|
-
on: _appEvents.on.bind(_appEvents),
|
|
321
|
-
emit: _appEvents.emit.bind(_appEvents),
|
|
322
|
-
};
|
|
323
|
-
|
|
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; }
|
|
338
|
-
|
|
339
|
-
export const fileDrop = {
|
|
340
|
-
setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
|
|
341
|
-
isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
|
|
342
|
-
onFilesDropped: (handler: (files: FileInfo[]) => void) => {
|
|
343
|
-
if (typeof window === 'undefined') return () => {};
|
|
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);
|
|
347
|
-
},
|
|
348
|
-
onDragEnter: (handler: () => void) => {
|
|
349
|
-
if (typeof window === 'undefined') return () => {};
|
|
350
|
-
window.addEventListener('plusui:fileDrop.dragEnter', handler);
|
|
351
|
-
return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
|
|
352
|
-
},
|
|
353
|
-
onDragLeave: (handler: () => void) => {
|
|
354
|
-
if (typeof window === 'undefined') return () => {};
|
|
355
|
-
window.addEventListener('plusui:fileDrop.dragLeave', handler);
|
|
356
|
-
return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
|
|
357
|
-
},
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
361
|
-
export function formatFileSize(bytes: number): string {
|
|
362
|
-
if (bytes === 0) return '0 Bytes';
|
|
363
|
-
const k = 1024;
|
|
364
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
365
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
366
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
|
|
370
|
-
|
|
371
|
-
// ─── Top-level on / emit ─────────────────────────────────────────────────────
|
|
372
|
-
//
|
|
373
|
-
// import plusui from 'plusui';
|
|
374
|
-
//
|
|
375
|
-
// plusui.emit('myEvent', { value: 42 }); // TS → C++
|
|
376
|
-
// plusui.on('myEvent', (data) => { ... }); // C++ → TS
|
|
377
|
-
//
|
|
378
|
-
// plusui.win.minimize();
|
|
379
|
-
// plusui.clipboard.on('changed', (data) => { ... });
|
|
380
|
-
//
|
|
381
|
-
export const on = connect.on.bind(connect) as typeof connect.on;
|
|
382
|
-
export const emit = connect.emit.bind(connect) as typeof connect.emit;
|
|
383
|
-
|
|
384
|
-
// ─── Default export — everything under one roof ───────────────────────────────
|
|
385
|
-
const plusui = {
|
|
386
|
-
/** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
|
|
387
|
-
feature: createFeatureConnect,
|
|
388
|
-
connection,
|
|
389
|
-
win,
|
|
390
|
-
browser,
|
|
391
|
-
router,
|
|
392
|
-
app,
|
|
393
|
-
clipboard,
|
|
394
|
-
fileDrop,
|
|
395
|
-
formatFileSize,
|
|
396
|
-
isImageFile,
|
|
397
|
-
on,
|
|
398
|
-
emit,
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
export default plusui;
|
|
402
|
-
|