diff --git a/v2/pkg/assetserver/assethandler.go b/v2/pkg/assetserver/assethandler.go index b8e2df076..b56a5d033 100644 --- a/v2/pkg/assetserver/assethandler.go +++ b/v2/pkg/assetserver/assethandler.go @@ -21,9 +21,6 @@ type Logger interface { Error(message string, args ...interface{}) } -//go:embed defaultindex.html -var defaultHTML []byte - const ( indexHTML = "index.html" ) @@ -120,7 +117,9 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi if err != nil { return err } - defer file.Close() + defer func() { + _ = file.Close() + }() statInfo, err := file.Stat() if err != nil { @@ -143,7 +142,9 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi if err != nil { return err } - defer file.Close() + defer func() { + _ = file.Close() + }() statInfo, err = file.Stat() if err != nil { diff --git a/v3/examples/dialogs-basic/.hidden_file b/v3/examples/dialogs-basic/.hidden_file new file mode 100644 index 000000000..e69de29bb diff --git a/v3/examples/dialogs-basic/README.md b/v3/examples/dialogs-basic/README.md new file mode 100644 index 000000000..c03911376 --- /dev/null +++ b/v3/examples/dialogs-basic/README.md @@ -0,0 +1,36 @@ +# Dialog Test Application + +This application is designed to test macOS file dialog functionality across different versions of macOS. It provides a comprehensive suite of tests for various dialog features and configurations. + +## Features Tested + +1. Basic file open dialog +2. Single extension filter +3. Multiple extension filter +4. Multiple file selection +5. Directory selection +6. Save dialog with extension +7. Complex filters +8. Hidden files +9. Default directory +10. Full featured dialog with all options + +## Running the Tests + +```bash +go run main.go +``` + +## Test Results + +When running tests: +- Each test will show the selected file(s) and their types +- For multiple selections, all selected files will be listed +- Errors will be displayed in an error dialog +- The application logs debug information to help track issues + +## Notes + +- This test application is primarily for development and testing purposes +- It can be used to verify dialog behavior across different macOS versions +- The tests are designed to not interfere with CI pipelines diff --git a/v3/examples/dialogs-basic/main.go b/v3/examples/dialogs-basic/main.go new file mode 100644 index 000000000..2618e5960 --- /dev/null +++ b/v3/examples/dialogs-basic/main.go @@ -0,0 +1,260 @@ +package main + +import ( + "fmt" + "log" + "log/slog" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +func main() { + app := application.New(application.Options{ + Name: "Dialog Test", + Description: "Test application for macOS dialogs", + Logger: application.DefaultLogger(slog.LevelDebug), + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + // Create main window + mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Title: "Dialog Tests", + Width: 800, + Height: 600, + MinWidth: 800, + MinHeight: 600, + }) + mainWindow.SetAlwaysOnTop(true) + + // Create main menu + menu := app.NewMenu() + app.SetMenu(menu) + menu.AddRole(application.AppMenu) + menu.AddRole(application.EditMenu) + menu.AddRole(application.WindowMenu) + + // Add test menu + testMenu := menu.AddSubmenu("Tests") + + // Test 1: Basic file open with no filters (no window) + testMenu.Add("1. Basic Open (No Window)").OnClick(func(ctx *application.Context) { + result, err := application.OpenFileDialog(). + CanChooseFiles(true). + PromptForSingleSelection() + showResult("Basic Open", result, err, nil) + }) + + // Test 1b: Basic file open with window + testMenu.Add("1b. Basic Open (With Window)").OnClick(func(ctx *application.Context) { + result, err := application.OpenFileDialog(). + CanChooseFiles(true). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Basic Open", result, err, mainWindow) + }) + + // Test 2: Open with single extension filter + testMenu.Add("2. Single Filter").OnClick(func(ctx *application.Context) { + result, err := application.OpenFileDialog(). + CanChooseFiles(true). + AddFilter("Text Files", "*.txt"). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Single Filter", result, err, mainWindow) + }) + + // Test 3: Open with multiple extension filter + testMenu.Add("3. Multiple Filter").OnClick(func(ctx *application.Context) { + result, err := application.OpenFileDialog(). + CanChooseFiles(true). + AddFilter("Documents", "*.txt;*.md;*.doc;*.docx"). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Multiple Filter", result, err, mainWindow) + }) + + // Test 4: Multiple file selection + testMenu.Add("4. Multiple Selection").OnClick(func(ctx *application.Context) { + results, err := application.OpenFileDialog(). + CanChooseFiles(true). + AddFilter("Images", "*.png;*.jpg;*.jpeg"). + AttachToWindow(mainWindow). + PromptForMultipleSelection() + if err != nil { + showError("Multiple Selection", err, mainWindow) + return + } + showResults("Multiple Selection", results, mainWindow) + }) + + // Test 5: Directory selection + testMenu.Add("5. Directory Selection").OnClick(func(ctx *application.Context) { + result, err := application.OpenFileDialog(). + CanChooseDirectories(true). + CanChooseFiles(false). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Directory Selection", result, err, mainWindow) + }) + + // Test 6: Save dialog with extension + testMenu.Add("6. Save Dialog").OnClick(func(ctx *application.Context) { + result, err := application.SaveFileDialog(). + SetFilename("test.txt"). + AddFilter("Text Files", "*.txt"). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Save Dialog", result, err, mainWindow) + }) + + // Test 7: Complex filters + testMenu.Add("7. Complex Filters").OnClick(func(ctx *application.Context) { + result, err := application.OpenFileDialog(). + CanChooseFiles(true). + AddFilter("All Documents", "*.txt;*.md;*.doc;*.docx;*.pdf"). + AddFilter("Text Files", "*.txt"). + AddFilter("Markdown", "*.md"). + AddFilter("Word Documents", "*.doc;*.docx"). + AddFilter("PDF Files", "*.pdf"). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Complex Filters", result, err, mainWindow) + }) + + // Test 8: Hidden files + testMenu.Add("8. Show Hidden").OnClick(func(ctx *application.Context) { + result, err := application.OpenFileDialog(). + CanChooseFiles(true). + ShowHiddenFiles(true). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Show Hidden", result, err, mainWindow) + }) + + // Test 9: Default directory + testMenu.Add("9. Default Directory").OnClick(func(ctx *application.Context) { + home, _ := os.UserHomeDir() + result, err := application.OpenFileDialog(). + CanChooseFiles(true). + SetDirectory(home). + AttachToWindow(mainWindow). + PromptForSingleSelection() + showResult("Default Directory", result, err, mainWindow) + }) + + // Test 10: Full featured dialog + testMenu.Add("10. Full Featured").OnClick(func(ctx *application.Context) { + home, _ := os.UserHomeDir() + dialog := application.OpenFileDialog(). + SetTitle("Full Featured Dialog"). + SetDirectory(home). + CanChooseFiles(true). + CanCreateDirectories(true). + ShowHiddenFiles(true). + ResolvesAliases(true). + AllowsOtherFileTypes(true). + AttachToWindow(mainWindow) + + if runtime.GOOS == "darwin" { + dialog.SetMessage("Please select files") + } + + dialog.AddFilter("All Supported", "*.txt;*.md;*.pdf;*.png;*.jpg") + dialog.AddFilter("Documents", "*.txt;*.md;*.pdf") + dialog.AddFilter("Images", "*.png;*.jpg;*.jpeg") + + results, err := dialog.PromptForMultipleSelection() + if err != nil { + showError("Full Featured", err, mainWindow) + return + } + showResults("Full Featured", results, mainWindow) + }) + + // Show the window + mainWindow.Show() + + // Run the app + if err := app.Run(); err != nil { + log.Fatal(err) + } +} + +func showResult(test string, result string, err error, window *application.WebviewWindow) { + if err != nil { + showError(test, err, window) + return + } + if result == "" { + dialog := application.InfoDialog(). + SetTitle(test). + SetMessage("No file selected") + if window != nil { + dialog.AttachToWindow(window) + } + dialog.Show() + return + } + dialog := application.InfoDialog(). + SetTitle(test). + SetMessage(fmt.Sprintf("Selected: %s\nType: %s", result, getFileType(result))) + if window != nil { + dialog.AttachToWindow(window) + } + dialog.Show() +} + +func showResults(test string, results []string, window *application.WebviewWindow) { + if len(results) == 0 { + dialog := application.InfoDialog(). + SetTitle(test). + SetMessage("No files selected") + if window != nil { + dialog.AttachToWindow(window) + } + dialog.Show() + return + } + var message strings.Builder + message.WriteString(fmt.Sprintf("Selected %d files:\n\n", len(results))) + for _, result := range results { + message.WriteString(fmt.Sprintf("%s (%s)\n", result, getFileType(result))) + } + dialog := application.InfoDialog(). + SetTitle(test). + SetMessage(message.String()) + if window != nil { + dialog.AttachToWindow(window) + } + dialog.Show() +} + +func showError(test string, err error, window *application.WebviewWindow) { + dialog := application.ErrorDialog(). + SetTitle(test). + SetMessage(fmt.Sprintf("Error: %v", err)) + if window != nil { + dialog.AttachToWindow(window) + } + dialog.Show() +} + +func getFileType(path string) string { + if path == "" { + return "unknown" + } + ext := strings.ToLower(filepath.Ext(path)) + if ext == "" { + if fi, err := os.Stat(path); err == nil && fi.IsDir() { + return "directory" + } + return "no extension" + } + return ext +} diff --git a/v3/examples/dialogs-basic/test.txt b/v3/examples/dialogs-basic/test.txt new file mode 100644 index 000000000..64b89ccaf --- /dev/null +++ b/v3/examples/dialogs-basic/test.txt @@ -0,0 +1 @@ +This is a sample text file to test filtering. \ No newline at end of file diff --git a/v3/examples/dialogs-basic/wails-logo-small.jpg b/v3/examples/dialogs-basic/wails-logo-small.jpg new file mode 100644 index 000000000..29cb1129e Binary files /dev/null and b/v3/examples/dialogs-basic/wails-logo-small.jpg differ diff --git a/v3/examples/dialogs-basic/wails-logo-small.png b/v3/examples/dialogs-basic/wails-logo-small.png new file mode 100644 index 000000000..cbd40bf90 Binary files /dev/null and b/v3/examples/dialogs-basic/wails-logo-small.png differ diff --git a/v3/internal/assetserver/assetserver.go b/v3/internal/assetserver/assetserver.go index ee6919b66..080c80fd7 100644 --- a/v3/internal/assetserver/assetserver.go +++ b/v3/internal/assetserver/assetserver.go @@ -14,6 +14,7 @@ const ( webViewRequestHeaderWindowId = "x-wails-window-id" webViewRequestHeaderWindowName = "x-wails-window-name" servicePrefix = "wails/services" + HeaderAcceptLanguage = "accept-language" ) type RuntimeHandler interface { @@ -100,7 +101,14 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userH a.writeBlob(rw, indexHTML, recorder.Body.Bytes()) case http.StatusNotFound: - a.writeBlob(rw, indexHTML, defaultIndexHTML()) + // Read the accept-language header + acceptLanguage := req.Header.Get(HeaderAcceptLanguage) + if acceptLanguage == "" { + acceptLanguage = "en" + } + // Set content type for default index.html + header.Set(HeaderContentType, "text/html; charset=utf-8") + a.writeBlob(rw, indexHTML, defaultIndexHTML(acceptLanguage)) default: rw.WriteHeader(recorder.Code) diff --git a/v3/internal/assetserver/assetserver_dev.go b/v3/internal/assetserver/assetserver_dev.go index 61f5a6b5c..0082ce79f 100644 --- a/v3/internal/assetserver/assetserver_dev.go +++ b/v3/internal/assetserver/assetserver_dev.go @@ -2,6 +2,43 @@ package assetserver +import ( + "embed" + _ "embed" + "io" + iofs "io/fs" +) + +//go:embed defaults +var defaultHTML embed.FS + +func defaultIndexHTML(language string) []byte { + result := []byte("index.html not found") + // Create an fs.Sub in the defaults directory + defaults, err := iofs.Sub(defaultHTML, "defaults") + if err != nil { + return result + } + // Get the 2 character language code + lang := "en" + if len(language) >= 2 { + lang = language[:2] + } + // Now we can read the index.html file in the format + // index..html. + + indexFile, err := defaults.Open("index." + lang + ".html") + if err != nil { + return result + } + + indexBytes, err := io.ReadAll(indexFile) + if err != nil { + return result + } + return indexBytes +} + func (a *AssetServer) LogDetails() { var info = []any{ "middleware", a.options.Middleware != nil, diff --git a/v3/internal/assetserver/assetserver_production.go b/v3/internal/assetserver/assetserver_production.go index 6a7731191..f698fab40 100644 --- a/v3/internal/assetserver/assetserver_production.go +++ b/v3/internal/assetserver/assetserver_production.go @@ -2,4 +2,8 @@ package assetserver +func defaultIndexHTML(_ string) []byte { + return []byte("index.html not found") +} + func (a *AssetServer) LogDetails() {} diff --git a/v3/internal/assetserver/build_dev.go b/v3/internal/assetserver/build_dev.go index dda03f125..7747a7142 100644 --- a/v3/internal/assetserver/build_dev.go +++ b/v3/internal/assetserver/build_dev.go @@ -11,13 +11,6 @@ import ( "os" ) -//go:embed defaultindex.html -var defaultHTML []byte - -func defaultIndexHTML() []byte { - return defaultHTML -} - func NewAssetFileServer(vfs fs.FS) http.Handler { devServerURL := GetDevServerURL() if devServerURL == "" { diff --git a/v3/internal/assetserver/defaultindex.html b/v3/internal/assetserver/defaultindex.html deleted file mode 100644 index 1ea97c405..000000000 --- a/v3/internal/assetserver/defaultindex.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - index.html not found - - - - -
index.html not found
-

Please try reloading the page

- - \ No newline at end of file diff --git a/v3/internal/assetserver/defaults/index.en.html b/v3/internal/assetserver/defaults/index.en.html new file mode 100644 index 000000000..4ecfa9ea7 --- /dev/null +++ b/v3/internal/assetserver/defaults/index.en.html @@ -0,0 +1,350 @@ + + + + + + Page Not Found - Wails + + + +
+ +
+ +
+ + +
+
+
+
⚠️
+ Missing index.html file +
+
+

No index.html file was found in the embedded assets. This page appears when the WebView window cannot find HTML content to display.

+
    +
  • + 1 + + If you are using the Assets option in your application, ensure you have an index.html file in your project's embedded assets directory. +
    + View Example → +
    +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // ... + app := application.New(application.Options{ + // ... + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + }) + // ... +} +
    +
    +
    +
  • +
  • + 2 + If the file doesn't exist but should, verify that your build process is configured to correctly include the HTML file in the embedded assets directory. +
  • +
  • + 3 + + An alternative solution is to use the HTML option in the WebviewWindow Options. +
    + View Example → +
    +func main() { + + // ... + + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + // ... + HTML: "<h1>Hello World!<h1>", + }) + // ... +} +
    +
    +
    +
  • +
+
+ +
+ + +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/v3/internal/assetserver/defaults/index.zh.html b/v3/internal/assetserver/defaults/index.zh.html new file mode 100644 index 000000000..7aeefcd49 --- /dev/null +++ b/v3/internal/assetserver/defaults/index.zh.html @@ -0,0 +1,302 @@ + + + + + + 页面未找到 - Wails + + + +
+ +
+ +
+ + +
+
+
+
+ 未找到 index.html 文件 +
请按照以下步骤解决此问题
+
+
+

+ 系统提示:在嵌入资源中未能找到 index.html 文件。 +
+ 不用担心,这个问题很容易解决。 +

+
    +
  • + 1 + + 如果您在应用程序中使用了 Assets 选项,请确保您的项目嵌入资源目录中有 index.html 文件。 +
    + 查看示例 ➜ +
    +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // ... + app := application.New(application.Options{ + // ... + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + }) + // ... +} +
    +
    +
    +
  • +
  • + 2 + 如果文件应该存在但不存在,请验证您的构建过程是否配置正确,以确保 HTML 文件包含在嵌入资源目录中。 +
  • +
  • + 3 + + 另一种解决方案是在 WebviewWindow 选项中使用 HTML 选项。 +
    + 查看示例 ➜ +
    +func main() { + + // ... + + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + // ... + HTML: "<h1>Hello World!<h1>", + }) + // ... +} +
    +
    +
    +
  • +
+
+ +
+ + +
+
+
+ + +
+

需要帮助?查看我们的文档或加入我们的社区

+
+
+ + diff --git a/v3/pkg/application/dialogs_darwin_delegate.h b/v3/pkg/application/dialogs_darwin_delegate.h index 07657f8b9..d1c732a91 100644 --- a/v3/pkg/application/dialogs_darwin_delegate.h +++ b/v3/pkg/application/dialogs_darwin_delegate.h @@ -3,10 +3,14 @@ #ifndef _DIALOGS_DELEGATE_H_ #define _DIALOGS_DELEGATE_H_ -#import #import -// create an NSOpenPanel delegate to handle the callback +// Conditionally import UniformTypeIdentifiers based on OS version +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 +#import +#endif + +// OpenPanel delegate to handle file filtering @interface OpenPanelDelegate : NSObject @property (nonatomic, strong) NSArray *allowedExtensions; @end diff --git a/v3/pkg/application/dialogs_darwin_delegate.m b/v3/pkg/application/dialogs_darwin_delegate.m index 5cbd46a2b..284f98ab8 100644 --- a/v3/pkg/application/dialogs_darwin_delegate.m +++ b/v3/pkg/application/dialogs_darwin_delegate.m @@ -8,29 +8,31 @@ if (url == nil) { return NO; } + NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory = NO; if ([fileManager fileExistsAtPath:url.path isDirectory:&isDirectory] && isDirectory) { return YES; } - if (self.allowedExtensions == nil) { + + // If no extensions specified, allow all files + if (self.allowedExtensions == nil || [self.allowedExtensions count] == 0) { return YES; } - NSString *extension = url.pathExtension; - if (extension == nil) { + + NSString *extension = [url.pathExtension lowercaseString]; + if (extension == nil || [extension isEqualToString:@""]) { return NO; } - if ([extension isEqualToString:@""]) { - return NO; - } - if ([self.allowedExtensions containsObject:extension]) { - return YES; + + // Check if the extension is in our allowed list (case insensitive) + for (NSString *allowedExt in self.allowedExtensions) { + if ([[allowedExt lowercaseString] isEqualToString:extension]) { + return YES; + } } + return NO; } @end - - - -