@mindexec/cli 0.2.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 +275 -0
- package/codex-runtime.js +960 -0
- package/launch-bridge.cjs +162 -0
- package/package.json +61 -0
- package/port-guard.cjs +232 -0
- package/scripts/setup-tree-sitter-grammars.mjs +59 -0
- package/server.js +8422 -0
- package/start-bridge.bat +32 -0
- package/start-bridge.sh +81 -0
- package/tree-sitter-grammars/README.md +18 -0
- package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
- package/wwwroot/MindExecution.Web.styles.css +3 -0
- package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/MindExecution.Plugins.Directory.u7utcng611.bundle.scp.css +7 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/directory-manager.js +202 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/exampleJsInterop.js +6 -0
- package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -0
- package/wwwroot/_content/MindExecution.Shared/MindExecution.Shared.wsano1j4wp.bundle.scp.css +4 -0
- package/wwwroot/_content/MindExecution.Shared/css/admin-dashboard.css +559 -0
- package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -0
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +2936 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +359 -0
- package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1721 -0
- package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -0
- package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +639 -0
- package/wwwroot/_content/MindExecution.Shared/js/helpers/InfiniteGridHelper.js +109 -0
- package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +7982 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +1059 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +15803 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1430 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +434 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +260 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7640 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9923 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +977 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1431 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +1716 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +553 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4541 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +489 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +372 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2075 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +646 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-overlay-v2.js +4323 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2260 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +1258 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +890 -0
- package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +594 -0
- package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +170 -0
- package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +788 -0
- package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +50 -0
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +188 -0
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker.js +208 -0
- package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -0
- package/wwwroot/_content/MindExecution.Shared/js/titlebar-handler.js +191 -0
- package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +37 -0
- package/wwwroot/_content/MindExecution.Shared/js/token-worker.js +28 -0
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +5626 -0
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +7 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +9 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
- package/wwwroot/_content/MindExecution.Shared/models/all-MiniLM-L6-v2-quantized.onnx +0 -0
- package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -0
- package/wwwroot/_framework/Google.Protobuf.9h59ukbel7.dll +0 -0
- package/wwwroot/_framework/Markdig.d1j7v41cl1.dll +0 -0
- package/wwwroot/_framework/MessagePack.Annotations.l6qv48kgpt.dll +0 -0
- package/wwwroot/_framework/MessagePack.eqoptzx9d5.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Authorization.k7dsih5y5g.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.6nyje9sa0g.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.Authorization.iycd6unprw.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.Web.487u3twia4.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Components.WebAssembly.d0gcnmlxxz.dll +0 -0
- package/wwwroot/_framework/Microsoft.AspNetCore.Metadata.h4yevl9adi.dll +0 -0
- package/wwwroot/_framework/Microsoft.CSharp.qrvp77qmhs.dll +0 -0
- package/wwwroot/_framework/Microsoft.Data.Sqlite.jdlxgv2jtg.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.4gjazp7kjf.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Abstractions.gocudnvz7b.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Relational.lt4rsvinuo.dll +0 -0
- package/wwwroot/_framework/Microsoft.EntityFrameworkCore.Sqlite.69luj0fa9r.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Caching.Abstractions.364t4jh3zz.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Caching.Memory.izlxhpzosu.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Configuration.8zq7hh41o7.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Configuration.Abstractions.8if74zs6ea.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Configuration.Json.duvlngw8i0.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.Abstractions.t2hh9kvx0o.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.DependencyInjection.n4tg99oy8l.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.DependencyModel.h0d06ixk3e.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Logging.Abstractions.rl32bkx2sd.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Logging.dlht1xei0t.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Options.qeunebioml.dll +0 -0
- package/wwwroot/_framework/Microsoft.Extensions.Primitives.18cr6vnuuz.dll +0 -0
- package/wwwroot/_framework/Microsoft.IO.RecyclableMemoryStream.r915vovvw4.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.Abstractions.1ejljk3erv.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.JsonWebTokens.1596zr8gne.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.Logging.229uyvpgio.dll +0 -0
- package/wwwroot/_framework/Microsoft.IdentityModel.Tokens.9sibtajc9f.dll +0 -0
- package/wwwroot/_framework/Microsoft.JSInterop.17lq4j1j7g.dll +0 -0
- package/wwwroot/_framework/Microsoft.JSInterop.WebAssembly.ryia5gxiad.dll +0 -0
- package/wwwroot/_framework/Microsoft.ML.OnnxRuntime.w9deo1m5ss.dll +0 -0
- package/wwwroot/_framework/Microsoft.ML.Tokenizers.cm2vuv2z61.dll +0 -0
- package/wwwroot/_framework/Microsoft.NET.StringTools.3qbrf4v2ki.dll +0 -0
- package/wwwroot/_framework/MimeMapping.og9ys58ylm.dll +0 -0
- package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
- package/wwwroot/_framework/MindExecution.Kernel.gwwc40sc45.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Admin.0jgrn1sckv.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Business.13mme2qcag.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Concept.dfp2mdt45q.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.Directory.3w4t6n3se0.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.PlanMaster.s0qpntz420.dll +0 -0
- package/wwwroot/_framework/MindExecution.Plugins.YouTube.iu11fq8d16.dll +0 -0
- package/wwwroot/_framework/MindExecution.Shared.7j27dcqnrc.dll +0 -0
- package/wwwroot/_framework/MindExecution.Web.pq1ty8ov2v.dll +0 -0
- package/wwwroot/_framework/Newtonsoft.Json.a56zs13vug.dll +0 -0
- package/wwwroot/_framework/SQLitePCLRaw.batteries_v2.rrd1nzawpp.dll +0 -0
- package/wwwroot/_framework/SQLitePCLRaw.core.1dxloztpfz.dll +0 -0
- package/wwwroot/_framework/SQLitePCLRaw.provider.e_sqlite3.oekyzl53i1.dll +0 -0
- package/wwwroot/_framework/Supabase.Core.s1pkj4aj0l.dll +0 -0
- package/wwwroot/_framework/Supabase.Functions.qz4nu782sg.dll +0 -0
- package/wwwroot/_framework/Supabase.Gotrue.twah27pkik.dll +0 -0
- package/wwwroot/_framework/Supabase.Postgrest.gmuuv369ih.dll +0 -0
- package/wwwroot/_framework/Supabase.Realtime.ox3kchdy3w.dll +0 -0
- package/wwwroot/_framework/Supabase.Storage.fnjnepaowr.dll +0 -0
- package/wwwroot/_framework/Supabase.azmaw5pgcz.dll +0 -0
- package/wwwroot/_framework/System.Collections.Concurrent.y1zmvuyipi.dll +0 -0
- package/wwwroot/_framework/System.Collections.Immutable.ug3j698qms.dll +0 -0
- package/wwwroot/_framework/System.Collections.NonGeneric.h66hj3863h.dll +0 -0
- package/wwwroot/_framework/System.Collections.Specialized.umr3y27ntj.dll +0 -0
- package/wwwroot/_framework/System.Collections.x53e19vfsj.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.Annotations.tz6gnt4ebt.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.Primitives.j7tiphu4rg.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.TypeConverter.ujlztox1gx.dll +0 -0
- package/wwwroot/_framework/System.ComponentModel.x9xz0ojfb6.dll +0 -0
- package/wwwroot/_framework/System.Console.ijzpqmj7ne.dll +0 -0
- package/wwwroot/_framework/System.Data.Common.1r0sqffq1p.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.DiagnosticSource.9upoqwq09o.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.Process.m99azzntjm.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.TraceSource.pl7wv26myr.dll +0 -0
- package/wwwroot/_framework/System.Diagnostics.Tracing.crlhfx6tut.dll +0 -0
- package/wwwroot/_framework/System.Drawing.Primitives.22e4y9ikq9.dll +0 -0
- package/wwwroot/_framework/System.Drawing.mi7d8hwowb.dll +0 -0
- package/wwwroot/_framework/System.Formats.Asn1.jx23sjiqnn.dll +0 -0
- package/wwwroot/_framework/System.IO.Compression.6fyoii3uej.dll +0 -0
- package/wwwroot/_framework/System.IO.Pipelines.vg77t4cd4d.dll +0 -0
- package/wwwroot/_framework/System.IdentityModel.Tokens.Jwt.t67es60z5b.dll +0 -0
- package/wwwroot/_framework/System.Linq.1bkoxlqgmq.dll +0 -0
- package/wwwroot/_framework/System.Linq.Expressions.24xqiypwdt.dll +0 -0
- package/wwwroot/_framework/System.Linq.Queryable.hvd01d6rsa.dll +0 -0
- package/wwwroot/_framework/System.Memory.8dx3lwgym4.dll +0 -0
- package/wwwroot/_framework/System.Net.Http.Json.3mhdm9l1rf.dll +0 -0
- package/wwwroot/_framework/System.Net.Http.eitrz660my.dll +0 -0
- package/wwwroot/_framework/System.Net.NetworkInformation.3pkuofcv9r.dll +0 -0
- package/wwwroot/_framework/System.Net.Ping.8clj5pklrp.dll +0 -0
- package/wwwroot/_framework/System.Net.Primitives.qrp4wcjz1p.dll +0 -0
- package/wwwroot/_framework/System.Net.WebSockets.Client.2u6pv01g69.dll +0 -0
- package/wwwroot/_framework/System.Net.WebSockets.qp6u31zvm5.dll +0 -0
- package/wwwroot/_framework/System.Numerics.Tensors.0c7z4mt3on.dll +0 -0
- package/wwwroot/_framework/System.Numerics.Vectors.kc7ufp2j4l.dll +0 -0
- package/wwwroot/_framework/System.ObjectModel.qv82fot1ib.dll +0 -0
- package/wwwroot/_framework/System.Private.CoreLib.rkafq04oma.dll +0 -0
- package/wwwroot/_framework/System.Private.Uri.t9542hmr6j.dll +0 -0
- package/wwwroot/_framework/System.Private.Xml.Linq.n8n3ptrbwu.dll +0 -0
- package/wwwroot/_framework/System.Private.Xml.rxd3tytisn.dll +0 -0
- package/wwwroot/_framework/System.Reactive.t3fuon548l.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Emit.9tjhp6y0j3.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Emit.ILGeneration.stxyk8zoo1.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Emit.Lightweight.6xrd5v8vg0.dll +0 -0
- package/wwwroot/_framework/System.Reflection.Primitives.wgn8fpwwvv.dll +0 -0
- package/wwwroot/_framework/System.Runtime.InteropServices.JavaScript.sliym526xh.dll +0 -0
- package/wwwroot/_framework/System.Runtime.InteropServices.RuntimeInformation.oji7zut14z.dll +0 -0
- package/wwwroot/_framework/System.Runtime.InteropServices.te07xr2we9.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Intrinsics.507y4h8nzq.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Loader.v7gk4bse0k.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Numerics.eqy5xjv3nd.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Serialization.Formatters.zpkrub8lab.dll +0 -0
- package/wwwroot/_framework/System.Runtime.Serialization.Primitives.vhkpnbxjip.dll +0 -0
- package/wwwroot/_framework/System.Runtime.jn319d5nyg.dll +0 -0
- package/wwwroot/_framework/System.Security.Claims.0ztig1q9vo.dll +0 -0
- package/wwwroot/_framework/System.Security.Cryptography.vttizqc9ho.dll +0 -0
- package/wwwroot/_framework/System.Text.Encoding.Extensions.utdd47ny8f.dll +0 -0
- package/wwwroot/_framework/System.Text.Encodings.Web.wah8r1zoe0.dll +0 -0
- package/wwwroot/_framework/System.Text.Json.kxlfxj0wrs.dll +0 -0
- package/wwwroot/_framework/System.Text.RegularExpressions.dbqn58klox.dll +0 -0
- package/wwwroot/_framework/System.Threading.42ao9vi047.dll +0 -0
- package/wwwroot/_framework/System.Threading.Channels.hfa7j0uv2w.dll +0 -0
- package/wwwroot/_framework/System.Threading.Thread.caul0pdqul.dll +0 -0
- package/wwwroot/_framework/System.Transactions.Local.fimi2hamzo.dll +0 -0
- package/wwwroot/_framework/System.Web.HttpUtility.gq8yz50p2e.dll +0 -0
- package/wwwroot/_framework/System.Xml.Linq.kitin4zjoj.dll +0 -0
- package/wwwroot/_framework/System.Xml.ReaderWriter.kzvw3qgxb0.dll +0 -0
- package/wwwroot/_framework/System.Xml.XDocument.c539ki6cuq.dll +0 -0
- package/wwwroot/_framework/System.m05i39uvk9.dll +0 -0
- package/wwwroot/_framework/Websocket.Client.vapounvmnl.dll +0 -0
- package/wwwroot/_framework/blazor.boot.json +305 -0
- package/wwwroot/_framework/blazor.webassembly.js +1 -0
- package/wwwroot/_framework/dotnet.js +4 -0
- package/wwwroot/_framework/dotnet.native.vz0adxojrz.wasm +0 -0
- package/wwwroot/_framework/dotnet.native.xsn1d6x2kd.js +16 -0
- package/wwwroot/_framework/dotnet.runtime.dstopyvqzi.js +4 -0
- package/wwwroot/_framework/icudt_CJK.tjcz0u77k5.dat +0 -0
- package/wwwroot/_framework/icudt_EFIGS.tptq2av103.dat +0 -0
- package/wwwroot/_framework/icudt_no_CJK.lfu7j35m59.dat +0 -0
- package/wwwroot/_framework/netstandard.0xet7jg7ky.dll +0 -0
- package/wwwroot/_headers +40 -0
- package/wwwroot/_redirects +1 -0
- package/wwwroot/appsettings.json +71 -0
- package/wwwroot/icon-192.png +0 -0
- package/wwwroot/icon-512.png +0 -0
- package/wwwroot/index.html +710 -0
- package/wwwroot/js/marketing-tool.js +180 -0
- package/wwwroot/manifest.webmanifest +22 -0
- package/wwwroot/robots.txt +4 -0
- package/wwwroot/service-worker-assets.js +857 -0
- package/wwwroot/service-worker.js +33 -0
- package/wwwroot/sitemap.xml +27 -0
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
// File: mind-map-dnd.js
|
|
2
|
+
// MindMap drag-and-drop handling (preprocess-then-create approach to reduce flicker)
|
|
3
|
+
window.MindMapDnD = (function () {
|
|
4
|
+
|
|
5
|
+
// ▼▼▼ [New] IndexedDB helper for directory handles ▼▼▼
|
|
6
|
+
const IDBHelper = {
|
|
7
|
+
dbName: 'MindMapDB',
|
|
8
|
+
storeName: 'handles',
|
|
9
|
+
async getDB() {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
12
|
+
request.onupgradeneeded = (e) => {
|
|
13
|
+
if (!e.target.result.objectStoreNames.contains(this.storeName)) {
|
|
14
|
+
e.target.result.createObjectStore(this.storeName);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
request.onsuccess = () => resolve(request.result);
|
|
18
|
+
request.onerror = () => reject(request.error);
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
async set(key, val) {
|
|
22
|
+
const db = await this.getDB();
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
25
|
+
tx.objectStore(this.storeName).put(val, key);
|
|
26
|
+
tx.oncomplete = () => resolve();
|
|
27
|
+
tx.onerror = () => reject(tx.error);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
async get(key) {
|
|
31
|
+
const db = await this.getDB();
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const tx = db.transaction(this.storeName, 'readonly');
|
|
34
|
+
const req = tx.objectStore(this.storeName).get(key);
|
|
35
|
+
req.onsuccess = () => resolve(req.result);
|
|
36
|
+
req.onerror = () => reject(req.error);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
// ▲▲▲ [New] ▲▲▲
|
|
41
|
+
|
|
42
|
+
if (!window.MindMapFileRegistry) {
|
|
43
|
+
window.MindMapFileRegistry = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function registerCoreHandlers() {
|
|
47
|
+
// 1. PDF handler
|
|
48
|
+
if (!window.MindMapFileRegistry.some(h => h.name === 'CorePDF')) {
|
|
49
|
+
window.MindMapFileRegistry.push({
|
|
50
|
+
name: 'CorePDF',
|
|
51
|
+
priority: 5,
|
|
52
|
+
check: (file) => file.type === 'application/pdf',
|
|
53
|
+
getType: () => 'pdf'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Image handler (updated)
|
|
58
|
+
if (!window.MindMapFileRegistry.some(h => h.name === 'CoreImage')) {
|
|
59
|
+
window.MindMapFileRegistry.push({
|
|
60
|
+
name: 'CoreImage',
|
|
61
|
+
priority: 5,
|
|
62
|
+
// [Fix] If MIME type is empty or octet-stream, fall back to extension check
|
|
63
|
+
check: (file) => {
|
|
64
|
+
if (file.type && file.type.startsWith('image/')) return true;
|
|
65
|
+
|
|
66
|
+
// Extension-based check (Windows/WebView2 compatibility)
|
|
67
|
+
const name = file.name ? file.name.toLowerCase() : '';
|
|
68
|
+
return /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/.test(name);
|
|
69
|
+
},
|
|
70
|
+
getType: () => 'image'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. Video handler
|
|
75
|
+
if (!window.MindMapFileRegistry.some(h => h.name === 'CoreVideo')) {
|
|
76
|
+
window.MindMapFileRegistry.push({
|
|
77
|
+
name: 'CoreVideo',
|
|
78
|
+
priority: 5,
|
|
79
|
+
check: (file) => {
|
|
80
|
+
if (file.type && file.type.startsWith('video/')) return true;
|
|
81
|
+
const name = file.name ? file.name.toLowerCase() : '';
|
|
82
|
+
return /\.(mp4|webm|mov|ogv|m4v)$/.test(name);
|
|
83
|
+
},
|
|
84
|
+
getType: () => 'video'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ▼▼▼ [New] 4. Text handler (basic .txt support) ▼▼▼
|
|
89
|
+
if (!window.MindMapFileRegistry.some(h => h.name === 'CoreText')) {
|
|
90
|
+
window.MindMapFileRegistry.push({
|
|
91
|
+
name: 'CoreText',
|
|
92
|
+
priority: 5,
|
|
93
|
+
check: (file) => {
|
|
94
|
+
// If MIME type is text/plain or extension is .txt or .md
|
|
95
|
+
const name = file.name ? file.name.toLowerCase() : '';
|
|
96
|
+
return file.type === 'text/plain' ||
|
|
97
|
+
name.endsWith('.txt') ||
|
|
98
|
+
name.endsWith('.md');
|
|
99
|
+
},
|
|
100
|
+
getType: () => 'text'
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// ▲▲▲ [New] ▲▲▲
|
|
104
|
+
|
|
105
|
+
// ▼▼▼ [New] 5. Code handler (source code files - Troika rendered) ▼▼▼
|
|
106
|
+
if (!window.MindMapFileRegistry.some(h => h.name === 'CoreCode')) {
|
|
107
|
+
const CODE_EXTENSIONS = [
|
|
108
|
+
'.js', '.jsx', '.ts', '.tsx', '.py', '.cs', '.java',
|
|
109
|
+
'.cpp', '.c', '.h', '.go', '.rs', '.rb', '.php',
|
|
110
|
+
'.swift', '.kt', '.json', '.xml', '.yaml', '.yml',
|
|
111
|
+
'.sql', '.html', '.css', '.scss', '.less', '.sh',
|
|
112
|
+
'.bat', '.ps1', '.csv', '.razor' // Added .razor for Blazor
|
|
113
|
+
];
|
|
114
|
+
window.MindMapFileRegistry.push({
|
|
115
|
+
name: 'CoreCode',
|
|
116
|
+
priority: 4, // Higher priority than text
|
|
117
|
+
check: (file) => {
|
|
118
|
+
const name = file.name ? file.name.toLowerCase() : '';
|
|
119
|
+
return CODE_EXTENSIONS.some(ext => name.endsWith(ext));
|
|
120
|
+
},
|
|
121
|
+
getType: () => 'code'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// ▲▲▲ [New] ▲▲▲
|
|
125
|
+
|
|
126
|
+
console.log('[MindMapDnD] Core file handlers (PDF, Image, Video, Text, Code) registered');
|
|
127
|
+
}
|
|
128
|
+
registerCoreHandlers();
|
|
129
|
+
|
|
130
|
+
const eventHandlers = new WeakMap();
|
|
131
|
+
const GRID_SNAP = 10;
|
|
132
|
+
|
|
133
|
+
function computeDroppedImageNodeSize(originalWidth, originalHeight) {
|
|
134
|
+
if (window.mindMap?.computeImageNodeSize) {
|
|
135
|
+
return window.mindMap.computeImageNodeSize(originalWidth, originalHeight, {
|
|
136
|
+
gridSnap: GRID_SNAP,
|
|
137
|
+
minWidth: 100,
|
|
138
|
+
minHeight: 100,
|
|
139
|
+
maxWidth: 4000,
|
|
140
|
+
maxHeight: 4000,
|
|
141
|
+
fallbackWidth: 470,
|
|
142
|
+
fallbackHeight: 350
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const width = Math.max(100, Math.round(Number(originalWidth || 470) / GRID_SNAP) * GRID_SNAP);
|
|
147
|
+
const height = Math.max(100, Math.round(Number(originalHeight || 350) / GRID_SNAP) * GRID_SNAP);
|
|
148
|
+
return { width, height };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ▼▼▼ [Profiling] Drag statistics tracking ▼▼▼
|
|
152
|
+
let _dragStats = {
|
|
153
|
+
startTime: 0,
|
|
154
|
+
dragOverCount: 0
|
|
155
|
+
};
|
|
156
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
157
|
+
|
|
158
|
+
function getBoundHandlers(module) {
|
|
159
|
+
if (!eventHandlers.has(module)) {
|
|
160
|
+
eventHandlers.set(module, {
|
|
161
|
+
boundOnDragEnter: onDragEnter.bind(null, module), // [New]
|
|
162
|
+
boundOnDragOver: onDragOver.bind(null, module),
|
|
163
|
+
boundOnDragLeave: onDragLeave.bind(null, module),
|
|
164
|
+
boundOnDrop: onDrop.bind(null, module)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return eventHandlers.get(module);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// [New] dragenter handler
|
|
171
|
+
function onDragEnter(module, e) {
|
|
172
|
+
// Ignore movements between child elements inside the container
|
|
173
|
+
if (module.container.contains(e.relatedTarget)) return;
|
|
174
|
+
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
e.stopPropagation();
|
|
177
|
+
if (e.dataTransfer) {
|
|
178
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ▼▼▼ [Profiling] Record drag start time ▼▼▼
|
|
182
|
+
_dragStats.startTime = performance.now();
|
|
183
|
+
_dragStats.dragOverCount = 0;
|
|
184
|
+
console.log('[DnD] 🟢 Drag entered. Tracking started.');
|
|
185
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function onDragOver(module, e) {
|
|
191
|
+
// ▼▼▼ [Optimization] Do the minimum work - avoid IPC overload ▼▼▼
|
|
192
|
+
// preventDefault() is required to allow drop
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
|
|
195
|
+
// Profiling counter (very lightweight)
|
|
196
|
+
_dragStats.dragOverCount++;
|
|
197
|
+
|
|
198
|
+
// Remove all extra work (like setting dropEffect)
|
|
199
|
+
// In WebView2, each call can trigger IPC traffic
|
|
200
|
+
return false;
|
|
201
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function onDragLeave(module, e) {
|
|
205
|
+
// Do not treat entering a child element as a leave
|
|
206
|
+
if (module.container.contains(e.relatedTarget)) return;
|
|
207
|
+
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractImageUrl(dataTransfer) {
|
|
213
|
+
if (!dataTransfer) return null;
|
|
214
|
+
const html = dataTransfer.getData('text/html');
|
|
215
|
+
if (html) {
|
|
216
|
+
const parser = new DOMParser();
|
|
217
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
218
|
+
const img = doc.querySelector('img');
|
|
219
|
+
if (img && img.src) return img.src;
|
|
220
|
+
}
|
|
221
|
+
const text = (dataTransfer.getData('text/plain') || '').trim();
|
|
222
|
+
if (text && (text.startsWith('data:image') || isImageUrl(text))) return text;
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractDroppedUrl(dataTransfer) {
|
|
227
|
+
if (!dataTransfer) return null;
|
|
228
|
+
const uri = dataTransfer.getData('text/uri-list') || dataTransfer.getData('URL');
|
|
229
|
+
if (uri) return uri.split(/\r?\n/).find(line => line && !line.startsWith('#')) || uri;
|
|
230
|
+
const text = (dataTransfer.getData('text/plain') || '').trim();
|
|
231
|
+
if (text && /^https?:\/\//i.test(text)) return text;
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isImageUrl(url) {
|
|
236
|
+
return /^https?:\/\/.+\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(String(url || '').trim());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function fetchImageAsBlobUrl(url) {
|
|
240
|
+
if (!url) return null;
|
|
241
|
+
if (url.startsWith('data:') || url.startsWith('blob:')) {
|
|
242
|
+
return url;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const response = await fetch(url, { mode: 'cors', credentials: 'omit' });
|
|
247
|
+
if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
|
|
248
|
+
const blob = await response.blob();
|
|
249
|
+
return URL.createObjectURL(blob);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.warn(`[MindMapDnD] CORS fetch failed (${e?.message || 'unknown'}). Image drop cancelled.`, e);
|
|
252
|
+
window.MindMapFeedback?.toast?.('The server blocked copying.', { kind: 'error' });
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function measureImageSize(src) {
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
const img = new Image();
|
|
260
|
+
img.onload = () => {
|
|
261
|
+
resolve(computeDroppedImageNodeSize(img.naturalWidth, img.naturalHeight));
|
|
262
|
+
};
|
|
263
|
+
img.onerror = () => {
|
|
264
|
+
console.warn('[MindMapDnD] Failed to measure image size. Using default.');
|
|
265
|
+
resolve(computeDroppedImageNodeSize(470, 350));
|
|
266
|
+
};
|
|
267
|
+
img.src = src;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const HOSTED_LOCAL_BRIDGE_STORAGE_KEY = 'MindExec.AllowHostedLocalBridge';
|
|
272
|
+
const CLOUD_VIDEO_DND_BLOCKED_MESSAGE = 'Local video files are not available in cloud mode. Use Local Bridge for large video workflows, or use YouTube, a direct video URL, or AI video.';
|
|
273
|
+
|
|
274
|
+
function isLoopbackHost(hostname) {
|
|
275
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getCurrentLoopbackBridgeUrl() {
|
|
279
|
+
const location = window.location;
|
|
280
|
+
if (!location || !/^https?:$/i.test(location.protocol || '') || !isLoopbackHost(location.hostname || '')) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return location.origin;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function preferCurrentLoopbackBridgeOrigin(bridgeUrl) {
|
|
288
|
+
const currentOrigin = getCurrentLoopbackBridgeUrl();
|
|
289
|
+
if (!bridgeUrl || !currentOrigin) {
|
|
290
|
+
return bridgeUrl;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const bridgeUri = new URL(bridgeUrl);
|
|
295
|
+
const currentUri = new URL(currentOrigin);
|
|
296
|
+
if (isLoopbackHost(bridgeUri.hostname)
|
|
297
|
+
&& isLoopbackHost(currentUri.hostname)
|
|
298
|
+
&& bridgeUri.protocol === currentUri.protocol
|
|
299
|
+
&& bridgeUri.port === currentUri.port) {
|
|
300
|
+
return currentOrigin;
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
return bridgeUrl;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return bridgeUrl;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Direct upload to Bridge server (faster than C# route)
|
|
310
|
+
const BRIDGE_URLS = [getCurrentLoopbackBridgeUrl(), 'http://127.0.0.1:5147', 'http://localhost:5147']
|
|
311
|
+
.filter((url, index, urls) => !!url && urls.indexOf(url) === index);
|
|
312
|
+
let bridgeAuthPromise = null;
|
|
313
|
+
let bridgeConnectedPromise = null;
|
|
314
|
+
let activeBridgeUrl = preferCurrentLoopbackBridgeOrigin(window.MindExecLocalBridge?.activeBridgeUrl || BRIDGE_URLS[0]);
|
|
315
|
+
|
|
316
|
+
function isHostedLocalBridgeExplicitlyEnabled() {
|
|
317
|
+
if (window.MindExecLocalBridge?.allowHostedAutoProbe === true) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
return window.localStorage?.getItem(HOSTED_LOCAL_BRIDGE_STORAGE_KEY) === 'true';
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function resetLocalBridgeProbeCache() {
|
|
329
|
+
bridgeAuthPromise = null;
|
|
330
|
+
bridgeConnectedPromise = null;
|
|
331
|
+
window.__mindExecutionBridgeAuthPromise = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function setActiveBridgeUrl(bridgeUrl) {
|
|
335
|
+
activeBridgeUrl = preferCurrentLoopbackBridgeOrigin(bridgeUrl || BRIDGE_URLS[0]);
|
|
336
|
+
window.MindExecLocalBridge = {
|
|
337
|
+
...(window.MindExecLocalBridge || {}),
|
|
338
|
+
activeBridgeUrl
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function setHostedLocalBridgeExplicitlyEnabled(enabled) {
|
|
343
|
+
const nextEnabled = enabled === true;
|
|
344
|
+
try {
|
|
345
|
+
if (nextEnabled) {
|
|
346
|
+
window.localStorage?.setItem(HOSTED_LOCAL_BRIDGE_STORAGE_KEY, 'true');
|
|
347
|
+
} else {
|
|
348
|
+
window.localStorage?.removeItem(HOSTED_LOCAL_BRIDGE_STORAGE_KEY);
|
|
349
|
+
}
|
|
350
|
+
} catch { }
|
|
351
|
+
|
|
352
|
+
window.MindExecLocalBridge = {
|
|
353
|
+
...(window.MindExecLocalBridge || {}),
|
|
354
|
+
allowHostedAutoProbe: nextEnabled,
|
|
355
|
+
hostedAccessEnabled: nextEnabled
|
|
356
|
+
};
|
|
357
|
+
resetLocalBridgeProbeCache();
|
|
358
|
+
window.dispatchEvent?.(new CustomEvent('mindexec-local-bridge-access-changed', {
|
|
359
|
+
detail: { enabled: nextEnabled }
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isLocalBridgeAutoProbeAllowed() {
|
|
364
|
+
if (window.__isNativeApp) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const hostname = window.location?.hostname || '';
|
|
369
|
+
return isLoopbackHost(hostname) || isHostedLocalBridgeExplicitlyEnabled();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function getBridgeUrlCandidates() {
|
|
373
|
+
const seen = new Set();
|
|
374
|
+
return [getCurrentLoopbackBridgeUrl(), activeBridgeUrl, ...BRIDGE_URLS].filter(url => {
|
|
375
|
+
if (!url || seen.has(url)) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
seen.add(url);
|
|
380
|
+
return true;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function buildBridgeStatusUrl(bridgeUrl) {
|
|
385
|
+
const nonce = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
386
|
+
return `${bridgeUrl}/api/status?mindexecOrigin=pages&cb=${encodeURIComponent(nonce)}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function rewriteBridgeAssetUrl(url, bridgeUrl) {
|
|
390
|
+
if (!url || !bridgeUrl) {
|
|
391
|
+
return url;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const parsed = new URL(url);
|
|
396
|
+
if (!isLoopbackHost(parsed.hostname) || !parsed.pathname.startsWith('/assets/')) {
|
|
397
|
+
return url;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return `${bridgeUrl}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
401
|
+
} catch {
|
|
402
|
+
return url;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function getBridgeAuthHeaders() {
|
|
407
|
+
if (!isLocalBridgeAutoProbeAllowed()) {
|
|
408
|
+
return {};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
bridgeAuthPromise ??= (async () => {
|
|
412
|
+
for (const bridgeUrl of getBridgeUrlCandidates()) {
|
|
413
|
+
try {
|
|
414
|
+
const response = await fetch(buildBridgeStatusUrl(bridgeUrl), {
|
|
415
|
+
cache: 'no-store',
|
|
416
|
+
targetAddressSpace: 'loopback'
|
|
417
|
+
});
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const status = await response.json();
|
|
423
|
+
setActiveBridgeUrl(bridgeUrl);
|
|
424
|
+
const token = status?.bridgeToken;
|
|
425
|
+
const header = status?.bridgeTokenHeader || 'X-Bridge-Token';
|
|
426
|
+
return token ? { [header]: token } : {};
|
|
427
|
+
} catch {
|
|
428
|
+
// Try next loopback spelling.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {};
|
|
433
|
+
})();
|
|
434
|
+
|
|
435
|
+
return bridgeAuthPromise;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function isBridgeConnected() {
|
|
439
|
+
if (!isLocalBridgeAutoProbeAllowed()) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
bridgeConnectedPromise ??= (async () => {
|
|
444
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
445
|
+
const timeoutId = controller ? window.setTimeout(() => controller.abort(), 900) : null;
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
for (const bridgeUrl of getBridgeUrlCandidates()) {
|
|
449
|
+
try {
|
|
450
|
+
const response = await fetch(buildBridgeStatusUrl(bridgeUrl), {
|
|
451
|
+
cache: 'no-store',
|
|
452
|
+
signal: controller?.signal,
|
|
453
|
+
targetAddressSpace: 'loopback'
|
|
454
|
+
});
|
|
455
|
+
if (response.ok) {
|
|
456
|
+
setActiveBridgeUrl(bridgeUrl);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
// Try next loopback spelling.
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return false;
|
|
465
|
+
} finally {
|
|
466
|
+
if (timeoutId !== null) {
|
|
467
|
+
window.clearTimeout(timeoutId);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
|
|
472
|
+
return bridgeConnectedPromise;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function enableHostedLocalBridgeAccessAndProbe() {
|
|
476
|
+
setHostedLocalBridgeExplicitlyEnabled(true);
|
|
477
|
+
return await isBridgeConnected();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
window.MindExecLocalBridge = {
|
|
481
|
+
...(window.MindExecLocalBridge || {}),
|
|
482
|
+
storageKey: HOSTED_LOCAL_BRIDGE_STORAGE_KEY,
|
|
483
|
+
allowHostedAutoProbe: isHostedLocalBridgeExplicitlyEnabled(),
|
|
484
|
+
hostedAccessEnabled: isHostedLocalBridgeExplicitlyEnabled(),
|
|
485
|
+
isHostedAccessEnabled: isHostedLocalBridgeExplicitlyEnabled,
|
|
486
|
+
enableHostedAccess: () => setHostedLocalBridgeExplicitlyEnabled(true),
|
|
487
|
+
disableHostedAccess: () => setHostedLocalBridgeExplicitlyEnabled(false),
|
|
488
|
+
enableHostedAccessAndProbe: enableHostedLocalBridgeAccessAndProbe,
|
|
489
|
+
activeBridgeUrl
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
async function uploadAssetToBridge(file) {
|
|
493
|
+
if (!isLocalBridgeAutoProbeAllowed()) {
|
|
494
|
+
return { success: false };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const headers = await getBridgeAuthHeaders();
|
|
499
|
+
for (const bridgeUrl of getBridgeUrlCandidates()) {
|
|
500
|
+
const formData = new FormData();
|
|
501
|
+
formData.append('file', file, file.name);
|
|
502
|
+
|
|
503
|
+
const response = await fetch(`${bridgeUrl}/api/assets/upload`, {
|
|
504
|
+
method: 'POST',
|
|
505
|
+
headers,
|
|
506
|
+
body: formData,
|
|
507
|
+
targetAddressSpace: 'loopback'
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (!response.ok) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const result = await response.json();
|
|
515
|
+
if (result.success) {
|
|
516
|
+
setActiveBridgeUrl(bridgeUrl);
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
url: rewriteBridgeAssetUrl(result.url, bridgeUrl), // Full HTTP URL for display
|
|
520
|
+
relativeUrl: result.relativeUrl, // Relative path for document storage
|
|
521
|
+
thumbnailUrl: rewriteBridgeAssetUrl(result.thumbnailUrl, bridgeUrl),
|
|
522
|
+
width: result.width,
|
|
523
|
+
height: result.height
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return { success: false };
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.warn(`[DnD] Bridge upload failed: ${err.message}`);
|
|
530
|
+
return { success: false };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function uploadImageToBridge(file) {
|
|
535
|
+
return uploadAssetToBridge(file);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function getDroppedItemForFile(droppedItems, file, index) {
|
|
539
|
+
const direct = droppedItems?.[index];
|
|
540
|
+
if (direct?.kind === 'file') {
|
|
541
|
+
const directFile = typeof direct.getAsFile === 'function' ? direct.getAsFile() : null;
|
|
542
|
+
if (!directFile
|
|
543
|
+
|| directFile === file
|
|
544
|
+
|| (directFile.name === file.name && directFile.size === file.size && directFile.lastModified === file.lastModified)) {
|
|
545
|
+
return direct;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const item of droppedItems || []) {
|
|
550
|
+
if (!item || item.kind !== 'file' || typeof item.getAsFile !== 'function') {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const itemFile = item.getAsFile();
|
|
555
|
+
if (itemFile
|
|
556
|
+
&& itemFile.name === file.name
|
|
557
|
+
&& itemFile.size === file.size
|
|
558
|
+
&& itemFile.lastModified === file.lastModified) {
|
|
559
|
+
return item;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function getBrowserExposedLocalPath(file) {
|
|
567
|
+
const candidates = [
|
|
568
|
+
file?.path,
|
|
569
|
+
file?.fullPath,
|
|
570
|
+
file?.mozFullPath
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
for (const candidate of candidates) {
|
|
574
|
+
const value = String(candidate || '').trim();
|
|
575
|
+
if (/^[a-z]:[\\/]/i.test(value) || value.startsWith('\\\\') || value.startsWith('/')) {
|
|
576
|
+
return value;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return '';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function rewriteBridgeLocalFileUrl(url, bridgeUrl) {
|
|
584
|
+
if (!url || !bridgeUrl) {
|
|
585
|
+
return url;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const parsed = new URL(url);
|
|
590
|
+
if (!isLoopbackHost(parsed.hostname) || !parsed.pathname.startsWith('/api/local-files/')) {
|
|
591
|
+
return url;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return `${bridgeUrl}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
595
|
+
} catch {
|
|
596
|
+
return url;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function registerLocalFilePathWithBridge(file, localPath) {
|
|
601
|
+
const normalizedPath = String(localPath || '').trim();
|
|
602
|
+
if (!normalizedPath || !isLocalBridgeAutoProbeAllowed()) {
|
|
603
|
+
return { success: false };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
const headers = {
|
|
608
|
+
...(await getBridgeAuthHeaders()),
|
|
609
|
+
'Content-Type': 'application/json'
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
for (const bridgeUrl of getBridgeUrlCandidates()) {
|
|
613
|
+
const response = await fetch(`${bridgeUrl}/api/local-files/register`, {
|
|
614
|
+
method: 'POST',
|
|
615
|
+
headers,
|
|
616
|
+
body: JSON.stringify({
|
|
617
|
+
path: normalizedPath,
|
|
618
|
+
fileName: file?.name || '',
|
|
619
|
+
size: Number(file?.size || 0),
|
|
620
|
+
mime: file?.type || ''
|
|
621
|
+
}),
|
|
622
|
+
targetAddressSpace: 'loopback'
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
if (!response.ok) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const result = await response.json();
|
|
630
|
+
if (result?.success && result?.url) {
|
|
631
|
+
setActiveBridgeUrl(bridgeUrl);
|
|
632
|
+
return {
|
|
633
|
+
success: true,
|
|
634
|
+
id: result.id || '',
|
|
635
|
+
url: rewriteBridgeLocalFileUrl(result.url, bridgeUrl),
|
|
636
|
+
fileName: result.fileName || file?.name || '',
|
|
637
|
+
size: Number(result.size || file?.size || 0),
|
|
638
|
+
mime: result.mime || file?.type || '',
|
|
639
|
+
lastModified: Number(result.lastModified || file?.lastModified || 0)
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch (error) {
|
|
644
|
+
console.warn('[DnD] Bridge local file registration failed:', error);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return { success: false };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function createLocalVideoReference(file, item, bridgeConnected) {
|
|
651
|
+
const localPath = getBrowserExposedLocalPath(file);
|
|
652
|
+
if (bridgeConnected && localPath) {
|
|
653
|
+
const bridgeResult = await registerLocalFilePathWithBridge(file, localPath);
|
|
654
|
+
if (bridgeResult.success) {
|
|
655
|
+
return {
|
|
656
|
+
response: bridgeResult.url,
|
|
657
|
+
displayUrl: bridgeResult.url,
|
|
658
|
+
metadata: {
|
|
659
|
+
assetKind: 'video',
|
|
660
|
+
sourceKind: 'local-path',
|
|
661
|
+
videoMode: 'local_bridge_stream',
|
|
662
|
+
storageScope: 'original_file',
|
|
663
|
+
persistPolicy: 'reference_only',
|
|
664
|
+
localBridgeFileId: bridgeResult.id,
|
|
665
|
+
localFilePath: localPath,
|
|
666
|
+
fileName: bridgeResult.fileName || file.name || '',
|
|
667
|
+
fileSize: String(bridgeResult.size || file.size || 0),
|
|
668
|
+
fileMime: bridgeResult.mime || file.type || '',
|
|
669
|
+
fileLastModified: String(bridgeResult.lastModified || file.lastModified || 0)
|
|
670
|
+
},
|
|
671
|
+
saved: true
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const registry = window.MindMapLocalFiles;
|
|
677
|
+
const entry = registry?.registerDroppedFile
|
|
678
|
+
? await registry.registerDroppedFile(file, item)
|
|
679
|
+
: null;
|
|
680
|
+
const objectUrl = entry?.objectUrl || URL.createObjectURL(file);
|
|
681
|
+
const reference = entry?.reference || objectUrl;
|
|
682
|
+
const sourceKind = entry?.sourceKind || 'session-file';
|
|
683
|
+
const durableHandle = entry?.durableHandle === true;
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
response: reference,
|
|
687
|
+
displayUrl: objectUrl,
|
|
688
|
+
metadata: {
|
|
689
|
+
assetKind: 'video',
|
|
690
|
+
sourceKind,
|
|
691
|
+
localFileId: entry?.id || '',
|
|
692
|
+
localFileReference: reference,
|
|
693
|
+
fileName: file.name || '',
|
|
694
|
+
fileSize: String(file.size || 0),
|
|
695
|
+
fileMime: file.type || '',
|
|
696
|
+
fileLastModified: String(file.lastModified || 0),
|
|
697
|
+
videoMode: durableHandle ? 'local_handle' : 'session_blob',
|
|
698
|
+
storageScope: durableHandle ? 'original_file' : 'session',
|
|
699
|
+
persistPolicy: durableHandle ? 'reference_only' : 'session_only'
|
|
700
|
+
},
|
|
701
|
+
saved: true
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function showCloudVideoDndBlockedToast(count = 1) {
|
|
706
|
+
const suffix = count > 1 ? ` (${count} files skipped)` : '';
|
|
707
|
+
window.MindMapFeedback?.toast?.(`${CLOUD_VIDEO_DND_BLOCKED_MESSAGE}${suffix}`, {
|
|
708
|
+
kind: 'error',
|
|
709
|
+
duration: 6800
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
// ▲▲▲ [New] ▲▲▲
|
|
713
|
+
|
|
714
|
+
function fileToDataUrl(file) {
|
|
715
|
+
return new Promise((resolve, reject) => {
|
|
716
|
+
const reader = new FileReader();
|
|
717
|
+
reader.onload = () => resolve(reader.result);
|
|
718
|
+
reader.onerror = () => reject(reader.error);
|
|
719
|
+
reader.readAsDataURL(file);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function handleBrowserImageDrop(module, e, imageUrl) {
|
|
724
|
+
const finalUrl = await fetchImageAsBlobUrl(imageUrl);
|
|
725
|
+
if (!finalUrl) {
|
|
726
|
+
console.log('[MindMapDnD] Image drop cancelled due to CORS/Fetch error.');
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const mouse = {
|
|
731
|
+
x: (e.clientX / module.container.clientWidth) * 2 - 1,
|
|
732
|
+
y: -(e.clientY / module.container.clientHeight) * 2 + 1
|
|
733
|
+
};
|
|
734
|
+
const vfov = (module.camera.fov * Math.PI) / 180;
|
|
735
|
+
const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
|
|
736
|
+
const viewWidth = viewHeight * module.camera.aspect;
|
|
737
|
+
const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
|
|
738
|
+
const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
|
|
739
|
+
|
|
740
|
+
const size = await measureImageSize(finalUrl);
|
|
741
|
+
|
|
742
|
+
const centeredX = worldX - (size.width / 2);
|
|
743
|
+
const centeredY = worldY + (size.height / 2);
|
|
744
|
+
|
|
745
|
+
const finalNode = {
|
|
746
|
+
TempId: `temp_browser_${Date.now()}`,
|
|
747
|
+
ContentType: 'image',
|
|
748
|
+
FileName: 'Web Image',
|
|
749
|
+
Response: finalUrl,
|
|
750
|
+
X: module.snapToGrid(centeredX),
|
|
751
|
+
Y: module.snapToGridY(centeredY),
|
|
752
|
+
Width: size.width,
|
|
753
|
+
Height: size.height,
|
|
754
|
+
IsLoading: false
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const truncatedUrl = finalUrl.length > 50 ? `${finalUrl.substring(0, 50)}...` : finalUrl;
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
const actualNodeIdsMap = await module.dotNetHelper.invokeMethodAsync('AddFileNodesFromJs', [finalNode]);
|
|
761
|
+
const actualId = actualNodeIdsMap?.[finalNode.TempId];
|
|
762
|
+
if (actualId) {
|
|
763
|
+
const batchDetails = [{
|
|
764
|
+
nodeId: actualId,
|
|
765
|
+
fileUrl: finalUrl,
|
|
766
|
+
width: size.width,
|
|
767
|
+
height: size.height
|
|
768
|
+
}];
|
|
769
|
+
await module.dotNetHelper.invokeMethodAsync('UpdateMultipleFileNodeDetails', batchDetails);
|
|
770
|
+
}
|
|
771
|
+
} catch (err) {
|
|
772
|
+
console.error('[MindMapDnD] Failed to add browser image:', err);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function handleDirectoryDrop(module, handle, e) {
|
|
777
|
+
console.log('[MindMapDnD] Directory dropped:', handle.name);
|
|
778
|
+
|
|
779
|
+
const mouse = {
|
|
780
|
+
x: (e.clientX / module.container.clientWidth) * 2 - 1,
|
|
781
|
+
y: -(e.clientY / module.container.clientHeight) * 2 + 1
|
|
782
|
+
};
|
|
783
|
+
const vfov = (module.camera.fov * Math.PI) / 180;
|
|
784
|
+
const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
|
|
785
|
+
const viewWidth = viewHeight * module.camera.aspect;
|
|
786
|
+
const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
|
|
787
|
+
const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
|
|
788
|
+
|
|
789
|
+
const centeredX = module.snapToGrid(worldX);
|
|
790
|
+
const centeredY = module.snapToGridY(worldY);
|
|
791
|
+
|
|
792
|
+
try {
|
|
793
|
+
const nodeId = await module.dotNetHelper.invokeMethodAsync('AddDirectoryNodeFromJs', handle.name, centeredX, centeredY);
|
|
794
|
+
if (nodeId) {
|
|
795
|
+
await IDBHelper.set(nodeId, handle);
|
|
796
|
+
console.log(`[MindMapDnD] Directory handle stored for node ${nodeId}`);
|
|
797
|
+
}
|
|
798
|
+
} catch (err) {
|
|
799
|
+
console.error('[MindMapDnD] Failed to add directory node:', err);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function onDrop(module, e) {
|
|
804
|
+
// ▼▼▼ [Profiling] Log drag statistics ▼▼▼
|
|
805
|
+
const dropTime = performance.now();
|
|
806
|
+
const totalDragTime = _dragStats.startTime > 0 ? (dropTime - _dragStats.startTime).toFixed(0) : '?';
|
|
807
|
+
console.log(`[DnD] 🔴 Drop received! dragTime=${totalDragTime}ms, dragOverCount=${_dragStats.dragOverCount}`);
|
|
808
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
809
|
+
|
|
810
|
+
// ▼▼▼ [Changed] Prevent duplicate processing ▼▼▼
|
|
811
|
+
if (e._dndProcessed) {
|
|
812
|
+
console.log('[MindMapDnD] Drop event already processed, skipping.');
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
e._dndProcessed = true;
|
|
816
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
817
|
+
|
|
818
|
+
console.log('[MindMapDnD] Drop event received!');
|
|
819
|
+
|
|
820
|
+
e.preventDefault();
|
|
821
|
+
e.stopPropagation();
|
|
822
|
+
e.stopImmediatePropagation(); // Also block other handlers on the same element
|
|
823
|
+
|
|
824
|
+
// ▼▼▼ [Critical fix] Capture file lists before starting async work ▼▼▼
|
|
825
|
+
// e.dataTransfer.files can be cleared by the browser once async work (await) begins.
|
|
826
|
+
const droppedFiles = e.dataTransfer.files ? Array.from(e.dataTransfer.files) : [];
|
|
827
|
+
const droppedItems = e.dataTransfer.items ? Array.from(e.dataTransfer.items) : [];
|
|
828
|
+
// ▲▲▲ [Critical fix] ▲▲▲
|
|
829
|
+
|
|
830
|
+
// 1. Directory handling (FileSystem API)
|
|
831
|
+
// (Note: droppedFiles is already copied into memory, so it's safe even with await.)
|
|
832
|
+
// [Optimized] If all files are already supported (image, PDF, etc.), skip directory checks
|
|
833
|
+
// (async handle requests) to avoid delay.
|
|
834
|
+
let shouldCheckDirectory = false;
|
|
835
|
+
if (droppedFiles.length === 0) {
|
|
836
|
+
// If the file list is empty (pure folder drop, etc.), we need to check
|
|
837
|
+
shouldCheckDirectory = true;
|
|
838
|
+
} else {
|
|
839
|
+
// If any file is not recognized (no extension, etc. => possibly a folder), check
|
|
840
|
+
const sortedRegistry = window.MindMapFileRegistry; // Registry is global usage
|
|
841
|
+
for (let droppedFileIndex = 0; droppedFileIndex < droppedFiles.length; droppedFileIndex++) {
|
|
842
|
+
const file = droppedFiles[droppedFileIndex];
|
|
843
|
+
let isKnown = false;
|
|
844
|
+
for (const handler of sortedRegistry) {
|
|
845
|
+
if (handler.check(file)) {
|
|
846
|
+
isKnown = true;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!isKnown) {
|
|
851
|
+
shouldCheckDirectory = true;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ▼▼▼ [Profiling] Measure directory-check time ▼▼▼
|
|
858
|
+
const dirCheckStart = performance.now();
|
|
859
|
+
|
|
860
|
+
if (shouldCheckDirectory && droppedItems.length > 0) {
|
|
861
|
+
console.log('[DnD] 📁 Checking for directory drop...');
|
|
862
|
+
for (const item of droppedItems) {
|
|
863
|
+
// webkitGetAsEntry (sync / non-standard) vs getAsFileSystemHandle (async / standard)
|
|
864
|
+
// getAsFileSystemHandle can be slow, so apply a timeout
|
|
865
|
+
if (item.kind === 'file' && typeof item.getAsFileSystemHandle === 'function') {
|
|
866
|
+
try {
|
|
867
|
+
// ▼▼▼ [Optimization] 500ms timeout to avoid blocking directory checks ▼▼▼
|
|
868
|
+
const handlePromise = item.getAsFileSystemHandle();
|
|
869
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
870
|
+
setTimeout(() => reject(new Error('Timeout')), 500)
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
const handle = await Promise.race([handlePromise, timeoutPromise]);
|
|
874
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
875
|
+
|
|
876
|
+
if (handle && handle.kind === 'directory') {
|
|
877
|
+
console.log(`[DnD] 📁 Directory detected in ${(performance.now() - dirCheckStart).toFixed(0)}ms`);
|
|
878
|
+
await handleDirectoryDrop(module, handle, e);
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
} catch (err) {
|
|
882
|
+
// If timeout/failure, skip quietly and continue as file handling
|
|
883
|
+
console.warn(`[DnD] Directory check skipped: ${err.message}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
console.log(`[DnD] 📁 Directory check completed in ${(performance.now() - dirCheckStart).toFixed(0)}ms`);
|
|
889
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
890
|
+
|
|
891
|
+
// 2. Regular file handling (use captured droppedFiles)
|
|
892
|
+
if (droppedFiles.length > 0) {
|
|
893
|
+
const timeLabel = `[DnD] Process_${Date.now()}`;
|
|
894
|
+
console.time(timeLabel);
|
|
895
|
+
|
|
896
|
+
// Debug: Log all dropped files
|
|
897
|
+
console.log(`[DnD] 📂 Dropped ${droppedFiles.length} files:`);
|
|
898
|
+
droppedFiles.forEach((f, i) => console.log(` [${i}] ${f.name} (type: ${f.type || 'unknown'}, size: ${f.size})`));
|
|
899
|
+
|
|
900
|
+
const matchedFiles = [];
|
|
901
|
+
const filesToProcess = [];
|
|
902
|
+
const sortedRegistry = [...window.MindMapFileRegistry].sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50));
|
|
903
|
+
|
|
904
|
+
for (let droppedFileIndex = 0; droppedFileIndex < droppedFiles.length; droppedFileIndex++) {
|
|
905
|
+
const file = droppedFiles[droppedFileIndex];
|
|
906
|
+
let matchedType = null;
|
|
907
|
+
for (const handler of sortedRegistry) {
|
|
908
|
+
if (handler.check(file)) {
|
|
909
|
+
matchedType = handler.getType(file);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (matchedType) {
|
|
914
|
+
matchedFiles.push({
|
|
915
|
+
file,
|
|
916
|
+
type: matchedType,
|
|
917
|
+
item: getDroppedItemForFile(droppedItems, file, droppedFileIndex)
|
|
918
|
+
});
|
|
919
|
+
} else {
|
|
920
|
+
console.warn(`[DnD] ⚠️ No handler matched for: ${file.name} (type: ${file.type})`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const bridgeConnected = !window.__isNativeApp && await isBridgeConnected();
|
|
925
|
+
let blockedVideoCount = 0;
|
|
926
|
+
for (const entry of matchedFiles) {
|
|
927
|
+
if (entry.type === 'video' && !window.__isNativeApp && !bridgeConnected) {
|
|
928
|
+
blockedVideoCount++;
|
|
929
|
+
console.warn(`[DnD] Blocked local video DnD in cloud mode: ${entry.file.name}`);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
filesToProcess.push(entry);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (blockedVideoCount > 0) {
|
|
937
|
+
showCloudVideoDndBlockedToast(blockedVideoCount);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (filesToProcess.length === 0) {
|
|
941
|
+
console.timeEnd(timeLabel);
|
|
942
|
+
console.log('[MindMapDnD] No matching file handlers found. Checking for web images...');
|
|
943
|
+
} else {
|
|
944
|
+
const mouse = {
|
|
945
|
+
x: (e.clientX / module.container.clientWidth) * 2 - 1,
|
|
946
|
+
y: -(e.clientY / module.container.clientHeight) * 2 + 1
|
|
947
|
+
};
|
|
948
|
+
const vfov = (module.camera.fov * Math.PI) / 180;
|
|
949
|
+
const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
|
|
950
|
+
const viewWidth = viewHeight * module.camera.aspect;
|
|
951
|
+
const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
|
|
952
|
+
const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
|
|
953
|
+
|
|
954
|
+
// ▼▼▼ [Optimization] Batch processing to prevent UI blocking ▼▼▼
|
|
955
|
+
const BATCH_SIZE = 5; // Process 5 files at a time
|
|
956
|
+
const preparedNodes = [];
|
|
957
|
+
|
|
958
|
+
// Helper to yield to main thread
|
|
959
|
+
const yieldToMain = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
960
|
+
|
|
961
|
+
// ▼▼▼ [Fix] Track cumulative X offset for proper spacing ▼▼▼
|
|
962
|
+
const NODE_GAP = 30;
|
|
963
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
964
|
+
|
|
965
|
+
// Process files in batches
|
|
966
|
+
for (let batchStart = 0; batchStart < filesToProcess.length; batchStart += BATCH_SIZE) {
|
|
967
|
+
const batchEnd = Math.min(batchStart + BATCH_SIZE, filesToProcess.length);
|
|
968
|
+
const batch = filesToProcess.slice(batchStart, batchEnd);
|
|
969
|
+
|
|
970
|
+
const batchResults = await Promise.all(batch.map(async ({ file, type, item }, batchIndex) => {
|
|
971
|
+
const index = batchStart + batchIndex;
|
|
972
|
+
const tempId = `temp_${Date.now()}_${index}`;
|
|
973
|
+
|
|
974
|
+
let finalResponse = '';
|
|
975
|
+
let displayUrl = ''; // For JS image loading
|
|
976
|
+
let finalWidth = module.aiNodeDefaultWidth || 470;
|
|
977
|
+
let finalHeight = module.aiNodeDefaultHeight || 350;
|
|
978
|
+
let isLoading = false;
|
|
979
|
+
const fileIndex = index;
|
|
980
|
+
let imageSaved = false; // Track if image was already saved
|
|
981
|
+
let videoSaved = false;
|
|
982
|
+
let metadata = null;
|
|
983
|
+
|
|
984
|
+
if (type === 'image') {
|
|
985
|
+
const isAnimatedGif = file.type === 'image/gif' || /\.gif$/i.test(file.name || '');
|
|
986
|
+
if (isAnimatedGif) {
|
|
987
|
+
metadata = {
|
|
988
|
+
assetKind: 'image',
|
|
989
|
+
fileName: file.name || '',
|
|
990
|
+
fileSize: String(file.size || 0),
|
|
991
|
+
fileMime: file.type || 'image/gif',
|
|
992
|
+
fileLastModified: String(file.lastModified || ''),
|
|
993
|
+
isAnimatedGif: 'true'
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
// ▼▼▼ [Optimization] Direct Bridge upload (fastest path) ▼▼▼
|
|
997
|
+
console.log(`[DnD] Processing image ${index + 1}: ${file.name}`);
|
|
998
|
+
|
|
999
|
+
// Try Bridge direct upload first (bypasses C# entirely)
|
|
1000
|
+
if (bridgeConnected) {
|
|
1001
|
+
const localSizingUrl = URL.createObjectURL(file);
|
|
1002
|
+
const measuredSizePromise = measureImageSize(localSizingUrl).catch((err) => {
|
|
1003
|
+
console.warn(`[DnD] Image ${index + 1} local measure failed: ${err?.message || err}`);
|
|
1004
|
+
return null;
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
const bridgeResult = await uploadImageToBridge(file);
|
|
1008
|
+
const measuredSize = await measuredSizePromise;
|
|
1009
|
+
if (bridgeResult.success) {
|
|
1010
|
+
displayUrl = bridgeResult.url; // HTTP URL for display
|
|
1011
|
+
finalResponse = bridgeResult.relativeUrl; // Relative path for storage
|
|
1012
|
+
if (isAnimatedGif && metadata) {
|
|
1013
|
+
metadata.OriginalPath = bridgeResult.relativeUrl || '';
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const imageNodeSize = measuredSize || computeDroppedImageNodeSize(
|
|
1017
|
+
bridgeResult.width || 0,
|
|
1018
|
+
bridgeResult.height || 0
|
|
1019
|
+
);
|
|
1020
|
+
finalWidth = imageNodeSize.width;
|
|
1021
|
+
finalHeight = imageNodeSize.height;
|
|
1022
|
+
|
|
1023
|
+
URL.revokeObjectURL(localSizingUrl);
|
|
1024
|
+
imageSaved = true; // Already saved by Bridge
|
|
1025
|
+
console.log(`[DnD] ✓ Bridge saved: ${file.name} -> ${bridgeResult.relativeUrl} (${finalWidth}x${finalHeight})`);
|
|
1026
|
+
} else {
|
|
1027
|
+
// Fallback to Blob URL + background save
|
|
1028
|
+
const size = measuredSize || computeDroppedImageNodeSize(470, 350);
|
|
1029
|
+
displayUrl = localSizingUrl;
|
|
1030
|
+
finalResponse = localSizingUrl;
|
|
1031
|
+
finalWidth = size.width;
|
|
1032
|
+
finalHeight = size.height;
|
|
1033
|
+
imageSaved = false;
|
|
1034
|
+
}
|
|
1035
|
+
} else {
|
|
1036
|
+
// MAUI/Native: use Blob URL + background save via C#
|
|
1037
|
+
try {
|
|
1038
|
+
const sizingUrl = URL.createObjectURL(file);
|
|
1039
|
+
const size = await measureImageSize(sizingUrl);
|
|
1040
|
+
displayUrl = sizingUrl;
|
|
1041
|
+
finalResponse = sizingUrl;
|
|
1042
|
+
finalWidth = size.width;
|
|
1043
|
+
finalHeight = size.height;
|
|
1044
|
+
imageSaved = false;
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
console.warn(`[DnD] Image ${index + 1} measure failed: ${err.message}`);
|
|
1047
|
+
displayUrl = URL.createObjectURL(file);
|
|
1048
|
+
finalResponse = displayUrl;
|
|
1049
|
+
const fallbackSize = computeDroppedImageNodeSize(470, 350);
|
|
1050
|
+
finalWidth = fallbackSize.width;
|
|
1051
|
+
finalHeight = fallbackSize.height;
|
|
1052
|
+
imageSaved = false;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
1056
|
+
isLoading = false;
|
|
1057
|
+
} else if (type === 'video') {
|
|
1058
|
+
console.log(`[DnD] Processing video ${index + 1}: ${file.name}`);
|
|
1059
|
+
finalWidth = 470;
|
|
1060
|
+
finalHeight = 264;
|
|
1061
|
+
const localReference = await createLocalVideoReference(file, item, bridgeConnected);
|
|
1062
|
+
displayUrl = localReference.displayUrl;
|
|
1063
|
+
finalResponse = localReference.response;
|
|
1064
|
+
metadata = localReference.metadata;
|
|
1065
|
+
videoSaved = localReference.saved === true;
|
|
1066
|
+
|
|
1067
|
+
// Legacy asset-copy path is intentionally unreachable for videos.
|
|
1068
|
+
if (false && bridgeConnected) {
|
|
1069
|
+
const bridgeResult = await uploadAssetToBridge(file);
|
|
1070
|
+
if (bridgeResult.success) {
|
|
1071
|
+
displayUrl = bridgeResult.url;
|
|
1072
|
+
finalResponse = bridgeResult.relativeUrl;
|
|
1073
|
+
videoSaved = true;
|
|
1074
|
+
console.log(`[DnD] ✓ Bridge saved video: ${file.name} -> ${bridgeResult.relativeUrl}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (!videoSaved) {
|
|
1079
|
+
displayUrl = window.__isNativeApp ? await fileToDataUrl(file) : URL.createObjectURL(file);
|
|
1080
|
+
finalResponse = displayUrl;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
isLoading = false;
|
|
1084
|
+
} else if (type === 'text') {
|
|
1085
|
+
finalResponse = '⏳ Analyzing file...';
|
|
1086
|
+
displayUrl = finalResponse;
|
|
1087
|
+
isLoading = true;
|
|
1088
|
+
} else if (type === 'code') {
|
|
1089
|
+
// ▼▼▼ [New] Code files: Troika-rendered, load content immediately ▼▼▼
|
|
1090
|
+
finalResponse = '⏳ Loading source code...';
|
|
1091
|
+
displayUrl = finalResponse;
|
|
1092
|
+
finalWidth = module.aiNodeDefaultWidth || 470;
|
|
1093
|
+
finalHeight = 400; // Taller default for code
|
|
1094
|
+
isLoading = true;
|
|
1095
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1096
|
+
} else if (type === 'pdf') {
|
|
1097
|
+
finalResponse = window.__isNativeApp ? await fileToDataUrl(file) : URL.createObjectURL(file);
|
|
1098
|
+
displayUrl = finalResponse;
|
|
1099
|
+
finalWidth = module.aiNodeDefaultWidth || 470;
|
|
1100
|
+
finalHeight = 56;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// X, Y will be recalculated after all dimensions are known
|
|
1104
|
+
const tempX = 0;
|
|
1105
|
+
const tempY = 0;
|
|
1106
|
+
|
|
1107
|
+
return {
|
|
1108
|
+
TempId: tempId,
|
|
1109
|
+
ContentType: type,
|
|
1110
|
+
FileName: file.name,
|
|
1111
|
+
Response: finalResponse, // For C# storage
|
|
1112
|
+
DisplayUrl: displayUrl, // For JS display
|
|
1113
|
+
FileIndex: fileIndex,
|
|
1114
|
+
X: tempX,
|
|
1115
|
+
Y: tempY,
|
|
1116
|
+
Width: finalWidth,
|
|
1117
|
+
Height: finalHeight,
|
|
1118
|
+
IsLoading: isLoading,
|
|
1119
|
+
ImageSaved: imageSaved, // Flag to skip background save
|
|
1120
|
+
VideoSaved: videoSaved,
|
|
1121
|
+
Metadata: metadata
|
|
1122
|
+
};
|
|
1123
|
+
}));
|
|
1124
|
+
|
|
1125
|
+
preparedNodes.push(...batchResults.filter(n => n !== null));
|
|
1126
|
+
|
|
1127
|
+
// Yield to main thread after each batch to prevent UI freeze
|
|
1128
|
+
if (batchEnd < filesToProcess.length) {
|
|
1129
|
+
await yieldToMain();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
console.log(`[DnD] Prepared batch ${Math.floor(batchStart / BATCH_SIZE) + 1}/${Math.ceil(filesToProcess.length / BATCH_SIZE)}`);
|
|
1133
|
+
}
|
|
1134
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
1135
|
+
|
|
1136
|
+
const nodesToSend = preparedNodes.filter(n => n !== null);
|
|
1137
|
+
|
|
1138
|
+
// ▼▼▼ [Fix] Recalculate X positions based on node widths + gap ▼▼▼
|
|
1139
|
+
if (nodesToSend.length > 0) {
|
|
1140
|
+
let nextLeftX = worldX - (nodesToSend[0].Width / 2);
|
|
1141
|
+
for (const node of nodesToSend) {
|
|
1142
|
+
const centeredY = worldY + (node.Height / 2);
|
|
1143
|
+
node.X = module.snapToGrid(nextLeftX);
|
|
1144
|
+
node.Y = module.snapToGridY(centeredY);
|
|
1145
|
+
nextLeftX = node.X + node.Width + NODE_GAP;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
// ▲▲▲ [Fix] ▲▲▲
|
|
1149
|
+
|
|
1150
|
+
if (nodesToSend.length > 0) {
|
|
1151
|
+
// 1. Get ID mapping from C# (batch call)
|
|
1152
|
+
const t4 = performance.now();
|
|
1153
|
+
const actualNodeIdsMap = await module.dotNetHelper.invokeMethodAsync('AddFileNodesFromJs', nodesToSend);
|
|
1154
|
+
const t5 = performance.now();
|
|
1155
|
+
console.log(`[DnD] AddFileNodesFromJs: ${(t5 - t4).toFixed(1)}ms for ${nodesToSend.length} nodes`);
|
|
1156
|
+
|
|
1157
|
+
// ▼▼▼ [Optimization] Add nodes to JS in batches to prevent UI blocking ▼▼▼
|
|
1158
|
+
const nodesToAdd = nodesToSend.map(dto => ({
|
|
1159
|
+
id: actualNodeIdsMap[dto.TempId],
|
|
1160
|
+
contentType: dto.ContentType,
|
|
1161
|
+
prompt: dto.FileName,
|
|
1162
|
+
response: dto.DisplayUrl || dto.Response, // Use DisplayUrl for images (HTTP URL)
|
|
1163
|
+
positionX: dto.X,
|
|
1164
|
+
positionY: dto.Y,
|
|
1165
|
+
positionZ: 0,
|
|
1166
|
+
width: dto.Width,
|
|
1167
|
+
height: dto.Height,
|
|
1168
|
+
isLoading: false,
|
|
1169
|
+
metadata: dto.Metadata || {}
|
|
1170
|
+
}));
|
|
1171
|
+
|
|
1172
|
+
const JS_BATCH_SIZE = 10;
|
|
1173
|
+
const t5b = performance.now();
|
|
1174
|
+
for (let i = 0; i < nodesToAdd.length; i += JS_BATCH_SIZE) {
|
|
1175
|
+
const batch = nodesToAdd.slice(i, i + JS_BATCH_SIZE);
|
|
1176
|
+
await window.mindMap.addNodes(batch);
|
|
1177
|
+
|
|
1178
|
+
// Yield to main thread between batches
|
|
1179
|
+
if (i + JS_BATCH_SIZE < nodesToAdd.length) {
|
|
1180
|
+
await yieldToMain();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const t5c = performance.now();
|
|
1184
|
+
console.log(`[DnD] JS addNodes: ${(t5c - t5b).toFixed(1)}ms for ${nodesToAdd.length} nodes`);
|
|
1185
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
1186
|
+
|
|
1187
|
+
// 2. Stream text files (BATCH PROCESSING to prevent Render Storm)
|
|
1188
|
+
// ▼▼▼ [Optimization] Batch text file processing ▼▼▼
|
|
1189
|
+
const textNodesToProcess = nodesToSend.filter(n => n.IsLoading && n.ContentType !== 'image');
|
|
1190
|
+
|
|
1191
|
+
if (textNodesToProcess.length > 0) {
|
|
1192
|
+
console.log(`[DnD] 📄 Batch processing ${textNodesToProcess.length} text files...`);
|
|
1193
|
+
|
|
1194
|
+
// Prepare all stream references at once
|
|
1195
|
+
const textFileStreams = [];
|
|
1196
|
+
for (const nodeDto of textNodesToProcess) {
|
|
1197
|
+
const realNodeId = actualNodeIdsMap?.[nodeDto.TempId];
|
|
1198
|
+
if (!realNodeId) continue;
|
|
1199
|
+
|
|
1200
|
+
const originalEntry = filesToProcess[nodeDto.FileIndex];
|
|
1201
|
+
if (!originalEntry) continue;
|
|
1202
|
+
|
|
1203
|
+
try {
|
|
1204
|
+
const streamRef = DotNet.createJSStreamReference(originalEntry.file);
|
|
1205
|
+
textFileStreams.push({
|
|
1206
|
+
nodeId: realNodeId,
|
|
1207
|
+
fileName: nodeDto.FileName,
|
|
1208
|
+
streamRef: streamRef
|
|
1209
|
+
});
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
console.error(`[DnD] Failed to create stream ref for ${nodeDto.FileName}:`, err);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Single batch call to C# (prevents N individual StateHasChanged calls)
|
|
1216
|
+
if (textFileStreams.length > 0) {
|
|
1217
|
+
try {
|
|
1218
|
+
const t6a = performance.now();
|
|
1219
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1220
|
+
'IngestMultipleTextFilesDirectly',
|
|
1221
|
+
textFileStreams
|
|
1222
|
+
);
|
|
1223
|
+
const t6b = performance.now();
|
|
1224
|
+
console.log(`[DnD] ✅ Batch text processing: ${(t6b - t6a).toFixed(1)}ms for ${textFileStreams.length} files`);
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
console.error('[DnD] Batch text processing failed, falling back to individual:', err);
|
|
1227
|
+
// Fallback to individual processing
|
|
1228
|
+
for (const stream of textFileStreams) {
|
|
1229
|
+
try {
|
|
1230
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1231
|
+
'IngestFileStreamDirectly',
|
|
1232
|
+
stream.streamRef,
|
|
1233
|
+
stream.nodeId,
|
|
1234
|
+
stream.fileName
|
|
1235
|
+
);
|
|
1236
|
+
} catch (e) {
|
|
1237
|
+
console.error(`[DnD] Individual stream error for ${stream.fileName}:`, e);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
1244
|
+
|
|
1245
|
+
// ▼▼▼ [Optimization] Save images in background (Blob URL displayed immediately) ▼▼▼
|
|
1246
|
+
const imageNodesToSave = nodesToSend.filter(n => n.ContentType === 'image' && !n.ImageSaved);
|
|
1247
|
+
if (imageNodesToSave.length > 0) {
|
|
1248
|
+
console.log(`[DnD] 🔄 Starting background save for ${imageNodesToSave.length} images...`);
|
|
1249
|
+
// Fire-and-forget: start sequential image saving in background
|
|
1250
|
+
(async () => {
|
|
1251
|
+
for (let i = 0; i < imageNodesToSave.length; i++) {
|
|
1252
|
+
const nodeDto = imageNodesToSave[i];
|
|
1253
|
+
const realNodeId = actualNodeIdsMap?.[nodeDto.TempId];
|
|
1254
|
+
if (!realNodeId) continue;
|
|
1255
|
+
|
|
1256
|
+
const originalEntry = filesToProcess[nodeDto.FileIndex];
|
|
1257
|
+
if (!originalEntry) continue;
|
|
1258
|
+
|
|
1259
|
+
try {
|
|
1260
|
+
const streamRef = DotNet.createJSStreamReference(originalEntry.file);
|
|
1261
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1262
|
+
'SaveImageStreamBackground',
|
|
1263
|
+
streamRef,
|
|
1264
|
+
realNodeId,
|
|
1265
|
+
nodeDto.FileName
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
if ((i + 1) % 10 === 0) {
|
|
1269
|
+
console.log(`[DnD] Fallback saved ${i + 1}/${imageNodesToSave.length} images`);
|
|
1270
|
+
}
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
console.warn('[DnD] Image save error:', err);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
console.log(`[DnD] ✅ Background saved ${imageNodesToSave.length} images to assets`);
|
|
1276
|
+
})();
|
|
1277
|
+
} else {
|
|
1278
|
+
const totalImages = nodesToSend.filter(n => n.ContentType === 'image').length;
|
|
1279
|
+
if (totalImages > 0) {
|
|
1280
|
+
console.log(`[DnD] ✅ All ${totalImages} images already saved via SaveImageAndGetInfo`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
// ▲▲▲ [Optimization] ▲▲▲
|
|
1284
|
+
|
|
1285
|
+
const videoNodesToSave = nodesToSend.filter(n => n.ContentType === 'video' && !n.VideoSaved);
|
|
1286
|
+
if (videoNodesToSave.length > 0) {
|
|
1287
|
+
console.log(`[DnD] 🔄 Starting background save for ${videoNodesToSave.length} videos...`);
|
|
1288
|
+
(async () => {
|
|
1289
|
+
for (let i = 0; i < videoNodesToSave.length; i++) {
|
|
1290
|
+
const nodeDto = videoNodesToSave[i];
|
|
1291
|
+
const realNodeId = actualNodeIdsMap?.[nodeDto.TempId];
|
|
1292
|
+
if (!realNodeId) continue;
|
|
1293
|
+
|
|
1294
|
+
const originalEntry = filesToProcess[nodeDto.FileIndex];
|
|
1295
|
+
if (!originalEntry) continue;
|
|
1296
|
+
|
|
1297
|
+
try {
|
|
1298
|
+
const streamRef = DotNet.createJSStreamReference(originalEntry.file);
|
|
1299
|
+
await module.dotNetHelper.invokeMethodAsync(
|
|
1300
|
+
'SaveVideoStreamBackground',
|
|
1301
|
+
streamRef,
|
|
1302
|
+
realNodeId,
|
|
1303
|
+
nodeDto.FileName
|
|
1304
|
+
);
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
console.warn('[DnD] Video save error:', err);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
console.log(`[DnD] ✅ Background saved ${videoNodesToSave.length} videos to assets`);
|
|
1310
|
+
})();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 3. Update PDF URL only (images were already added in JS)
|
|
1314
|
+
// ▼▼▼ [Changed] Images already handled in JS addNodes; update PDFs only ▼▼▼
|
|
1315
|
+
const batchDetails = nodesToSend
|
|
1316
|
+
.map(node => {
|
|
1317
|
+
const nodeId = actualNodeIdsMap?.[node.TempId];
|
|
1318
|
+
if (!nodeId) return null;
|
|
1319
|
+
// Images and text have already been handled
|
|
1320
|
+
if (node.ContentType === 'image') return null;
|
|
1321
|
+
if (node.ContentType === 'video') return null;
|
|
1322
|
+
if (node.IsLoading) return null; // text still loading
|
|
1323
|
+
|
|
1324
|
+
return {
|
|
1325
|
+
nodeId: nodeId,
|
|
1326
|
+
fileUrl: node.Response,
|
|
1327
|
+
width: node.Width,
|
|
1328
|
+
height: node.Height
|
|
1329
|
+
};
|
|
1330
|
+
})
|
|
1331
|
+
.filter(detail => detail !== null);
|
|
1332
|
+
|
|
1333
|
+
// Execute all work in parallel
|
|
1334
|
+
// ▼▼▼ [Profiling] Post-processing time ▼▼▼
|
|
1335
|
+
const t6 = performance.now();
|
|
1336
|
+
await Promise.all([
|
|
1337
|
+
batchDetails.length > 0 ? module.dotNetHelper.invokeMethodAsync('UpdateMultipleFileNodeDetails', batchDetails) : Promise.resolve()
|
|
1338
|
+
]);
|
|
1339
|
+
const t7 = performance.now();
|
|
1340
|
+
console.log(`[DnD] UpdateMultipleFileNodeDetails: ${(t7 - t6).toFixed(1)}ms`);
|
|
1341
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
console.timeEnd(timeLabel);
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// 3. Web resource (URL) handling
|
|
1350
|
+
const imageUrl = extractImageUrl(e.dataTransfer);
|
|
1351
|
+
console.log(`[DnD] extractImageUrl result: ${imageUrl ? imageUrl.substring(0, 50) + '...' : 'null'}`);
|
|
1352
|
+
const isWebResource = imageUrl && (imageUrl.startsWith('http') || imageUrl.startsWith('data:'));
|
|
1353
|
+
if (isWebResource) {
|
|
1354
|
+
console.log('[DnD] Processing as web resource (should NOT happen if file was processed)');
|
|
1355
|
+
await handleBrowserImageDrop(module, e, imageUrl);
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const droppedUrl = extractDroppedUrl(e.dataTransfer);
|
|
1360
|
+
if (droppedUrl && /^https?:\/\//i.test(droppedUrl)) {
|
|
1361
|
+
const mouse = {
|
|
1362
|
+
x: (e.clientX / module.container.clientWidth) * 2 - 1,
|
|
1363
|
+
y: -(e.clientY / module.container.clientHeight) * 2 + 1
|
|
1364
|
+
};
|
|
1365
|
+
const vfov = (module.camera.fov * Math.PI) / 180;
|
|
1366
|
+
const viewHeight = 2 * Math.tan(vfov / 2) * module.camera.position.z;
|
|
1367
|
+
const viewWidth = viewHeight * module.camera.aspect;
|
|
1368
|
+
const worldX = module.camera.position.x + (mouse.x * viewWidth) / 2;
|
|
1369
|
+
const worldY = module.camera.position.y + (mouse.y * viewHeight) / 2;
|
|
1370
|
+
|
|
1371
|
+
await module.dotNetHelper?.invokeMethodAsync(
|
|
1372
|
+
'AddUrlNodeFromJs',
|
|
1373
|
+
droppedUrl,
|
|
1374
|
+
module.snapToGrid(worldX - 230),
|
|
1375
|
+
module.snapToGridY(worldY + 130)
|
|
1376
|
+
);
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// ▼▼▼ [Profiling] Drop handling complete ▼▼▼
|
|
1381
|
+
const totalProcessTime = performance.now() - dropTime;
|
|
1382
|
+
console.log(`[DnD] ✅ Drop processing completed in ${totalProcessTime.toFixed(0)}ms`);
|
|
1383
|
+
// ▲▲▲ [Profiling] ▲▲▲
|
|
1384
|
+
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return {
|
|
1389
|
+
addEventListeners: function (moduleInstance) {
|
|
1390
|
+
// ▼▼▼ [Changed] Prevent duplicate registration ▼▼▼
|
|
1391
|
+
if (moduleInstance._dndListenersAttached) {
|
|
1392
|
+
console.log('[MindMapDnD] Event listeners already attached, skipping.');
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
// ▲▲▲ [Changed] ▲▲▲
|
|
1396
|
+
|
|
1397
|
+
// ▼▼▼ [New] Disable JS drag listeners in MAUI native app ▼▼▼
|
|
1398
|
+
// C# NativeDropService handles it to avoid IPC bottlenecks and duplicated UI.
|
|
1399
|
+
if (window.__isNativeApp === true) {
|
|
1400
|
+
console.log('[MindMapDnD] Native app detected - JS drag handlers DISABLED (C# handles drop).');
|
|
1401
|
+
moduleInstance._dndListenersAttached = true;
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
// ▲▲▲ [New] ▲▲▲
|
|
1405
|
+
|
|
1406
|
+
const handlers = getBoundHandlers(moduleInstance);
|
|
1407
|
+
if (moduleInstance.container) {
|
|
1408
|
+
moduleInstance.container.addEventListener('dragenter', handlers.boundOnDragEnter); // [New]
|
|
1409
|
+
moduleInstance.container.addEventListener('dragover', handlers.boundOnDragOver);
|
|
1410
|
+
moduleInstance.container.addEventListener('dragleave', handlers.boundOnDragLeave);
|
|
1411
|
+
moduleInstance.container.addEventListener('drop', handlers.boundOnDrop);
|
|
1412
|
+
moduleInstance._dndListenersAttached = true;
|
|
1413
|
+
console.log('[MindMapDnD] Event listeners attached to container.');
|
|
1414
|
+
}
|
|
1415
|
+
},
|
|
1416
|
+
removeEventListeners: function (moduleInstance) {
|
|
1417
|
+
const handlers = getBoundHandlers(moduleInstance);
|
|
1418
|
+
if (moduleInstance.container) {
|
|
1419
|
+
moduleInstance.container.removeEventListener('dragenter', handlers.boundOnDragEnter); // [New]
|
|
1420
|
+
moduleInstance.container.removeEventListener('dragover', handlers.boundOnDragOver);
|
|
1421
|
+
moduleInstance.container.removeEventListener('dragleave', handlers.boundOnDragLeave);
|
|
1422
|
+
moduleInstance.container.removeEventListener('drop', handlers.boundOnDrop);
|
|
1423
|
+
}
|
|
1424
|
+
moduleInstance._dndListenersAttached = false;
|
|
1425
|
+
eventHandlers.delete(moduleInstance);
|
|
1426
|
+
},
|
|
1427
|
+
IDBHelper: IDBHelper
|
|
1428
|
+
};
|
|
1429
|
+
})();
|
|
1430
|
+
console.log('✓ mind-map-dnd.js loaded (Fixed: Async DataTransfer Safety + Text Support).');
|