diff --git a/app.go b/app.go index a48a559..0cca479 100644 --- a/app.go +++ b/app.go @@ -104,6 +104,8 @@ func NewApp() *App { "allow_github_check": true, // Allow pinging GitHub for derod updates "hide_balance": false, "hide_address": false, + "avatar_hidden": false, + "privacy_mode": false, }, history: make([]string, 0), consoleLogs: make([]ConsoleLog, 0), @@ -305,6 +307,12 @@ func (a *App) startBackgroundServices() { } }() + // Sync simulator UI when derod survived a previous session (network=simulator, :20000 up). + go func() { + time.Sleep(1 * time.Second) + a.ensureSimulatorReconnectedIfNeeded() + }() + // Auto-start Gnomon if enabled go func() { time.Sleep(2 * time.Second) // Wait for daemon connection test diff --git a/app_settings.go b/app_settings.go index ed51b03..5e86c96 100644 --- a/app_settings.go +++ b/app_settings.go @@ -30,6 +30,8 @@ var persistedSettingKeys = []string{ "epoch_enabled", "hide_balance", "hide_address", + "avatar_hidden", + "privacy_mode", } // Settings Functions diff --git a/file_service.go b/file_service.go index b7a5392..3f33a97 100644 --- a/file_service.go +++ b/file_service.go @@ -817,6 +817,165 @@ func (a *App) SelectFiles() map[string]interface{} { } } +// LoadFilesFromPaths reads dropped or resolved filesystem paths into the same shape as SelectFiles. +func (a *App) LoadFilesFromPaths(paths []string) map[string]interface{} { + if len(paths) == 0 { + return map[string]interface{}{ + "success": false, + "error": "No paths provided", + } + } + + var files []map[string]interface{} + for _, filePath := range paths { + info, err := os.Stat(filePath) + if err != nil { + a.logToConsole(fmt.Sprintf("[WARN] LoadFilesFromPaths: Could not stat %s - %v", filePath, err)) + continue + } + if info.IsDir() { + continue + } + + content, err := os.ReadFile(filePath) + if err != nil { + a.logToConsole(fmt.Sprintf("[WARN] LoadFilesFromPaths: Could not read %s - %v", filePath, err)) + continue + } + + files = append(files, map[string]interface{}{ + "name": info.Name(), + "path": filePath, + "subDir": "/", + "size": info.Size(), + "type": detectMimeType(info.Name()), + "data": string(content), + }) + } + + if len(files) == 0 { + return map[string]interface{}{ + "success": false, + "error": "No readable files in drop", + } + } + + return map[string]interface{}{ + "success": true, + "files": files, + } +} + +// ResolveDropPaths picks a folder path suitable for batch upload from native file-drop paths. +func (a *App) ResolveDropPaths(paths []string) map[string]interface{} { + if len(paths) == 0 { + return map[string]interface{}{ + "success": false, + "error": "No paths provided", + } + } + + absPaths := make([]string, 0, len(paths)) + for _, p := range paths { + p = strings.TrimSpace(p) + if p == "" { + continue + } + abs, err := filepath.Abs(p) + if err != nil { + continue + } + absPaths = append(absPaths, abs) + } + + if len(absPaths) == 0 { + return map[string]interface{}{ + "success": false, + "error": "No valid paths in drop", + } + } + + if len(absPaths) == 1 { + info, err := os.Stat(absPaths[0]) + if err != nil { + return map[string]interface{}{ + "success": false, + "error": fmt.Sprintf("Cannot access path: %v", err), + } + } + folder := absPaths[0] + if !info.IsDir() { + folder = filepath.Dir(absPaths[0]) + } + return map[string]interface{}{ + "success": true, + "folderPath": folder, + "paths": paths, + } + } + + dirs := make([]string, len(absPaths)) + for i, p := range absPaths { + info, err := os.Stat(p) + if err != nil { + return map[string]interface{}{ + "success": false, + "error": fmt.Sprintf("Cannot access %s: %v", p, err), + } + } + if info.IsDir() { + dirs[i] = p + } else { + dirs[i] = filepath.Dir(p) + } + } + + folder := dirs[0] + for _, d := range dirs[1:] { + folder = longestCommonPathPrefix(folder, d) + if folder == "" { + break + } + } + if folder == "" { + folder = dirs[0] + } + + return map[string]interface{}{ + "success": true, + "folderPath": folder, + "paths": paths, + } +} + +// longestCommonPathPrefix returns the longest shared directory prefix for two absolute paths. +func longestCommonPathPrefix(a, b string) string { + a = filepath.Clean(a) + b = filepath.Clean(b) + if a == b { + return a + } + + sep := string(filepath.Separator) + aParts := strings.Split(a, sep) + bParts := strings.Split(b, sep) + common := make([]string, 0, len(aParts)) + for i := 0; i < len(aParts) && i < len(bParts); i++ { + if aParts[i] != bParts[i] { + break + } + common = append(common, aParts[i]) + } + if len(common) == 0 { + return "" + } + prefix := filepath.Join(common...) + if strings.HasPrefix(a, sep) && !strings.HasPrefix(prefix, sep) { + prefix = sep + prefix + } + return prefix +} + // detectMimeType returns the MIME type based on file extension func detectMimeType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) diff --git a/file_service_test.go b/file_service_test.go index 8bd694b..d72a090 100644 --- a/file_service_test.go +++ b/file_service_test.go @@ -992,3 +992,43 @@ func TestJSONRoundTrip_VirtualShardDOCInfo(t *testing.T) { } } +func TestLongestCommonPathPrefix(t *testing.T) { + a := "/home/user/project/static/index.html" + b := "/home/user/project/static/assets/icon.png" + got := longestCommonPathPrefix(a, b) + want := "/home/user/project/static" + if got != want { + t.Errorf("longestCommonPathPrefix() = %q, want %q", got, want) + } +} + +func TestResolveDropPaths_MultiFile(t *testing.T) { + app := &App{} + dir := t.TempDir() + sub := filepath.Join(dir, "static") + if err := os.MkdirAll(sub, 0755); err != nil { + t.Fatal(err) + } + html := filepath.Join(sub, "index.html") + if err := os.WriteFile(html, []byte(""), 0644); err != nil { + t.Fatal(err) + } + assetDir := filepath.Join(sub, "assets") + if err := os.MkdirAll(assetDir, 0755); err != nil { + t.Fatal(err) + } + ico := filepath.Join(assetDir, "favicon.ico") + if err := os.WriteFile(ico, []byte("ico"), 0644); err != nil { + t.Fatal(err) + } + + result := app.ResolveDropPaths([]string{html, ico}) + if result["success"] != true { + t.Fatalf("ResolveDropPaths failed: %v", result["error"]) + } + folder, _ := result["folderPath"].(string) + if folder != sub { + t.Errorf("folderPath = %q, want %q", folder, sub) + } +} + diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 16ea606..320186a 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -116,17 +116,16 @@ }; window.addEventListener('focus', handleWindowFocus); - // Secondary defense against webview navigating to dropped files. - // Primary fix is DisableWebViewDrop: true in main.go (Go/native level). - // This JS layer catches any edge cases the native flag might miss. - const preventFileDrop = (e) => { - if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + // Prevent WebKit from navigating to dropped files (Linux #3686). + // Only preventDefault — do not stopPropagation, so Wails drag listeners still run. + const preventFileDropNavigation = (e) => { + if (e.dataTransfer?.types?.includes('Files')) { e.preventDefault(); - e.stopPropagation(); } }; - window.addEventListener('dragover', preventFileDrop, true); - window.addEventListener('drop', preventFileDrop, true); + for (const evtName of ['dragover', 'drop', 'dragleave', 'dragenter']) { + window.addEventListener(evtName, preventFileDropNavigation, true); + } // Minimum splash duration (allows animation to complete) const splashMinTime = new Promise(resolve => setTimeout(resolve, 3500)); diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index 36bdc9b..00e47ed 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -1,6 +1,6 @@ ` +} + // getXSWDBridgeScript returns the XSWD bridge script to inject into TELA apps // This script intercepts WebSocket connections to localhost:44326 and routes them // through Hologram's XSWD server via postMessage diff --git a/simulator_manager.go b/simulator_manager.go index 9a13723..7deb51d 100644 --- a/simulator_manager.go +++ b/simulator_manager.go @@ -449,6 +449,32 @@ func (sm *SimulatorManager) ReconnectSimulatorMode() error { return nil } +// ensureSimulatorReconnectedIfNeeded attaches SimulatorManager to an already-running +// local simulator when settings say simulator but this session has not initialized it +// (e.g. derod survived an app rebuild). Keeps Settings "Running" in sync with the sidebar. +func (a *App) ensureSimulatorReconnectedIfNeeded() { + if net, _ := a.settings["network"].(string); net != "simulator" { + return + } + if a.simulatorManager != nil && a.simulatorManager.isInitialized { + return + } + if err := a.daemonClient.TestConnection(); err != nil { + return + } + if info, err := a.daemonClient.GetInfo(); err == nil { + if inferred, ok := inferNetworkModeFromDaemonInfo(info, a.daemonClient.GetEndpoint()); ok && inferred != NetworkSimulator { + return + } + } + if a.simulatorManager == nil { + a.simulatorManager = NewSimulatorManager(a) + } + if err := a.simulatorManager.ReconnectSimulatorMode(); err != nil { + a.logToConsole(fmt.Sprintf("[WARN] Auto-reconnect simulator: %v", err)) + } +} + // StopSimulatorMode stops all simulator services func (sm *SimulatorManager) StopSimulatorMode() map[string]interface{} { sm.Lock() @@ -668,6 +694,8 @@ func (a *App) StopSimulatorMode() map[string]interface{} { // GetSimulatorStatus returns the current simulator status func (a *App) GetSimulatorStatus() map[string]interface{} { + a.ensureSimulatorReconnectedIfNeeded() + if a.simulatorManager == nil { return map[string]interface{}{ "success": true, diff --git a/simulator_wallets.go b/simulator_wallets.go index c42c3ca..7cc3943 100644 --- a/simulator_wallets.go +++ b/simulator_wallets.go @@ -638,19 +638,7 @@ func (swm *SimulatorWalletManager) CloseAll() { // never re-initialized (daemon still running from previous session), this // will transparently reconnect before returning wallets. func (a *App) GetSimulatorTestWallets() map[string]interface{} { - // Auto-reconnect: network is simulator but manager not initialized yet - if (a.simulatorManager == nil || !a.simulatorManager.isInitialized) { - if net, _ := a.settings["network"].(string); net == "simulator" { - if err := a.daemonClient.TestConnection(); err == nil { - if a.simulatorManager == nil { - a.simulatorManager = NewSimulatorManager(a) - } - if err := a.simulatorManager.ReconnectSimulatorMode(); err != nil { - a.logToConsole(fmt.Sprintf("[WARN] Simulator reconnect failed: %v", err)) - } - } - } - } + a.ensureSimulatorReconnectedIfNeeded() if a.simulatorManager == nil || a.simulatorManager.walletManager == nil { return map[string]interface{}{ diff --git a/tela_deploy_helpers.go b/tela_deploy_helpers.go index d636916..aee3d9d 100644 --- a/tela_deploy_helpers.go +++ b/tela_deploy_helpers.go @@ -58,6 +58,12 @@ func (a *App) validateDocContent(content string, fileName string) error { return nil } +// mustCompressDocContent enforces DVM-safe storage for DOC payloads. +// Static assets are binary and CSS frequently trips DVM parsing when embedded raw. +func mustCompressDocContent(docType string, userRequested bool) bool { + return userRequested || docType == tela.DOC_STATIC || docType == tela.DOC_CSS +} + // getCodeSizeInKB calculates the size of code in KB, counting newlines (from tela-cli) func getCodeSizeInKB(code string) float64 { newLines := strings.Count(code, "\n") @@ -248,11 +254,12 @@ func (a *App) prepareDOCForDeployment(docInfo DOCInfo, wallet *walletapi.Wallet_ return nil, fmt.Errorf("invalid docType %q for %s - must be one of: TELA-HTML-1, TELA-JS-1, TELA-CSS-1, TELA-JSON-1, TELA-MD-1, TELA-GO-1, TELA-STATIC-1", docInfo.DocType, docInfo.Name) } - // Handle compression if requested + // Handle compression (forced for static/CSS for DVM safety) docCode := string(data) fileName := docInfo.Name - if docInfo.Compressed { + shouldCompress := mustCompressDocContent(docInfo.DocType, docInfo.Compressed) + if shouldCompress { ext := filepath.Ext(fileName) if !tela.IsCompressedExt(ext) { compressed, err := tela.Compress(data, tela.COMPRESSION_GZIP) @@ -262,6 +269,10 @@ func (a *App) prepareDOCForDeployment(docInfo DOCInfo, wallet *walletapi.Wallet_ docCode = compressed fileName = fileName + tela.COMPRESSION_GZIP + if !docInfo.Compressed { + a.logToConsole(fmt.Sprintf("[COMPRESS] Forced compression for %s (%s)", docInfo.Name, docInfo.DocType)) + } + // Log compression results originalSize := len(data) compressedSize := len(compressed) @@ -307,7 +318,7 @@ func (a *App) prepareDOCForDeployment(docInfo DOCInfo, wallet *walletapi.Wallet_ }, } - if docInfo.Compressed { + if shouldCompress { doc.Compression = tela.COMPRESSION_GZIP } diff --git a/version.go b/version.go index 88a449c..c83acd9 100644 --- a/version.go +++ b/version.go @@ -3,7 +3,7 @@ package main // Version information - set at build time via ldflags // Example: go build -ldflags "-X main.AppVersion=1.0.0 -X main.BuildDate=2026-04-18 -X main.GitCommit=abc1234" var ( - AppVersion = "1.0.1" + AppVersion = "1.0.5" BuildDate = "dev" GitCommit = "unknown" )