plusui-native 0.2.7 → 0.2.9
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 +87 -24
- package/templates/base/README.md.template +26 -4
- package/templates/react/frontend/src/App.tsx +89 -3
- package/templates/react/frontend/src/plusui.ts +47 -0
- package/templates/react/frontend/src/styles/app.css +188 -0
- package/templates/react/main.cpp.template +23 -0
- package/templates/solid/frontend/src/App.tsx +91 -2
- package/templates/solid/frontend/src/plusui.ts +47 -0
- package/templates/solid/frontend/src/styles/app.css +188 -0
- package/templates/solid/main.cpp.template +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plusui-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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-bindgen": "^0.1.
|
|
30
|
+
"plusui-native-builder": "^0.1.9",
|
|
31
|
+
"plusui-native-bindgen": "^0.1.9"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"plusui-native-bindgen": "^0.1.
|
|
34
|
+
"plusui-native-bindgen": "^0.1.9"
|
|
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';
|
|
@@ -198,11 +198,62 @@ function runCMake(args, options = {}) {
|
|
|
198
198
|
function getAppBindgenPaths() {
|
|
199
199
|
return {
|
|
200
200
|
featuresDir: join(process.cwd(), 'src', 'features'),
|
|
201
|
-
outputDir: join(process.cwd(), 'src', '
|
|
202
|
-
frontendOutputDir: join(process.cwd(), 'frontend', 'src', '
|
|
201
|
+
outputDir: join(process.cwd(), 'src', 'Bindings'),
|
|
202
|
+
frontendOutputDir: join(process.cwd(), 'frontend', 'src', 'Bindings'),
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
function findLikelyProjectDirs(baseDir) {
|
|
207
|
+
try {
|
|
208
|
+
const entries = execSync('npm pkg get name', { cwd: baseDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
209
|
+
if (entries) {
|
|
210
|
+
// noop; just to ensure cwd is a Node project when possible
|
|
211
|
+
}
|
|
212
|
+
} catch {}
|
|
213
|
+
|
|
214
|
+
const candidates = [];
|
|
215
|
+
try {
|
|
216
|
+
const dirs = execSync(process.platform === 'win32' ? 'dir /b /ad' : 'ls -1 -d */', {
|
|
217
|
+
cwd: baseDir,
|
|
218
|
+
encoding: 'utf8',
|
|
219
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
220
|
+
shell: true,
|
|
221
|
+
})
|
|
222
|
+
.split(/\r?\n/)
|
|
223
|
+
.map(s => s.trim().replace(/[\\/]$/, ''))
|
|
224
|
+
.filter(Boolean);
|
|
225
|
+
|
|
226
|
+
for (const dirName of dirs) {
|
|
227
|
+
const fullDir = join(baseDir, dirName);
|
|
228
|
+
if (existsSync(join(fullDir, 'CMakeLists.txt')) && existsSync(join(fullDir, 'package.json'))) {
|
|
229
|
+
candidates.push(dirName);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
|
|
234
|
+
return candidates;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ensureProjectRoot(commandName) {
|
|
238
|
+
const hasCMake = existsSync(join(process.cwd(), 'CMakeLists.txt'));
|
|
239
|
+
const hasPackage = existsSync(join(process.cwd(), 'package.json'));
|
|
240
|
+
|
|
241
|
+
if (hasCMake && hasPackage) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const likelyDirs = findLikelyProjectDirs(process.cwd());
|
|
246
|
+
let hint = '';
|
|
247
|
+
|
|
248
|
+
if (likelyDirs.length === 1) {
|
|
249
|
+
hint = `\n\nHint: you may be in a parent folder. Try:\n cd ${likelyDirs[0]}\n plusui ${commandName}`;
|
|
250
|
+
} else if (likelyDirs.length > 1) {
|
|
251
|
+
hint = `\n\nHint: run this command from your app folder (one containing CMakeLists.txt and package.json).`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
error(`This command must be run from a PlusUI project root (missing CMakeLists.txt and/or package.json in ${process.cwd()}).${hint}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
206
257
|
function ensureBuildLayout() {
|
|
207
258
|
const buildRoot = join(process.cwd(), 'build');
|
|
208
259
|
for (const platform of Object.values(PLATFORMS)) {
|
|
@@ -210,11 +261,20 @@ function ensureBuildLayout() {
|
|
|
210
261
|
}
|
|
211
262
|
}
|
|
212
263
|
|
|
264
|
+
function getDevBuildDir() {
|
|
265
|
+
const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
|
|
266
|
+
return join('.plusui', 'dev', platformFolder);
|
|
267
|
+
}
|
|
268
|
+
|
|
213
269
|
function resolveBindgenScriptPath() {
|
|
214
270
|
const candidates = [
|
|
271
|
+
resolve(__dirname, '../../plusui-bindgen/src/advanced-bindgen.js'),
|
|
215
272
|
resolve(__dirname, '../../plusui-bindgen/src/index.js'),
|
|
273
|
+
resolve(__dirname, '../../plusui-native-bindgen/src/advanced-bindgen.js'),
|
|
216
274
|
resolve(__dirname, '../../plusui-native-bindgen/src/index.js'),
|
|
275
|
+
resolve(__dirname, '../../../plusui-native-bindgen/src/advanced-bindgen.js'),
|
|
217
276
|
resolve(__dirname, '../../../plusui-native-bindgen/src/index.js'),
|
|
277
|
+
resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'advanced-bindgen.js'),
|
|
218
278
|
resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'index.js'),
|
|
219
279
|
];
|
|
220
280
|
|
|
@@ -275,6 +335,7 @@ async function createProject(name, options = {}) {
|
|
|
275
335
|
// ============================================================
|
|
276
336
|
|
|
277
337
|
function buildFrontend() {
|
|
338
|
+
ensureProjectRoot('build:frontend');
|
|
278
339
|
logSection('Building Frontend');
|
|
279
340
|
|
|
280
341
|
if (existsSync('frontend')) {
|
|
@@ -287,6 +348,7 @@ function buildFrontend() {
|
|
|
287
348
|
}
|
|
288
349
|
|
|
289
350
|
function buildBackend(platform = null, devMode = false) {
|
|
351
|
+
ensureProjectRoot(devMode ? 'dev:backend' : 'build:backend');
|
|
290
352
|
const targetPlatform = platform || process.platform;
|
|
291
353
|
const platformConfig = PLATFORMS[targetPlatform] || PLATFORMS[Object.keys(PLATFORMS).find(k => PLATFORMS[k].folder.toLowerCase() === targetPlatform?.toLowerCase())];
|
|
292
354
|
|
|
@@ -329,6 +391,7 @@ function buildBackend(platform = null, devMode = false) {
|
|
|
329
391
|
}
|
|
330
392
|
|
|
331
393
|
async function generateIcons(inputPath = null) {
|
|
394
|
+
ensureProjectRoot('icons');
|
|
332
395
|
logSection('Generating Platform Icons');
|
|
333
396
|
|
|
334
397
|
const { IconGenerator } = await import('./assets/icon-generator.js');
|
|
@@ -347,6 +410,7 @@ async function generateIcons(inputPath = null) {
|
|
|
347
410
|
}
|
|
348
411
|
|
|
349
412
|
async function embedResources(platform = null) {
|
|
413
|
+
ensureProjectRoot('embed');
|
|
350
414
|
const targetPlatform = platform || process.platform;
|
|
351
415
|
logSection(`Embedding Resources (${targetPlatform})`);
|
|
352
416
|
|
|
@@ -369,6 +433,7 @@ async function embedResources(platform = null) {
|
|
|
369
433
|
}
|
|
370
434
|
|
|
371
435
|
async function embedAssets() {
|
|
436
|
+
ensureProjectRoot('build');
|
|
372
437
|
const assetsDir = join(process.cwd(), 'assets');
|
|
373
438
|
if (!existsSync(assetsDir)) {
|
|
374
439
|
try {
|
|
@@ -417,6 +482,7 @@ async function embedAssets() {
|
|
|
417
482
|
|
|
418
483
|
|
|
419
484
|
async function build(production = true) {
|
|
485
|
+
ensureProjectRoot('build');
|
|
420
486
|
logSection('Building PlusUI Application');
|
|
421
487
|
|
|
422
488
|
// Embed assets
|
|
@@ -435,6 +501,7 @@ async function build(production = true) {
|
|
|
435
501
|
}
|
|
436
502
|
|
|
437
503
|
function buildAll() {
|
|
504
|
+
ensureProjectRoot('build:all');
|
|
438
505
|
logSection('Building for All Platforms');
|
|
439
506
|
|
|
440
507
|
ensureBuildLayout();
|
|
@@ -474,6 +541,7 @@ let viteServer = null;
|
|
|
474
541
|
let cppProcess = null;
|
|
475
542
|
|
|
476
543
|
async function startViteServer() {
|
|
544
|
+
ensureProjectRoot('dev:frontend');
|
|
477
545
|
log('Starting Vite dev server...', 'blue');
|
|
478
546
|
|
|
479
547
|
viteServer = await createViteServer({
|
|
@@ -491,13 +559,13 @@ async function startViteServer() {
|
|
|
491
559
|
}
|
|
492
560
|
|
|
493
561
|
async function startBackend() {
|
|
562
|
+
ensureProjectRoot('dev');
|
|
494
563
|
logSection('Building C++ Backend (Dev Mode)');
|
|
495
564
|
|
|
496
565
|
const projectName = getProjectName();
|
|
497
566
|
killProcessByName(projectName);
|
|
498
567
|
|
|
499
|
-
const
|
|
500
|
-
const buildDir = join('build', platformFolder, 'dev');
|
|
568
|
+
const buildDir = getDevBuildDir();
|
|
501
569
|
|
|
502
570
|
// Configure with dev mode if not configured
|
|
503
571
|
if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
|
|
@@ -598,6 +666,7 @@ function killProcessByName(name) {
|
|
|
598
666
|
}
|
|
599
667
|
|
|
600
668
|
async function dev() {
|
|
669
|
+
ensureProjectRoot('dev');
|
|
601
670
|
logSection('PlusUI Development Mode');
|
|
602
671
|
|
|
603
672
|
const toolCheck = checkTools();
|
|
@@ -643,6 +712,7 @@ async function dev() {
|
|
|
643
712
|
}
|
|
644
713
|
|
|
645
714
|
async function devFrontend() {
|
|
715
|
+
ensureProjectRoot('dev:frontend');
|
|
646
716
|
logSection('Frontend Development Mode');
|
|
647
717
|
|
|
648
718
|
// specific port cleaning
|
|
@@ -665,13 +735,13 @@ async function devFrontend() {
|
|
|
665
735
|
}
|
|
666
736
|
|
|
667
737
|
function devBackend() {
|
|
738
|
+
ensureProjectRoot('dev:backend');
|
|
668
739
|
logSection('Backend Development Mode');
|
|
669
740
|
|
|
670
741
|
const projectName = getProjectName();
|
|
671
742
|
killProcessByName(projectName);
|
|
672
743
|
|
|
673
|
-
const
|
|
674
|
-
const buildDir = join('build', platformFolder, 'dev');
|
|
744
|
+
const buildDir = getDevBuildDir();
|
|
675
745
|
|
|
676
746
|
if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
|
|
677
747
|
log('Configuring CMake...', 'blue');
|
|
@@ -742,6 +812,7 @@ function devBackend() {
|
|
|
742
812
|
// ============================================================
|
|
743
813
|
|
|
744
814
|
function run() {
|
|
815
|
+
ensureProjectRoot('run');
|
|
745
816
|
const projectName = getProjectName();
|
|
746
817
|
const platform = PLATFORMS[process.platform];
|
|
747
818
|
const buildDir = `build/${platform?.folder || 'Windows'}`;
|
|
@@ -774,9 +845,10 @@ function run() {
|
|
|
774
845
|
// ============================================================
|
|
775
846
|
|
|
776
847
|
async function clean() {
|
|
848
|
+
ensureProjectRoot('clean');
|
|
777
849
|
logSection('Cleaning Build Artifacts');
|
|
778
850
|
|
|
779
|
-
const dirs = ['build', 'frontend/dist'];
|
|
851
|
+
const dirs = ['build', '.plusui', 'frontend/dist'];
|
|
780
852
|
|
|
781
853
|
for (const dir of dirs) {
|
|
782
854
|
if (existsSync(dir)) {
|
|
@@ -793,6 +865,7 @@ async function clean() {
|
|
|
793
865
|
// ============================================================
|
|
794
866
|
|
|
795
867
|
async function runBindgen(providedArgs = null, options = {}) {
|
|
868
|
+
ensureProjectRoot('bindgen');
|
|
796
869
|
logSection('Running Binding Generator');
|
|
797
870
|
|
|
798
871
|
const { skipIfNoInput = false, source = 'manual' } = options;
|
|
@@ -815,22 +888,12 @@ async function runBindgen(providedArgs = null, options = {}) {
|
|
|
815
888
|
let defaultFrontendOutputDir = null;
|
|
816
889
|
|
|
817
890
|
if (bindgenArgs.length === 0) {
|
|
818
|
-
const {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
defaultFrontendOutputDir = frontendOutputDir;
|
|
825
|
-
log(`App mode: ${appFeaturesDir} -> ${appOutputDir}`, 'dim');
|
|
826
|
-
} else {
|
|
827
|
-
if (skipIfNoInput) {
|
|
828
|
-
log(`No src/features folder found; skipping binding refresh for ${source}.`, 'dim');
|
|
829
|
-
return;
|
|
830
|
-
} else {
|
|
831
|
-
error('No bindgen input found. Create src/features in your app or pass paths: plusui bindgen <featuresDir> <outputDir>');
|
|
832
|
-
}
|
|
833
|
-
}
|
|
891
|
+
const { outputDir: appOutputDir, frontendOutputDir } = getAppBindgenPaths();
|
|
892
|
+
bindgenArgs = [process.cwd(), appOutputDir];
|
|
893
|
+
usedDefaultAppMode = true;
|
|
894
|
+
defaultOutputDir = appOutputDir;
|
|
895
|
+
defaultFrontendOutputDir = frontendOutputDir;
|
|
896
|
+
log(`Project mode: ${process.cwd()} -> ${appOutputDir}`, 'dim');
|
|
834
897
|
}
|
|
835
898
|
|
|
836
899
|
// Spawn node process
|
|
@@ -27,22 +27,44 @@ This will:
|
|
|
27
27
|
- Changes to frontend code reflect instantly
|
|
28
28
|
- Auto-refresh app bindings when `src/features` exists
|
|
29
29
|
|
|
30
|
+
Dev build intermediates are stored in `.plusui/dev/...` so your `build/` folder stays focused on release/platform outputs.
|
|
31
|
+
|
|
30
32
|
Note: You can still run `npm run bind` manually anytime.
|
|
31
33
|
|
|
32
34
|
## Bindings (App-level)
|
|
33
35
|
|
|
34
|
-
Generate bindings for your app
|
|
36
|
+
Generate bidirectional bindings for your app:
|
|
35
37
|
```bash
|
|
36
38
|
npm run bind
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
Default bindgen paths:
|
|
40
|
-
- Input
|
|
41
|
-
- Output: `src/
|
|
42
|
+
- Input: project root scan (frontend + backend files)
|
|
43
|
+
- Output: `src/Bindings`
|
|
44
|
+
|
|
45
|
+
Generated structure:
|
|
46
|
+
- `src/Bindings/NativeBindings/CPP_IO`
|
|
47
|
+
- `src/Bindings/NativeBindings/WEB_IO`
|
|
48
|
+
- `src/Bindings/CustomBindings/CPP_IO`
|
|
49
|
+
- `src/Bindings/CustomBindings/WEB_IO`
|
|
50
|
+
- `include/Bindings/NativeBindings/CPP_IO` (generated `.hpp` headers)
|
|
51
|
+
- `include/Bindings/CustomBindings/CPP_IO` (generated `.hpp` headers)
|
|
52
|
+
|
|
53
|
+
Scan extensions:
|
|
54
|
+
- `WEB_IO`: `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.html`
|
|
55
|
+
- `CPP_IO`: `.h`, `.hpp`, `.hh`, `.hxx`, `.cpp`, `.cc`, `.cxx`
|
|
56
|
+
|
|
57
|
+
Custom binding kinds detected:
|
|
58
|
+
- `method`
|
|
59
|
+
- `service`
|
|
60
|
+
- `stream`
|
|
61
|
+
- `event`
|
|
62
|
+
|
|
63
|
+
`plusui bind` scans your whole project structure and does not require a specific feature folder.
|
|
42
64
|
|
|
43
65
|
You can also pass custom paths:
|
|
44
66
|
```bash
|
|
45
|
-
plusui bindgen <
|
|
67
|
+
plusui bindgen <projectRoot> <outputDir>
|
|
46
68
|
```
|
|
47
69
|
|
|
48
70
|
## Assets & Icons
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
-
import { win, browser, router, app } from './plusui';
|
|
2
|
+
import { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from './plusui';
|
|
3
3
|
|
|
4
4
|
// Define routes for your app (optional - for SPA routing)
|
|
5
5
|
const routes = {
|
|
@@ -14,6 +14,11 @@ function App() {
|
|
|
14
14
|
const [currentUrl, setCurrentUrl] = useState('');
|
|
15
15
|
const [canGoBack, setCanGoBack] = useState(false);
|
|
16
16
|
const [canGoForward, setCanGoForward] = useState(false);
|
|
17
|
+
|
|
18
|
+
// FileDrop state
|
|
19
|
+
const [files, setFiles] = useState<FileInfo[]>([]);
|
|
20
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
21
|
+
const [dropZoneStyle, setDropZoneStyle] = useState('');
|
|
17
22
|
|
|
18
23
|
useEffect(() => {
|
|
19
24
|
// Setup routes
|
|
@@ -26,7 +31,27 @@ function App() {
|
|
|
26
31
|
browser.canGoForward().then(setCanGoForward);
|
|
27
32
|
});
|
|
28
33
|
|
|
29
|
-
//
|
|
34
|
+
// Setup FileDrop listeners
|
|
35
|
+
const unsubDrop = fileDrop.onFilesDropped((droppedFiles) => {
|
|
36
|
+
console.log('Files dropped:', droppedFiles);
|
|
37
|
+
setFiles(prev => [...prev, ...droppedFiles]);
|
|
38
|
+
setIsDragging(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const unsubEnter = fileDrop.onDragEnter(() => {
|
|
42
|
+
setIsDragging(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const unsubLeave = fileDrop.onDragLeave(() => {
|
|
46
|
+
setIsDragging(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
unsub();
|
|
51
|
+
unsubDrop();
|
|
52
|
+
unsubEnter();
|
|
53
|
+
unsubLeave();
|
|
54
|
+
}al state
|
|
30
55
|
browser.getUrl().then(setCurrentUrl);
|
|
31
56
|
browser.canGoBack().then(setCanGoBack);
|
|
32
57
|
browser.canGoForward().then(setCanGoForward);
|
|
@@ -101,7 +126,68 @@ function App() {
|
|
|
101
126
|
<div className="button-group">
|
|
102
127
|
<button onClick={handleGoBack} className="button" disabled={!canGoBack}>Back</button>
|
|
103
128
|
<button onClick={handleGoForward} className="button" disabled={!canGoForward}>Forward</button>
|
|
104
|
-
<button
|
|
129
|
+
<button onClcard">
|
|
130
|
+
<h2>FileDrop - Drag & Drop Files</h2>
|
|
131
|
+
|
|
132
|
+
<div style={{ marginBottom: '1rem' }}>
|
|
133
|
+
<label style={{ marginRight: '0.5rem', fontSize: '0.9em' }}>Style:</label>
|
|
134
|
+
<select
|
|
135
|
+
value={dropZoneStyle}
|
|
136
|
+
onChange={(e) => setDropZoneStyle(e.target.value)}
|
|
137
|
+
style={{
|
|
138
|
+
padding: '0.5rem',
|
|
139
|
+
borderRadius: '0.25rem',
|
|
140
|
+
border: '1px solid rgba(255,255,255,0.3)',
|
|
141
|
+
background: 'rgba(255,255,255,0.1)',
|
|
142
|
+
color: '#fff',
|
|
143
|
+
fontSize: '0.9em'
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<option value="">Default</option>
|
|
147
|
+
<option value="filedrop-compact">Compact</option>
|
|
148
|
+
<option value="filedrop-inline">Inline</option>
|
|
149
|
+
<option value="filedrop-minimal">Minimal</option>
|
|
150
|
+
<option value="filedrop-bold">Bold</option>
|
|
151
|
+
</select>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className={`filedrop-zone ${dropZoneStyle} ${isDragging ? 'filedrop-active' : ''}`}>
|
|
155
|
+
<div className="filedrop-content">
|
|
156
|
+
<svg className="filedrop-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
157
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
158
|
+
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
159
|
+
</svg>
|
|
160
|
+
<div className="filedrop-text">
|
|
161
|
+
{isDragging ? 'Drop files here' : 'Drag & drop files'}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="filedrop-hint">All file types supported</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{files.length > 0 && (
|
|
168
|
+
<div className="filedrop-files">
|
|
169
|
+
{files.map((file, i) => (
|
|
170
|
+
<div key={i} className="filedrop-file-item">
|
|
171
|
+
<div className="filedrop-file-icon">📄</div>
|
|
172
|
+
<div className="filedrop-file-info">
|
|
173
|
+
<div className="filedrop-file-name">{file.name}</div>
|
|
174
|
+
<div className="filedrop-file-meta">
|
|
175
|
+
{formatFileSize(file.size)} • {file.type}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<button
|
|
179
|
+
className="filedrop-file-remove"
|
|
180
|
+
onClick={() => setFiles(files.filter((_, idx) => idx !== i))}
|
|
181
|
+
>
|
|
182
|
+
✕
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="ick={handleReload} className="button">Reload</button>
|
|
105
191
|
</div>
|
|
106
192
|
</div>
|
|
107
193
|
|
|
@@ -115,3 +115,50 @@ export const router = {
|
|
|
115
115
|
export const app = {
|
|
116
116
|
quit: async () => invoke('app.quit', []),
|
|
117
117
|
};
|
|
118
|
+
|
|
119
|
+
// FileDrop API
|
|
120
|
+
export interface FileInfo {
|
|
121
|
+
path: string;
|
|
122
|
+
name: string;
|
|
123
|
+
type: string;
|
|
124
|
+
size: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const fileDrop = {
|
|
128
|
+
setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
|
|
129
|
+
isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
|
|
130
|
+
onFilesDropped: (handler: (files: FileInfo[]) => void) => {
|
|
131
|
+
if (typeof window === 'undefined') return () => {};
|
|
132
|
+
const eventHandler = (event: Event) => {
|
|
133
|
+
const custom = event as CustomEvent<{ files?: FileInfo[] }>;
|
|
134
|
+
handler(custom.detail?.files ?? []);
|
|
135
|
+
};
|
|
136
|
+
window.addEventListener('plusui:fileDrop.filesDropped', eventHandler);
|
|
137
|
+
return () => window.removeEventListener('plusui:fileDrop.filesDropped', eventHandler);
|
|
138
|
+
},
|
|
139
|
+
onDragEnter: (handler: () => void) => {
|
|
140
|
+
if (typeof window === 'undefined') return () => {};
|
|
141
|
+
const eventHandler = () => handler();
|
|
142
|
+
window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
143
|
+
return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
144
|
+
},
|
|
145
|
+
onDragLeave: (handler: () => void) => {
|
|
146
|
+
if (typeof window === 'undefined') return () => {};
|
|
147
|
+
const eventHandler = () => handler();
|
|
148
|
+
window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
149
|
+
return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Helper functions
|
|
154
|
+
export function formatFileSize(bytes: number): string {
|
|
155
|
+
if (bytes === 0) return '0 Bytes';
|
|
156
|
+
const k = 1024;
|
|
157
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
158
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
159
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isImageFile(file: FileInfo): boolean {
|
|
163
|
+
return file.type.startsWith('image/');
|
|
164
|
+
}
|
|
@@ -138,3 +138,191 @@ body {
|
|
|
138
138
|
border-radius: 0.25rem;
|
|
139
139
|
font-family: 'Courier New', monospace;
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/* ============================================================================
|
|
143
|
+
* FILEDROP STYLES
|
|
144
|
+
* ============================================================================ */
|
|
145
|
+
|
|
146
|
+
.filedrop-zone {
|
|
147
|
+
position: relative;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
min-height: 180px;
|
|
153
|
+
padding: 2rem;
|
|
154
|
+
border: 2px dashed rgba(255, 255, 255, 0.3);
|
|
155
|
+
border-radius: 0.75rem;
|
|
156
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
157
|
+
transition: all 0.2s ease-in-out;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
user-select: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.filedrop-zone:hover {
|
|
163
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
164
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.filedrop-zone.filedrop-active {
|
|
168
|
+
border-color: #60a5fa;
|
|
169
|
+
background-color: rgba(96, 165, 250, 0.1);
|
|
170
|
+
border-width: 3px;
|
|
171
|
+
transform: scale(1.02);
|
|
172
|
+
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.filedrop-content {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 1rem;
|
|
180
|
+
text-align: center;
|
|
181
|
+
pointer-events: none;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.filedrop-icon {
|
|
185
|
+
width: 3rem;
|
|
186
|
+
height: 3rem;
|
|
187
|
+
color: rgba(255, 255, 255, 0.6);
|
|
188
|
+
transition: all 0.2s ease-in-out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.filedrop-zone.filedrop-active .filedrop-icon {
|
|
192
|
+
color: #60a5fa;
|
|
193
|
+
transform: scale(1.2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.filedrop-text {
|
|
197
|
+
font-size: 1rem;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
color: rgba(255, 255, 255, 0.9);
|
|
200
|
+
transition: color 0.2s ease-in-out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.filedrop-zone.filedrop-active .filedrop-text {
|
|
204
|
+
color: #93c5fd;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.filedrop-hint {
|
|
209
|
+
font-size: 0.875rem;
|
|
210
|
+
color: rgba(255, 255, 255, 0.6);
|
|
211
|
+
transition: color 0.2s ease-in-out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.filedrop-zone.filedrop-active .filedrop-hint {
|
|
215
|
+
color: rgba(255, 255, 255, 0.8);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.filedrop-zone.filedrop-compact {
|
|
219
|
+
min-height: 120px;
|
|
220
|
+
padding: 1.5rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.filedrop-zone.filedrop-compact .filedrop-icon {
|
|
224
|
+
width: 2rem;
|
|
225
|
+
height: 2rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.filedrop-zone.filedrop-inline {
|
|
229
|
+
min-height: 80px;
|
|
230
|
+
padding: 1rem;
|
|
231
|
+
flex-direction: row;
|
|
232
|
+
justify-content: flex-start;
|
|
233
|
+
gap: 1rem;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.filedrop-zone.filedrop-inline .filedrop-content {
|
|
237
|
+
flex-direction: row;
|
|
238
|
+
align-items: center;
|
|
239
|
+
text-align: left;
|
|
240
|
+
gap: 0.75rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.filedrop-zone.filedrop-minimal {
|
|
244
|
+
border-style: solid;
|
|
245
|
+
border-width: 1px;
|
|
246
|
+
background-color: transparent;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.filedrop-zone.filedrop-bold {
|
|
250
|
+
border-width: 3px;
|
|
251
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.filedrop-files {
|
|
255
|
+
margin-top: 1rem;
|
|
256
|
+
width: 100%;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.filedrop-file-item {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 0.75rem;
|
|
263
|
+
padding: 0.75rem;
|
|
264
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
265
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
266
|
+
border-radius: 0.5rem;
|
|
267
|
+
margin-bottom: 0.5rem;
|
|
268
|
+
transition: all 0.2s ease-in-out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.filedrop-file-item:hover {
|
|
272
|
+
background-color: rgba(255, 255, 255, 0.15);
|
|
273
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.filedrop-file-icon {
|
|
277
|
+
width: 2rem;
|
|
278
|
+
height: 2rem;
|
|
279
|
+
flex-shrink: 0;
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
284
|
+
border-radius: 0.375rem;
|
|
285
|
+
font-size: 1.2rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.filedrop-file-info {
|
|
289
|
+
flex: 1;
|
|
290
|
+
min-width: 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.filedrop-file-name {
|
|
294
|
+
font-size: 0.875rem;
|
|
295
|
+
font-weight: 500;
|
|
296
|
+
color: #fff;
|
|
297
|
+
white-space: nowrap;
|
|
298
|
+
overflow: hidden;
|
|
299
|
+
text-overflow: ellipsis;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.filedrop-file-meta {
|
|
303
|
+
font-size: 0.75rem;
|
|
304
|
+
color: rgba(255, 255, 255, 0.6);
|
|
305
|
+
margin-top: 0.125rem;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.filedrop-file-remove {
|
|
309
|
+
flex-shrink: 0;
|
|
310
|
+
width: 1.5rem;
|
|
311
|
+
height: 1.5rem;
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
justify-content: center;
|
|
315
|
+
border-radius: 0.25rem;
|
|
316
|
+
color: rgba(255, 255, 255, 0.6);
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
transition: all 0.2s ease-in-out;
|
|
319
|
+
pointer-events: auto;
|
|
320
|
+
background: transparent;
|
|
321
|
+
border: none;
|
|
322
|
+
font-size: 1rem;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.filedrop-file-remove:hover {
|
|
326
|
+
background-color: rgba(239, 68, 68, 0.2);
|
|
327
|
+
color: #fca5a5;
|
|
328
|
+
}
|
|
@@ -75,6 +75,7 @@ struct ServicesConfig {
|
|
|
75
75
|
bool enableDisplay = true; // Enable multi-display detection
|
|
76
76
|
bool enableKeyboard = true; // Enable keyboard shortcuts/hotkeys
|
|
77
77
|
bool enableMenu = true; // Enable custom menu bar
|
|
78
|
+
bool enableFileDrop = true; // Enable drag & drop file handling
|
|
78
79
|
} servicesConfig;
|
|
79
80
|
|
|
80
81
|
// ============================================================================
|
|
@@ -169,6 +170,24 @@ int main() {
|
|
|
169
170
|
// });
|
|
170
171
|
// Call from JS: const version = await app.invoke('getVersion');
|
|
171
172
|
|
|
173
|
+
// ========================================
|
|
174
|
+
// FILE DROP EVENTS (Drag & Drop)
|
|
175
|
+
// ========================================
|
|
176
|
+
// Listen for files dropped into the window
|
|
177
|
+
plusui::event::on("fileDrop.filesDropped", [](const std::string& data) {
|
|
178
|
+
std::cout << "Files dropped: " << data << std::endl;
|
|
179
|
+
// Parse the JSON data to get file info
|
|
180
|
+
// You can process files here in C++
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
plusui::event::on("fileDrop.dragEnter", [](const std::string&) {
|
|
184
|
+
std::cout << "Drag entered window" << std::endl;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
plusui::event::on("fileDrop.dragLeave", [](const std::string&) {
|
|
188
|
+
std::cout << "Drag left window" << std::endl;
|
|
189
|
+
});
|
|
190
|
+
|
|
172
191
|
// ========================================
|
|
173
192
|
// RUN APPLICATION
|
|
174
193
|
// ========================================
|
|
@@ -198,4 +217,8 @@ int main() {
|
|
|
198
217
|
// DISPLAY: display.getAll(), display.getPrimary(), display.getCurrent()
|
|
199
218
|
//
|
|
200
219
|
// CLIPBOARD: clipboard.writeText(str), clipboard.readText(), clipboard.clear()
|
|
220
|
+
//
|
|
221
|
+
// FILEDROP: fileDrop.onFilesDropped(callback), fileDrop.setEnabled(bool),
|
|
222
|
+
// fileDrop.onDragEnter(callback), fileDrop.onDragLeave(callback),
|
|
223
|
+
// fileDrop.startDrag([paths])
|
|
201
224
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createSignal, onMount, Show } from 'solid-js';
|
|
2
|
-
import { win, browser, router, app } from './plusui';
|
|
1
|
+
import { createSignal, onMount, onCleanup, Show, For } from 'solid-js';
|
|
2
|
+
import { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from './plusui';
|
|
3
3
|
|
|
4
4
|
// Define routes for your app (optional - for SPA routing)
|
|
5
5
|
const routes = {
|
|
@@ -14,6 +14,11 @@ function App() {
|
|
|
14
14
|
const [currentUrl, setCurrentUrl] = createSignal('');
|
|
15
15
|
const [canGoBack, setCanGoBack] = createSignal(false);
|
|
16
16
|
const [canGoForward, setCanGoForward] = createSignal(false);
|
|
17
|
+
|
|
18
|
+
// FileDrop state
|
|
19
|
+
const [files, setFiles] = createSignal<FileInfo[]>([]);
|
|
20
|
+
const [isDragging, setIsDragging] = createSignal(false);
|
|
21
|
+
const [dropZoneStyle, setDropZoneStyle] = createSignal('');
|
|
17
22
|
|
|
18
23
|
onMount(() => {
|
|
19
24
|
// Setup routes
|
|
@@ -30,6 +35,27 @@ function App() {
|
|
|
30
35
|
browser.getUrl().then(setCurrentUrl);
|
|
31
36
|
browser.canGoBack().then(setCanGoBack);
|
|
32
37
|
browser.canGoForward().then(setCanGoForward);
|
|
38
|
+
|
|
39
|
+
// Setup FileDrop listeners
|
|
40
|
+
const unsubDrop = fileDrop.onFilesDropped((droppedFiles) => {
|
|
41
|
+
console.log('Files dropped:', droppedFiles);
|
|
42
|
+
setFiles(prev => [...prev, ...droppedFiles]);
|
|
43
|
+
setIsDragging(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const unsubEnter = fileDrop.onDragEnter(() => {
|
|
47
|
+
setIsDragging(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const unsubLeave = fileDrop.onDragLeave(() => {
|
|
51
|
+
setIsDragging(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
onCleanup(() => {
|
|
55
|
+
unsubDrop();
|
|
56
|
+
unsubEnter();
|
|
57
|
+
unsubLeave();
|
|
58
|
+
});
|
|
33
59
|
});
|
|
34
60
|
|
|
35
61
|
const handleMinimize = async () => await win.minimize();
|
|
@@ -119,6 +145,69 @@ function App() {
|
|
|
119
145
|
<button onClick={handleQuit} class="button button-danger">Quit App</button>
|
|
120
146
|
</div>
|
|
121
147
|
|
|
148
|
+
<div class="card">
|
|
149
|
+
<h2>FileDrop - Drag & Drop Files</h2>
|
|
150
|
+
|
|
151
|
+
<div style={{ 'margin-bottom': '1rem' }}>
|
|
152
|
+
<label style={{ 'margin-right': '0.5rem', 'font-size': '0.9em' }}>Style:</label>
|
|
153
|
+
<select
|
|
154
|
+
value={dropZoneStyle()}
|
|
155
|
+
onChange={(e) => setDropZoneStyle(e.target.value)}
|
|
156
|
+
style={{
|
|
157
|
+
padding: '0.5rem',
|
|
158
|
+
'border-radius': '0.25rem',
|
|
159
|
+
border: '1px solid rgba(255,255,255,0.3)',
|
|
160
|
+
background: 'rgba(255,255,255,0.1)',
|
|
161
|
+
color: '#fff',
|
|
162
|
+
'font-size': '0.9em'
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<option value="">Default</option>
|
|
166
|
+
<option value="filedrop-compact">Compact</option>
|
|
167
|
+
<option value="filedrop-inline">Inline</option>
|
|
168
|
+
<option value="filedrop-minimal">Minimal</option>
|
|
169
|
+
<option value="filedrop-bold">Bold</option>
|
|
170
|
+
</select>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class={`filedrop-zone ${dropZoneStyle()} ${isDragging() ? 'filedrop-active' : ''}`}>
|
|
174
|
+
<div class="filedrop-content">
|
|
175
|
+
<svg class="filedrop-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
176
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2}
|
|
177
|
+
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
178
|
+
</svg>
|
|
179
|
+
<div class="filedrop-text">
|
|
180
|
+
{isDragging() ? 'Drop files here' : 'Drag & drop files'}
|
|
181
|
+
</div>
|
|
182
|
+
<div class="filedrop-hint">All file types supported</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<Show when={files().length > 0}>
|
|
187
|
+
<div class="filedrop-files">
|
|
188
|
+
<For each={files()}>
|
|
189
|
+
{(file, i) => (
|
|
190
|
+
<div class="filedrop-file-item">
|
|
191
|
+
<div class="filedrop-file-icon">📄</div>
|
|
192
|
+
<div class="filedrop-file-info">
|
|
193
|
+
<div class="filedrop-file-name">{file.name}</div>
|
|
194
|
+
<div class="filedrop-file-meta">
|
|
195
|
+
{formatFileSize(file.size)} • {file.type}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<button
|
|
199
|
+
class="filedrop-file-remove"
|
|
200
|
+
onClick={() => setFiles(files().filter((_, idx) => idx !== i()))}
|
|
201
|
+
>
|
|
202
|
+
✕
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</For>
|
|
207
|
+
</div>
|
|
208
|
+
</Show>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
122
211
|
<div class="info">
|
|
123
212
|
<p>Edit <code>frontend/src/App.tsx</code> to modify the UI.</p>
|
|
124
213
|
<p>Edit <code>main.cpp</code> to add C++ functionality.</p>
|
|
@@ -115,3 +115,50 @@ export const router = {
|
|
|
115
115
|
export const app = {
|
|
116
116
|
quit: async () => invoke('app.quit', []),
|
|
117
117
|
};
|
|
118
|
+
|
|
119
|
+
// FileDrop API
|
|
120
|
+
export interface FileInfo {
|
|
121
|
+
path: string;
|
|
122
|
+
name: string;
|
|
123
|
+
type: string;
|
|
124
|
+
size: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const fileDrop = {
|
|
128
|
+
setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
|
|
129
|
+
isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
|
|
130
|
+
onFilesDropped: (handler: (files: FileInfo[]) => void) => {
|
|
131
|
+
if (typeof window === 'undefined') return () => {};
|
|
132
|
+
const eventHandler = (event: Event) => {
|
|
133
|
+
const custom = event as CustomEvent<{ files?: FileInfo[] }>;
|
|
134
|
+
handler(custom.detail?.files ?? []);
|
|
135
|
+
};
|
|
136
|
+
window.addEventListener('plusui:fileDrop.filesDropped', eventHandler);
|
|
137
|
+
return () => window.removeEventListener('plusui:fileDrop.filesDropped', eventHandler);
|
|
138
|
+
},
|
|
139
|
+
onDragEnter: (handler: () => void) => {
|
|
140
|
+
if (typeof window === 'undefined') return () => {};
|
|
141
|
+
const eventHandler = () => handler();
|
|
142
|
+
window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
143
|
+
return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
|
|
144
|
+
},
|
|
145
|
+
onDragLeave: (handler: () => void) => {
|
|
146
|
+
if (typeof window === 'undefined') return () => {};
|
|
147
|
+
const eventHandler = () => handler();
|
|
148
|
+
window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
149
|
+
return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Helper functions
|
|
154
|
+
export function formatFileSize(bytes: number): string {
|
|
155
|
+
if (bytes === 0) return '0 Bytes';
|
|
156
|
+
const k = 1024;
|
|
157
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
158
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
159
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isImageFile(file: FileInfo): boolean {
|
|
163
|
+
return file.type.startsWith('image/');
|
|
164
|
+
}
|
|
@@ -138,3 +138,191 @@ body {
|
|
|
138
138
|
border-radius: 0.25rem;
|
|
139
139
|
font-family: 'Courier New', monospace;
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/* ============================================================================
|
|
143
|
+
* FILEDROP STYLES
|
|
144
|
+
* ============================================================================ */
|
|
145
|
+
|
|
146
|
+
.filedrop-zone {
|
|
147
|
+
position: relative;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
min-height: 180px;
|
|
153
|
+
padding: 2rem;
|
|
154
|
+
border: 2px dashed rgba(255, 255, 255, 0.3);
|
|
155
|
+
border-radius: 0.75rem;
|
|
156
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
157
|
+
transition: all 0.2s ease-in-out;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
user-select: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.filedrop-zone:hover {
|
|
163
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
164
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.filedrop-zone.filedrop-active {
|
|
168
|
+
border-color: #60a5fa;
|
|
169
|
+
background-color: rgba(96, 165, 250, 0.1);
|
|
170
|
+
border-width: 3px;
|
|
171
|
+
transform: scale(1.02);
|
|
172
|
+
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.filedrop-content {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 1rem;
|
|
180
|
+
text-align: center;
|
|
181
|
+
pointer-events: none;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.filedrop-icon {
|
|
185
|
+
width: 3rem;
|
|
186
|
+
height: 3rem;
|
|
187
|
+
color: rgba(255, 255, 255, 0.6);
|
|
188
|
+
transition: all 0.2s ease-in-out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.filedrop-zone.filedrop-active .filedrop-icon {
|
|
192
|
+
color: #60a5fa;
|
|
193
|
+
transform: scale(1.2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.filedrop-text {
|
|
197
|
+
font-size: 1rem;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
color: rgba(255, 255, 255, 0.9);
|
|
200
|
+
transition: color 0.2s ease-in-out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.filedrop-zone.filedrop-active .filedrop-text {
|
|
204
|
+
color: #93c5fd;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.filedrop-hint {
|
|
209
|
+
font-size: 0.875rem;
|
|
210
|
+
color: rgba(255, 255, 255, 0.6);
|
|
211
|
+
transition: color 0.2s ease-in-out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.filedrop-zone.filedrop-active .filedrop-hint {
|
|
215
|
+
color: rgba(255, 255, 255, 0.8);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.filedrop-zone.filedrop-compact {
|
|
219
|
+
min-height: 120px;
|
|
220
|
+
padding: 1.5rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.filedrop-zone.filedrop-compact .filedrop-icon {
|
|
224
|
+
width: 2rem;
|
|
225
|
+
height: 2rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.filedrop-zone.filedrop-inline {
|
|
229
|
+
min-height: 80px;
|
|
230
|
+
padding: 1rem;
|
|
231
|
+
flex-direction: row;
|
|
232
|
+
justify-content: flex-start;
|
|
233
|
+
gap: 1rem;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.filedrop-zone.filedrop-inline .filedrop-content {
|
|
237
|
+
flex-direction: row;
|
|
238
|
+
align-items: center;
|
|
239
|
+
text-align: left;
|
|
240
|
+
gap: 0.75rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.filedrop-zone.filedrop-minimal {
|
|
244
|
+
border-style: solid;
|
|
245
|
+
border-width: 1px;
|
|
246
|
+
background-color: transparent;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.filedrop-zone.filedrop-bold {
|
|
250
|
+
border-width: 3px;
|
|
251
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.filedrop-files {
|
|
255
|
+
margin-top: 1rem;
|
|
256
|
+
width: 100%;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.filedrop-file-item {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 0.75rem;
|
|
263
|
+
padding: 0.75rem;
|
|
264
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
265
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
266
|
+
border-radius: 0.5rem;
|
|
267
|
+
margin-bottom: 0.5rem;
|
|
268
|
+
transition: all 0.2s ease-in-out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.filedrop-file-item:hover {
|
|
272
|
+
background-color: rgba(255, 255, 255, 0.15);
|
|
273
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.filedrop-file-icon {
|
|
277
|
+
width: 2rem;
|
|
278
|
+
height: 2rem;
|
|
279
|
+
flex-shrink: 0;
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
284
|
+
border-radius: 0.375rem;
|
|
285
|
+
font-size: 1.2rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.filedrop-file-info {
|
|
289
|
+
flex: 1;
|
|
290
|
+
min-width: 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.filedrop-file-name {
|
|
294
|
+
font-size: 0.875rem;
|
|
295
|
+
font-weight: 500;
|
|
296
|
+
color: #fff;
|
|
297
|
+
white-space: nowrap;
|
|
298
|
+
overflow: hidden;
|
|
299
|
+
text-overflow: ellipsis;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.filedrop-file-meta {
|
|
303
|
+
font-size: 0.75rem;
|
|
304
|
+
color: rgba(255, 255, 255, 0.6);
|
|
305
|
+
margin-top: 0.125rem;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.filedrop-file-remove {
|
|
309
|
+
flex-shrink: 0;
|
|
310
|
+
width: 1.5rem;
|
|
311
|
+
height: 1.5rem;
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
justify-content: center;
|
|
315
|
+
border-radius: 0.25rem;
|
|
316
|
+
color: rgba(255, 255, 255, 0.6);
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
transition: all 0.2s ease-in-out;
|
|
319
|
+
pointer-events: auto;
|
|
320
|
+
background: transparent;
|
|
321
|
+
border: none;
|
|
322
|
+
font-size: 1rem;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.filedrop-file-remove:hover {
|
|
326
|
+
background-color: rgba(239, 68, 68, 0.2);
|
|
327
|
+
color: #fca5a5;
|
|
328
|
+
}
|
|
@@ -75,6 +75,7 @@ struct ServicesConfig {
|
|
|
75
75
|
bool enableDisplay = true; // Enable multi-display detection
|
|
76
76
|
bool enableKeyboard = true; // Enable keyboard shortcuts/hotkeys
|
|
77
77
|
bool enableMenu = true; // Enable custom menu bar
|
|
78
|
+
bool enableFileDrop = true; // Enable drag & drop file handling
|
|
78
79
|
} servicesConfig;
|
|
79
80
|
|
|
80
81
|
// ============================================================================
|
|
@@ -160,6 +161,24 @@ int main() {
|
|
|
160
161
|
// });
|
|
161
162
|
// Call from JS: const version = await app.invoke('getVersion');
|
|
162
163
|
|
|
164
|
+
// ========================================
|
|
165
|
+
// FILE DROP EVENTS (Drag & Drop)
|
|
166
|
+
// ========================================
|
|
167
|
+
// Listen for files dropped into the window
|
|
168
|
+
plusui::event::on("fileDrop.filesDropped", [](const std::string& data) {
|
|
169
|
+
std::cout << "Files dropped: " << data << std::endl;
|
|
170
|
+
// Parse the JSON data to get file info
|
|
171
|
+
// You can process files here in C++
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
plusui::event::on("fileDrop.dragEnter", [](const std::string&) {
|
|
175
|
+
std::cout << "Drag entered window" << std::endl;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
plusui::event::on("fileDrop.dragLeave", [](const std::string&) {
|
|
179
|
+
std::cout << "Drag left window" << std::endl;
|
|
180
|
+
});
|
|
181
|
+
|
|
163
182
|
// ========================================
|
|
164
183
|
// RUN APPLICATION
|
|
165
184
|
// ========================================
|
|
@@ -189,4 +208,8 @@ int main() {
|
|
|
189
208
|
// DISPLAY: display.getAll(), display.getPrimary(), display.getCurrent()
|
|
190
209
|
//
|
|
191
210
|
// CLIPBOARD: clipboard.writeText(str), clipboard.readText(), clipboard.clear()
|
|
211
|
+
//
|
|
212
|
+
// FILEDROP: fileDrop.onFilesDropped(callback), fileDrop.setEnabled(bool),
|
|
213
|
+
// fileDrop.onDragEnter(callback), fileDrop.onDragLeave(callback),
|
|
214
|
+
// fileDrop.startDrag([paths])
|
|
192
215
|
|