electrobun 0.0.19-beta.99 → 0.1.0
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/README.md +1 -1
- package/dist/api/browser/webviewtag.ts +54 -2
- package/dist/api/bun/ElectrobunConfig.ts +171 -0
- package/dist/api/bun/core/BrowserWindow.ts +4 -0
- package/dist/api/bun/core/Tray.ts +14 -0
- package/dist/api/bun/core/Updater.ts +4 -3
- package/dist/api/bun/index.ts +2 -0
- package/dist/api/bun/proc/native.ts +107 -5
- package/dist/main.js +5 -4
- package/package.json +4 -2
- package/src/cli/index.ts +565 -148
- package/templates/hello-world/bun.lock +164 -2
- package/templates/hello-world/electrobun.config.ts +28 -0
- package/templates/hello-world/src/bun/index.ts +2 -2
- package/templates/hello-world/src/mainview/index.html +5 -6
- package/templates/hello-world/src/mainview/index.ts +1 -5
- package/templates/interactive-playground/README.md +26 -0
- package/templates/interactive-playground/assets/tray-icon.png +0 -0
- package/templates/interactive-playground/electrobun.config.ts +36 -0
- package/templates/interactive-playground/package-lock.json +36 -0
- package/templates/interactive-playground/package.json +15 -0
- package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
- package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
- package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
- package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
- package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
- package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
- package/templates/interactive-playground/src/bun/index.ts +124 -0
- package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
- package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
- package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
- package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
- package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
- package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
- package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
- package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
- package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
- package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
- package/templates/interactive-playground/src/mainview/index.css +538 -0
- package/templates/interactive-playground/src/mainview/index.html +103 -0
- package/templates/interactive-playground/src/mainview/index.ts +238 -0
- package/templates/multitab-browser/README.md +34 -0
- package/templates/multitab-browser/bun.lock +224 -0
- package/templates/multitab-browser/electrobun.config.ts +32 -0
- package/templates/multitab-browser/package-lock.json +20 -0
- package/templates/multitab-browser/package.json +12 -0
- package/templates/multitab-browser/src/bun/index.ts +144 -0
- package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
- package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
- package/templates/multitab-browser/src/mainview/index.css +487 -0
- package/templates/multitab-browser/src/mainview/index.html +94 -0
- package/templates/multitab-browser/src/mainview/index.ts +634 -0
- package/templates/photo-booth/README.md +108 -0
- package/templates/photo-booth/bun.lock +239 -0
- package/templates/photo-booth/electrobun.config.ts +28 -0
- package/templates/photo-booth/package.json +16 -0
- package/templates/photo-booth/src/bun/index.ts +92 -0
- package/templates/photo-booth/src/mainview/index.css +465 -0
- package/templates/photo-booth/src/mainview/index.html +124 -0
- package/templates/photo-booth/src/mainview/index.ts +499 -0
- package/tests/bun.lock +14 -0
- package/tests/electrobun.config.ts +45 -0
- package/tests/package-lock.json +36 -0
- package/tests/package.json +13 -0
- package/tests/src/bun/index.ts +100 -0
- package/tests/src/bun/test-runner.ts +508 -0
- package/tests/src/mainview/index.html +110 -0
- package/tests/src/mainview/index.ts +458 -0
- package/tests/src/mainview/styles/main.css +451 -0
- package/tests/src/testviews/tray-test.html +57 -0
- package/tests/src/testviews/webview-mask.html +114 -0
- package/tests/src/testviews/webview-navigation.html +36 -0
- package/tests/src/testviews/window-create.html +17 -0
- package/tests/src/testviews/window-events.html +29 -0
- package/tests/src/testviews/window-focus.html +37 -0
- package/tests/src/webviewtag/index.ts +11 -0
- package/templates/hello-world/electrobun.config +0 -18
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class Sidebar {
|
|
2
|
+
private currentDemo: string = 'windows';
|
|
3
|
+
private onDemoChange?: (demo: string) => void;
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
this.initializeEventListeners();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
private initializeEventListeners() {
|
|
10
|
+
const navItems = document.querySelectorAll('.nav-item');
|
|
11
|
+
|
|
12
|
+
navItems.forEach(item => {
|
|
13
|
+
item.addEventListener('click', () => {
|
|
14
|
+
const demo = item.getAttribute('data-demo');
|
|
15
|
+
if (demo && demo !== this.currentDemo) {
|
|
16
|
+
this.setActiveDemo(demo);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Search functionality
|
|
22
|
+
const searchInput = document.getElementById('search') as HTMLInputElement;
|
|
23
|
+
searchInput?.addEventListener('input', (e) => {
|
|
24
|
+
this.filterDemos((e.target as HTMLInputElement).value);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setActiveDemo(demo: string) {
|
|
29
|
+
// Remove active class from all items
|
|
30
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
31
|
+
item.classList.remove('active');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Add active class to selected item
|
|
35
|
+
const selectedItem = document.querySelector(`[data-demo="${demo}"]`);
|
|
36
|
+
selectedItem?.classList.add('active');
|
|
37
|
+
|
|
38
|
+
this.currentDemo = demo;
|
|
39
|
+
this.onDemoChange?.(demo);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private filterDemos(query: string) {
|
|
43
|
+
const lowerQuery = query.toLowerCase();
|
|
44
|
+
const navItems = document.querySelectorAll('.nav-item');
|
|
45
|
+
|
|
46
|
+
navItems.forEach(item => {
|
|
47
|
+
const label = item.querySelector('.nav-label')?.textContent?.toLowerCase() || '';
|
|
48
|
+
const demo = item.getAttribute('data-demo')?.toLowerCase() || '';
|
|
49
|
+
|
|
50
|
+
if (label.includes(lowerQuery) || demo.includes(lowerQuery)) {
|
|
51
|
+
(item as HTMLElement).style.display = 'flex';
|
|
52
|
+
} else {
|
|
53
|
+
(item as HTMLElement).style.display = 'none';
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onDemoChangeCallback(callback: (demo: string) => void) {
|
|
59
|
+
this.onDemoChange = callback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getCurrentDemo() {
|
|
63
|
+
return this.currentDemo;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ToastType = 'info' | 'success' | 'warning' | 'error';
|
|
2
|
+
|
|
3
|
+
export class Toast {
|
|
4
|
+
static show(message: string, type: ToastType = 'info', duration: number = 3000) {
|
|
5
|
+
const container = document.getElementById('toast-container');
|
|
6
|
+
if (!container) return;
|
|
7
|
+
|
|
8
|
+
const toast = document.createElement('div');
|
|
9
|
+
toast.className = `toast ${type}`;
|
|
10
|
+
toast.innerHTML = `
|
|
11
|
+
<div class="toast-content">
|
|
12
|
+
<div class="toast-message">${this.escapeHtml(message)}</div>
|
|
13
|
+
</div>
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
container.appendChild(toast);
|
|
17
|
+
|
|
18
|
+
// Auto-remove after duration
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
this.remove(toast);
|
|
21
|
+
}, duration);
|
|
22
|
+
|
|
23
|
+
// Allow manual dismissal
|
|
24
|
+
toast.addEventListener('click', () => {
|
|
25
|
+
this.remove(toast);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static remove(toast: HTMLElement) {
|
|
30
|
+
toast.style.animation = 'slideOut 0.3s ease forwards';
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
toast.remove();
|
|
33
|
+
}, 300);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static escapeHtml(text: string): string {
|
|
37
|
+
const div = document.createElement('div');
|
|
38
|
+
div.textContent = text;
|
|
39
|
+
return div.innerHTML;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static success(message: string, duration?: number) {
|
|
43
|
+
this.show(message, 'success', duration);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static error(message: string, duration?: number) {
|
|
47
|
+
this.show(message, 'error', duration);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static warning(message: string, duration?: number) {
|
|
51
|
+
this.show(message, 'warning', duration);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static info(message: string, duration?: number) {
|
|
55
|
+
this.show(message, 'info', duration);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
export class FileDemo {
|
|
2
|
+
private selectedFiles: string[] = [];
|
|
3
|
+
|
|
4
|
+
render() {
|
|
5
|
+
return `
|
|
6
|
+
<div class="demo-section">
|
|
7
|
+
<div class="demo-header">
|
|
8
|
+
<span class="demo-icon">🗂️</span>
|
|
9
|
+
<div>
|
|
10
|
+
<h2 class="demo-title">File Operations</h2>
|
|
11
|
+
<p class="demo-description">Test file dialogs, file system operations, and drag & drop functionality</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="demo-controls">
|
|
16
|
+
<h3>File Dialog</h3>
|
|
17
|
+
<div class="control-group">
|
|
18
|
+
<label class="control-checkbox">
|
|
19
|
+
<input type="checkbox" id="file-multiple"> Allow multiple selection
|
|
20
|
+
</label>
|
|
21
|
+
|
|
22
|
+
<label class="control-label">File Types:</label>
|
|
23
|
+
<select id="file-types" class="control-input" style="width: 150px;">
|
|
24
|
+
<option value="">All Files (*)</option>
|
|
25
|
+
<option value="txt,md">Text Files</option>
|
|
26
|
+
<option value="png,jpg,jpeg,gif">Images</option>
|
|
27
|
+
<option value="pdf">PDF Files</option>
|
|
28
|
+
<option value="json,js,ts">Code Files</option>
|
|
29
|
+
</select>
|
|
30
|
+
|
|
31
|
+
<button class="btn btn-primary" id="open-file-dialog">Open File Dialog</button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<h3>File Operations</h3>
|
|
35
|
+
<div class="control-group">
|
|
36
|
+
<button class="btn btn-secondary" id="move-to-trash" disabled>Move Selected to Trash</button>
|
|
37
|
+
<button class="btn btn-secondary" id="show-in-finder" disabled>Show in Finder</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<h3>Drag & Drop Test</h3>
|
|
41
|
+
<div class="drop-zone" id="drop-zone" style="border: 2px dashed #cbd5e0; border-radius: 0.5rem; padding: 2rem; text-align: center; color: #718096; margin: 1rem 0; background: #f7fafc;">
|
|
42
|
+
<div>📁 Drag and drop files here</div>
|
|
43
|
+
<div style="font-size: 0.875rem; margin-top: 0.5rem;">Or click "Open File Dialog" above</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="demo-results">
|
|
48
|
+
<div class="results-header">Selected Files (<span id="file-count">0</span>):</div>
|
|
49
|
+
<div id="file-list" class="file-list">
|
|
50
|
+
<div class="no-files" style="text-align: center; color: #718096; padding: 2rem;">
|
|
51
|
+
No files selected yet.
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
initialize(rpc: any) {
|
|
60
|
+
const openDialogBtn = document.getElementById('open-file-dialog');
|
|
61
|
+
const moveToTrashBtn = document.getElementById('move-to-trash');
|
|
62
|
+
const showInFinderBtn = document.getElementById('show-in-finder');
|
|
63
|
+
const dropZone = document.getElementById('drop-zone');
|
|
64
|
+
|
|
65
|
+
openDialogBtn?.addEventListener('click', async () => {
|
|
66
|
+
const multiple = (document.getElementById('file-multiple') as HTMLInputElement).checked;
|
|
67
|
+
const fileTypes = (document.getElementById('file-types') as HTMLSelectElement).value;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const files = await rpc.request.openFileDialog({
|
|
71
|
+
multiple,
|
|
72
|
+
fileTypes: fileTypes ? fileTypes.split(',') : undefined
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.selectedFiles = files;
|
|
76
|
+
this.updateFileList();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error opening file dialog:', error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
moveToTrashBtn?.addEventListener('click', async () => {
|
|
83
|
+
if (this.selectedFiles.length === 0) return;
|
|
84
|
+
|
|
85
|
+
const confirmed = confirm(`Are you sure you want to move ${this.selectedFiles.length} file(s) to trash?`);
|
|
86
|
+
if (!confirmed) return;
|
|
87
|
+
|
|
88
|
+
for (const file of this.selectedFiles) {
|
|
89
|
+
try {
|
|
90
|
+
await rpc.request.moveToTrash(file);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(`Error moving ${file} to trash:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.selectedFiles = [];
|
|
97
|
+
this.updateFileList();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
showInFinderBtn?.addEventListener('click', async () => {
|
|
101
|
+
if (this.selectedFiles.length === 0) return;
|
|
102
|
+
|
|
103
|
+
// Show the first selected file in finder
|
|
104
|
+
try {
|
|
105
|
+
await rpc.request.showInFinder(this.selectedFiles[0]);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Error showing in finder:', error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Set up drag and drop
|
|
112
|
+
if (dropZone) {
|
|
113
|
+
this.setupDragAndDrop(dropZone);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private setupDragAndDrop(dropZone: HTMLElement) {
|
|
118
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
119
|
+
dropZone.addEventListener(eventName, (e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
['dragenter', 'dragover'].forEach(eventName => {
|
|
126
|
+
dropZone.addEventListener(eventName, () => {
|
|
127
|
+
dropZone.style.background = '#ebf8ff';
|
|
128
|
+
dropZone.style.borderColor = '#4299e1';
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
['dragleave', 'drop'].forEach(eventName => {
|
|
133
|
+
dropZone.addEventListener(eventName, () => {
|
|
134
|
+
dropZone.style.background = '#f7fafc';
|
|
135
|
+
dropZone.style.borderColor = '#cbd5e0';
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
dropZone.addEventListener('drop', (e) => {
|
|
140
|
+
const dt = e.dataTransfer;
|
|
141
|
+
if (!dt) return;
|
|
142
|
+
|
|
143
|
+
const files = Array.from(dt.files).map(file => file.path || file.name);
|
|
144
|
+
this.selectedFiles = files;
|
|
145
|
+
this.updateFileList();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private updateFileList() {
|
|
150
|
+
const container = document.getElementById('file-list');
|
|
151
|
+
const count = document.getElementById('file-count');
|
|
152
|
+
const moveToTrashBtn = document.getElementById('move-to-trash');
|
|
153
|
+
const showInFinderBtn = document.getElementById('show-in-finder');
|
|
154
|
+
|
|
155
|
+
if (!container || !count) return;
|
|
156
|
+
|
|
157
|
+
count.textContent = this.selectedFiles.length.toString();
|
|
158
|
+
|
|
159
|
+
// Enable/disable action buttons
|
|
160
|
+
const hasFiles = this.selectedFiles.length > 0;
|
|
161
|
+
moveToTrashBtn?.toggleAttribute('disabled', !hasFiles);
|
|
162
|
+
showInFinderBtn?.toggleAttribute('disabled', !hasFiles);
|
|
163
|
+
|
|
164
|
+
if (this.selectedFiles.length === 0) {
|
|
165
|
+
container.innerHTML = `
|
|
166
|
+
<div class="no-files" style="text-align: center; color: #718096; padding: 2rem;">
|
|
167
|
+
No files selected yet.
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
container.innerHTML = this.selectedFiles.map((file, index) => `
|
|
174
|
+
<div class="file-item" style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 1rem; margin-bottom: 0.5rem;">
|
|
175
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
176
|
+
<div style="flex: 1; min-width: 0;">
|
|
177
|
+
<div style="font-weight: 500; color: #2d3748; margin-bottom: 0.25rem;">
|
|
178
|
+
${this.getFileName(file)}
|
|
179
|
+
</div>
|
|
180
|
+
<div style="color: #718096; font-size: 0.875rem; word-break: break-all;">
|
|
181
|
+
${file}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div style="margin-left: 1rem;">
|
|
185
|
+
<button class="btn btn-small btn-secondary remove-file" data-index="${index}">Remove</button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
`).join('');
|
|
190
|
+
|
|
191
|
+
// Add remove button listeners
|
|
192
|
+
const removeButtons = document.querySelectorAll('.remove-file');
|
|
193
|
+
removeButtons.forEach(btn => {
|
|
194
|
+
btn.addEventListener('click', (e) => {
|
|
195
|
+
const index = parseInt((e.target as HTMLElement).getAttribute('data-index') || '0');
|
|
196
|
+
this.selectedFiles.splice(index, 1);
|
|
197
|
+
this.updateFileList();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private getFileName(path: string): string {
|
|
203
|
+
return path.split('/').pop() || path.split('\\').pop() || path;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle events from the backend
|
|
207
|
+
onFileSelected(data: { paths: string[] }) {
|
|
208
|
+
this.selectedFiles = data.paths;
|
|
209
|
+
this.updateFileList();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export class MenuDemo {
|
|
2
|
+
private rpc: any;
|
|
3
|
+
|
|
4
|
+
render() {
|
|
5
|
+
return `
|
|
6
|
+
<div class="demo-section">
|
|
7
|
+
<div class="demo-header">
|
|
8
|
+
<span class="demo-icon">🎛️</span>
|
|
9
|
+
<div>
|
|
10
|
+
<h2 class="demo-title">Menu Systems</h2>
|
|
11
|
+
<p class="demo-description">Test application menus and context menus</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="demo-controls">
|
|
16
|
+
<h3>Application Menu</h3>
|
|
17
|
+
<div class="control-group">
|
|
18
|
+
<div style="padding: 1rem; background: #f7fafc; border-radius: 0.5rem;">
|
|
19
|
+
<p style="color: #4a5568; margin-bottom: 0.5rem;">
|
|
20
|
+
The application menu is automatically set up. Look at the menu bar at the top of your screen.
|
|
21
|
+
</p>
|
|
22
|
+
<p style="color: #718096; font-size: 0.875rem;">
|
|
23
|
+
Try: Edit → Custom Menu Item 🚀
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<h3>Context Menu</h3>
|
|
29
|
+
<div class="control-group">
|
|
30
|
+
<button class="btn btn-primary" id="show-context-menu">Show Context Menu</button>
|
|
31
|
+
<span style="color: #718096; font-size: 0.875rem;">Click to show a context menu at cursor position</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="control-group">
|
|
35
|
+
<div style="padding: 1rem; background: #ebf8ff; border: 1px solid #90cdf4; border-radius: 0.5rem;">
|
|
36
|
+
<p style="color: #2b6cb0; font-size: 0.875rem;">
|
|
37
|
+
<strong>Tip:</strong> You can also trigger context menus from your app by right-clicking in your UI and preventing the default action.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="demo-results">
|
|
44
|
+
<div class="results-header">Menu Events:</div>
|
|
45
|
+
<div id="menu-events" class="menu-events">
|
|
46
|
+
<div class="no-events" style="text-align: center; color: #718096; padding: 2rem;">
|
|
47
|
+
Click menu items to see events here
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
initialize(rpc: any) {
|
|
56
|
+
this.rpc = rpc;
|
|
57
|
+
const showContextBtn = document.getElementById('show-context-menu');
|
|
58
|
+
|
|
59
|
+
showContextBtn?.addEventListener('click', async () => {
|
|
60
|
+
try {
|
|
61
|
+
// Show context menu at a default position
|
|
62
|
+
await rpc.request.showContextMenu({ x: 100, y: 100 });
|
|
63
|
+
this.addMenuEvent('Context menu shown');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error showing context menu:', error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private addMenuEvent(event: string) {
|
|
71
|
+
const container = document.getElementById('menu-events');
|
|
72
|
+
if (!container) return;
|
|
73
|
+
|
|
74
|
+
const time = new Date().toLocaleTimeString();
|
|
75
|
+
const eventHtml = `
|
|
76
|
+
<div style="background: #f8fafc; border-left: 3px solid #4299e1; padding: 0.75rem; margin-bottom: 0.5rem; border-radius: 0 0.25rem 0.25rem 0;">
|
|
77
|
+
<div style="color: #718096; font-size: 0.75rem;">${time}</div>
|
|
78
|
+
<div style="color: #2d3748;">${event}</div>
|
|
79
|
+
</div>
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
// Remove "no events" message if it exists
|
|
83
|
+
const noEvents = container.querySelector('.no-events');
|
|
84
|
+
if (noEvents) {
|
|
85
|
+
container.innerHTML = '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Add new event at the top
|
|
89
|
+
container.insertAdjacentHTML('afterbegin', eventHtml);
|
|
90
|
+
|
|
91
|
+
// Keep only last 10 events
|
|
92
|
+
const events = container.children;
|
|
93
|
+
while (events.length > 10) {
|
|
94
|
+
container.removeChild(events[events.length - 1]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle events from the backend
|
|
99
|
+
onMenuClicked(data: { action: string }) {
|
|
100
|
+
this.addMenuEvent(`Menu clicked: ${data.action}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
export class RPCDemo {
|
|
2
|
+
private testResults: Array<{ operation: string; result: any; duration: number }> = [];
|
|
3
|
+
|
|
4
|
+
render() {
|
|
5
|
+
return `
|
|
6
|
+
<div class="demo-section">
|
|
7
|
+
<div class="demo-header">
|
|
8
|
+
<span class="demo-icon">📡</span>
|
|
9
|
+
<div>
|
|
10
|
+
<h2 class="demo-title">RPC Communication</h2>
|
|
11
|
+
<p class="demo-description">Test bidirectional communication between the webview and bun process</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="demo-controls">
|
|
16
|
+
<h3>Math Operations</h3>
|
|
17
|
+
<div class="control-group">
|
|
18
|
+
<label class="control-label">A:</label>
|
|
19
|
+
<input type="number" id="math-a" class="control-input" value="10">
|
|
20
|
+
|
|
21
|
+
<label class="control-label">B:</label>
|
|
22
|
+
<input type="number" id="math-b" class="control-input" value="5">
|
|
23
|
+
|
|
24
|
+
<label class="control-label">Operation:</label>
|
|
25
|
+
<select id="math-operation" class="control-input" style="width: 120px;">
|
|
26
|
+
<option value="add">Add (+)</option>
|
|
27
|
+
<option value="subtract">Subtract (-)</option>
|
|
28
|
+
<option value="multiply">Multiply (×)</option>
|
|
29
|
+
<option value="divide">Divide (÷)</option>
|
|
30
|
+
<option value="power">Power (^)</option>
|
|
31
|
+
</select>
|
|
32
|
+
|
|
33
|
+
<button class="btn btn-primary" id="do-math">Calculate</button>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<h3>Data Transfer Test</h3>
|
|
37
|
+
<div class="control-group">
|
|
38
|
+
<label class="control-label">Data Size:</label>
|
|
39
|
+
<select id="data-size" class="control-input" style="width: 150px;">
|
|
40
|
+
<option value="1024">1 KB</option>
|
|
41
|
+
<option value="10240">10 KB</option>
|
|
42
|
+
<option value="102400">100 KB</option>
|
|
43
|
+
<option value="1048576">1 MB</option>
|
|
44
|
+
<option value="2097152">2 MB</option>
|
|
45
|
+
</select>
|
|
46
|
+
|
|
47
|
+
<button class="btn btn-primary" id="test-big-data">Test Echo</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<h3>Performance Test</h3>
|
|
51
|
+
<div class="control-group">
|
|
52
|
+
<label class="control-label">Message Size:</label>
|
|
53
|
+
<select id="perf-size" class="control-input" style="width: 120px;">
|
|
54
|
+
<option value="100">100 B</option>
|
|
55
|
+
<option value="1024">1 KB</option>
|
|
56
|
+
<option value="10240">10 KB</option>
|
|
57
|
+
</select>
|
|
58
|
+
|
|
59
|
+
<label class="control-label">Count:</label>
|
|
60
|
+
<input type="number" id="perf-count" class="control-input" value="10" min="1" max="100">
|
|
61
|
+
|
|
62
|
+
<button class="btn btn-primary" id="run-performance-test">Run Test</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="demo-results">
|
|
67
|
+
<div class="results-header">Test Results:</div>
|
|
68
|
+
<div id="rpc-results" class="rpc-results">
|
|
69
|
+
<div class="no-results" style="text-align: center; color: #718096; padding: 2rem;">
|
|
70
|
+
No tests run yet. Use the controls above to start testing RPC communication.
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
initialize(rpc: any) {
|
|
79
|
+
const mathBtn = document.getElementById('do-math');
|
|
80
|
+
const bigDataBtn = document.getElementById('test-big-data');
|
|
81
|
+
const perfTestBtn = document.getElementById('run-performance-test');
|
|
82
|
+
|
|
83
|
+
mathBtn?.addEventListener('click', async () => {
|
|
84
|
+
const a = parseFloat((document.getElementById('math-a') as HTMLInputElement).value);
|
|
85
|
+
const b = parseFloat((document.getElementById('math-b') as HTMLInputElement).value);
|
|
86
|
+
const operation = (document.getElementById('math-operation') as HTMLSelectElement).value;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const startTime = Date.now();
|
|
90
|
+
const result = await rpc.request.doMath({ a, b, operation });
|
|
91
|
+
const duration = Date.now() - startTime;
|
|
92
|
+
|
|
93
|
+
this.addTestResult({
|
|
94
|
+
operation: `${a} ${operation} ${b}`,
|
|
95
|
+
result,
|
|
96
|
+
duration
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.addTestResult({
|
|
100
|
+
operation: `${a} ${operation} ${b}`,
|
|
101
|
+
result: `Error: ${error.message}`,
|
|
102
|
+
duration: 0
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
bigDataBtn?.addEventListener('click', async () => {
|
|
108
|
+
const size = parseInt((document.getElementById('data-size') as HTMLSelectElement).value);
|
|
109
|
+
const testData = 'x'.repeat(size);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
const result = await rpc.request.echoBigData(testData);
|
|
114
|
+
const duration = Date.now() - startTime;
|
|
115
|
+
|
|
116
|
+
this.addTestResult({
|
|
117
|
+
operation: `Echo ${this.formatBytes(size)}`,
|
|
118
|
+
result: `Received ${this.formatBytes(result.length)}`,
|
|
119
|
+
duration
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
this.addTestResult({
|
|
123
|
+
operation: `Echo ${this.formatBytes(size)}`,
|
|
124
|
+
result: `Error: ${error.message}`,
|
|
125
|
+
duration: 0
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
perfTestBtn?.addEventListener('click', async () => {
|
|
131
|
+
const size = parseInt((document.getElementById('perf-size') as HTMLSelectElement).value);
|
|
132
|
+
const count = parseInt((document.getElementById('perf-count') as HTMLInputElement).value);
|
|
133
|
+
|
|
134
|
+
perfTestBtn.textContent = 'Running...';
|
|
135
|
+
perfTestBtn.setAttribute('disabled', 'true');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const results = await this.runPerformanceTest(rpc, size, count);
|
|
139
|
+
this.addTestResult({
|
|
140
|
+
operation: `Performance: ${count} × ${this.formatBytes(size)}`,
|
|
141
|
+
result: `${results.messagesPerSecond.toFixed(1)} msg/sec, avg: ${results.averageTime.toFixed(1)}ms`,
|
|
142
|
+
duration: results.totalTime
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.addTestResult({
|
|
146
|
+
operation: `Performance: ${count} × ${this.formatBytes(size)}`,
|
|
147
|
+
result: `Error: ${error.message}`,
|
|
148
|
+
duration: 0
|
|
149
|
+
});
|
|
150
|
+
} finally {
|
|
151
|
+
perfTestBtn.textContent = 'Run Test';
|
|
152
|
+
perfTestBtn.removeAttribute('disabled');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async runPerformanceTest(rpc: any, messageSize: number, messageCount: number) {
|
|
158
|
+
const testData = 'x'.repeat(messageSize);
|
|
159
|
+
const results: number[] = [];
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < messageCount; i++) {
|
|
163
|
+
const messageStart = Date.now();
|
|
164
|
+
await rpc.request.echoBigData(testData);
|
|
165
|
+
results.push(Date.now() - messageStart);
|
|
166
|
+
|
|
167
|
+
// Update progress
|
|
168
|
+
const progress = ((i + 1) / messageCount) * 100;
|
|
169
|
+
const perfTestBtn = document.getElementById('run-performance-test');
|
|
170
|
+
if (perfTestBtn) {
|
|
171
|
+
perfTestBtn.textContent = `Running... ${progress.toFixed(0)}%`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const totalTime = Date.now() - startTime;
|
|
176
|
+
const averageTime = results.reduce((a, b) => a + b, 0) / results.length;
|
|
177
|
+
const messagesPerSecond = (messageCount / totalTime) * 1000;
|
|
178
|
+
|
|
179
|
+
return { totalTime, averageTime, messagesPerSecond };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private addTestResult(result: { operation: string; result: any; duration: number }) {
|
|
183
|
+
this.testResults.unshift(result);
|
|
184
|
+
|
|
185
|
+
// Keep only last 20 results
|
|
186
|
+
if (this.testResults.length > 20) {
|
|
187
|
+
this.testResults = this.testResults.slice(0, 20);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.renderResults();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private renderResults() {
|
|
194
|
+
const container = document.getElementById('rpc-results');
|
|
195
|
+
if (!container) return;
|
|
196
|
+
|
|
197
|
+
if (this.testResults.length === 0) {
|
|
198
|
+
container.innerHTML = `
|
|
199
|
+
<div class="no-results" style="text-align: center; color: #718096; padding: 2rem;">
|
|
200
|
+
No tests run yet. Use the controls above to start testing RPC communication.
|
|
201
|
+
</div>
|
|
202
|
+
`;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
container.innerHTML = this.testResults.map(result => `
|
|
207
|
+
<div class="result-entry" style="background: #f8fafc; border-left: 3px solid #4299e1; padding: 1rem; margin-bottom: 0.5rem; border-radius: 0 0.25rem 0.25rem 0;">
|
|
208
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
|
209
|
+
<strong>${result.operation}</strong>
|
|
210
|
+
<span style="color: #718096; font-size: 0.875rem;">${result.duration}ms</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div style="color: #2d3748;">${result.result}</div>
|
|
213
|
+
</div>
|
|
214
|
+
`).join('');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private formatBytes(bytes: number): string {
|
|
218
|
+
if (bytes === 0) return '0 B';
|
|
219
|
+
const k = 1024;
|
|
220
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
221
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
222
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle events from the backend
|
|
226
|
+
onRpcTestResult(data: { operation: string; result: any; duration: number }) {
|
|
227
|
+
this.addTestResult(data);
|
|
228
|
+
}
|
|
229
|
+
}
|