plusui-native-core 0.1.98 → 0.1.99

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.
@@ -1,43 +1,2 @@
1
- /**
2
- * FileDrop API
3
- *
4
- * Handle files dragged and dropped onto the application window.
5
- * Also supports initiating drag operations from the app.
6
- *
7
- * Actions:
8
- * fileDrop.setEnabled(enabled) - Enable or disable file drop
9
- * fileDrop.isEnabled() - Check if file drop is enabled
10
- * fileDrop.startDrag(filePaths) - Start a drag operation with the given files
11
- * fileDrop.clearCallbacks() - Remove all registered drop callbacks
12
- *
13
- * Events:
14
- * fileDrop.onFilesDropped(cb) - Fires when files are dropped onto the window
15
- * fileDrop.onDragEnter(cb) - Fires when a drag enters the window
16
- * fileDrop.onDragLeave(cb) - Fires when a drag leaves the window
17
- *
18
- * Helpers (pure functions):
19
- * readFileAsText(path) - Read a file path as a text string
20
- * readFileAsDataUrl(path) - Read a file path as a base64 data URL
21
- * filterFilesByExtension(files, ext) - Filter FileInfo[] by extension(s)
22
- * filterFilesByMimeType(files, mime) - Filter FileInfo[] by MIME type(s)
23
- * isImageFile(file) - Check if FileInfo is an image
24
- * isVideoFile(file) - Check if FileInfo is a video
25
- * isAudioFile(file) - Check if FileInfo is audio
26
- * isTextFile(file) - Check if FileInfo is a text file
27
- * formatFileSize(bytes) - Format byte count as a human-readable string
28
- */
29
-
30
- export type { FileInfo, DragEvent, FileDropAPI } from '../FileDrop/filedrop';
31
-
32
- export {
33
- fileDrop,
34
- readFileAsText,
35
- readFileAsDataUrl,
36
- filterFilesByExtension,
37
- filterFilesByMimeType,
38
- isImageFile,
39
- isVideoFile,
40
- isAudioFile,
41
- isTextFile,
42
- formatFileSize,
43
- } from '../FileDrop/filedrop';
1
+ export type { FileInfo, DropZone } from '../FileDrop/filedrop';
2
+ export { fileDrop, formatFileSize, createDropZone } from '../FileDrop/filedrop';
@@ -1,9 +1,5 @@
1
1
  /**
2
2
  * PlusUI API
3
- *
4
- * window.show(), window.hide(), window.close()
5
- * window.maximize(), window.minimize(), window.restore()
6
- * window.setSize(w, h), window.setPosition(x, y)
7
3
  */
8
4
 
9
5
  export { window, win } from './window-api';
@@ -14,7 +10,7 @@ export { router } from './router-api';
14
10
  export { keyboard, KeyCode, KeyMod } from './keyboard-api';
15
11
  export { tray } from './tray-api';
16
12
  export { display } from './display-api';
17
- export { fileDrop, formatFileSize } from './filedrop-api';
13
+ export { fileDrop, formatFileSize, createDropZone } from './filedrop-api';
18
14
  export { menu } from './menu-api';
19
15
  export { gpu, GPUBufferUsage, GPUTextureUsage, GPUMapMode, GPUShaderStage, GPUColorWrite } from './webgpu-api';
20
16
 
@@ -22,19 +18,17 @@ export { connect, createChannel, type Channel } from './Connect_API';
22
18
  export { _client } from '../Connection/connect';
23
19
 
24
20
  export type { WindowSize, WindowPosition, WindowRect } from './window-api';
21
+ export type { FileInfo, DropZone } from './filedrop-api';
25
22
  export type { ClipboardAPI } from './clipboard-api';
26
23
  export type { KeyEvent, Shortcut } from './keyboard-api';
27
24
  export type { TrayMenuItem, TrayIconData } from './tray-api';
28
25
  export type { Display, DisplayMode, DisplayBounds, DisplayResolution } from './display-api';
29
- export type { FileInfo, FileDropAPI } from './filedrop-api';
30
26
  export type { MenuItem, MenuItemType, MenuBarData, ContextMenuOptions, ContextInfo } from './menu-api';
31
27
  export type { AppConfig } from './app-api';
32
28
  export type { GPUAdapter, GPUDevice, GPUBuffer, GPUTexture, GPUShaderModule, GPURenderPipeline, GPUComputePipeline, GPUQueue, GPUCommandEncoder } from './webgpu-api';
33
29
  export type { BrowserState, NavigateCallback, StateCallback, LoadCallback, ErrorCallback } from './browser-api';
34
30
  export type { RouteMap, RouteChangeCallback, RouteConfig } from './router-api';
35
31
 
36
- export { readFileAsText, readFileAsDataUrl, filterFilesByExtension, filterFilesByMimeType, isImageFile, isVideoFile, isAudioFile, isTextFile } from './filedrop-api';
37
-
38
32
  import { window, win } from './window-api';
39
33
  import { clipboard } from './clipboard-api';
40
34
  import { app } from './app-api';
@@ -43,7 +37,7 @@ import { router } from './router-api';
43
37
  import { keyboard, KeyCode, KeyMod } from './keyboard-api';
44
38
  import { tray } from './tray-api';
45
39
  import { display } from './display-api';
46
- import { fileDrop, formatFileSize } from './filedrop-api';
40
+ import { fileDrop, formatFileSize, createDropZone } from './filedrop-api';
47
41
  import { menu } from './menu-api';
48
42
  import { gpu, GPUBufferUsage, GPUTextureUsage, GPUMapMode, GPUShaderStage, GPUColorWrite } from './webgpu-api';
49
43
  import { connect, createChannel } from './Connect_API';
@@ -60,6 +54,7 @@ const plusui = {
60
54
  tray,
61
55
  display,
62
56
  fileDrop,
57
+ createDropZone,
63
58
  menu,
64
59
  gpu,
65
60
  connect,
@@ -1,15 +1,3 @@
1
- /**
2
- * FileDrop API - Cross-platform drag & drop file handling
3
- *
4
- * Direct method calls - no .on/.emit
5
- *
6
- * Usage:
7
- * import { fileDrop } from '@plusui/api';
8
- *
9
- * await fileDrop.setEnabled(true);
10
- * fileDrop.onFilesDropped((files) => { ... });
11
- */
12
-
13
1
  export interface FileInfo {
14
2
  path: string;
15
3
  name: string;
@@ -17,147 +5,87 @@ export interface FileInfo {
17
5
  size: number;
18
6
  }
19
7
 
20
- export interface DragEvent {
21
- x?: number;
22
- y?: number;
23
- }
24
-
25
- export interface FileDropAPI {
26
- setEnabled(enabled: boolean): Promise<void>;
27
- isEnabled(): Promise<boolean>;
28
- startDrag(filePaths: string[]): Promise<boolean>;
29
- clearCallbacks(): Promise<void>;
30
- onFilesDropped(callback: (files: FileInfo[]) => void): () => void;
31
- onDragEnter(callback: (event: DragEvent) => void): () => void;
32
- onDragLeave(callback: (event: DragEvent) => void): () => void;
33
- }
34
-
35
- let _invoke: ((name: string, args?: unknown[]) => Promise<unknown>) | null = null;
36
- let _event: ((event: string, callback: (data: unknown) => void) => () => void) | null = null;
8
+ let callId = 0;
9
+ const pending = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void }>();
10
+ const zoneCallbacks = new Map<string, { onFiles: (files: FileInfo[]) => void }>();
37
11
 
38
- export function setInvokeFn(fn: (name: string, args?: unknown[]) => Promise<unknown>) {
39
- _invoke = fn;
12
+ function getGlobal(): any {
13
+ if (typeof window !== 'undefined') return window;
14
+ if (typeof globalThis !== 'undefined') return globalThis;
15
+ return {};
40
16
  }
41
17
 
42
- export function setEventFn(fn: (event: string, callback: (data: unknown) => void) => () => void) {
43
- _event = fn;
44
- }
45
-
46
- async function invoke<T = unknown>(name: string, args?: unknown[]): Promise<T> {
47
- if (!_invoke) {
48
- if (typeof window !== 'undefined' && (window as any).__invoke__) {
49
- _invoke = (window as any).__invoke__;
50
- } else {
51
- throw new Error('FileDrop API not initialized');
52
- }
53
- }
54
- return _invoke!(name, args) as Promise<T>;
55
- }
18
+ const g = getGlobal();
56
19
 
57
- function eventHandler(event: string, callback: (data: unknown) => void): () => void {
58
- if (!_event) {
59
- if (typeof window !== 'undefined' && (window as any).__on__) {
60
- _event = (window as any).__on__;
61
- } else {
62
- return () => {};
63
- }
20
+ g.__response__ = function(id: string, result: any, error?: any) {
21
+ const p = pending.get(id);
22
+ if (p) {
23
+ pending.delete(id);
24
+ error ? p.reject(new Error(error)) : p.resolve(result);
64
25
  }
65
- return _event!(event, callback);
66
- }
67
-
68
- export const fileDrop: FileDropAPI = {
69
- async setEnabled(enabled: boolean): Promise<void> {
70
- await invoke('fileDrop.setEnabled', [enabled]);
71
- },
72
-
73
- async isEnabled(): Promise<boolean> {
74
- return invoke<boolean>('fileDrop.isEnabled');
75
- },
76
-
77
- async startDrag(filePaths: string[]): Promise<boolean> {
78
- if (!filePaths || filePaths.length === 0) {
79
- throw new Error('filePaths must be a non-empty array');
80
- }
81
- return invoke<boolean>('fileDrop.startDrag', [filePaths]);
82
- },
83
-
84
- async clearCallbacks(): Promise<void> {
85
- await invoke('fileDrop.clearCallbacks');
86
- },
87
-
88
- onFilesDropped(callback: (files: FileInfo[]) => void): () => void {
89
- return eventHandler('fileDrop:filesDropped', callback as (data: unknown) => void);
90
- },
91
-
92
- onDragEnter(callback: (event: DragEvent) => void): () => void {
93
- return eventHandler('fileDrop:dragEnter', callback as (data: unknown) => void);
94
- },
95
-
96
- onDragLeave(callback: (event: DragEvent) => void): () => void {
97
- return eventHandler('fileDrop:dragLeave', callback as (data: unknown) => void);
98
- },
99
26
  };
100
27
 
101
- export async function readFileAsText(filePath: string): Promise<string> {
102
- const response = await fetch(`file://${filePath}`);
103
- return await response.text();
104
- }
28
+ g.__plusui_fileDrop__ = function(zoneName: string, files: FileInfo[]) {
29
+ const zone = zoneCallbacks.get(zoneName);
30
+ if (zone && zone.onFiles) zone.onFiles(files);
31
+ };
105
32
 
106
- export async function readFileAsDataUrl(filePath: string): Promise<string> {
107
- const response = await fetch(`file://${filePath}`);
108
- const blob = await response.blob();
33
+ async function invoke<T>(method: string, params: any[] = []): Promise<T> {
109
34
  return new Promise((resolve, reject) => {
110
- const reader = new FileReader();
111
- reader.onload = () => resolve(reader.result as string);
112
- reader.onerror = reject;
113
- reader.readAsDataURL(blob);
114
- });
115
- }
116
-
117
- export function filterFilesByExtension(
118
- files: FileInfo[],
119
- extensions: string[]
120
- ): FileInfo[] {
121
- const lowerExtensions = extensions.map((ext) => ext.toLowerCase());
122
- return files.filter((file) => {
123
- const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
124
- return lowerExtensions.includes(ext);
35
+ const id = String(++callId);
36
+ pending.set(id, { resolve, reject });
37
+ const payload = JSON.stringify({ id, method, params });
38
+
39
+ if (g.__native_invoke__) {
40
+ g.__native_invoke__(payload);
41
+ } else if (g.chrome && g.chrome.webview && g.chrome.webview.postMessage) {
42
+ g.chrome.webview.postMessage(payload);
43
+ } else {
44
+ pending.delete(id);
45
+ console.warn('[PlusUI] ' + method + ' - native bridge not ready');
46
+ resolve(null as T);
47
+ }
125
48
  });
126
49
  }
127
50
 
128
- export function filterFilesByMimeType(
129
- files: FileInfo[],
130
- mimeTypes: string[]
131
- ): FileInfo[] {
132
- return files.filter((file) => mimeTypes.includes(file.type));
51
+ export interface DropZone {
52
+ onFiles: (callback: (files: FileInfo[]) => void) => () => void;
53
+ element: HTMLElement | null;
133
54
  }
134
55
 
135
- export function isImageFile(file: FileInfo): boolean {
136
- return file.type.startsWith('image/');
137
- }
138
-
139
- export function isVideoFile(file: FileInfo): boolean {
140
- return file.type.startsWith('video/');
141
- }
56
+ export function createDropZone(name: string, el?: HTMLElement | null): DropZone {
57
+ const element = el || document.querySelector(`[data-dropzone="${name}"]`) as HTMLElement;
58
+
59
+ if (element) {
60
+ element.setAttribute('data-dropzone', name);
61
+ element.addEventListener('drop', (e: DragEvent) => {
62
+ e.preventDefault();
63
+ e.stopPropagation();
64
+ element.classList.remove('dropzone-active');
65
+ });
66
+ }
142
67
 
143
- export function isAudioFile(file: FileInfo): boolean {
144
- return file.type.startsWith('audio/');
68
+ return {
69
+ element,
70
+ onFiles: (callback: (files: FileInfo[]) => void) => {
71
+ zoneCallbacks.set(name, { onFiles: callback });
72
+ return () => zoneCallbacks.delete(name);
73
+ }
74
+ };
145
75
  }
146
76
 
147
- export function isTextFile(file: FileInfo): boolean {
148
- return file.type.startsWith('text/') ||
149
- file.type === 'application/json' ||
150
- file.type === 'application/xml';
151
- }
77
+ export const fileDrop = {
78
+ setEnabled: (enabled: boolean) => invoke<void>('fileDrop.setEnabled', [enabled]),
79
+ isEnabled: () => invoke<boolean>('fileDrop.isEnabled'),
80
+ createDropZone,
81
+ };
152
82
 
153
83
  export function formatFileSize(bytes: number): string {
154
- if (bytes === 0) return '0 Bytes';
155
-
84
+ if (bytes === 0) return '0 B';
156
85
  const k = 1024;
157
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
86
+ const sizes = ['B', 'KB', 'MB', 'GB'];
158
87
  const i = Math.floor(Math.log(bytes) / Math.log(k));
159
-
160
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
88
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
161
89
  }
162
90
 
163
91
  export default fileDrop;
@@ -1024,65 +1024,56 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1024
1024
  // This prevents files from being loaded in the browser
1025
1025
  // and allows the native FileDrop API to handle them
1026
1026
  // instead
1027
- if (pImpl->config.disableWebviewDragDrop) {
1028
- std::string disableDragDropScript = R"(
1029
- (function() {
1030
- if (window.__plusui_nativeFileDropOnly) return;
1031
- window.__plusui_nativeFileDropOnly = true;
1032
-
1033
- var isOverDropZone = function(e) {
1034
- if (!e) return false;
1035
- var target = null;
1036
- if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) {
1037
- target = document.elementFromPoint(e.clientX, e.clientY);
1038
- }
1039
- if (!target && e.target && e.target.nodeType === 1) {
1040
- target = e.target;
1041
- }
1042
- if (!target || !target.closest) return false;
1043
- return !!target.closest('[data-dropzone="true"], .filedrop-zone, .dropzone');
1044
- };
1045
-
1046
- var zoneActive = false;
1047
- var emitZoneState = function(next) {
1048
- if (next === zoneActive) return;
1049
- zoneActive = next;
1050
- window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));
1051
- };
1052
-
1053
- var block = function(e) {
1054
- if (!e) return false;
1055
- var overDropZone = isOverDropZone(e);
1056
- if (e.type === 'drop') {
1057
- emitZoneState(false);
1058
- } else {
1059
- emitZoneState(overDropZone);
1060
- }
1061
- e.preventDefault();
1062
- e.stopPropagation();
1063
- if (typeof e.stopImmediatePropagation === 'function') {
1064
- e.stopImmediatePropagation();
1065
- }
1066
- if (e.dataTransfer) {
1067
- try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {}
1068
- }
1069
- return false;
1070
- };
1071
-
1072
- window.__plusui_dragDropEvents = ['dragenter', 'dragover', 'dragleave', 'drop'];
1073
- window.__plusui_dragDropBlocker = block;
1074
-
1075
- window.__plusui_dragDropEvents.forEach(function(eventName) {
1076
- window.addEventListener(eventName, block, true);
1077
- document.addEventListener(eventName, block, true);
1078
- });
1079
- })();
1080
- )";
1081
- pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1082
- std::wstring(disableDragDropScript.begin(),
1083
- disableDragDropScript.end())
1084
- .c_str(),
1085
- nullptr);
1027
+ std::string dropzoneScript = R"(
1028
+ (function() {
1029
+ if (window.__plusui_dropzone_init) return;
1030
+ window.__plusui_dropzone_init = true;
1031
+
1032
+ var activeZone = null;
1033
+
1034
+ var findDropZone = function(e) {
1035
+ var target = null;
1036
+ if (e && typeof e.clientX === 'number' && document.elementFromPoint) {
1037
+ target = document.elementFromPoint(e.clientX, e.clientY);
1038
+ }
1039
+ if (!target && e && e.target) target = e.target;
1040
+ if (!target || !target.closest) return null;
1041
+ return target.closest('[data-dropzone]');
1042
+ };
1043
+
1044
+ var updateActiveZone = function(zone) {
1045
+ if (activeZone === zone) return;
1046
+ if (activeZone) activeZone.classList.remove('dropzone-active');
1047
+ activeZone = zone;
1048
+ if (activeZone) activeZone.classList.add('dropzone-active');
1049
+ };
1050
+
1051
+ var handleDrag = function(e) {
1052
+ var zone = findDropZone(e);
1053
+ updateActiveZone(zone);
1054
+ if (e.dataTransfer) {
1055
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1056
+ }
1057
+ if (!zone) {
1058
+ e.preventDefault();
1059
+ e.stopPropagation();
1060
+ }
1061
+ };
1062
+
1063
+ ['dragenter', 'dragover', 'dragleave'].forEach(function(ev) {
1064
+ document.addEventListener(ev, handleDrag, true);
1065
+ });
1066
+
1067
+ document.addEventListener('drop', function(e) {
1068
+ updateActiveZone(null);
1069
+ }, true);
1070
+ })();
1071
+ )";
1072
+ pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1073
+ std::wstring(dropzoneScript.begin(),
1074
+ dropzoneScript.end())
1075
+ .c_str(),
1076
+ nullptr);
1086
1077
  }
1087
1078
 
1088
1079
  // Set up WebMessageReceived handler for JS->C++ bridge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.98",
3
+ "version": "0.1.99",
4
4
  "description": "PlusUI Core framework (frontend + backend implementations)",
5
5
  "type": "module",
6
6
  "main": "./Core/Features/API/index.ts",