mirror of
https://github.com/wavetermdev/wails.git
synced 2026-04-22 15:26:15 -07:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c808d513ac | |||
| 08faf0d1fa | |||
| f1739138c3 | |||
| 84023d543a | |||
| 69e492beb1 | |||
| 8242834fbd | |||
| 814e1ec059 | |||
| 12efb8b981 | |||
| b9558cc3cb | |||
| 022baad9e5 | |||
| 9629b1414e | |||
| ea44cf850c | |||
| 61c8c9eac0 | |||
| c89f59dc7c | |||
| 1fed110afd | |||
| 4ea6a2d120 | |||
| f84bb6a287 | |||
| 869a6f761a | |||
| 2d3029482d | |||
| 250e9f91ba |
@@ -5,12 +5,19 @@ posts before creating new ones. Here are the different ways to provide feedback:
|
||||
|
||||
=== "Bugs"
|
||||
|
||||
If you find a bug, please let us know by posting into the [v3 Alpha Feedback](https://discord.gg/3mgVyGua) channel on Discord:
|
||||
|
||||
If you find a bug, please let us know by posting into the [v3 Alpha Feedback](https://discord.gg/3mgVyGua) channel on Discord.
|
||||
|
||||
- The post should clearly state what the bug is and have a simple reproducible example. If the docs are unclear what *should* happen, please include that in the post.
|
||||
- The post should be given the `Bug` tag.
|
||||
- Please include the output of `wails doctor` in your post.
|
||||
- *Remember* unexpected behaviour isn't necessarily a bug - it might just not do what you expect it to do. Use [Suggestions](#suggestions) for this.
|
||||
- If the bug is behaviour that does not align with current documentation, e.g. a window does not resize properly, please do the following:
|
||||
- Update an existing example in the `v3/example` directory or create a new example in the `v3/examples` folder that clearly shows the issue.
|
||||
- Open a [PR](https://github.com/wailsapp/wails/pulls) with the title `[v3 alpha test] <description of bug>`.
|
||||
- Please include a link to the PR in your post.
|
||||
|
||||
!!! warning
|
||||
*Remember*, unexpected behaviour isn't necessarily a bug - it might just not do what you expect it to do. Use [Suggestions](#suggestions) for this.
|
||||
|
||||
|
||||
=== "Fixes"
|
||||
|
||||
|
||||
@@ -5,12 +5,19 @@
|
||||
|
||||
=== "错误"
|
||||
|
||||
如果您发现错误,请在Discord的[v3 Alpha Feedback](https://discord.gg/3mgVyGua)频道中发布信息:
|
||||
如果您发现了一个错误,请通过在Discord上发布到[v3 Alpha 反馈](https://discord.gg/3mgVyGua)频道来告知我们。
|
||||
|
||||
- 帖子应清楚地说明是什么错误,并提供一个简单的可复现示例。如果文档对于*应该*发生什么不清楚,请在帖子中包括这一点。
|
||||
- 帖子应该被标记为`Bug`标签。
|
||||
- 请在您的帖子中包括`wails doctor`的输出结果。
|
||||
- 如果错误是与当前文档不一致的行为,例如窗口无法正确调整大小,请执行以下操作:
|
||||
- 在`v3/example`目录中更新一个现有示例或在`v3/examples`文件夹中创建一个清楚显示问题的新示例。
|
||||
- 使用标题为“[v3 alpha test] <错误描述>”的[PR](https://github.com/wailsapp/wails/pulls)打开一个请求。
|
||||
- 请在您的帖子中包括指向PR的链接。
|
||||
|
||||
!!! warning
|
||||
*记住*,异常行为不一定是错误 - 它可能只是不按照您预期的方式工作。请使用[建议](#suggestions)来反馈这类情况。
|
||||
|
||||
- 帖子应清楚地说明错误所在,并提供一个简单的可重现示例。如果文档不清楚应该发生什么,请在帖子中说明。
|
||||
- 帖子应标记为`Bug`。
|
||||
- 请在帖子中包含`wails doctor`的输出。
|
||||
- 请记住,意外行为不一定是错误-它可能只是不符合您的期望。请使用[建议](#suggestions)。
|
||||
|
||||
=== "修复"
|
||||
|
||||
|
||||
+22
-20
@@ -69,26 +69,27 @@ plugins:
|
||||
nav:
|
||||
- 主页: index.md
|
||||
- 入门:
|
||||
# - 介绍: introduction.md
|
||||
- 安装: getting-started/installation.md
|
||||
# - 创建项目: getting-started/creating-a-project.md
|
||||
# - 它是如何工作的: how-does-it-work.md
|
||||
# - 开发您的应用程序: getting-started/developing-your-application.md
|
||||
# - 编译您的项目: getting-started/compiling-your-project.md
|
||||
# - API 参考:
|
||||
# - 运行时:
|
||||
# - 介绍: reference/runtime/introduction.md
|
||||
# - CLI: reference/cli.md
|
||||
# - 社区:
|
||||
# - 模板: community/templates.md
|
||||
# - 指南:
|
||||
# - Angular: guides/angular.md
|
||||
# - 展示:
|
||||
# - Emaillit: community/showcase/emailit.md
|
||||
# - 贡献: community/contributing.md
|
||||
# - 链接: community/links.md
|
||||
- 下一步: getting-started/next-steps.md
|
||||
- 反馈: getting-started/feedback.md
|
||||
# - 介绍: introduction.md
|
||||
- 安装: getting-started/installation.md
|
||||
# - 创建项目: getting-started/creating-a-project.md
|
||||
# - 它是如何工作的: how-does-it-work.md
|
||||
# - 开发您的应用程序: getting-started/developing-your-application.md
|
||||
# - 编译您的项目: getting-started/compiling-your-project.md
|
||||
# - API 参考:
|
||||
# - 运行时:
|
||||
# - 介绍: reference/runtime/introduction.md
|
||||
# - CLI: reference/cli.md
|
||||
# - 社区:
|
||||
# - 模板: community/templates.md
|
||||
# - 指南:
|
||||
# - Angular: guides/angular.md
|
||||
# - 展示:
|
||||
# - Emaillit: community/showcase/emailit.md
|
||||
# - 贡献: community/contributing.md
|
||||
# - 链接: community/links.md
|
||||
- 下一步: getting-started/next-steps.md
|
||||
- 反馈: getting-started/feedback.md
|
||||
- 反馈: getting-started/feedback.md
|
||||
- v3 版本中的新内容是什么?: whats-new.md
|
||||
- API:
|
||||
- 应用程序: API/application.md
|
||||
@@ -149,6 +150,7 @@ nav:
|
||||
# - Links: community/links.md
|
||||
- Next Steps: getting-started/next-steps.md
|
||||
- Feedback: getting-started/feedback.md
|
||||
- Feedback: getting-started/feedback.md
|
||||
- What's New in v3?: whats-new.md
|
||||
- API:
|
||||
- Application: API/application.md
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
" ",Mac,Windows,Linux
|
||||
`wails init`," "," "," "
|
||||
`wails build`," "," "," "
|
||||
`wails dev`," "," "," "
|
||||
`wails package`," "," "," "
|
||||
`wails init`,:material-check-bold:,:material-check-bold:,:material-check-bold:
|
||||
`wails build`,:material-check-bold:,:material-check-bold:,:material-check-bold:
|
||||
`wails dev`," ",:material-check-bold:," "
|
||||
`wails package`," ",:material-check-bold:,:material-check-bold:
|
||||
|
||||
|
@@ -26,9 +26,11 @@ func init() {
|
||||
|
||||
func main() {
|
||||
app := clir.NewCli("wails", "The Wails CLI", "v3")
|
||||
app.NewSubCommandFunction("build", "Build the project", commands.Build)
|
||||
app.NewSubCommandFunction("doctor", "System status report", commands.Doctor)
|
||||
app.NewSubCommandFunction("init", "Initialise a new project", commands.Init)
|
||||
app.NewSubCommandFunction("build", "Build the project", commands.Build)
|
||||
app.NewSubCommandFunction("dev", "Run in Dev mode", commands.Dev)
|
||||
app.NewSubCommandFunction("package", "Package application", commands.Package)
|
||||
app.NewSubCommandFunction("doctor", "System status report", commands.Doctor)
|
||||
task := app.NewSubCommand("task", "Run and list tasks")
|
||||
var taskFlags commands.RunTaskOptions
|
||||
task.AddFlags(&taskFlags)
|
||||
@@ -42,12 +44,16 @@ func main() {
|
||||
generate.NewSubCommandFunction("syso", "Generate Windows .syso file", commands.GenerateSyso)
|
||||
generate.NewSubCommandFunction("bindings", "Generate bindings + models", commands.GenerateBindings)
|
||||
generate.NewSubCommandFunction("constants", "Generate JS constants from Go", commands.GenerateConstants)
|
||||
generate.NewSubCommandFunction(".desktop", "Generate .desktop file", commands.GenerateDotDesktop)
|
||||
generate.NewSubCommandFunction("appimage", "Generate Linux AppImage", commands.GenerateAppImage)
|
||||
|
||||
plugin := app.NewSubCommand("plugin", "Plugin tools")
|
||||
//plugin.NewSubCommandFunction("list", "List plugins", commands.PluginList)
|
||||
plugin.NewSubCommandFunction("init", "Initialise a new plugin", commands.PluginInit)
|
||||
//plugin.NewSubCommandFunction("add", "Add a plugin", commands.PluginAdd)
|
||||
tool := app.NewSubCommand("tool", "Various tools")
|
||||
tool.NewSubCommandFunction("checkport", "Checks if a port is open. Useful for testing if vite is running.", commands.ToolCheckPort)
|
||||
tool.NewSubCommandFunction("watcher", "Watches files and runs a command when they change", commands.Watcher)
|
||||
|
||||
app.NewSubCommandFunction("version", "Print the version", commands.Version)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ version: '3'
|
||||
|
||||
vars:
|
||||
APP_NAME: "buildtest{{exeExt}}"
|
||||
|
||||
|
||||
tasks:
|
||||
|
||||
build:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
module github.com/wailsapp/wails/v3
|
||||
|
||||
go 1.21
|
||||
go 1.21.1
|
||||
|
||||
toolchain go1.21.4
|
||||
|
||||
require (
|
||||
github.com/atterpac/refresh v0.2.4
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/ebitengine/purego v0.4.0-alpha.4
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
@@ -10,6 +13,7 @@ require (
|
||||
github.com/go-task/task/v3 v3.31.0
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/pat v1.0.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
@@ -21,7 +25,7 @@ require (
|
||||
github.com/leaanthony/gosod v1.0.3
|
||||
github.com/leaanthony/u v1.1.0
|
||||
github.com/leaanthony/winicon v1.0.0
|
||||
github.com/lmittmann/tint v1.0.0
|
||||
github.com/lmittmann/tint v1.0.3
|
||||
github.com/markbates/goth v1.77.0
|
||||
github.com/matryer/is v1.4.0
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
@@ -34,13 +38,14 @@ require (
|
||||
github.com/wailsapp/go-webview2 v1.0.9
|
||||
github.com/wailsapp/mimetype v1.4.1
|
||||
golang.org/x/net v0.10.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/sys v0.14.0
|
||||
modernc.org/sqlite v1.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.1.1 // indirect
|
||||
atomicgo.dev/keyboard v0.2.8 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.16 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
@@ -76,10 +81,15 @@ require (
|
||||
github.com/radovskyb/watcher v1.0.7 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/rjeczalik/notify v0.9.3 // indirect
|
||||
github.com/sajari/fuzzy v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||
@@ -107,5 +117,3 @@ require (
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
mvdan.cc/sh/v3 v3.7.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/ebitengine/purego v0.4.0-alpha.4 => github.com/tmclane/purego v0.0.0-20230818202843-0b72c8c9140f
|
||||
|
||||
@@ -39,6 +39,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||
@@ -63,6 +65,16 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYU
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/atterpac/refresh v0.1.0 h1:vFdy+4+Q4X7ENyemskVpGFCGAUt8zi0S05vX1O5tfq8=
|
||||
github.com/atterpac/refresh v0.1.0/go.mod h1:KiBQUk5h76Z3Yvw/k3Ys2JGDrKmKmflwS9XB6B5N9qY=
|
||||
github.com/atterpac/refresh v0.2.1 h1:xWI4PsP88wFJueMq8DscZg+IA3pxMoaS9g8/Gl+p85I=
|
||||
github.com/atterpac/refresh v0.2.1/go.mod h1:KiBQUk5h76Z3Yvw/k3Ys2JGDrKmKmflwS9XB6B5N9qY=
|
||||
github.com/atterpac/refresh v0.2.2 h1:PD+zKYz4LBViPzLUqLPm9p6aiRr6vNd1m1UTHsx8ln8=
|
||||
github.com/atterpac/refresh v0.2.2/go.mod h1:KiBQUk5h76Z3Yvw/k3Ys2JGDrKmKmflwS9XB6B5N9qY=
|
||||
github.com/atterpac/refresh v0.2.3 h1:bV6VGbeUTUFiTvYGKadPZIfIWKhXgUbadHSp8r86JIo=
|
||||
github.com/atterpac/refresh v0.2.3/go.mod h1:KiBQUk5h76Z3Yvw/k3Ys2JGDrKmKmflwS9XB6B5N9qY=
|
||||
github.com/atterpac/refresh v0.2.4 h1:rTD9qchbLa0qPIVr+qpyqkePe6+1Cs14ETXDguEvttI=
|
||||
github.com/atterpac/refresh v0.2.4/go.mod h1:KiBQUk5h76Z3Yvw/k3Ys2JGDrKmKmflwS9XB6B5N9qY=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -83,6 +95,7 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -173,6 +186,8 @@ github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@@ -259,8 +274,8 @@ github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abR
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c=
|
||||
github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q=
|
||||
github.com/lmittmann/tint v1.0.0 h1:fzEj70K1L58uyoePQxKe+ezDZJ5pybiWGdA0JeFvvyw=
|
||||
github.com/lmittmann/tint v1.0.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/lmittmann/tint v1.0.3 h1:W5PHeA2D8bBJVvabNfQD/XW9HPLZK1XoPZH0cq8NouQ=
|
||||
github.com/lmittmann/tint v1.0.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.77.0 h1:s3scqnWv/Zq/a5M766V0FKsLfOdFNdh/HEkuWCKbvT8=
|
||||
github.com/markbates/goth v1.77.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
||||
@@ -316,6 +331,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
|
||||
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY=
|
||||
github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
@@ -326,6 +343,8 @@ github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXn
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -346,6 +365,10 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8=
|
||||
github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tmclane/purego v0.0.0-20230818202843-0b72c8c9140f h1:/HXk9aFXP97CJRzOIphm4pzySmJLqIMhKu1kD5usz1E=
|
||||
github.com/tmclane/purego v0.0.0-20230818202843-0b72c8c9140f/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.9 h1:lrU+q0cf1wgLdR69rN+ZnRtMJNaJRrcQ4ELxoO7/xjs=
|
||||
@@ -361,6 +384,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
@@ -475,6 +500,7 @@ golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -517,8 +543,10 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -62,6 +64,9 @@ type AssetServer struct {
|
||||
// External dev server proxy
|
||||
wsHandler *httputil.ReverseProxy
|
||||
|
||||
// External dev server URL
|
||||
devServerURL string
|
||||
|
||||
assetServerWebView
|
||||
}
|
||||
|
||||
@@ -91,41 +96,45 @@ func NewAssetServer(options *Options, servingFromDisk bool, logger *slog.Logger,
|
||||
}
|
||||
|
||||
// Check if proxy required
|
||||
externalURL, err := options.getExternalURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if externalURL != nil {
|
||||
result.wsHandler = httputil.NewSingleHostReverseProxy(externalURL)
|
||||
err := result.checkExternalURL()
|
||||
result.devServerURL = GetDevServerURL()
|
||||
if result.devServerURL != "" {
|
||||
logger.Info("Using External DevServer", "url", result.devServerURL)
|
||||
// Parse devServerURL into url.URL
|
||||
devServerURL, err := url.Parse(result.devServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = result.checkDevServerURL(devServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.wsHandler = httputil.NewSingleHostReverseProxy(devServerURL)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *AssetServer) checkExternalURL() error {
|
||||
req, err := http.NewRequest("OPTIONS", "/", nil)
|
||||
func (d *AssetServer) checkDevServerURL(devServerURL *url.URL) error {
|
||||
// Open a connection to the devserver URL
|
||||
hostPort := devServerURL.Hostname() + ":" + devServerURL.Port()
|
||||
_, err := net.DialTimeout("tcp", hostPort, 1*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
d.wsHandler.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusNoContent {
|
||||
return fmt.Errorf("unable to connect to external server: %s. Please check it's running.", d.options.ExternalURL)
|
||||
return fmt.Errorf("unable to connect to dev server: %s. Please check it's running", d.devServerURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AssetServer) LogDetails() {
|
||||
if d.debug {
|
||||
d.logger.Info("AssetServer Info:",
|
||||
var info = []any{
|
||||
"assetsFS", d.options.Assets != nil,
|
||||
"middleware", d.options.Middleware != nil,
|
||||
"handler", d.options.Handler != nil,
|
||||
"externalURL", d.options.ExternalURL,
|
||||
)
|
||||
"devServerURL", d.devServerURL,
|
||||
}
|
||||
if d.devServerURL != "" {
|
||||
info = append(info, "devServerURL", d.devServerURL)
|
||||
}
|
||||
d.logger.Info("AssetServer Info:", info...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,23 +2,10 @@
|
||||
|
||||
package assetserver
|
||||
|
||||
/*
|
||||
The assetserver for the dev mode.
|
||||
Depending on the UserAgent it injects a websocket based IPC script into `index.html` or the default desktop IPC. The
|
||||
default desktop IPC is injected when the webview accesses the devserver.
|
||||
*/
|
||||
//func NewDevAssetServer(handler http.Handler, servingFromDisk bool, logger *slog.Logger, runtime RuntimeAssets, runtimeHandler RuntimeHandler) (*AssetServer, error) {
|
||||
// result, err := NewAssetServerWithHandler(handler, servingFromDisk, logger, runtime, true, runtimeHandler)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// result.ipcJS = func(req *http.Request) []byte {
|
||||
// if strings.Contains(req.UserAgent(), WailsUserAgentValue) {
|
||||
// return runtime.DesktopIPC()
|
||||
// }
|
||||
// return runtime.WebsocketIPC()
|
||||
// }
|
||||
//
|
||||
// return result, nil
|
||||
//}
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func GetDevServerURL() string {
|
||||
return os.Getenv("WAILS_DEVSERVER_URL")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build production
|
||||
|
||||
package assetserver
|
||||
|
||||
func GetDevServerURL() string {
|
||||
return ""
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Options defines the configuration of the AssetServer.
|
||||
@@ -34,10 +33,6 @@ type Options struct {
|
||||
// Multiple Middlewares can be chained together with:
|
||||
// ChainMiddleware(middleware ...Middleware) Middleware
|
||||
Middleware Middleware
|
||||
|
||||
// ExternalURL is the URL that the assets are served from
|
||||
// This is useful when using a development server like `vite` or `snowpack` which serves the assets on a different port.
|
||||
ExternalURL string
|
||||
}
|
||||
|
||||
// Validate the options
|
||||
@@ -48,10 +43,3 @@ func (o Options) Validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o Options) getExternalURL() (*url.URL, error) {
|
||||
if o.ExternalURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return url.Parse(o.ExternalURL)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/wailsapp/wails/v3/internal/s"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed linuxdeploy-plugin-gtk.sh
|
||||
var gtkPlugin []byte
|
||||
|
||||
func log(p *pterm.ProgressbarPrinter, message string) {
|
||||
p.UpdateTitle(message)
|
||||
pterm.Info.Println(message)
|
||||
p.Increment()
|
||||
}
|
||||
|
||||
type GenerateAppImageOptions struct {
|
||||
Binary string `description:"The binary to package including path"`
|
||||
Icon string `description:"Path to the icon"`
|
||||
DesktopFile string `description:"Path to the desktop file"`
|
||||
OutputDir string `description:"Path to the output directory" default:"."`
|
||||
BuildDir string `description:"Path to the build directory"`
|
||||
}
|
||||
|
||||
func GenerateAppImage(options *GenerateAppImageOptions) error {
|
||||
|
||||
defer func() {
|
||||
pterm.DefaultSpinner.Stop()
|
||||
}()
|
||||
|
||||
if options.Binary == "" {
|
||||
return fmt.Errorf("binary not provided")
|
||||
}
|
||||
if options.Icon == "" {
|
||||
return fmt.Errorf("icon path not provided")
|
||||
}
|
||||
if options.DesktopFile == "" {
|
||||
return fmt.Errorf("desktop file path not provided")
|
||||
}
|
||||
if options.BuildDir == "" {
|
||||
// Create temp directory
|
||||
var err error
|
||||
options.BuildDir, err = os.MkdirTemp("", "wails-appimage-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var err error
|
||||
options.OutputDir, err = filepath.Abs(options.OutputDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return generateAppImage(options)
|
||||
}
|
||||
|
||||
func generateAppImage(options *GenerateAppImageOptions) error {
|
||||
numberOfSteps := 5
|
||||
p, _ := pterm.DefaultProgressbar.WithTotal(numberOfSteps).WithTitle("Generating AppImage").Start()
|
||||
|
||||
// Get the last path of the binary and normalise the name
|
||||
name := normaliseName(filepath.Base(options.Binary))
|
||||
|
||||
appDir := filepath.Join(options.BuildDir, name+"-x86_64.AppDir")
|
||||
s.RMDIR(appDir)
|
||||
|
||||
log(p, "Preparing AppImage Directory: "+appDir)
|
||||
|
||||
usrBin := filepath.Join(appDir, "usr", "bin")
|
||||
s.MKDIR(options.BuildDir)
|
||||
s.MKDIR(usrBin)
|
||||
s.COPY(options.Binary, usrBin)
|
||||
s.CHMOD(filepath.Join(usrBin, filepath.Base(options.Binary)), 0755)
|
||||
dotDirIcon := filepath.Join(appDir, ".DirIcon")
|
||||
s.COPY(options.Icon, dotDirIcon)
|
||||
iconLink := filepath.Join(appDir, filepath.Base(options.Icon))
|
||||
s.DELETE(iconLink)
|
||||
s.SYMLINK(".DirIcon", iconLink)
|
||||
s.COPY(options.DesktopFile, appDir)
|
||||
|
||||
// Download linuxdeploy and make it executable
|
||||
s.CD(options.BuildDir)
|
||||
|
||||
// Download necessary files
|
||||
log(p, "Downloading AppImage tooling")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
if !s.EXISTS(filepath.Join(options.BuildDir, "linuxdeploy-x86_64.AppImage")) {
|
||||
s.DOWNLOAD("https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage", filepath.Join(options.BuildDir, "linuxdeploy-x86_64.AppImage"))
|
||||
}
|
||||
s.CHMOD(filepath.Join(options.BuildDir, "linuxdeploy-x86_64.AppImage"), 0755)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
target := filepath.Join(appDir, "AppRun")
|
||||
if !s.EXISTS(target) {
|
||||
s.DOWNLOAD("https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-x86_64", target)
|
||||
}
|
||||
s.CHMOD(target, 0755)
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
log(p, "Processing GTK files.")
|
||||
files := s.FINDFILES("/usr/lib", "WebKitNetworkProcess", "WebKitWebProcess", "libwebkit2gtkinjectedbundle.so")
|
||||
if len(files) != 3 {
|
||||
return fmt.Errorf("unable to locate all WebKit libraries")
|
||||
}
|
||||
s.CD(appDir)
|
||||
for _, file := range files {
|
||||
targetDir := filepath.Dir(file)
|
||||
// Strip leading forward slash
|
||||
if targetDir[0] == '/' {
|
||||
targetDir = targetDir[1:]
|
||||
}
|
||||
var err error
|
||||
targetDir, err = filepath.Abs(targetDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.MKDIR(targetDir)
|
||||
s.COPY(file, targetDir)
|
||||
}
|
||||
// Copy GTK Plugin
|
||||
err := os.WriteFile(filepath.Join(options.BuildDir, "linuxdeploy-plugin-gtk.sh"), gtkPlugin, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine GTK Version
|
||||
// Run ldd on the binary and capture the output
|
||||
targetBinary := filepath.Join(appDir, "usr", "bin", options.Binary)
|
||||
lddOutput, err := s.EXEC(fmt.Sprintf("ldd %s", targetBinary))
|
||||
if err != nil {
|
||||
println(string(lddOutput))
|
||||
return err
|
||||
}
|
||||
lddString := string(lddOutput)
|
||||
// Check if GTK3 is present
|
||||
var DeployGtkVersion string
|
||||
if s.CONTAINS(lddString, "libgtk-x11-2.0.so") {
|
||||
DeployGtkVersion = "2"
|
||||
} else if s.CONTAINS(lddString, "libgtk-3.so") {
|
||||
DeployGtkVersion = "3"
|
||||
} else if s.CONTAINS(lddString, "libgtk-4.so") {
|
||||
DeployGtkVersion = "4"
|
||||
} else {
|
||||
return fmt.Errorf("unable to determine GTK version")
|
||||
}
|
||||
// Run linuxdeploy to bundle the application
|
||||
s.CD(options.BuildDir)
|
||||
log(p, "Generating AppImage (This may take a while...)")
|
||||
cmd := fmt.Sprintf("./linuxdeploy-x86_64.AppImage --appimage-extract-and-run --appdir %s --output appimage --plugin gtk", appDir)
|
||||
s.SETENV("DEPLOY_GTK_VERSION", DeployGtkVersion)
|
||||
output, err := s.EXEC(cmd)
|
||||
if err != nil {
|
||||
println(output)
|
||||
return err
|
||||
}
|
||||
|
||||
// Move file to output directory
|
||||
targetFile := filepath.Join(options.BuildDir, name+"-x86_64.AppImage")
|
||||
s.MOVE(targetFile, options.OutputDir)
|
||||
|
||||
log(p, "AppImage created: "+targetFile)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//go:build full_test
|
||||
|
||||
package commands_test
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/internal/commands"
|
||||
"github.com/wailsapp/wails/v3/internal/s"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_generateAppImage(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options *commands.GenerateAppImageOptions
|
||||
wantErr bool
|
||||
setup func()
|
||||
teardown func()
|
||||
}{
|
||||
{
|
||||
name: "Should fail if binary path is not provided",
|
||||
options: &commands.GenerateAppImageOptions{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Should fail if Icon is not provided",
|
||||
options: &commands.GenerateAppImageOptions{
|
||||
Binary: "testapp",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Should fail if desktop file is not provided",
|
||||
options: &commands.GenerateAppImageOptions{
|
||||
Binary: "testapp",
|
||||
Icon: "testicon",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Should work if inputs are valid",
|
||||
options: &commands.GenerateAppImageOptions{
|
||||
Binary: "testapp",
|
||||
Icon: "appicon.png",
|
||||
DesktopFile: "testapp.desktop",
|
||||
},
|
||||
setup: func() {
|
||||
// Compile the test application
|
||||
s.CD("appimage_testfiles")
|
||||
testDir := s.CWD()
|
||||
_, err := s.EXEC(`go build -ldflags="-s -w" -o testapp`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.DEFER(func() {
|
||||
s.CD(testDir)
|
||||
s.RM("testapp")
|
||||
s.RM("testapp-x86_64.AppImage")
|
||||
})
|
||||
},
|
||||
teardown: func() {
|
||||
s.CALLDEFER()
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.setup != nil {
|
||||
tt.setup()
|
||||
}
|
||||
if err := commands.GenerateAppImage(tt.options); (err != nil) != tt.wantErr {
|
||||
t.Errorf("generateAppImage() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.teardown != nil {
|
||||
tt.teardown()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,417 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := application.New(application.Options{
|
||||
Name: "WebviewWindow Demo",
|
||||
Description: "A demo of the WebviewWindow API",
|
||||
Assets: application.AlphaAssets,
|
||||
Mac: application.MacOptions{
|
||||
ApplicationShouldTerminateAfterLastWindowClosed: false,
|
||||
},
|
||||
})
|
||||
app.On(events.Mac.ApplicationDidFinishLaunching, func(event *application.Event) {
|
||||
log.Println("ApplicationDidFinishLaunching")
|
||||
})
|
||||
|
||||
var hiddenWindows []*application.WebviewWindow
|
||||
|
||||
currentWindow := func(fn func(window *application.WebviewWindow)) {
|
||||
if app.CurrentWindow() != nil {
|
||||
fn(app.CurrentWindow())
|
||||
} else {
|
||||
println("Current WebviewWindow is nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Create a custom menu
|
||||
menu := app.NewMenu()
|
||||
menu.AddRole(application.AppMenu)
|
||||
|
||||
windowCounter := 1
|
||||
|
||||
// Let's make a "Demo" menu
|
||||
myMenu := menu.AddSubmenu("New")
|
||||
|
||||
myMenu.Add("New WebviewWindow").
|
||||
SetAccelerator("CmdOrCtrl+N").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindow().
|
||||
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
|
||||
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
|
||||
SetURL("https://wails.io").
|
||||
Show()
|
||||
windowCounter++
|
||||
})
|
||||
myMenu.Add("New WebviewWindow (Hides on Close one time)").
|
||||
SetAccelerator("CmdOrCtrl+H").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
// This will be called when the user clicks the close button
|
||||
// on the window. It will hide the window for 5 seconds.
|
||||
// If the user clicks the close button again, the window will
|
||||
// close.
|
||||
ShouldClose: func(window *application.WebviewWindow) bool {
|
||||
if !lo.Contains(hiddenWindows, window) {
|
||||
hiddenWindows = append(hiddenWindows, window)
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
window.Show()
|
||||
}()
|
||||
window.Hide()
|
||||
return false
|
||||
}
|
||||
// Remove the window from the hiddenWindows list
|
||||
hiddenWindows = lo.Without(hiddenWindows, window)
|
||||
return true
|
||||
},
|
||||
}).
|
||||
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
|
||||
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
|
||||
SetURL("https://wails.io").
|
||||
Show()
|
||||
windowCounter++
|
||||
})
|
||||
myMenu.Add("New Frameless WebviewWindow").
|
||||
SetAccelerator("CmdOrCtrl+F").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
X: rand.Intn(1000),
|
||||
Y: rand.Intn(800),
|
||||
BackgroundColour: application.NewRGB(33, 37, 41),
|
||||
Frameless: true,
|
||||
Mac: application.MacWindow{
|
||||
InvisibleTitleBarHeight: 50,
|
||||
},
|
||||
}).Show()
|
||||
windowCounter++
|
||||
})
|
||||
myMenu.Add("New WebviewWindow (ignores mouse events").
|
||||
SetAccelerator("CmdOrCtrl+F").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
HTML: "<div style='width: 100%; height: 95%; border: 3px solid red; background-color: \"0000\";'></div>",
|
||||
X: rand.Intn(1000),
|
||||
Y: rand.Intn(800),
|
||||
IgnoreMouseEvents: true,
|
||||
BackgroundType: application.BackgroundTypeTransparent,
|
||||
Mac: application.MacWindow{
|
||||
InvisibleTitleBarHeight: 50,
|
||||
},
|
||||
}).Show()
|
||||
windowCounter++
|
||||
})
|
||||
if runtime.GOOS == "darwin" {
|
||||
myMenu.Add("New WebviewWindow (MacTitleBarHiddenInset)").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Mac: application.MacWindow{
|
||||
TitleBar: application.MacTitleBarHiddenInset,
|
||||
InvisibleTitleBarHeight: 25,
|
||||
},
|
||||
}).
|
||||
SetBackgroundColour(application.NewRGB(33, 37, 41)).
|
||||
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
|
||||
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
|
||||
SetHTML("<br/><br/><p>A MacTitleBarHiddenInset WebviewWindow example</p>").
|
||||
Show()
|
||||
windowCounter++
|
||||
})
|
||||
myMenu.Add("New WebviewWindow (MacTitleBarHiddenInsetUnified)").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Mac: application.MacWindow{
|
||||
TitleBar: application.MacTitleBarHiddenInsetUnified,
|
||||
InvisibleTitleBarHeight: 50,
|
||||
},
|
||||
}).
|
||||
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
|
||||
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
|
||||
SetHTML("<br/><br/><p>A MacTitleBarHiddenInsetUnified WebviewWindow example</p>").
|
||||
Show()
|
||||
windowCounter++
|
||||
})
|
||||
myMenu.Add("New WebviewWindow (MacTitleBarHidden)").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Mac: application.MacWindow{
|
||||
TitleBar: application.MacTitleBarHidden,
|
||||
InvisibleTitleBarHeight: 25,
|
||||
},
|
||||
}).
|
||||
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
|
||||
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
|
||||
SetHTML("<br/><br/><p>A MacTitleBarHidden WebviewWindow example</p>").
|
||||
Show()
|
||||
windowCounter++
|
||||
})
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
myMenu.Add("New WebviewWindow (Mica)").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Title: "WebviewWindow " + strconv.Itoa(windowCounter),
|
||||
X: rand.Intn(1000),
|
||||
Y: rand.Intn(800),
|
||||
BackgroundType: application.BackgroundTypeTranslucent,
|
||||
HTML: "<html style='background-color: rgba(0,0,0,0);'><body></body></html>",
|
||||
Windows: application.WindowsWindow{
|
||||
BackdropType: application.Mica,
|
||||
},
|
||||
}).Show()
|
||||
windowCounter++
|
||||
})
|
||||
myMenu.Add("New WebviewWindow (Acrylic)").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Title: "WebviewWindow " + strconv.Itoa(windowCounter),
|
||||
X: rand.Intn(1000),
|
||||
Y: rand.Intn(800),
|
||||
BackgroundType: application.BackgroundTypeTranslucent,
|
||||
HTML: "<html style='background-color: rgba(0,0,0,0);'><body></body></html>",
|
||||
Windows: application.WindowsWindow{
|
||||
BackdropType: application.Acrylic,
|
||||
},
|
||||
}).Show()
|
||||
windowCounter++
|
||||
})
|
||||
myMenu.Add("New WebviewWindow (Tabbed)").
|
||||
OnClick(func(ctx *application.Context) {
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Title: "WebviewWindow " + strconv.Itoa(windowCounter),
|
||||
X: rand.Intn(1000),
|
||||
Y: rand.Intn(800),
|
||||
BackgroundType: application.BackgroundTypeTranslucent,
|
||||
HTML: "<html style='background-color: rgba(0,0,0,0);'><body></body></html>",
|
||||
Windows: application.WindowsWindow{
|
||||
BackdropType: application.Tabbed,
|
||||
},
|
||||
}).Show()
|
||||
windowCounter++
|
||||
})
|
||||
}
|
||||
|
||||
sizeMenu := menu.AddSubmenu("Size")
|
||||
sizeMenu.Add("Set Size (800,600)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetSize(800, 600)
|
||||
})
|
||||
})
|
||||
|
||||
sizeMenu.Add("Set Size (Random)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetSize(rand.Intn(800)+200, rand.Intn(600)+200)
|
||||
})
|
||||
})
|
||||
sizeMenu.Add("Set Min Size (200,200)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetMinSize(200, 200)
|
||||
})
|
||||
})
|
||||
sizeMenu.Add("Set Max Size (600,600)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetFullscreenButtonEnabled(false)
|
||||
w.SetMaxSize(600, 600)
|
||||
})
|
||||
})
|
||||
sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
width, height := w.Size()
|
||||
application.InfoDialog().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
|
||||
})
|
||||
})
|
||||
|
||||
sizeMenu.Add("Reset Min Size").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetMinSize(0, 0)
|
||||
})
|
||||
})
|
||||
|
||||
sizeMenu.Add("Reset Max Size").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetMaxSize(0, 0)
|
||||
w.SetFullscreenButtonEnabled(true)
|
||||
})
|
||||
})
|
||||
positionMenu := menu.AddSubmenu("Position")
|
||||
positionMenu.Add("Set Relative Position (0,0)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetRelativePosition(0, 0)
|
||||
})
|
||||
})
|
||||
positionMenu.Add("Set Relative Position (Random)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetRelativePosition(rand.Intn(1000), rand.Intn(800))
|
||||
})
|
||||
})
|
||||
|
||||
positionMenu.Add("Get Relative Position").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
x, y := w.RelativePosition()
|
||||
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
|
||||
})
|
||||
})
|
||||
|
||||
positionMenu.Add("Set Absolute Position (0,0)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetAbsolutePosition(0, 0)
|
||||
})
|
||||
})
|
||||
|
||||
positionMenu.Add("Set Absolute Position (Random)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetAbsolutePosition(rand.Intn(1000), rand.Intn(800))
|
||||
})
|
||||
})
|
||||
|
||||
positionMenu.Add("Get Absolute Position").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
x, y := w.AbsolutePosition()
|
||||
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
|
||||
})
|
||||
})
|
||||
|
||||
positionMenu.Add("Center").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.Center()
|
||||
})
|
||||
})
|
||||
stateMenu := menu.AddSubmenu("State")
|
||||
stateMenu.Add("Minimise (for 2 secs)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.Minimise()
|
||||
time.Sleep(2 * time.Second)
|
||||
w.Restore()
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Maximise").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.Maximise()
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Fullscreen").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.Fullscreen()
|
||||
})
|
||||
})
|
||||
stateMenu.Add("UnFullscreen").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.UnFullscreen()
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Restore").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.Restore()
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Hide (for 2 seconds)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.Hide()
|
||||
time.Sleep(2 * time.Second)
|
||||
w.Show()
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Always on Top").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetAlwaysOnTop(true)
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Not always on Top").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetAlwaysOnTop(false)
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Google.com").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetURL("https://google.com")
|
||||
})
|
||||
})
|
||||
stateMenu.Add("wails.io").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetURL("https://wails.io")
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) {
|
||||
screen, err := app.GetPrimaryScreen()
|
||||
if err != nil {
|
||||
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("Screen: %+v", screen)
|
||||
application.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show()
|
||||
})
|
||||
stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) {
|
||||
screens, err := app.GetScreens()
|
||||
if err != nil {
|
||||
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
|
||||
return
|
||||
}
|
||||
for _, screen := range screens {
|
||||
msg := fmt.Sprintf("Screen: %+v", screen)
|
||||
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
|
||||
}
|
||||
})
|
||||
stateMenu.Add("Get Screen for WebviewWindow").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
screen, err := w.GetScreen()
|
||||
if err != nil {
|
||||
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("Screen: %+v", screen)
|
||||
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
|
||||
})
|
||||
})
|
||||
stateMenu.Add("Disable for 5s").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetEnabled(false)
|
||||
time.Sleep(5 * time.Second)
|
||||
w.SetEnabled(true)
|
||||
})
|
||||
})
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
stateMenu.Add("Flash Start").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
time.Sleep(2 * time.Second)
|
||||
w.Flash(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
printMenu := menu.AddSubmenu("Print")
|
||||
printMenu.Add("Print").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
_ = w.Print()
|
||||
})
|
||||
})
|
||||
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
BackgroundColour: application.NewRGB(33, 37, 41),
|
||||
Mac: application.MacWindow{
|
||||
DisableShadow: true,
|
||||
},
|
||||
})
|
||||
|
||||
app.SetMenu(menu)
|
||||
err := app.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=testapp
|
||||
Exec=testapp
|
||||
Icon=appicon
|
||||
Categories=Development;
|
||||
Terminal=false
|
||||
Keywords=wails
|
||||
Version=1.0
|
||||
StartupNotify=false
|
||||
@@ -22,7 +22,7 @@ type BuildAssetsOptions struct {
|
||||
ProductDescription string `description:"The description of the product" default:"My Product Description"`
|
||||
ProductVersion string `description:"The version of the product" default:"0.1.0"`
|
||||
ProductCompany string `description:"The company of the product" default:"My Company"`
|
||||
ProductCopyright string `description:"The copyright notice"`
|
||||
ProductCopyright string `description:"The copyright notice" default:"\u00a9 now, My Company"`
|
||||
ProductComments string `description:"Comments to add to the generated files" default:"This is a comment"`
|
||||
ProductIdentifier string `description:"The product identifier, e.g com.mycompany.myproduct"`
|
||||
Silent bool `description:"Suppress output to console"`
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/flags"
|
||||
)
|
||||
|
||||
func Build(_ *flags.Build) error {
|
||||
pterm.Info.Println("`wails build` is an alias for `wails task build`. Use `wails task` for much better control over your builds.")
|
||||
os.Args = []string{"wails", "task", "build"}
|
||||
return RunTask(&RunTaskOptions{}, []string{})
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>My Productname</string>
|
||||
<string>{{.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user