Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var persistedSettingKeys = []string{
"epoch_enabled",
"hide_balance",
"hide_address",
"avatar_hidden",
"privacy_mode",
}

// Settings Functions
Expand Down
159 changes: 159 additions & 0 deletions file_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
40 changes: 40 additions & 0 deletions file_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html></html>"), 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)
}
}

15 changes: 7 additions & 8 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading
Loading