mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-15 07:05:50 +01:00
Compare commits
99 commits
vk-c5bd-co
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51d30325fc |
||
|
|
f8595e3052 |
||
|
|
4d0abeb37c |
||
|
|
033650d792 |
||
|
|
4c49f27edf |
||
|
|
c84578721c |
||
|
|
354fee648e |
||
|
|
da3ce17161 |
||
|
|
bbd1b33122 |
||
|
|
ae40ca4ac1 |
||
|
|
093aa2d663 |
||
|
|
e906751c89 |
||
|
|
718fd92f85 |
||
|
|
01b661f6a5 |
||
|
|
896344eb66 |
||
|
|
8fd0340404 |
||
|
|
bc4ee373b5 | ||
|
|
5d39f1aa9a | ||
|
|
a27995940a |
||
|
|
4e8b705cda |
||
|
|
7ee2b2d856 |
||
|
|
696c55c6ee |
||
|
|
0357730294 |
||
|
|
158fd41a8b |
||
|
|
fc2b7309c1 |
||
|
|
18b3d0587f |
||
|
|
60d148e4b1 |
||
|
|
423f7e4498 |
||
|
|
4dbde817f4 |
||
|
|
95689bdeb7 |
||
|
|
7d4fe64352 |
||
|
|
e27e85aae9 | ||
|
|
dd7094e5d3 | ||
|
|
6630fa5647 |
||
|
|
7957d23c25 |
||
|
|
b25fe7496f |
||
|
|
00b0ce936d |
||
|
|
bc62140ac0 |
||
|
|
ebe8de8d2b |
||
|
|
6a2619880c |
||
|
|
db11f350cd |
||
|
|
216c9961a5 |
||
|
|
764f9bb175 |
||
|
|
23bd70e057 |
||
|
|
a0cb86ebff |
||
|
|
7aaa203c5f |
||
|
|
44a9d617e8 |
||
|
|
8e77d9358e |
||
|
|
7b8355a385 |
||
|
|
cb3305a2fc |
||
|
|
c73b2dd753 |
||
|
|
e37a7b9d4b |
||
|
|
ef7d3301c9 |
||
|
|
d16fd500df |
||
|
|
27afad59f6 |
||
|
|
4f71b22665 |
||
|
|
467f70ada7 |
||
|
|
9578b627fd |
||
|
|
4c464b3092 |
||
|
|
ba2c9989e6 |
||
|
|
9f3b00bed9 |
||
|
|
e0e96c3bf8 |
||
|
|
9125ef64be |
||
|
|
e5deb1611e |
||
|
|
8d73bec252 |
||
|
|
17bb94bbbf |
||
|
|
ac867f6587 | ||
|
|
cb1c2d86c0 | ||
|
|
8513b77fd8 | ||
|
|
9c78746441 | ||
|
|
7a383e080f | ||
|
|
9df647bb99 |
||
|
|
fc1c90e0ce |
||
|
|
385f1dafb9 |
||
|
|
70d2d52be4 |
||
|
|
6e9b6a1ee5 |
||
|
|
366e6afa3f |
||
|
|
de7a04b57a |
||
|
|
322088d3f8 |
||
|
|
f01afbc852 |
||
|
|
4df077f9b2 |
||
|
|
db22cb234d |
||
|
|
152ac2a7ce |
||
|
|
c113a068ad |
||
|
|
6943a56e8f |
||
|
|
909c16c517 | ||
|
|
5ee47ccf7b |
||
|
|
dfff549002 |
||
|
|
855dcadd79 | ||
|
|
2b8276da42 | ||
|
|
a35b676e05 | ||
|
|
45167ff743 | ||
|
|
140a110e6d |
||
|
|
bc3299fff6 |
||
|
|
1589a2f835 |
||
|
|
fcaff5e09d |
||
|
|
b489af776c |
||
|
|
b7f2d4a43b | ||
|
|
0015f8d3cb |
794 changed files with 79928 additions and 6776 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -70,7 +70,7 @@ body:
|
|||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: systemetails
|
||||
id: systemdetails
|
||||
attributes:
|
||||
label: System Details
|
||||
description: Please add the output of `wails doctor`.
|
||||
|
|
|
|||
423
.github/workflows/build-cross-image.yml
vendored
Normal file
423
.github/workflows/build-cross-image.yml
vendored
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
name: Build Cross-Compiler Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch containing Dockerfile'
|
||||
required: true
|
||||
default: 'v3-alpha'
|
||||
sdk_version:
|
||||
description: 'macOS SDK version'
|
||||
required: true
|
||||
default: '14.5'
|
||||
zig_version:
|
||||
description: 'Zig version'
|
||||
required: true
|
||||
default: '0.14.0'
|
||||
image_version:
|
||||
description: 'Image version tag'
|
||||
required: true
|
||||
default: 'latest'
|
||||
skip_tests:
|
||||
description: 'Skip cross-compilation tests'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
push:
|
||||
branches:
|
||||
- v3-alpha
|
||||
paths:
|
||||
- 'v3/internal/commands/build_assets/docker/Dockerfile.cross'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: wailsapp/wails-cross
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
image_tag: ${{ steps.vars.outputs.image_version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set build variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "sdk_version=${{ inputs.sdk_version || '14.5' }}" >> $GITHUB_OUTPUT
|
||||
echo "zig_version=${{ inputs.zig_version || '0.14.0' }}" >> $GITHUB_OUTPUT
|
||||
echo "image_version=${{ inputs.image_version || 'latest' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ steps.vars.outputs.image_version }}
|
||||
type=raw,value=sdk-${{ steps.vars.outputs.sdk_version }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: v3/internal/commands/build_assets/docker
|
||||
file: v3/internal/commands/build_assets/docker/Dockerfile.cross
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.meta.outputs.labels }}
|
||||
io.wails.zig.version=${{ steps.vars.outputs.zig_version }}
|
||||
io.wails.sdk.version=${{ steps.vars.outputs.sdk_version }}
|
||||
build-args: |
|
||||
ZIG_VERSION=${{ steps.vars.outputs.zig_version }}
|
||||
MACOS_SDK_VERSION=${{ steps.vars.outputs.sdk_version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Test cross-compilation for all platforms
|
||||
test-cross-compile:
|
||||
needs: build
|
||||
if: ${{ inputs.skip_tests != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Darwin targets (Zig + macOS SDK) - no platform emulation needed
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
platform: ""
|
||||
expected_file: "Mach-O 64-bit.*arm64"
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
platform: ""
|
||||
expected_file: "Mach-O 64-bit.*x86_64"
|
||||
# Linux targets (GCC) - need platform to match architecture
|
||||
- os: linux
|
||||
arch: amd64
|
||||
platform: "linux/amd64"
|
||||
expected_file: "ELF 64-bit LSB.*x86-64"
|
||||
- os: linux
|
||||
arch: arm64
|
||||
platform: "linux/arm64"
|
||||
expected_file: "ELF 64-bit LSB.*ARM aarch64"
|
||||
# Windows targets (Zig + mingw) - no platform emulation needed
|
||||
- os: windows
|
||||
arch: amd64
|
||||
platform: ""
|
||||
expected_file: "PE32\\+ executable.*x86-64"
|
||||
- os: windows
|
||||
arch: arm64
|
||||
platform: ""
|
||||
expected_file: "PE32\\+ executable.*Aarch64"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != ''
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create test CGO project
|
||||
run: |
|
||||
mkdir -p test-project
|
||||
cd test-project
|
||||
|
||||
# Create a minimal CGO test program
|
||||
cat > main.go << 'EOF'
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
result := C.add(1, 2)
|
||||
fmt.Printf("CGO test: 1 + 2 = %d\n", result)
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > go.mod << 'EOF'
|
||||
module test-cgo
|
||||
|
||||
go 1.21
|
||||
EOF
|
||||
|
||||
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (CGO)
|
||||
run: |
|
||||
cd test-project
|
||||
PLATFORM_FLAG=""
|
||||
if [ -n "${{ matrix.platform }}" ]; then
|
||||
PLATFORM_FLAG="--platform ${{ matrix.platform }}"
|
||||
fi
|
||||
|
||||
docker run --rm $PLATFORM_FLAG \
|
||||
-v "$(pwd):/app" \
|
||||
-e APP_NAME="test-cgo" \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
|
||||
${{ matrix.os }} ${{ matrix.arch }}
|
||||
|
||||
- name: Verify binary format
|
||||
run: |
|
||||
cd test-project/bin
|
||||
ls -la
|
||||
|
||||
# Find the built binary
|
||||
if [ "${{ matrix.os }}" = "windows" ]; then
|
||||
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }}.exe 2>/dev/null || ls *.exe | head -1)
|
||||
else
|
||||
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
|
||||
fi
|
||||
|
||||
echo "Binary: $BINARY"
|
||||
FILE_OUTPUT=$(file "$BINARY")
|
||||
echo "File output: $FILE_OUTPUT"
|
||||
|
||||
# Verify the binary format matches expected
|
||||
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
|
||||
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }}"
|
||||
else
|
||||
echo "❌ Binary format mismatch!"
|
||||
echo "Expected pattern: ${{ matrix.expected_file }}"
|
||||
echo "Got: $FILE_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check library dependencies (Linux only)
|
||||
if: matrix.os == 'linux'
|
||||
run: |
|
||||
cd test-project/bin
|
||||
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
|
||||
|
||||
echo "## Library Dependencies for $BINARY"
|
||||
echo ""
|
||||
|
||||
# Use readelf to show dynamic dependencies
|
||||
echo "### NEEDED libraries:"
|
||||
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
|
||||
|
||||
# Verify expected libraries are linked
|
||||
echo ""
|
||||
echo "### Verifying required libraries..."
|
||||
NEEDED=$(readelf -d "$BINARY" | grep NEEDED)
|
||||
|
||||
MISSING=""
|
||||
for lib in libwebkit2gtk-4.1.so libgtk-3.so libglib-2.0.so libc.so; do
|
||||
if echo "$NEEDED" | grep -q "$lib"; then
|
||||
echo "✅ $lib"
|
||||
else
|
||||
echo "❌ $lib MISSING"
|
||||
MISSING="$MISSING $lib"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Missing required libraries:$MISSING"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test non-CGO builds (pure Go cross-compilation)
|
||||
test-non-cgo:
|
||||
needs: build
|
||||
if: ${{ inputs.skip_tests != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
expected_file: "Mach-O 64-bit.*arm64"
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
expected_file: "Mach-O 64-bit.*x86_64"
|
||||
- os: linux
|
||||
arch: amd64
|
||||
expected_file: "ELF 64-bit LSB"
|
||||
- os: linux
|
||||
arch: arm64
|
||||
expected_file: "ELF 64-bit LSB.*ARM aarch64"
|
||||
- os: windows
|
||||
arch: amd64
|
||||
expected_file: "PE32\\+ executable.*x86-64"
|
||||
- os: windows
|
||||
arch: arm64
|
||||
expected_file: "PE32\\+ executable.*Aarch64"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create test non-CGO project
|
||||
run: |
|
||||
mkdir -p test-project
|
||||
cd test-project
|
||||
|
||||
# Create a pure Go test program (no CGO)
|
||||
cat > main.go << 'EOF'
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Pure Go cross-compilation test")
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > go.mod << 'EOF'
|
||||
module test-pure-go
|
||||
|
||||
go 1.21
|
||||
EOF
|
||||
|
||||
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)
|
||||
run: |
|
||||
cd test-project
|
||||
|
||||
# For non-CGO, we can use any platform since Go handles cross-compilation
|
||||
# We set CGO_ENABLED=0 to ensure pure Go build
|
||||
docker run --rm \
|
||||
-v "$(pwd):/app" \
|
||||
-e APP_NAME="test-pure-go" \
|
||||
-e CGO_ENABLED=0 \
|
||||
--entrypoint /bin/sh \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
|
||||
-c "GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o bin/test-pure-go-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} ."
|
||||
|
||||
- name: Verify binary format
|
||||
run: |
|
||||
cd test-project/bin
|
||||
ls -la
|
||||
|
||||
# Find the built binary
|
||||
if [ "${{ matrix.os }}" = "windows" ]; then
|
||||
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}.exe"
|
||||
else
|
||||
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
fi
|
||||
|
||||
echo "Binary: $BINARY"
|
||||
FILE_OUTPUT=$(file "$BINARY")
|
||||
echo "File output: $FILE_OUTPUT"
|
||||
|
||||
# Verify the binary format matches expected
|
||||
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
|
||||
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)"
|
||||
else
|
||||
echo "❌ Binary format mismatch!"
|
||||
echo "Expected pattern: ${{ matrix.expected_file }}"
|
||||
echo "Got: $FILE_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check library dependencies (Linux only)
|
||||
if: matrix.os == 'linux'
|
||||
run: |
|
||||
cd test-project/bin
|
||||
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
|
||||
echo "## Library Dependencies for $BINARY (non-CGO)"
|
||||
echo ""
|
||||
|
||||
# Non-CGO builds should have minimal dependencies (just libc or statically linked)
|
||||
echo "### NEEDED libraries:"
|
||||
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
|
||||
|
||||
# Verify NO GTK/WebKit libraries (since CGO is disabled)
|
||||
NEEDED=$(readelf -d "$BINARY" | grep NEEDED || true)
|
||||
if echo "$NEEDED" | grep -q "libwebkit\|libgtk"; then
|
||||
echo "❌ ERROR: Non-CGO binary should not link to GTK/WebKit!"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Confirmed: No GTK/WebKit dependencies (expected for non-CGO)"
|
||||
fi
|
||||
|
||||
# Summary job
|
||||
test-summary:
|
||||
needs: [build, test-cross-compile, test-non-cgo]
|
||||
if: always() && inputs.skip_tests != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check test results
|
||||
run: |
|
||||
echo "## Cross-Compilation Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then
|
||||
echo "✅ **CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [ "${{ needs.test-non-cgo.result }}" = "success" ]; then
|
||||
echo "✅ **Non-CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **Non-CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Tested Platforms" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | Architecture | CGO | Non-CGO |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-------------|-----|---------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Darwin | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Darwin | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Fail if any test failed
|
||||
if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then
|
||||
echo ""
|
||||
echo "❌ Some tests failed. Check the individual job logs for details."
|
||||
exit 1
|
||||
fi
|
||||
44
.github/workflows/claude-code-review.yml
vendored
Normal file
44
.github/workflows/claude-code-review.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
50
.github/workflows/claude.yml
vendored
Normal file
50
.github/workflows/claude.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
623
.github/workflows/nightly-release-v3.yml
vendored
623
.github/workflows/nightly-release-v3.yml
vendored
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
name: Nightly Release v3-alpha
|
||||
|
||||
on:
|
||||
|
|
@ -35,9 +34,10 @@ jobs:
|
|||
token: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
go-version: '1.24'
|
||||
cache: true
|
||||
cache-dependency-path: 'v3/go.sum'
|
||||
|
||||
- name: Install Task
|
||||
|
|
@ -46,20 +46,6 @@ jobs:
|
|||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Verify Go and Task installation
|
||||
run: |
|
||||
echo "Go version:"
|
||||
go version
|
||||
echo ""
|
||||
echo "Task version:"
|
||||
task --version
|
||||
echo ""
|
||||
echo "Working directory:"
|
||||
pwd
|
||||
echo ""
|
||||
echo "v3 directory contents:"
|
||||
ls -la v3/
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
|
@ -84,24 +70,16 @@ jobs:
|
|||
run: |
|
||||
echo "🔍 Checking UNRELEASED_CHANGELOG.md for content..."
|
||||
|
||||
# Check if the file exists and has content
|
||||
if [ -f "v3/UNRELEASED_CHANGELOG.md" ]; then
|
||||
echo "Found v3/UNRELEASED_CHANGELOG.md"
|
||||
|
||||
# Run the release script in check mode to see if there's content
|
||||
cd v3/tasks/release
|
||||
|
||||
# Use the release script itself to check for content
|
||||
if go run release.go --check-only 2>/dev/null; then
|
||||
echo "has_unreleased_content=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ Found unreleased changelog content"
|
||||
else
|
||||
echo "has_unreleased_content=false" >> $GITHUB_OUTPUT
|
||||
echo "ℹ️ No unreleased changelog content found"
|
||||
fi
|
||||
# Run the release script in check mode to see if there's content
|
||||
cd v3/tasks/release
|
||||
|
||||
# Use the release script itself to check for content
|
||||
if go run release.go --check-only 2>/dev/null; then
|
||||
echo "has_unreleased_content=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ Found unreleased changelog content"
|
||||
else
|
||||
echo "⚠️ v3/UNRELEASED_CHANGELOG.md not found"
|
||||
echo "has_unreleased_content=false" >> $GITHUB_OUTPUT
|
||||
echo "ℹ️ No unreleased changelog content found"
|
||||
fi
|
||||
|
||||
- name: Quick change detection and early exit
|
||||
|
|
@ -185,579 +163,48 @@ jobs:
|
|||
echo "🔨 FORCE RELEASE: Overriding change detection"
|
||||
fi
|
||||
|
||||
- name: Extract changelog content before release
|
||||
id: extract_changelog
|
||||
if: |
|
||||
steps.quick_check.outputs.should_continue == 'true' ||
|
||||
github.event.inputs.force_release == 'true'
|
||||
run: |
|
||||
cd v3
|
||||
|
||||
echo "📝 Extracting changelog content before release..."
|
||||
|
||||
# Use the new --create-release-notes flag
|
||||
cd tasks/release
|
||||
if go run release.go --create-release-notes ../../release_notes.md; then
|
||||
echo "✅ Successfully created release notes"
|
||||
echo "has_content=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# Show content preview
|
||||
echo "Release notes preview:"
|
||||
head -10 ../../release_notes.md
|
||||
else
|
||||
echo "⚠️ Failed to create release notes or no content found"
|
||||
echo "No changelog updates in this release." > ../../release_notes.md
|
||||
echo "has_content=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
cd ../..
|
||||
|
||||
- name: Run release script
|
||||
id: release
|
||||
if: |
|
||||
steps.quick_check.outputs.should_continue == 'true' ||
|
||||
github.event.inputs.force_release == 'true'
|
||||
env:
|
||||
WAILS_REPO_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
run: |
|
||||
cd v3
|
||||
|
||||
echo "🚀 Running release task..."
|
||||
echo "======================================================="
|
||||
|
||||
# Initialize error tracking
|
||||
RELEASE_ERRORS=""
|
||||
RELEASE_SUCCESS=true
|
||||
|
||||
# Store the original version for comparison
|
||||
ORIGINAL_VERSION=$(cat internal/version/version.txt 2>/dev/null || echo "unknown")
|
||||
echo "📌 Current version: $ORIGINAL_VERSION"
|
||||
|
||||
# Run the release task and capture output with error handling
|
||||
cd v3/tasks/release
|
||||
ARGS=()
|
||||
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
|
||||
echo "🧪 DRY RUN MODE: Simulating release task execution"
|
||||
# In dry run, we'll simulate the task without making actual changes
|
||||
OUTPUT=$(task release 2>&1 || true)
|
||||
RELEASE_EXIT_CODE=0 # Always succeed in dry run
|
||||
echo "$OUTPUT"
|
||||
else
|
||||
echo "🚀 LIVE MODE: Executing release task"
|
||||
OUTPUT=$(task release 2>&1)
|
||||
RELEASE_EXIT_CODE=$?
|
||||
echo "$OUTPUT"
|
||||
|
||||
if [ $RELEASE_EXIT_CODE -ne 0 ]; then
|
||||
echo "❌ Release task failed with exit code $RELEASE_EXIT_CODE"
|
||||
RELEASE_ERRORS="$RELEASE_ERRORS\n- Release task execution failed: $OUTPUT"
|
||||
RELEASE_SUCCESS=false
|
||||
else
|
||||
echo "✅ Release task completed successfully"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify version file exists and is readable
|
||||
if [ ! -f "internal/version/version.txt" ]; then
|
||||
echo "❌ Version file not found: internal/version/version.txt"
|
||||
RELEASE_ERRORS="$RELEASE_ERRORS\n- Version file not found after release task execution"
|
||||
RELEASE_SUCCESS=false
|
||||
RELEASE_VERSION="unknown"
|
||||
else
|
||||
RELEASE_VERSION=$(cat internal/version/version.txt 2>/dev/null || echo "unknown")
|
||||
if [ "$RELEASE_VERSION" == "unknown" ]; then
|
||||
echo "❌ Failed to read version from file"
|
||||
RELEASE_ERRORS="$RELEASE_ERRORS\n- Failed to read version from version.txt"
|
||||
RELEASE_SUCCESS=false
|
||||
else
|
||||
echo "✅ Successfully read version: $RELEASE_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if version changed
|
||||
VERSION_CHANGED="false"
|
||||
if [ "$ORIGINAL_VERSION" != "$RELEASE_VERSION" ] && [ "$RELEASE_VERSION" != "unknown" ]; then
|
||||
echo "✅ Version changed from $ORIGINAL_VERSION to $RELEASE_VERSION"
|
||||
VERSION_CHANGED="true"
|
||||
else
|
||||
echo "ℹ️ Version unchanged: $RELEASE_VERSION"
|
||||
fi
|
||||
|
||||
RELEASE_TAG="${RELEASE_VERSION}"
|
||||
RELEASE_TITLE="Wails ${RELEASE_VERSION}"
|
||||
|
||||
# Set outputs for next steps
|
||||
echo "version=$RELEASE_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "title=$RELEASE_TITLE" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=true" >> $GITHUB_OUTPUT
|
||||
echo "is_latest=false" >> $GITHUB_OUTPUT
|
||||
echo "has_changes=${{ steps.changelog_check.outputs.has_unreleased_content }}" >> $GITHUB_OUTPUT
|
||||
echo "success=$RELEASE_SUCCESS" >> $GITHUB_OUTPUT
|
||||
echo "version_changed=$VERSION_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
# Copy pre-extracted release notes
|
||||
if [ "$RELEASE_SUCCESS" == "true" ]; then
|
||||
echo "📝 Using pre-extracted release notes..."
|
||||
|
||||
# Use the release_notes.md file created earlier
|
||||
if [ -f "release_notes.md" ]; then
|
||||
cp release_notes.md release-notes.txt
|
||||
echo "✅ Successfully copied release notes"
|
||||
echo "release_notes_file=release-notes.txt" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "ℹ️ No pre-extracted release notes found"
|
||||
echo "No detailed changelog available for this release." > release-notes.txt
|
||||
echo "release_notes_file=release-notes.txt" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "release_notes_file=" >> $GITHUB_OUTPUT
|
||||
echo "⚠️ Skipping release notes generation due to release task failure"
|
||||
fi
|
||||
|
||||
# Set error output for later steps
|
||||
if [ -n "$RELEASE_ERRORS" ]; then
|
||||
echo "release_errors<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$RELEASE_ERRORS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "has_release_errors=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_release_errors=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create and push git tag
|
||||
id: git_tag
|
||||
if: |
|
||||
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
|
||||
steps.check_tag.outputs.has_tag == 'false' &&
|
||||
github.event.inputs.dry_run != 'true' &&
|
||||
steps.release.outputs.success == 'true' &&
|
||||
steps.release.outputs.version_changed == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
run: |
|
||||
echo "🏷️ Creating and pushing git tag: ${{ steps.release.outputs.tag }}"
|
||||
|
||||
# Initialize error tracking
|
||||
GIT_ERRORS=""
|
||||
GIT_SUCCESS=true
|
||||
|
||||
# Create git tag with error handling
|
||||
if git tag -a "${{ steps.release.outputs.tag }}" -m "Release ${{ steps.release.outputs.version }}" 2>&1; then
|
||||
echo "✅ Successfully created git tag: ${{ steps.release.outputs.tag }}"
|
||||
else
|
||||
echo "❌ Failed to create git tag"
|
||||
GIT_ERRORS="$GIT_ERRORS\n- Failed to create git tag: ${{ steps.release.outputs.tag }}"
|
||||
GIT_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Push tag with retry logic and error handling
|
||||
if [ "$GIT_SUCCESS" == "true" ]; then
|
||||
RETRY_COUNT=0
|
||||
MAX_RETRIES=3
|
||||
PUSH_SUCCESS=false
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$PUSH_SUCCESS" == "false" ]; do
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
echo "🔄 Attempting to push tag (attempt $RETRY_COUNT/$MAX_RETRIES)..."
|
||||
|
||||
if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" "${{ steps.release.outputs.tag }}" 2>&1; then
|
||||
echo "✅ Successfully pushed git tag to origin"
|
||||
PUSH_SUCCESS=true
|
||||
else
|
||||
echo "❌ Failed to push git tag (attempt $RETRY_COUNT/$MAX_RETRIES)"
|
||||
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
|
||||
echo "⏳ Waiting 5 seconds before retry..."
|
||||
sleep 5
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$PUSH_SUCCESS" == "false" ]; then
|
||||
echo "❌ Failed to push git tag after $MAX_RETRIES attempts"
|
||||
GIT_ERRORS="$GIT_ERRORS\n- Failed to push git tag after $MAX_RETRIES attempts"
|
||||
GIT_SUCCESS=false
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set outputs for later steps
|
||||
echo "success=$GIT_SUCCESS" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -n "$GIT_ERRORS" ]; then
|
||||
echo "git_tag_errors<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$GIT_ERRORS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "has_git_errors=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_git_errors=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
id: git_commit
|
||||
if: |
|
||||
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
|
||||
github.event.inputs.dry_run != 'true' &&
|
||||
steps.release.outputs.success == 'true' &&
|
||||
steps.release.outputs.version_changed == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
run: |
|
||||
echo "📝 Committing and pushing changes..."
|
||||
|
||||
# Initialize error tracking
|
||||
COMMIT_ERRORS=""
|
||||
COMMIT_SUCCESS=true
|
||||
|
||||
# Add any changes made by the release script with error handling
|
||||
if git add . 2>&1; then
|
||||
echo "✅ Successfully staged changes"
|
||||
else
|
||||
echo "❌ Failed to stage changes"
|
||||
COMMIT_ERRORS="$COMMIT_ERRORS\n- Failed to stage changes with git add"
|
||||
COMMIT_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if [ "$COMMIT_SUCCESS" == "true" ]; then
|
||||
if ! git diff --cached --quiet; then
|
||||
echo "📝 Changes detected, creating commit..."
|
||||
|
||||
# Create commit with error handling
|
||||
if git commit -m "${{ steps.release.outputs.version }}" 2>&1; then
|
||||
echo "✅ Successfully created commit"
|
||||
|
||||
# Push changes with retry logic
|
||||
RETRY_COUNT=0
|
||||
MAX_RETRIES=3
|
||||
PUSH_SUCCESS=false
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$PUSH_SUCCESS" == "false" ]; do
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
echo "🔄 Attempting to push changes (attempt $RETRY_COUNT/$MAX_RETRIES)..."
|
||||
|
||||
if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" v3-alpha 2>&1; then
|
||||
echo "✅ Successfully pushed changes to v3-alpha branch"
|
||||
PUSH_SUCCESS=true
|
||||
else
|
||||
echo "❌ Failed to push changes (attempt $RETRY_COUNT/$MAX_RETRIES)"
|
||||
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
|
||||
echo "⏳ Waiting 5 seconds before retry..."
|
||||
sleep 5
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$PUSH_SUCCESS" == "false" ]; then
|
||||
echo "❌ Failed to push changes after $MAX_RETRIES attempts"
|
||||
COMMIT_ERRORS="$COMMIT_ERRORS\n- Failed to push changes after $MAX_RETRIES attempts"
|
||||
COMMIT_SUCCESS=false
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to create commit"
|
||||
COMMIT_ERRORS="$COMMIT_ERRORS\n- Failed to create git commit"
|
||||
COMMIT_SUCCESS=false
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ No changes to commit"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set outputs for later steps
|
||||
echo "success=$COMMIT_SUCCESS" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -n "$COMMIT_ERRORS" ]; then
|
||||
echo "commit_errors<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$COMMIT_ERRORS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "has_commit_errors=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_commit_errors=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Read release notes
|
||||
id: read_notes
|
||||
if: |
|
||||
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
|
||||
steps.release.outputs.release_notes_file != '' &&
|
||||
steps.release.outputs.version_changed == 'true'
|
||||
run: |
|
||||
cd v3
|
||||
if [ -f "release-notes.txt" ]; then
|
||||
# Read the release notes and add header
|
||||
echo "### Changes in this release:" > formatted-release-notes.txt
|
||||
echo "" >> formatted-release-notes.txt
|
||||
cat release-notes.txt >> formatted-release-notes.txt
|
||||
|
||||
# Read the formatted notes for output
|
||||
RELEASE_NOTES=$(cat formatted-release-notes.txt)
|
||||
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "release_notes=No release notes available" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Test GitHub Release Creation (DRY RUN)
|
||||
if: |
|
||||
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
|
||||
github.event.inputs.dry_run == 'true' &&
|
||||
steps.release.outputs.version_changed == 'true'
|
||||
run: |
|
||||
echo "🧪 DRY RUN: Would create GitHub release with the following parameters:"
|
||||
echo "======================================================================="
|
||||
echo "Tag Name: ${{ steps.release.outputs.tag }}"
|
||||
echo "Release Name: ${{ steps.release.outputs.title }}"
|
||||
echo "Is Prerelease: ${{ steps.release.outputs.is_prerelease }}"
|
||||
echo "Is Latest: ${{ steps.release.outputs.is_latest }}"
|
||||
echo "Has Changes: ${{ steps.release.outputs.has_changes }}"
|
||||
echo ""
|
||||
echo "Release Body Preview:"
|
||||
echo "## Wails v3 Alpha Release - ${{ steps.release.outputs.version }}"
|
||||
echo ""
|
||||
cat << 'RELEASE_NOTES_EOF'
|
||||
${{ steps.read_notes.outputs.release_notes }}
|
||||
RELEASE_NOTES_EOF
|
||||
echo ""
|
||||
echo ""
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "🤖 This is an automated nightly release generated from the latest changes in the v3-alpha branch."
|
||||
echo ""
|
||||
echo "**Installation:**"
|
||||
echo "\`\`\`bash"
|
||||
echo "go install github.com/wailsapp/wails/v3/cmd/wails@${{ steps.release.outputs.tag }}"
|
||||
echo "\`\`\`"
|
||||
echo ""
|
||||
echo "**⚠️ Alpha Warning:** This is pre-release software and may contain bugs or incomplete features."
|
||||
echo ""
|
||||
echo "✅ DRY RUN: GitHub release creation test completed successfully!"
|
||||
|
||||
- name: Create GitHub Release (LIVE)
|
||||
id: github_release
|
||||
if: |
|
||||
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
|
||||
github.event.inputs.dry_run != 'true' &&
|
||||
steps.release.outputs.success == 'true' &&
|
||||
steps.release.outputs.version_changed == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "🚀 Creating GitHub release using gh CLI..."
|
||||
|
||||
# Create release notes in a temporary file
|
||||
cat > release_notes.md << 'EOF'
|
||||
## Wails v3 Alpha Release - ${{ steps.release.outputs.version }}
|
||||
|
||||
${{ steps.read_notes.outputs.release_notes }}
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
🤖 This is an automated nightly release generated from the latest changes in the v3-alpha branch.
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
go install github.com/wailsapp/wails/v3/cmd/wails@${{ steps.release.outputs.tag }}
|
||||
```
|
||||
|
||||
**⚠️ Alpha Warning:** This is pre-release software and may contain bugs or incomplete features.
|
||||
EOF
|
||||
|
||||
# Create the release
|
||||
if gh release create "${{ steps.release.outputs.tag }}" \
|
||||
--title "${{ steps.release.outputs.title }}" \
|
||||
--notes-file release_notes.md \
|
||||
--target v3-alpha \
|
||||
--prerelease; then
|
||||
echo "✅ Successfully created GitHub release"
|
||||
echo "outcome=success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ Failed to create GitHub release"
|
||||
echo "outcome=failure" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
|
||||
- name: Handle GitHub Release Creation Result
|
||||
id: release_result
|
||||
if: |
|
||||
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
|
||||
github.event.inputs.dry_run != 'true' &&
|
||||
steps.release.outputs.success == 'true' &&
|
||||
steps.release.outputs.version_changed == 'true'
|
||||
run: |
|
||||
echo "📋 Checking GitHub release creation result..."
|
||||
|
||||
# Initialize error tracking
|
||||
GITHUB_ERRORS=""
|
||||
GITHUB_SUCCESS=true
|
||||
|
||||
# Check if GitHub release creation succeeded
|
||||
if [ "${{ steps.github_release.outcome }}" == "success" ]; then
|
||||
echo "✅ GitHub release created successfully"
|
||||
echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ steps.release.outputs.tag }}"
|
||||
else
|
||||
echo "❌ GitHub release creation failed"
|
||||
GITHUB_ERRORS="$GITHUB_ERRORS\n- GitHub release creation failed with outcome: ${{ steps.github_release.outcome }}"
|
||||
GITHUB_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Set outputs for summary
|
||||
echo "success=$GITHUB_SUCCESS" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -n "$GITHUB_ERRORS" ]; then
|
||||
echo "github_errors<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$GITHUB_ERRORS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "has_github_errors=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_github_errors=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Error Summary and Reporting
|
||||
id: error_summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "📊 Generating comprehensive error summary..."
|
||||
|
||||
# Initialize error tracking
|
||||
TOTAL_ERRORS=0
|
||||
ERROR_SUMMARY=""
|
||||
OVERALL_SUCCESS=true
|
||||
|
||||
# Check for changelog errors
|
||||
if [ "${{ steps.changelog_check.outputs.has_errors }}" == "true" ]; then
|
||||
echo "❌ Changelog processing errors detected"
|
||||
ERROR_SUMMARY="$ERROR_SUMMARY\n### 📄 Changelog Processing Errors\n${{ steps.changelog_check.outputs.changelog_errors }}\n"
|
||||
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
|
||||
OVERALL_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Check for release script errors
|
||||
if [ "${{ steps.release.outputs.has_release_errors }}" == "true" ]; then
|
||||
echo "❌ Release script errors detected"
|
||||
ERROR_SUMMARY="$ERROR_SUMMARY\n### 🚀 Release Script Errors\n${{ steps.release.outputs.release_errors }}\n"
|
||||
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
|
||||
OVERALL_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Check for git tag errors
|
||||
if [ "${{ steps.git_tag.outputs.has_git_errors }}" == "true" ]; then
|
||||
echo "❌ Git tag errors detected"
|
||||
ERROR_SUMMARY="$ERROR_SUMMARY\n### 🏷️ Git Tag Errors\n${{ steps.git_tag.outputs.git_tag_errors }}\n"
|
||||
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
|
||||
OVERALL_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Check for git commit errors
|
||||
if [ "${{ steps.git_commit.outputs.has_commit_errors }}" == "true" ]; then
|
||||
echo "❌ Git commit errors detected"
|
||||
ERROR_SUMMARY="$ERROR_SUMMARY\n### 📝 Git Commit Errors\n${{ steps.git_commit.outputs.commit_errors }}\n"
|
||||
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
|
||||
OVERALL_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Check for GitHub release errors
|
||||
if [ "${{ steps.release_result.outputs.has_github_errors }}" == "true" ]; then
|
||||
echo "❌ GitHub release errors detected"
|
||||
ERROR_SUMMARY="$ERROR_SUMMARY\n### 🐙 GitHub Release Errors\n${{ steps.release_result.outputs.github_errors }}\n"
|
||||
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
|
||||
OVERALL_SUCCESS=false
|
||||
fi
|
||||
|
||||
# Set outputs for final summary
|
||||
echo "total_errors=$TOTAL_ERRORS" >> $GITHUB_OUTPUT
|
||||
echo "overall_success=$OVERALL_SUCCESS" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -n "$ERROR_SUMMARY" ]; then
|
||||
echo "error_summary<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$ERROR_SUMMARY" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Log summary
|
||||
if [ "$OVERALL_SUCCESS" == "true" ]; then
|
||||
echo "✅ Workflow completed successfully with no errors"
|
||||
else
|
||||
echo "⚠️ Workflow completed with $TOTAL_ERRORS error categories"
|
||||
ARGS+=(--dry-run)
|
||||
fi
|
||||
go run release.go "${ARGS[@]}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
|
||||
echo "## 🧪 DRY RUN Release Test Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## 🧪 DRY RUN Release Summary" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## 🚀 Nightly Release Summary" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "================================" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version:** ${{ steps.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Tag:** ${{ steps.release.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version Changed:** ${{ steps.release.outputs.version_changed }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Has existing tag:** ${{ steps.check_tag.outputs.has_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Has unreleased changelog content:** ${{ steps.changelog_check.outputs.has_unreleased_content }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Has changes:** ${{ steps.release.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Is prerelease:** ${{ steps.release.outputs.is_prerelease }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Is latest:** ${{ steps.release.outputs.is_latest }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Overall status
|
||||
if [ "${{ steps.error_summary.outputs.overall_success }}" == "true" ]; then
|
||||
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
|
||||
echo "- **Mode:** 🧪 DRY RUN (no actual release created)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status:** ✅ Test completed successfully" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ -n "${{ steps.release.outputs.release_version }}" ]; then
|
||||
echo "- **Version:** ${{ steps.release.outputs.release_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Tag:** ${{ steps.release.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status:** ${{ steps.release.outcome == 'success' && '✅ Success' || '⚠️ Failed' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Mode:** ${{ steps.release.outputs.release_dry_run == 'true' && '🧪 Dry Run' || '🚀 Live release' }}" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -n "${{ steps.release.outputs.release_url }}" ]; then
|
||||
echo "- **Release URL:** ${{ steps.release.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Changelog" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then
|
||||
echo "✅ Unreleased changelog processed and reset." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "- **Mode:** 🚀 Live release" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status:** ✅ Release created successfully" >> $GITHUB_STEP_SUMMARY
|
||||
echo "ℹ️ No unreleased changelog content detected." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "- **Mode:** ${{ github.event.inputs.dry_run == 'true' && '🧪 DRY RUN' || '🚀 Live release' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status:** ⚠️ Completed with ${{ steps.error_summary.outputs.total_errors }} error(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Release script did not run (skipped or failed before execution)." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Release Processing" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ steps.release.outputs.version_changed }}" == "true" ]; then
|
||||
echo "✅ **Version was incremented** and release created" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "ℹ️ **Version was not changed** - no release created" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Changelog Processing" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then
|
||||
echo "✅ **UNRELEASED_CHANGELOG.md** had content and was processed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Content moved to main changelog" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- UNRELEASED_CHANGELOG.md reset with template" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "ℹ️ **UNRELEASED_CHANGELOG.md** had no content to process" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# Error reporting section
|
||||
if [ "${{ steps.error_summary.outputs.total_errors }}" -gt 0 ]; then
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## ⚠️ Error Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Total Error Categories:** ${{ steps.error_summary.outputs.total_errors }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.error_summary.outputs.error_summary }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🔧 Troubleshooting Tips" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Check the individual step logs above for detailed error messages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Verify GitHub token permissions (contents: write, pull-requests: read)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Ensure UNRELEASED_CHANGELOG.md follows the expected format" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Check for network connectivity issues if git/GitHub operations failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Re-run the workflow with 'force_release=true' if needed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Release Notes Preview" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -n "${{ steps.read_notes.outputs.release_notes }}" ]; then
|
||||
echo "${{ steps.read_notes.outputs.release_notes }}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "No specific release notes generated" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "*Generated by automated nightly release workflow with enhanced error handling and changelog integration*" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Set final workflow status
|
||||
if [ "${{ steps.error_summary.outputs.overall_success }}" != "true" ]; then
|
||||
echo "⚠️ Workflow completed with errors. Check the summary above for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
29
.github/workflows/pr-master.yml
vendored
29
.github/workflows/pr-master.yml
vendored
|
|
@ -1,17 +1,26 @@
|
|||
name: PR Checks (master)
|
||||
|
||||
# Updated to ensure "Run Go Tests" runs for pull requests as expected.
|
||||
# Key fix: the test_go job previously required github.event.review.state == 'approved'
|
||||
# which only exists on pull_request_review events. That prevented the job from
|
||||
# running for regular pull_request events (opened / synchronize / reopened).
|
||||
# New logic: run tests for pull_request events, and also allow running when a
|
||||
# pull_request_review is submitted with state == 'approved'.
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- master
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch: {}
|
||||
|
||||
name: PR Checks (master)
|
||||
|
||||
jobs:
|
||||
check_docs:
|
||||
name: Check Docs
|
||||
if: ${{github.repository == 'wailsapp/wails' && github.base_ref == 'master'}}
|
||||
if: ${{ github.repository == 'wailsapp/wails' && github.base_ref == 'master' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
@ -23,7 +32,6 @@ jobs:
|
|||
files: |
|
||||
website/**/*.mdx
|
||||
website/**/*.md
|
||||
|
||||
- name: Run step only when files change.
|
||||
if: steps.verify-changed-files.outputs.files_changed != 'true'
|
||||
run: |
|
||||
|
|
@ -32,11 +40,18 @@ jobs:
|
|||
test_go:
|
||||
name: Run Go Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
# Run when:
|
||||
# - the event is a pull_request (opened/synchronize/reopened) OR
|
||||
# - the event is a pull_request_review AND the review state is 'approved'
|
||||
# plus other existing filters (not the update-sponsors branch, repo and base_ref)
|
||||
if: >
|
||||
github.repository == 'wailsapp/wails' &&
|
||||
github.base_ref == 'master' &&
|
||||
github.event.pull_request.head.ref != 'update-sponsors' &&
|
||||
github.event.review.state == 'approved' &&
|
||||
github.repository == 'wailsapp/wails' &&
|
||||
github.base_ref == 'master'
|
||||
(
|
||||
github.event_name == 'pull_request' ||
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state == 'approved')
|
||||
)
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest, ubuntu-24.04]
|
||||
|
|
|
|||
129
.github/workflows/unreleased-changelog-trigger.yml
vendored
Normal file
129
.github/workflows/unreleased-changelog-trigger.yml
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
name: Auto Release on Changelog Update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v3-alpha
|
||||
paths:
|
||||
- 'v3/UNRELEASED_CHANGELOG.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Run in dry-run mode (no actual release)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
name: Check Release Permissions
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
authorized: ${{ steps.check.outputs.authorized }}
|
||||
steps:
|
||||
- name: Check if user is authorized for releases
|
||||
id: check
|
||||
run: |
|
||||
# Only allow specific users to trigger releases
|
||||
AUTHORIZED_USERS="leaanthony"
|
||||
|
||||
if [[ "$AUTHORIZED_USERS" == *"${{ github.actor }}"* ]]; then
|
||||
echo "✅ User ${{ github.actor }} is authorized for releases"
|
||||
echo "authorized=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ User ${{ github.actor }} is not authorized for releases"
|
||||
echo "authorized=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
trigger-release:
|
||||
name: Trigger v3-alpha Release
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-permissions
|
||||
if: needs.check-permissions.outputs.authorized == 'true'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: v3-alpha
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
|
||||
- name: Check for unreleased changelog content
|
||||
id: changelog_check
|
||||
run: |
|
||||
echo "🔍 Checking UNRELEASED_CHANGELOG.md for content..."
|
||||
|
||||
cd v3
|
||||
# Check if UNRELEASED_CHANGELOG.md has actual content beyond the template
|
||||
if [ -f "UNRELEASED_CHANGELOG.md" ]; then
|
||||
# Use a simple check for actual content (bullet points starting with -)
|
||||
CONTENT_LINES=$(grep -E "^\s*-\s+[^[:space:]]" UNRELEASED_CHANGELOG.md | wc -l)
|
||||
if [ "$CONTENT_LINES" -gt 0 ]; then
|
||||
echo "✅ Found $CONTENT_LINES content lines in UNRELEASED_CHANGELOG.md"
|
||||
echo "has_content=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "ℹ️ No actual content found in UNRELEASED_CHANGELOG.md"
|
||||
echo "has_content=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "❌ UNRELEASED_CHANGELOG.md not found"
|
||||
echo "has_content=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Trigger nightly release workflow
|
||||
if: steps.changelog_check.outputs.has_content == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
|
||||
script: |
|
||||
const response = await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'nightly-release-v3.yml',
|
||||
ref: 'v3-alpha',
|
||||
inputs: {
|
||||
force_release: 'true',
|
||||
dry_run: '${{ github.event.inputs.dry_run || "false" }}'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🚀 Successfully triggered nightly release workflow');
|
||||
console.log(`Workflow dispatch response status: ${response.status}`);
|
||||
|
||||
// Create a summary
|
||||
core.summary
|
||||
.addHeading('🚀 Auto Release Triggered')
|
||||
.addRaw('The v3-alpha release workflow has been automatically triggered due to changes in UNRELEASED_CHANGELOG.md')
|
||||
.addTable([
|
||||
[{data: 'Trigger', header: true}, {data: 'Value', header: true}],
|
||||
['Repository', context.repo.repo],
|
||||
['Branch', 'v3-alpha'],
|
||||
['Actor', context.actor],
|
||||
['Dry Run', '${{ github.event.inputs.dry_run || "false" }}'],
|
||||
['Force Release', 'true']
|
||||
])
|
||||
.addRaw('\n---\n*This release was automatically triggered by the unreleased-changelog-trigger workflow*')
|
||||
.write();
|
||||
|
||||
- name: No content found
|
||||
if: steps.changelog_check.outputs.has_content == 'false'
|
||||
run: |
|
||||
echo "ℹ️ No content found in UNRELEASED_CHANGELOG.md, skipping release trigger"
|
||||
echo "## ℹ️ No Release Triggered" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Reason:** UNRELEASED_CHANGELOG.md does not contain actual changelog content" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Action:** No release workflow was triggered" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "To trigger a release, add actual changelog entries to the UNRELEASED_CHANGELOG.md file." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Unauthorized user
|
||||
if: needs.check-permissions.outputs.authorized == 'false'
|
||||
run: |
|
||||
echo "❌ User ${{ github.actor }} is not authorized to trigger releases"
|
||||
echo "## ❌ Unauthorized Release Attempt" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**User:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Action:** Release trigger was blocked due to insufficient permissions" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Only authorized users can trigger automatic releases via changelog updates." >> $GITHUB_STEP_SUMMARY
|
||||
|
|
@ -25,7 +25,7 @@ Erschaffe Desktop Anwendungen mit Go & Web Technologien.
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
@ -98,9 +98,9 @@ The installation instructions are on the [official website](https://wails.io/doc
|
|||
This project is supported by these kind people / companies:
|
||||
<img src="website/static/img/sponsors.svg" style="width:100%;max-width:800px;"/>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://wails.io/img/sponsor/jetbrains-grayscale.webp" style="width: 100px"/>
|
||||
</p>
|
||||
## Powered By
|
||||
|
||||
[](https://jb.gg/OpenSource)
|
||||
|
||||
## FAQ
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome" />
|
||||
</a>
|
||||
<a href="https://discord.gg/BrRSWTaxVK">
|
||||
<img alt="Discord" src="https://dcbadge.vercel.app/api/server/BrRSWTaxVK?style=flat"/>
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1042734330029547630?logo=discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/wailsapp/wails/actions/workflows/build-and-test.yml" rel="nofollow">
|
||||
|
|
|
|||
|
|
@ -255,7 +255,6 @@ func diagnoseEnvironment(f *flags.Doctor) error {
|
|||
if len(dependenciesMissing) != 0 {
|
||||
pterm.Println("Fatal:")
|
||||
pterm.Println("Required dependencies missing: " + strings.Join(dependenciesMissing, " "))
|
||||
pterm.Println("Please read this article on how to resolve this: https://wails.io/guides/resolving-missing-packages")
|
||||
}
|
||||
|
||||
pterm.Println() // Spacer for sponsor message
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
v2.10.2
|
||||
v2.11.0
|
||||
|
|
@ -27,7 +27,7 @@ require (
|
|||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+
|
|||
github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
|
||||
github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
|
|
|
|||
4
v2/examples/dragdrop-test/.gitignore
vendored
Normal file
4
v2/examples/dragdrop-test/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
frontend/wailsjs
|
||||
19
v2/examples/dragdrop-test/README.md
Normal file
19
v2/examples/dragdrop-test/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# README
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails Vanilla template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
27
v2/examples/dragdrop-test/app.go
Normal file
27
v2/examples/dragdrop-test/app.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
35
v2/examples/dragdrop-test/build/README.md
Normal file
35
v2/examples/dragdrop-test/build/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Build Directory
|
||||
|
||||
The build directory is used to house all the build files and assets for your application.
|
||||
|
||||
The structure is:
|
||||
|
||||
* bin - Output directory
|
||||
* darwin - macOS specific files
|
||||
* windows - Windows specific files
|
||||
|
||||
## Mac
|
||||
|
||||
The `darwin` directory holds files specific to Mac builds.
|
||||
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||
and
|
||||
build with `wails build`.
|
||||
|
||||
The directory contains the following files:
|
||||
|
||||
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||
|
||||
## Windows
|
||||
|
||||
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||
build with `wails build`.
|
||||
|
||||
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||
will be created using the `appicon.png` file in the build directory.
|
||||
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||
as well as the application itself (right click the exe -> properties -> details)
|
||||
- `wails.exe.manifest` - The main application manifest file.
|
||||
BIN
v2/examples/dragdrop-test/build/appicon.png
Normal file
BIN
v2/examples/dragdrop-test/build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
68
v2/examples/dragdrop-test/build/darwin/Info.dev.plist
Normal file
68
v2/examples/dragdrop-test/build/darwin/Info.dev.plist
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
63
v2/examples/dragdrop-test/build/darwin/Info.plist
Normal file
63
v2/examples/dragdrop-test/build/darwin/Info.plist
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
v2/examples/dragdrop-test/build/windows/icon.ico
Normal file
BIN
v2/examples/dragdrop-test/build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
15
v2/examples/dragdrop-test/build/windows/info.json
Normal file
15
v2/examples/dragdrop-test/build/windows/info.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"fixed": {
|
||||
"file_version": "{{.Info.ProductVersion}}"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||
"CompanyName": "{{.Info.CompanyName}}",
|
||||
"FileDescription": "{{.Info.ProductName}}",
|
||||
"LegalCopyright": "{{.Info.Copyright}}",
|
||||
"ProductName": "{{.Info.ProductName}}",
|
||||
"Comments": "{{.Info.Comments}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
114
v2/examples/dragdrop-test/build/windows/installer/project.nsi
Normal file
114
v2/examples/dragdrop-test/build/windows/installer/project.nsi
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
# DO NOT EDIT - Generated automatically by `wails build`
|
||||
|
||||
!include "x64.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
!endif
|
||||
!ifndef UNINST_KEY_NAME
|
||||
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||
|
||||
!ifndef REQUEST_EXECUTION_LEVEL
|
||||
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||
!endif
|
||||
|
||||
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!ifdef ARG_WAILS_AMD64_BINARY
|
||||
!define SUPPORTS_AMD64
|
||||
!endif
|
||||
|
||||
!ifdef ARG_WAILS_ARM64_BINARY
|
||||
!define SUPPORTS_ARM64
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_AMD64
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "amd64_arm64"
|
||||
!else
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
!else
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "arm64"
|
||||
!else
|
||||
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro wails.checkArchitecture
|
||||
!ifndef WAILS_WIN10_REQUIRED
|
||||
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||
!endif
|
||||
|
||||
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||
!endif
|
||||
|
||||
${If} ${AtLeastWin10}
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
IfSilent silentArch notSilentArch
|
||||
silentArch:
|
||||
SetErrorLevel 65
|
||||
Abort
|
||||
notSilentArch:
|
||||
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||
Quit
|
||||
${else}
|
||||
IfSilent silentWin notSilentWin
|
||||
silentWin:
|
||||
SetErrorLevel 64
|
||||
Abort
|
||||
notSilentWin:
|
||||
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
!macro wails.files
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.writeUninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!macroend
|
||||
|
||||
!macro wails.deleteUninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!macroend
|
||||
|
||||
!macro wails.setShellContext
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||
SetShellVarContext all
|
||||
${else}
|
||||
SetShellVarContext current
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
# Install webview2 by launching the bootstrapper
|
||||
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||
!macro wails.webview2runtime
|
||||
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||
!endif
|
||||
|
||||
SetRegView 64
|
||||
# If the admin key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
SetDetailsPrint both
|
||||
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||
SetDetailsPrint listonly
|
||||
|
||||
InitPluginsDir
|
||||
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||
|
||||
SetDetailsPrint both
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
!macroend
|
||||
15
v2/examples/dragdrop-test/build/windows/wails.exe.manifest
Normal file
15
v2/examples/dragdrop-test/build/windows/wails.exe.manifest
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
12
v2/examples/dragdrop-test/frontend/index.html
Normal file
12
v2/examples/dragdrop-test/frontend/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>dragdrop-test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
653
v2/examples/dragdrop-test/frontend/package-lock.json
generated
Normal file
653
v2/examples/dragdrop-test/frontend/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
|
||||
"integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
|
||||
"integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
|
||||
"integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.15.18",
|
||||
"@esbuild/linux-loong64": "0.15.18",
|
||||
"esbuild-android-64": "0.15.18",
|
||||
"esbuild-android-arm64": "0.15.18",
|
||||
"esbuild-darwin-64": "0.15.18",
|
||||
"esbuild-darwin-arm64": "0.15.18",
|
||||
"esbuild-freebsd-64": "0.15.18",
|
||||
"esbuild-freebsd-arm64": "0.15.18",
|
||||
"esbuild-linux-32": "0.15.18",
|
||||
"esbuild-linux-64": "0.15.18",
|
||||
"esbuild-linux-arm": "0.15.18",
|
||||
"esbuild-linux-arm64": "0.15.18",
|
||||
"esbuild-linux-mips64le": "0.15.18",
|
||||
"esbuild-linux-ppc64le": "0.15.18",
|
||||
"esbuild-linux-riscv64": "0.15.18",
|
||||
"esbuild-linux-s390x": "0.15.18",
|
||||
"esbuild-netbsd-64": "0.15.18",
|
||||
"esbuild-openbsd-64": "0.15.18",
|
||||
"esbuild-sunos-64": "0.15.18",
|
||||
"esbuild-windows-32": "0.15.18",
|
||||
"esbuild-windows-64": "0.15.18",
|
||||
"esbuild-windows-arm64": "0.15.18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
|
||||
"integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
|
||||
"integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-32": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
|
||||
"integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
|
||||
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
|
||||
"integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-mips64le": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
|
||||
"integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-ppc64le": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
|
||||
"integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-riscv64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
|
||||
"integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-s390x": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
|
||||
"integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-netbsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-openbsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sunos-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
|
||||
"integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-32": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
|
||||
"integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
|
||||
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.79.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz",
|
||||
"integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.15.9",
|
||||
"postcss": "^8.4.18",
|
||||
"resolve": "^1.22.1",
|
||||
"rollup": "^2.79.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
v2/examples/dragdrop-test/frontend/package.json
Normal file
13
v2/examples/dragdrop-test/frontend/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
229
v2/examples/dragdrop-test/frontend/src/app.css
Normal file
229
v2/examples/dragdrop-test/frontend/src/app.css
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/* #app styles are in style.css to avoid conflicts */
|
||||
|
||||
.compact-container {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin: 15px 0;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.drag-source {
|
||||
background: white;
|
||||
border: 2px solid #5c6bc0;
|
||||
padding: 12px;
|
||||
min-width: 140px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.drag-source h4 {
|
||||
color: #3949ab;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
background: #f5f5f5;
|
||||
color: #1a1a1a;
|
||||
padding: 8px;
|
||||
margin: 6px 0;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border: 2px solid #c5cae9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.draggable:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
background: #e8eaf6;
|
||||
border-color: #7986cb;
|
||||
}
|
||||
|
||||
.draggable.dragging {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
background: #c5cae9;
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #9e9e9e;
|
||||
padding: 12px;
|
||||
min-width: 180px;
|
||||
min-height: 120px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.drop-zone h4 {
|
||||
color: #5c6bc0;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.drop-zone.drag-over {
|
||||
background: #e3f2fd;
|
||||
border-color: #2196F3;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
.file-drop-zone {
|
||||
background: #fff8e1;
|
||||
border: 2px dashed #ffc107;
|
||||
padding: 12px;
|
||||
min-width: 180px;
|
||||
min-height: 120px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-drop-zone h4 {
|
||||
color: #f57c00;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-drop-zone.drag-over {
|
||||
background: #fff3cd;
|
||||
border-color: #ff9800;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
.dropped-item {
|
||||
background: linear-gradient(135deg, #42a5f5 0%, #66bb6a 100%);
|
||||
color: white;
|
||||
padding: 6px 8px;
|
||||
margin: 4px 2px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
animation: slideIn 0.3s ease;
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dropped-file {
|
||||
background: #fff;
|
||||
border: 2px solid #ff9800;
|
||||
color: #333;
|
||||
padding: 6px 8px;
|
||||
margin: 4px 0;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
animation: slideIn 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#dropMessage, #fileDropMessage {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 15px auto;
|
||||
max-width: 1000px;
|
||||
padding: 12px;
|
||||
background: #2c3e50;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.status h4 {
|
||||
color: white;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#eventLog {
|
||||
background: #1a1a1a;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
margin: 2px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.log-entry.drag-start {
|
||||
color: #81c784;
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
}
|
||||
|
||||
.log-entry.drag-over {
|
||||
color: #64b5f6;
|
||||
background: rgba(100, 181, 246, 0.1);
|
||||
}
|
||||
|
||||
.log-entry.drag-enter {
|
||||
color: #ffb74d;
|
||||
background: rgba(255, 183, 77, 0.1);
|
||||
}
|
||||
|
||||
.log-entry.drag-leave {
|
||||
color: #ba68c8;
|
||||
background: rgba(186, 104, 200, 0.1);
|
||||
}
|
||||
|
||||
.log-entry.drop {
|
||||
color: #e57373;
|
||||
background: rgba(229, 115, 115, 0.1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-entry.drag-end {
|
||||
color: #90a4ae;
|
||||
background: rgba(144, 164, 174, 0.1);
|
||||
}
|
||||
|
||||
.log-entry.file-drop {
|
||||
color: #ffc107;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-entry.page-loaded {
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.log-entry.wails-status {
|
||||
color: #00bcd4;
|
||||
background: rgba(0, 188, 212, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-size: 1.8em;
|
||||
margin: 10px 0 8px 0;
|
||||
}
|
||||
93
v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt
Normal file
93
v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
231
v2/examples/dragdrop-test/frontend/src/main.js
Normal file
231
v2/examples/dragdrop-test/frontend/src/main.js
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import './style.css';
|
||||
import './app.css';
|
||||
|
||||
// CRITICAL: Register global handlers IMMEDIATELY to prevent file drops from opening new windows
|
||||
// This must be done as early as possible, before any other code runs
|
||||
(function() {
|
||||
// Helper function to check if drag event contains files
|
||||
function isFileDrop(e) {
|
||||
return e.dataTransfer && e.dataTransfer.types &&
|
||||
(e.dataTransfer.types.includes('Files') ||
|
||||
Array.from(e.dataTransfer.types).includes('Files'));
|
||||
}
|
||||
|
||||
// Global dragover handler - MUST prevent default for file drops
|
||||
window.addEventListener('dragover', function(e) {
|
||||
if (isFileDrop(e)) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
}, true); // Use capture phase to handle before any other handlers
|
||||
|
||||
// Global drop handler - MUST prevent default for file drops
|
||||
window.addEventListener('drop', function(e) {
|
||||
if (isFileDrop(e)) {
|
||||
e.preventDefault();
|
||||
console.log('Global handler prevented file drop navigation');
|
||||
}
|
||||
}, true); // Use capture phase to handle before any other handlers
|
||||
|
||||
// Global dragleave handler
|
||||
window.addEventListener('dragleave', function(e) {
|
||||
if (isFileDrop(e)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, true); // Use capture phase
|
||||
|
||||
console.log('Global file drop prevention handlers registered');
|
||||
})();
|
||||
|
||||
document.querySelector('#app').innerHTML = `
|
||||
<h1>Wails Drag & Drop Test</h1>
|
||||
|
||||
<div class="compact-container">
|
||||
<div class="drag-source">
|
||||
<h4>HTML5 Source</h4>
|
||||
<div class="draggable" draggable="true" data-item="Item 1">Item 1</div>
|
||||
<div class="draggable" draggable="true" data-item="Item 2">Item 2</div>
|
||||
<div class="draggable" draggable="true" data-item="Item 3">Item 3</div>
|
||||
</div>
|
||||
|
||||
<div class="drop-zone" id="dropZone">
|
||||
<h4>HTML5 Drop</h4>
|
||||
<p id="dropMessage">Drop here</p>
|
||||
</div>
|
||||
|
||||
<div class="file-drop-zone" id="fileDropZone">
|
||||
<h4>File Drop</h4>
|
||||
<p id="fileDropMessage">Drop files here</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h4>Event Log</h4>
|
||||
<div id="eventLog"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Get all draggable items and drop zones
|
||||
const draggables = document.querySelectorAll('.draggable');
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileDropZone = document.getElementById('fileDropZone');
|
||||
const eventLog = document.getElementById('eventLog');
|
||||
const dropMessage = document.getElementById('dropMessage');
|
||||
const fileDropMessage = document.getElementById('fileDropMessage');
|
||||
|
||||
let draggedItem = null;
|
||||
let eventCounter = 0;
|
||||
|
||||
// Function to log events
|
||||
function logEvent(eventName, details = '') {
|
||||
eventCounter++;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry ${eventName.replace(' ', '-').toLowerCase()}`;
|
||||
logEntry.textContent = `[${timestamp}] ${eventCounter}. ${eventName} ${details}`;
|
||||
eventLog.insertBefore(logEntry, eventLog.firstChild);
|
||||
|
||||
// Keep only last 20 events
|
||||
while (eventLog.children.length > 20) {
|
||||
eventLog.removeChild(eventLog.lastChild);
|
||||
}
|
||||
|
||||
console.log(`Event: ${eventName} ${details}`);
|
||||
}
|
||||
|
||||
// Add event listeners to draggable items
|
||||
draggables.forEach(item => {
|
||||
// Drag start
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
draggedItem = e.target;
|
||||
e.target.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
e.dataTransfer.setData('text/plain', e.target.dataset.item);
|
||||
logEvent('drag-start', `- Started dragging: ${e.target.dataset.item}`);
|
||||
});
|
||||
|
||||
// Drag end
|
||||
item.addEventListener('dragend', (e) => {
|
||||
e.target.classList.remove('dragging');
|
||||
logEvent('drag-end', `- Ended dragging: ${e.target.dataset.item}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners to HTML drop zone
|
||||
dropZone.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-over');
|
||||
logEvent('drag-enter', '- Entered HTML drop zone');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
// Don't log every dragover to avoid spam
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
if (e.target === dropZone) {
|
||||
dropZone.classList.remove('drag-over');
|
||||
logEvent('drag-leave', '- Left HTML drop zone');
|
||||
}
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
|
||||
const data = e.dataTransfer.getData('text/plain');
|
||||
logEvent('drop', `- Dropped in HTML zone: ${data}`);
|
||||
|
||||
if (draggedItem) {
|
||||
// Create a copy of the dragged item
|
||||
const droppedElement = document.createElement('div');
|
||||
droppedElement.className = 'dropped-item';
|
||||
droppedElement.textContent = data;
|
||||
|
||||
// Remove the placeholder message if it exists
|
||||
if (dropMessage) {
|
||||
dropMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
dropZone.appendChild(droppedElement);
|
||||
}
|
||||
|
||||
draggedItem = null;
|
||||
});
|
||||
|
||||
// Add event listeners to file drop zone
|
||||
fileDropZone.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
fileDropZone.classList.add('drag-over');
|
||||
logEvent('drag-enter', '- Entered file drop zone');
|
||||
});
|
||||
|
||||
fileDropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
|
||||
fileDropZone.addEventListener('dragleave', (e) => {
|
||||
if (e.target === fileDropZone) {
|
||||
fileDropZone.classList.remove('drag-over');
|
||||
logEvent('drag-leave', '- Left file drop zone');
|
||||
}
|
||||
});
|
||||
|
||||
fileDropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
fileDropZone.classList.remove('drag-over');
|
||||
|
||||
const files = [...e.dataTransfer.files];
|
||||
if (files.length > 0) {
|
||||
logEvent('file-drop', `- Dropped ${files.length} file(s)`);
|
||||
|
||||
// Hide the placeholder message
|
||||
if (fileDropMessage) {
|
||||
fileDropMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// Display dropped files
|
||||
files.forEach(file => {
|
||||
const fileElement = document.createElement('div');
|
||||
fileElement.className = 'dropped-file';
|
||||
|
||||
// Format file size
|
||||
let size = file.size;
|
||||
let unit = 'bytes';
|
||||
if (size > 1024 * 1024) {
|
||||
size = (size / (1024 * 1024)).toFixed(2);
|
||||
unit = 'MB';
|
||||
} else if (size > 1024) {
|
||||
size = (size / 1024).toFixed(2);
|
||||
unit = 'KB';
|
||||
}
|
||||
|
||||
fileElement.textContent = `📄 ${file.name} (${size} ${unit})`;
|
||||
fileDropZone.appendChild(fileElement);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Log when page loads
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
logEvent('page-loaded', '- Wails drag-and-drop test page ready');
|
||||
console.log('Wails Drag and Drop test application loaded');
|
||||
|
||||
// Check if Wails drag and drop is enabled
|
||||
if (window.wails && window.wails.flags) {
|
||||
logEvent('wails-status', `- Wails DnD enabled: ${window.wails.flags.enableWailsDragAndDrop}`);
|
||||
}
|
||||
|
||||
// IMPORTANT: Register Wails drag-and-drop handlers to prevent browser navigation
|
||||
// This will ensure external files don't open in new windows when dropped anywhere
|
||||
if (window.runtime && window.runtime.OnFileDrop) {
|
||||
window.runtime.OnFileDrop((x, y, paths) => {
|
||||
logEvent('wails-file-drop', `- Wails received ${paths.length} file(s) at (${x}, ${y})`);
|
||||
console.log('Wails OnFileDrop:', paths);
|
||||
}, false); // false = don't require drop target, handle all file drops
|
||||
logEvent('wails-setup', '- Wails OnFileDrop handlers registered');
|
||||
}
|
||||
});
|
||||
33
v2/examples/dragdrop-test/frontend/src/style.css
Normal file
33
v2/examples/dragdrop-test/frontend/src/style.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
4
v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
4
v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
249
v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
242
v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js
Normal file
242
v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
37
v2/examples/dragdrop-test/go.mod
Normal file
37
v2/examples/dragdrop-test/go.mod
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
module dragdrop-test
|
||||
|
||||
go 1.23
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.10.1
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/wailsapp/wails/v2 => E:/releases/wails/v2
|
||||
79
v2/examples/dragdrop-test/go.sum
Normal file
79
v2/examples/dragdrop-test/go.sum
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
36
v2/examples/dragdrop-test/main.go
Normal file
36
v2/examples/dragdrop-test/main.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Wails Drag & Drop Test",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
13
v2/examples/dragdrop-test/wails.json
Normal file
13
v2/examples/dragdrop-test/wails.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "dragdrop-test",
|
||||
"outputfilename": "dragdrop-test",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "Lea Anthony",
|
||||
"email": "lea.anthony@gmail.com"
|
||||
}
|
||||
}
|
||||
76
v2/examples/panic-recovery-test/README.md
Normal file
76
v2/examples/panic-recovery-test/README.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Panic Recovery Test
|
||||
|
||||
This example demonstrates the Linux signal handler issue (#3965) and verifies the fix using `runtime.ResetSignalHandlers()`.
|
||||
|
||||
## The Problem
|
||||
|
||||
On Linux, WebKit installs signal handlers without the `SA_ONSTACK` flag, which prevents Go from recovering panics caused by nil pointer dereferences (SIGSEGV). Without the fix, the application crashes with:
|
||||
|
||||
```
|
||||
signal 11 received but handler not on signal stack
|
||||
fatal error: non-Go code set up signal handler without SA_ONSTACK flag
|
||||
```
|
||||
|
||||
## The Solution
|
||||
|
||||
Call `runtime.ResetSignalHandlers()` immediately before code that might panic:
|
||||
|
||||
```go
|
||||
import "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Printf("Recovered: %v", err)
|
||||
}
|
||||
}()
|
||||
runtime.ResetSignalHandlers()
|
||||
// Code that might panic...
|
||||
}()
|
||||
```
|
||||
|
||||
## How to Reproduce
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Linux with WebKit2GTK 4.1 installed
|
||||
- Go 1.21+
|
||||
- Wails CLI
|
||||
|
||||
### Steps
|
||||
|
||||
1. Build the example:
|
||||
```bash
|
||||
cd v2/examples/panic-recovery-test
|
||||
wails build -tags webkit2_41
|
||||
```
|
||||
|
||||
2. Run the application:
|
||||
```bash
|
||||
./build/bin/panic-recovery-test
|
||||
```
|
||||
|
||||
3. Wait ~10 seconds (the app auto-calls `Greet` after 5s, then waits another 5s before the nil pointer dereference)
|
||||
|
||||
### Expected Result (with fix)
|
||||
|
||||
The panic is recovered and you see:
|
||||
```
|
||||
------------------------------"invalid memory address or nil pointer dereference"
|
||||
```
|
||||
|
||||
The application continues running.
|
||||
|
||||
### Without the fix
|
||||
|
||||
Comment out the `runtime.ResetSignalHandlers()` call in `app.go` and rebuild. The application will crash with a fatal signal 11 error.
|
||||
|
||||
## Files
|
||||
|
||||
- `app.go` - Contains the `Greet` function that demonstrates panic recovery
|
||||
- `frontend/src/main.js` - Auto-calls `Greet` after 5 seconds to trigger the test
|
||||
|
||||
## Related
|
||||
|
||||
- Issue: https://github.com/wailsapp/wails/issues/3965
|
||||
- Original fix PR: https://github.com/wailsapp/wails/pull/2152
|
||||
44
v2/examples/panic-recovery-test/app.go
Normal file
44
v2/examples/panic-recovery-test/app.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) string {
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Printf("------------------------------%#v\n", err)
|
||||
}
|
||||
}()
|
||||
time.Sleep(5 * time.Second)
|
||||
// Fix signal handlers right before potential panic using the Wails runtime
|
||||
runtime.ResetSignalHandlers()
|
||||
// Nil pointer dereference - causes SIGSEGV
|
||||
var t *time.Time
|
||||
fmt.Println(t.Unix())
|
||||
}()
|
||||
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
12
v2/examples/panic-recovery-test/frontend/index.html
Normal file
12
v2/examples/panic-recovery-test/frontend/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>panic-test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
13
v2/examples/panic-recovery-test/frontend/package.json
Normal file
13
v2/examples/panic-recovery-test/frontend/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
54
v2/examples/panic-recovery-test/frontend/src/app.css
Normal file
54
v2/examples/panic-recovery-test/frontend/src/app.css
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#logo {
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
margin: auto;
|
||||
padding: 10% 0 0;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-origin: content-box;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
|
||||
.input-box .btn {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-box .btn:hover {
|
||||
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box .input {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
background-color: rgba(240, 240, 240, 1);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.input-box .input:hover {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.input-box .input:focus {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
55
v2/examples/panic-recovery-test/frontend/src/main.js
Normal file
55
v2/examples/panic-recovery-test/frontend/src/main.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import './style.css';
|
||||
import './app.css';
|
||||
|
||||
import logo from './assets/images/logo-universal.png';
|
||||
import {Greet} from '../wailsjs/go/main/App';
|
||||
|
||||
document.querySelector('#app').innerHTML = `
|
||||
<img id="logo" class="logo">
|
||||
<div class="result" id="result">Please enter your name below 👇</div>
|
||||
<div class="input-box" id="input">
|
||||
<input class="input" id="name" type="text" autocomplete="off" />
|
||||
<button class="btn" onclick="greet()">Greet</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('logo').src = logo;
|
||||
|
||||
let nameElement = document.getElementById("name");
|
||||
nameElement.focus();
|
||||
let resultElement = document.getElementById("result");
|
||||
|
||||
// Setup the greet function
|
||||
window.greet = function () {
|
||||
// Get name
|
||||
let name = nameElement.value;
|
||||
|
||||
// Check if the input is empty
|
||||
if (name === "") return;
|
||||
|
||||
// Call App.Greet(name)
|
||||
try {
|
||||
Greet(name)
|
||||
.then((result) => {
|
||||
// Update result with data back from App.Greet()
|
||||
resultElement.innerText = result;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-call Greet after 5 seconds to trigger the panic test
|
||||
setTimeout(() => {
|
||||
console.log("Auto-calling Greet to trigger panic test...");
|
||||
Greet("PanicTest")
|
||||
.then((result) => {
|
||||
resultElement.innerText = result + " (auto-called - panic will occur in 5s)";
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error:", err);
|
||||
});
|
||||
}, 5000);
|
||||
26
v2/examples/panic-recovery-test/frontend/src/style.css
Normal file
26
v2/examples/panic-recovery-test/frontend/src/style.css
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
4
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
4
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
7
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js
Executable file
7
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
249
v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
5
v2/examples/panic-recovery-test/go.mod
Normal file
5
v2/examples/panic-recovery-test/go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module panic-recovery-test
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.11.0
|
||||
36
v2/examples/panic-recovery-test/main.go
Normal file
36
v2/examples/panic-recovery-test/main.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "panic-test",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
13
v2/examples/panic-recovery-test/wails.json
Normal file
13
v2/examples/panic-recovery-test/wails.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "panic-recovery-test",
|
||||
"outputfilename": "panic-recovery-test",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "Lea Anthony",
|
||||
"email": "lea.anthony@gmail.com"
|
||||
}
|
||||
}
|
||||
10
v2/go.mod
10
v2/go.mod
|
|
@ -17,7 +17,7 @@ require (
|
|||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jackmordaunt/icns v1.0.0
|
||||
github.com/jaypipes/ghw v0.13.0
|
||||
github.com/jaypipes/ghw v0.21.3
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/labstack/gommon v0.4.2
|
||||
github.com/leaanthony/clir v1.3.0
|
||||
|
|
@ -51,9 +51,9 @@ require (
|
|||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
|
|
@ -72,7 +72,7 @@ require (
|
|||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/itchyny/gojq v0.12.13 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/jaypipes/pcidb v1.0.1 // indirect
|
||||
github.com/jaypipes/pcidb v1.1.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
|
|
@ -82,7 +82,6 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
|
|
@ -101,6 +100,7 @@ require (
|
|||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.4 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.3 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/image v0.12.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
|
|
@ -108,6 +108,6 @@ require (
|
|||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect
|
||||
mvdan.cc/sh/v3 v3.7.0 // indirect
|
||||
)
|
||||
|
|
|
|||
23
v2/go.sum
23
v2/go.sum
|
|
@ -8,6 +8,8 @@ atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
|||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||
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=
|
||||
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||
|
|
@ -24,8 +26,6 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
|
|||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
|
||||
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
|
|
@ -88,7 +88,7 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
|
|||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
|
||||
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
|
|
@ -117,10 +117,10 @@ github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm
|
|||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ=
|
||||
github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo=
|
||||
github.com/jaypipes/ghw v0.13.0 h1:log8MXuB8hzTNnSktqpXMHc0c/2k/WgjOMSUtnI1RV4=
|
||||
github.com/jaypipes/ghw v0.13.0/go.mod h1:In8SsaDqlb1oTyrbmTC14uy+fbBMvp+xdqX51MidlD8=
|
||||
github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic=
|
||||
github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4=
|
||||
github.com/jaypipes/ghw v0.21.3 h1:v5mUHM+RN854Vqmk49Uh213jyUA4+8uqaRajlYESsh8=
|
||||
github.com/jaypipes/ghw v0.21.3/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
|
||||
github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
|
||||
github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
|
|
@ -178,8 +178,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
|||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
||||
|
|
@ -263,6 +261,8 @@ github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
|||
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
|
||||
github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
|
|
@ -340,7 +340,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
|
@ -348,7 +347,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw=
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk=
|
||||
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
|
||||
mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8=
|
||||
|
|
|
|||
|
|
@ -123,7 +123,15 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
|
|||
// if we have enums for this package, add them as well
|
||||
var enums, enumsExist = b.enumsToGenerateTS[packageName]
|
||||
if enumsExist {
|
||||
for enumName, enum := range enums {
|
||||
// Sort the enum names first to make the output deterministic
|
||||
sortedEnumNames := make([]string, 0, len(enums))
|
||||
for enumName := range enums {
|
||||
sortedEnumNames = append(sortedEnumNames, enumName)
|
||||
}
|
||||
sort.Strings(sortedEnumNames)
|
||||
|
||||
for _, enumName := range sortedEnumNames {
|
||||
enum := enums[enumName]
|
||||
fqemumname := packageName + "." + enumName
|
||||
if seen.Contains(fqemumname) {
|
||||
continue
|
||||
|
|
@ -172,7 +180,7 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
|
|||
}
|
||||
|
||||
// Sort the package names first to make the output deterministic
|
||||
sortedPackageNames := make([]string, 0)
|
||||
sortedPackageNames := make([]string, 0, len(models))
|
||||
for packageName := range models {
|
||||
sortedPackageNames = append(sortedPackageNames, packageName)
|
||||
}
|
||||
|
|
|
|||
271
v2/internal/binding/binding_test/binding_enum_ordering_test.go
Normal file
271
v2/internal/binding/binding_test/binding_enum_ordering_test.go
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
package binding_test
|
||||
|
||||
// Test for PR #4664: Fix generated enums ordering
|
||||
// This test ensures that enum output is deterministic regardless of map iteration order
|
||||
|
||||
// ZFirstEnum - named with Z prefix to test alphabetical sorting
|
||||
type ZFirstEnum int
|
||||
|
||||
const (
|
||||
ZFirstEnumValue1 ZFirstEnum = iota
|
||||
ZFirstEnumValue2
|
||||
)
|
||||
|
||||
var AllZFirstEnumValues = []struct {
|
||||
Value ZFirstEnum
|
||||
TSName string
|
||||
}{
|
||||
{ZFirstEnumValue1, "ZValue1"},
|
||||
{ZFirstEnumValue2, "ZValue2"},
|
||||
}
|
||||
|
||||
// ASecondEnum - named with A prefix to test alphabetical sorting
|
||||
type ASecondEnum int
|
||||
|
||||
const (
|
||||
ASecondEnumValue1 ASecondEnum = iota
|
||||
ASecondEnumValue2
|
||||
)
|
||||
|
||||
var AllASecondEnumValues = []struct {
|
||||
Value ASecondEnum
|
||||
TSName string
|
||||
}{
|
||||
{ASecondEnumValue1, "AValue1"},
|
||||
{ASecondEnumValue2, "AValue2"},
|
||||
}
|
||||
|
||||
// MMiddleEnum - named with M prefix to test alphabetical sorting
|
||||
type MMiddleEnum int
|
||||
|
||||
const (
|
||||
MMiddleEnumValue1 MMiddleEnum = iota
|
||||
MMiddleEnumValue2
|
||||
)
|
||||
|
||||
var AllMMiddleEnumValues = []struct {
|
||||
Value MMiddleEnum
|
||||
TSName string
|
||||
}{
|
||||
{MMiddleEnumValue1, "MValue1"},
|
||||
{MMiddleEnumValue2, "MValue2"},
|
||||
}
|
||||
|
||||
type EntityWithMultipleEnums struct {
|
||||
Name string `json:"name"`
|
||||
EnumZ ZFirstEnum `json:"enumZ"`
|
||||
EnumA ASecondEnum `json:"enumA"`
|
||||
EnumM MMiddleEnum `json:"enumM"`
|
||||
}
|
||||
|
||||
func (e EntityWithMultipleEnums) Get() EntityWithMultipleEnums {
|
||||
return e
|
||||
}
|
||||
|
||||
// EnumOrderingTest tests that multiple enums in the same package are output
|
||||
// in alphabetical order by enum name. Before PR #4664, the order was
|
||||
// non-deterministic due to Go map iteration order.
|
||||
var EnumOrderingTest = BindingTest{
|
||||
name: "EnumOrderingTest",
|
||||
structs: []interface{}{
|
||||
&EntityWithMultipleEnums{},
|
||||
},
|
||||
enums: []interface{}{
|
||||
// Intentionally add enums in non-alphabetical order
|
||||
AllZFirstEnumValues,
|
||||
AllASecondEnumValues,
|
||||
AllMMiddleEnumValues,
|
||||
},
|
||||
exemptions: nil,
|
||||
shouldError: false,
|
||||
TsGenerationOptionsTest: TsGenerationOptionsTest{
|
||||
TsPrefix: "",
|
||||
TsSuffix: "",
|
||||
},
|
||||
// Expected output should have enums in alphabetical order: ASecondEnum, MMiddleEnum, ZFirstEnum
|
||||
want: `export namespace binding_test {
|
||||
|
||||
export enum ASecondEnum {
|
||||
AValue1 = 0,
|
||||
AValue2 = 1,
|
||||
}
|
||||
export enum MMiddleEnum {
|
||||
MValue1 = 0,
|
||||
MValue2 = 1,
|
||||
}
|
||||
export enum ZFirstEnum {
|
||||
ZValue1 = 0,
|
||||
ZValue2 = 1,
|
||||
}
|
||||
export class EntityWithMultipleEnums {
|
||||
name: string;
|
||||
enumZ: ZFirstEnum;
|
||||
enumA: ASecondEnum;
|
||||
enumM: MMiddleEnum;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new EntityWithMultipleEnums(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.enumZ = source["enumZ"];
|
||||
this.enumA = source["enumA"];
|
||||
this.enumM = source["enumM"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
// EnumElementOrderingEnum tests sorting of enum elements by TSName
|
||||
type EnumElementOrderingEnum string
|
||||
|
||||
const (
|
||||
EnumElementZ EnumElementOrderingEnum = "z_value"
|
||||
EnumElementA EnumElementOrderingEnum = "a_value"
|
||||
EnumElementM EnumElementOrderingEnum = "m_value"
|
||||
)
|
||||
|
||||
// AllEnumElementOrderingValues intentionally lists values out of alphabetical order
|
||||
// to test that AddEnum sorts them
|
||||
var AllEnumElementOrderingValues = []struct {
|
||||
Value EnumElementOrderingEnum
|
||||
TSName string
|
||||
}{
|
||||
{EnumElementZ, "Zebra"},
|
||||
{EnumElementA, "Apple"},
|
||||
{EnumElementM, "Mango"},
|
||||
}
|
||||
|
||||
type EntityWithUnorderedEnumElements struct {
|
||||
Name string `json:"name"`
|
||||
Enum EnumElementOrderingEnum `json:"enum"`
|
||||
}
|
||||
|
||||
func (e EntityWithUnorderedEnumElements) Get() EntityWithUnorderedEnumElements {
|
||||
return e
|
||||
}
|
||||
|
||||
// EnumElementOrderingTest tests that enum elements are sorted alphabetically
|
||||
// by their TSName within an enum. Before PR #4664, elements appeared in the
|
||||
// order they were added, which could be arbitrary.
|
||||
var EnumElementOrderingTest = BindingTest{
|
||||
name: "EnumElementOrderingTest",
|
||||
structs: []interface{}{
|
||||
&EntityWithUnorderedEnumElements{},
|
||||
},
|
||||
enums: []interface{}{
|
||||
AllEnumElementOrderingValues,
|
||||
},
|
||||
exemptions: nil,
|
||||
shouldError: false,
|
||||
TsGenerationOptionsTest: TsGenerationOptionsTest{
|
||||
TsPrefix: "",
|
||||
TsSuffix: "",
|
||||
},
|
||||
// Expected output should have enum elements sorted: Apple, Mango, Zebra
|
||||
want: `export namespace binding_test {
|
||||
|
||||
export enum EnumElementOrderingEnum {
|
||||
Apple = "a_value",
|
||||
Mango = "m_value",
|
||||
Zebra = "z_value",
|
||||
}
|
||||
export class EntityWithUnorderedEnumElements {
|
||||
name: string;
|
||||
enum: EnumElementOrderingEnum;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new EntityWithUnorderedEnumElements(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.enum = source["enum"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
// TSNameEnumElementOrdering tests sorting with TSName() method enum
|
||||
type TSNameEnumElementOrdering string
|
||||
|
||||
const (
|
||||
TSNameEnumZ TSNameEnumElementOrdering = "z_value"
|
||||
TSNameEnumA TSNameEnumElementOrdering = "a_value"
|
||||
TSNameEnumM TSNameEnumElementOrdering = "m_value"
|
||||
)
|
||||
|
||||
func (v TSNameEnumElementOrdering) TSName() string {
|
||||
switch v {
|
||||
case TSNameEnumZ:
|
||||
return "Zebra"
|
||||
case TSNameEnumA:
|
||||
return "Apple"
|
||||
case TSNameEnumM:
|
||||
return "Mango"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// AllTSNameEnumValues intentionally out of order
|
||||
var AllTSNameEnumValues = []TSNameEnumElementOrdering{TSNameEnumZ, TSNameEnumA, TSNameEnumM}
|
||||
|
||||
type EntityWithTSNameEnumOrdering struct {
|
||||
Name string `json:"name"`
|
||||
Enum TSNameEnumElementOrdering `json:"enum"`
|
||||
}
|
||||
|
||||
func (e EntityWithTSNameEnumOrdering) Get() EntityWithTSNameEnumOrdering {
|
||||
return e
|
||||
}
|
||||
|
||||
// TSNameEnumElementOrderingTest tests that enums using TSName() method
|
||||
// also have their elements sorted alphabetically by the TSName.
|
||||
var TSNameEnumElementOrderingTest = BindingTest{
|
||||
name: "TSNameEnumElementOrderingTest",
|
||||
structs: []interface{}{
|
||||
&EntityWithTSNameEnumOrdering{},
|
||||
},
|
||||
enums: []interface{}{
|
||||
AllTSNameEnumValues,
|
||||
},
|
||||
exemptions: nil,
|
||||
shouldError: false,
|
||||
TsGenerationOptionsTest: TsGenerationOptionsTest{
|
||||
TsPrefix: "",
|
||||
TsSuffix: "",
|
||||
},
|
||||
// Expected output should have enum elements sorted: Apple, Mango, Zebra
|
||||
want: `export namespace binding_test {
|
||||
|
||||
export enum TSNameEnumElementOrdering {
|
||||
Apple = "a_value",
|
||||
Mango = "m_value",
|
||||
Zebra = "z_value",
|
||||
}
|
||||
export class EntityWithTSNameEnumOrdering {
|
||||
name: string;
|
||||
enum: TSNameEnumElementOrdering;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new EntityWithTSNameEnumOrdering(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.enum = source["enum"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
|
@ -55,6 +55,10 @@ func TestBindings_GenerateModels(t *testing.T) {
|
|||
Generics2Test,
|
||||
IgnoredTest,
|
||||
DeepElementsTest,
|
||||
// PR #4664: Enum ordering tests
|
||||
EnumOrderingTest,
|
||||
EnumElementOrderingTest,
|
||||
TSNameEnumElementOrderingTest,
|
||||
}
|
||||
|
||||
testLogger := &logger.Logger{}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "CustomProtocol.h"
|
||||
#import "message.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
|
|
@ -19,6 +20,17 @@
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>> * _Nullable))restorationHandler {
|
||||
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
|
||||
NSURL *url = userActivity.webpageURL;
|
||||
if (url) {
|
||||
HandleOpenURL((char*)[[url absoluteString] UTF8String]);
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
|
||||
return NO;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,21 @@ void UpdateMenuItem(void* nsmenuitem, int checked);
|
|||
void RunMainLoop(void);
|
||||
void ReleaseContext(void *inctx);
|
||||
|
||||
/* Notifications */
|
||||
bool IsNotificationAvailable(void *inctx);
|
||||
bool CheckBundleIdentifier(void *inctx);
|
||||
bool EnsureDelegateInitialized(void *inctx);
|
||||
void RequestNotificationAuthorization(void *inctx, int channelID);
|
||||
void CheckNotificationAuthorization(void *inctx, int channelID);
|
||||
void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json);
|
||||
void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json);
|
||||
void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle);
|
||||
void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId);
|
||||
void RemoveAllPendingNotifications(void *inctx);
|
||||
void RemovePendingNotification(void *inctx, const char *identifier);
|
||||
void RemoveAllDeliveredNotifications(void *inctx);
|
||||
void RemoveDeliveredNotification(void *inctx, const char *identifier);
|
||||
|
||||
NSString* safeInit(const char* input);
|
||||
|
||||
#endif /* Application_h */
|
||||
|
|
|
|||
|
|
@ -367,6 +367,74 @@ void AppendSeparator(void* inMenu) {
|
|||
}
|
||||
|
||||
|
||||
bool IsNotificationAvailable(void *inctx) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
return [ctx IsNotificationAvailable];
|
||||
}
|
||||
|
||||
bool CheckBundleIdentifier(void *inctx) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
return [ctx CheckBundleIdentifier];
|
||||
}
|
||||
|
||||
bool EnsureDelegateInitialized(void *inctx) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
return [ctx EnsureDelegateInitialized];
|
||||
}
|
||||
|
||||
void RequestNotificationAuthorization(void *inctx, int channelID) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
[ctx RequestNotificationAuthorization:channelID];
|
||||
}
|
||||
|
||||
void CheckNotificationAuthorization(void *inctx, int channelID) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
[ctx CheckNotificationAuthorization:channelID];
|
||||
}
|
||||
|
||||
void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
[ctx SendNotification:channelID :identifier :title :subtitle :body :data_json];
|
||||
}
|
||||
|
||||
void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
|
||||
[ctx SendNotificationWithActions:channelID :identifier :title :subtitle :body :categoryId :actions_json];
|
||||
}
|
||||
|
||||
void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
|
||||
[ctx RegisterNotificationCategory:channelID :categoryId :actions_json :hasReplyField :replyPlaceholder :replyButtonTitle];
|
||||
}
|
||||
|
||||
void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
|
||||
[ctx RemoveNotificationCategory:channelID :categoryId];
|
||||
}
|
||||
|
||||
void RemoveAllPendingNotifications(void *inctx) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
[ctx RemoveAllPendingNotifications];
|
||||
}
|
||||
|
||||
void RemovePendingNotification(void *inctx, const char *identifier) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
[ctx RemovePendingNotification:identifier];
|
||||
}
|
||||
|
||||
void RemoveAllDeliveredNotifications(void *inctx) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
[ctx RemoveAllDeliveredNotifications];
|
||||
}
|
||||
|
||||
void RemoveDeliveredNotification(void *inctx, const char *identifier) {
|
||||
WailsContext *ctx = (__bridge WailsContext*)inctx;
|
||||
[ctx RemoveDeliveredNotification:identifier];
|
||||
}
|
||||
|
||||
|
||||
void Run(void *inctx, const char* url) {
|
||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
extern void HandleCustomProtocol(char*);
|
||||
extern void HandleOpenURL(char*);
|
||||
|
||||
@interface CustomProtocolSchemeHandler : NSObject
|
||||
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
|
||||
|
||||
HandleCustomProtocol((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]);
|
||||
HandleOpenURL((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]);
|
||||
}
|
||||
@end
|
||||
|
||||
|
|
|
|||
|
|
@ -92,10 +92,24 @@ struct Preferences {
|
|||
- (void) ShowApplication;
|
||||
- (void) Quit;
|
||||
|
||||
-(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength;
|
||||
- (void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength;
|
||||
- (void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters;
|
||||
- (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters;
|
||||
|
||||
- (bool) IsNotificationAvailable;
|
||||
- (bool) CheckBundleIdentifier;
|
||||
- (bool) EnsureDelegateInitialized;
|
||||
- (void) RequestNotificationAuthorization:(int)channelID;
|
||||
- (void) CheckNotificationAuthorization:(int)channelID;
|
||||
- (void) SendNotification:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)dataJSON;
|
||||
- (void) SendNotificationWithActions:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)categoryId :(const char *)actionsJSON;
|
||||
- (void) RegisterNotificationCategory:(int)channelID :(const char *)categoryId :(const char *)actionsJSON :(bool)hasReplyField :(const char *)replyPlaceholder :(const char *)replyButtonTitle;
|
||||
- (void) RemoveNotificationCategory:(int)channelID :(const char *)categoryId;
|
||||
- (void) RemoveAllPendingNotifications;
|
||||
- (void) RemovePendingNotification:(const char *)identifier;
|
||||
- (void) RemoveAllDeliveredNotifications;
|
||||
- (void) RemoveDeliveredNotification:(const char *)identifier;
|
||||
|
||||
- (void) loadRequest:(NSString*)url;
|
||||
- (void) ExecJS:(NSString*)script;
|
||||
- (NSScreen*) getCurrentScreen;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
// Created by Lea Anthony on 10/10/21.
|
||||
//
|
||||
|
||||
#include "Application.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
#import "WailsContext.h"
|
||||
|
|
@ -36,6 +37,14 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
|||
|
||||
@end
|
||||
|
||||
// Notifications
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#endif
|
||||
|
||||
extern void captureResult(int channelID, bool success, const char* error);
|
||||
extern void didReceiveNotificationResponse(const char *jsonPayload, const char* error);
|
||||
|
||||
@implementation WailsContext
|
||||
|
||||
- (void) SetSize:(int)width :(int)height {
|
||||
|
|
@ -215,7 +224,13 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
|||
|
||||
// Webview stuff here!
|
||||
WKWebViewConfiguration *config = [WKWebViewConfiguration new];
|
||||
config.suppressesIncrementalRendering = true;
|
||||
// Disable suppressesIncrementalRendering on macOS 26+ to prevent WebView crashes
|
||||
// during rapid UI updates. See: https://github.com/wailsapp/wails/issues/4592
|
||||
if (@available(macOS 26.0, *)) {
|
||||
config.suppressesIncrementalRendering = false;
|
||||
} else {
|
||||
config.suppressesIncrementalRendering = true;
|
||||
}
|
||||
config.applicationNameForUserAgent = @"wails.io";
|
||||
[config setURLSchemeHandler:self forURLScheme:@"wails"];
|
||||
|
||||
|
|
@ -717,6 +732,357 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
|||
|
||||
}
|
||||
|
||||
/***** Notifications ******/
|
||||
- (bool) IsNotificationAvailable {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (bool) CheckBundleIdentifier {
|
||||
NSBundle *main = [NSBundle mainBundle];
|
||||
if (main.bundleIdentifier == nil) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
willPresentNotification:(UNNotification *)notification
|
||||
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(macos(10.14)) {
|
||||
UNNotificationPresentationOptions options = UNNotificationPresentationOptionSound;
|
||||
|
||||
if (@available(macOS 11.0, *)) {
|
||||
// These options are only available in macOS 11.0+
|
||||
options = UNNotificationPresentationOptionList |
|
||||
UNNotificationPresentationOptionBanner |
|
||||
UNNotificationPresentationOptionSound;
|
||||
}
|
||||
|
||||
completionHandler(options);
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
||||
withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(macos(10.14)) {
|
||||
|
||||
NSMutableDictionary *payload = [NSMutableDictionary dictionary];
|
||||
|
||||
[payload setObject:response.notification.request.identifier forKey:@"id"];
|
||||
[payload setObject:response.actionIdentifier forKey:@"actionIdentifier"];
|
||||
[payload setObject:response.notification.request.content.title ?: @"" forKey:@"title"];
|
||||
[payload setObject:response.notification.request.content.body ?: @"" forKey:@"body"];
|
||||
|
||||
if (response.notification.request.content.categoryIdentifier) {
|
||||
[payload setObject:response.notification.request.content.categoryIdentifier forKey:@"categoryId"];
|
||||
}
|
||||
|
||||
if (response.notification.request.content.subtitle) {
|
||||
[payload setObject:response.notification.request.content.subtitle forKey:@"subtitle"];
|
||||
}
|
||||
|
||||
if (response.notification.request.content.userInfo) {
|
||||
[payload setObject:response.notification.request.content.userInfo forKey:@"userInfo"];
|
||||
}
|
||||
|
||||
if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
|
||||
UNTextInputNotificationResponse *textResponse = (UNTextInputNotificationResponse *)response;
|
||||
[payload setObject:textResponse.userText forKey:@"userText"];
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
didReceiveNotificationResponse(NULL, [errorMsg UTF8String]);
|
||||
} else {
|
||||
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
didReceiveNotificationResponse([jsonString UTF8String], NULL);
|
||||
}
|
||||
|
||||
completionHandler();
|
||||
}
|
||||
|
||||
- (bool) EnsureDelegateInitialized {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
center.delegate = (id<UNUserNotificationCenterDelegate>)self;
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void) RequestNotificationAuthorization :(int)channelID {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
if (![self EnsureDelegateInitialized]) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
|
||||
|
||||
[center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
} else {
|
||||
captureResult(channelID, granted, NULL);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
captureResult(channelID, false, "Notifications not available on macOS versions prior to 10.14");
|
||||
}
|
||||
}
|
||||
|
||||
- (void) CheckNotificationAuthorization :(int) channelID {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
if (![self EnsureDelegateInitialized]) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
||||
BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized);
|
||||
captureResult(channelID, isAuthorized, NULL);
|
||||
}];
|
||||
} else {
|
||||
captureResult(channelID, false, "Notifications not available on macOS versions prior to 10.14");
|
||||
}
|
||||
}
|
||||
|
||||
- (UNMutableNotificationContent *)createNotificationContent:(const char *)title subtitle:(const char *)subtitle body:(const char *)body dataJSON:(const char *)dataJSON error:(NSError **)contentError API_AVAILABLE(macos(10.14)) {
|
||||
if (title == NULL) title = "";
|
||||
if (body == NULL) body = "";
|
||||
|
||||
NSString *nsTitle = [NSString stringWithUTF8String:title];
|
||||
NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
|
||||
NSString *nsBody = [NSString stringWithUTF8String:body];
|
||||
|
||||
UNMutableNotificationContent *content = [[[UNMutableNotificationContent alloc] init] autorelease];
|
||||
content.title = nsTitle;
|
||||
if (![nsSubtitle isEqualToString:@""]) {
|
||||
content.subtitle = nsSubtitle;
|
||||
}
|
||||
content.body = nsBody;
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
|
||||
// Parse JSON data if provided
|
||||
if (dataJSON) {
|
||||
NSString *dataJsonStr = [NSString stringWithUTF8String:dataJSON];
|
||||
NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
if (!error && parsedData) {
|
||||
content.userInfo = parsedData;
|
||||
} else if (error) {
|
||||
if (contentError) *contentError = error;
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
- (void) sendNotificationWithRequest:(UNNotificationRequest *)request channelID:(int)channelID API_AVAILABLE(macos(10.14)) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
} else {
|
||||
captureResult(channelID, true, NULL);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) SendNotification:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)dataJSON API_AVAILABLE(macos(10.14)) {
|
||||
if (![self EnsureDelegateInitialized]) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
|
||||
NSError *contentError = nil;
|
||||
UNMutableNotificationContent *content = [self createNotificationContent:title subtitle:subtitle body:body dataJSON:dataJSON error:&contentError];
|
||||
if (contentError) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNTimeIntervalNotificationTrigger *trigger = nil;
|
||||
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger];
|
||||
|
||||
[self sendNotificationWithRequest:request channelID:channelID];
|
||||
}
|
||||
|
||||
- (void) SendNotificationWithActions:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)categoryId :(const char *)dataJSON API_AVAILABLE(macos(10.14)) {
|
||||
if (![self EnsureDelegateInitialized]) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
||||
|
||||
NSError *contentError = nil;
|
||||
UNMutableNotificationContent *content = [self createNotificationContent:title subtitle:subtitle body:body dataJSON:dataJSON error:&contentError];
|
||||
if (contentError) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
content.categoryIdentifier = nsCategoryId;
|
||||
|
||||
UNTimeIntervalNotificationTrigger *trigger = nil;
|
||||
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger];
|
||||
|
||||
[self sendNotificationWithRequest:request channelID:channelID];
|
||||
}
|
||||
|
||||
- (void) RegisterNotificationCategory:(int)channelID :(const char *)categoryId :(const char *)actionsJSON :(bool)hasReplyField :(const char *)replyPlaceholder :(const char *)replyButtonTitle API_AVAILABLE(macos(10.14)) {
|
||||
if (![self EnsureDelegateInitialized]) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
||||
NSString *actionsJsonStr = actionsJSON ? [NSString stringWithUTF8String:actionsJSON] : @"[]";
|
||||
|
||||
NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
NSArray *actionsArray = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableArray *actions = [NSMutableArray array];
|
||||
for (NSDictionary *actionDict in actionsArray) {
|
||||
NSString *actionId = actionDict[@"id"];
|
||||
NSString *actionTitle = actionDict[@"title"];
|
||||
BOOL destructive = [actionDict[@"destructive"] boolValue];
|
||||
|
||||
if (actionId && actionTitle) {
|
||||
UNNotificationActionOptions options = UNNotificationActionOptionNone;
|
||||
if (destructive) options |= UNNotificationActionOptionDestructive;
|
||||
|
||||
UNNotificationAction *action = [UNNotificationAction actionWithIdentifier:actionId
|
||||
title:actionTitle
|
||||
options:options];
|
||||
[actions addObject:action];
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReplyField) {
|
||||
// Defensive NULL checks: if hasReplyField is true, both strings must be non-NULL
|
||||
if (!replyPlaceholder || !replyButtonTitle) {
|
||||
NSString *errorMsg = @"hasReplyField is true but replyPlaceholder or replyButtonTitle is NULL";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
NSString *placeholder = [NSString stringWithUTF8String:replyPlaceholder];
|
||||
NSString *buttonTitle = [NSString stringWithUTF8String:replyButtonTitle];
|
||||
UNTextInputNotificationAction *textAction =
|
||||
[UNTextInputNotificationAction actionWithIdentifier:@"TEXT_REPLY"
|
||||
title:buttonTitle
|
||||
options:UNNotificationActionOptionNone
|
||||
textInputButtonTitle:buttonTitle
|
||||
textInputPlaceholder:placeholder];
|
||||
[actions addObject:textAction];
|
||||
}
|
||||
|
||||
UNNotificationCategory *newCategory = [UNNotificationCategory categoryWithIdentifier:nsCategoryId
|
||||
actions:actions
|
||||
intentIdentifiers:@[]
|
||||
options:UNNotificationCategoryOptionNone];
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
|
||||
NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories];
|
||||
|
||||
// Remove existing category with same identifier if found
|
||||
UNNotificationCategory *existingCategory = nil;
|
||||
for (UNNotificationCategory *category in updatedCategories) {
|
||||
if ([category.identifier isEqualToString:nsCategoryId]) {
|
||||
existingCategory = category;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingCategory) {
|
||||
[updatedCategories removeObject:existingCategory];
|
||||
}
|
||||
|
||||
// Add the new category
|
||||
[updatedCategories addObject:newCategory];
|
||||
[center setNotificationCategories:updatedCategories];
|
||||
|
||||
captureResult(channelID, true, NULL);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) RemoveNotificationCategory:(int)channelID :(const char *)categoryId API_AVAILABLE(macos(10.14)) {
|
||||
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
[center getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
|
||||
NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories];
|
||||
|
||||
// Find and remove the matching category
|
||||
UNNotificationCategory *categoryToRemove = nil;
|
||||
for (UNNotificationCategory *category in updatedCategories) {
|
||||
if ([category.identifier isEqualToString:nsCategoryId]) {
|
||||
categoryToRemove = category;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (categoryToRemove) {
|
||||
[updatedCategories removeObject:categoryToRemove];
|
||||
[center setNotificationCategories:updatedCategories];
|
||||
captureResult(channelID, true, NULL);
|
||||
} else {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Category '%@' not found", nsCategoryId];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) RemoveAllPendingNotifications API_AVAILABLE(macos(10.14)) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removeAllPendingNotificationRequests];
|
||||
}
|
||||
|
||||
- (void) RemovePendingNotification:(const char *)identifier API_AVAILABLE(macos(10.14)) {
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removePendingNotificationRequestsWithIdentifiers:@[nsIdentifier]];
|
||||
}
|
||||
|
||||
- (void) RemoveAllDeliveredNotifications API_AVAILABLE(macos(10.14)) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removeAllDeliveredNotifications];
|
||||
}
|
||||
|
||||
- (void) RemoveDeliveredNotification:(const char *)identifier API_AVAILABLE(macos(10.14)) {
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removeDeliveredNotificationsWithIdentifiers:@[nsIdentifier]];
|
||||
}
|
||||
|
||||
|
||||
- (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen {
|
||||
self.aboutTitle = title;
|
||||
self.aboutDescription = description;
|
||||
|
|
@ -725,7 +1091,7 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
|||
self.aboutImage = [[NSImage alloc] initWithData:imageData];
|
||||
}
|
||||
|
||||
-(void) About {
|
||||
- (void) About {
|
||||
|
||||
WailsAlert *alert = [WailsAlert new];
|
||||
[alert setAlertStyle:NSAlertStyleInformational];
|
||||
|
|
|
|||
|
|
@ -518,8 +518,8 @@ func HandleOpenFile(filePath *C.char) {
|
|||
openFilepathBuffer <- goFilepath
|
||||
}
|
||||
|
||||
//export HandleCustomProtocol
|
||||
func HandleCustomProtocol(url *C.char) {
|
||||
//export HandleOpenURL
|
||||
func HandleOpenURL(url *C.char) {
|
||||
goUrl := C.GoString(url)
|
||||
openUrlBuffer <- goUrl
|
||||
}
|
||||
|
|
|
|||
465
v2/internal/frontend/desktop/darwin/notifications.go
Normal file
465
v2/internal/frontend/desktop/darwin/notifications.go
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package darwin
|
||||
|
||||
/*
|
||||
#cgo CFLAGS:-x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework Cocoa
|
||||
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
|
||||
#cgo LDFLAGS: -framework UserNotifications
|
||||
#endif
|
||||
|
||||
#import "Application.h"
|
||||
#import "WailsContext.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||
)
|
||||
|
||||
// Package-scoped variable only accessible within this file
|
||||
var (
|
||||
currentFrontend *Frontend
|
||||
frontendMutex sync.RWMutex
|
||||
// Notification channels
|
||||
channels map[int]chan notificationChannel
|
||||
channelsLock sync.Mutex
|
||||
nextChannelID int
|
||||
|
||||
notificationResultCallback func(result frontend.NotificationResult)
|
||||
callbackLock sync.RWMutex
|
||||
)
|
||||
|
||||
const DefaultActionIdentifier = "DEFAULT_ACTION"
|
||||
const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier"
|
||||
|
||||
// setCurrentFrontend sets the current frontend instance
|
||||
// This is called when RequestNotificationAuthorization or CheckNotificationAuthorization is called
|
||||
func setCurrentFrontend(f *Frontend) {
|
||||
frontendMutex.Lock()
|
||||
defer frontendMutex.Unlock()
|
||||
currentFrontend = f
|
||||
}
|
||||
|
||||
// getCurrentFrontend gets the current frontend instance
|
||||
func getCurrentFrontend() *Frontend {
|
||||
frontendMutex.RLock()
|
||||
defer frontendMutex.RUnlock()
|
||||
return currentFrontend
|
||||
}
|
||||
|
||||
type notificationChannel struct {
|
||||
Success bool
|
||||
Error error
|
||||
}
|
||||
|
||||
func (f *Frontend) InitializeNotifications() error {
|
||||
if !f.IsNotificationAvailable() {
|
||||
return fmt.Errorf("notifications are not available on this system")
|
||||
}
|
||||
if !f.checkBundleIdentifier() {
|
||||
return fmt.Errorf("notifications require a valid bundle identifier")
|
||||
}
|
||||
if !bool(C.EnsureDelegateInitialized(f.mainWindow.context)) {
|
||||
return fmt.Errorf("failed to initialize notification center delegate")
|
||||
}
|
||||
|
||||
channels = make(map[int]chan notificationChannel)
|
||||
nextChannelID = 0
|
||||
|
||||
setCurrentFrontend(f)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupNotifications is a macOS stub that does nothing.
|
||||
// (Linux-specific cleanup)
|
||||
func (f *Frontend) CleanupNotifications() {
|
||||
// No cleanup needed on macOS
|
||||
}
|
||||
|
||||
func (f *Frontend) IsNotificationAvailable() bool {
|
||||
return bool(C.IsNotificationAvailable(f.mainWindow.context))
|
||||
}
|
||||
|
||||
func (f *Frontend) checkBundleIdentifier() bool {
|
||||
return bool(C.CheckBundleIdentifier(f.mainWindow.context))
|
||||
}
|
||||
|
||||
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, resultCh := f.registerChannel()
|
||||
|
||||
C.RequestNotificationAuthorization(f.mainWindow.context, C.int(id))
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
close(resultCh)
|
||||
return result.Success, result.Error
|
||||
case <-ctx.Done():
|
||||
f.cleanupChannel(id)
|
||||
return false, fmt.Errorf("notification authorization timed out after 3 minutes: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, resultCh := f.registerChannel()
|
||||
|
||||
C.CheckNotificationAuthorization(f.mainWindow.context, C.int(id))
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
close(resultCh)
|
||||
return result.Success, result.Error
|
||||
case <-ctx.Done():
|
||||
f.cleanupChannel(id)
|
||||
return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
||||
func (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cIdentifier := C.CString(options.ID)
|
||||
cTitle := C.CString(options.Title)
|
||||
cSubtitle := C.CString(options.Subtitle)
|
||||
cBody := C.CString(options.Body)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
defer C.free(unsafe.Pointer(cTitle))
|
||||
defer C.free(unsafe.Pointer(cSubtitle))
|
||||
defer C.free(unsafe.Pointer(cBody))
|
||||
|
||||
var cDataJSON *C.char
|
||||
if options.Data != nil {
|
||||
jsonData, err := json.Marshal(options.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification data: %w", err)
|
||||
}
|
||||
cDataJSON = C.CString(string(jsonData))
|
||||
defer C.free(unsafe.Pointer(cDataJSON))
|
||||
}
|
||||
|
||||
id, resultCh := f.registerChannel()
|
||||
C.SendNotification(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cDataJSON)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
close(resultCh)
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("sending notification failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
f.cleanupChannel(id)
|
||||
return fmt.Errorf("sending notification timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotificationWithActions sends a notification with additional actions and inputs.
|
||||
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
|
||||
// If a NotificationCategory is not registered a basic notification will be sent.
|
||||
func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cIdentifier := C.CString(options.ID)
|
||||
cTitle := C.CString(options.Title)
|
||||
cSubtitle := C.CString(options.Subtitle)
|
||||
cBody := C.CString(options.Body)
|
||||
cCategoryID := C.CString(options.CategoryID)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
defer C.free(unsafe.Pointer(cTitle))
|
||||
defer C.free(unsafe.Pointer(cSubtitle))
|
||||
defer C.free(unsafe.Pointer(cBody))
|
||||
defer C.free(unsafe.Pointer(cCategoryID))
|
||||
|
||||
var cDataJSON *C.char
|
||||
if options.Data != nil {
|
||||
jsonData, err := json.Marshal(options.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification data: %w", err)
|
||||
}
|
||||
cDataJSON = C.CString(string(jsonData))
|
||||
defer C.free(unsafe.Pointer(cDataJSON))
|
||||
}
|
||||
|
||||
id, resultCh := f.registerChannel()
|
||||
C.SendNotificationWithActions(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
close(resultCh)
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("sending notification failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
f.cleanupChannel(id)
|
||||
return fmt.Errorf("sending notification timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
// Registering a category with the same name as a previously registered NotificationCategory will override it.
|
||||
func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cCategoryID := C.CString(category.ID)
|
||||
defer C.free(unsafe.Pointer(cCategoryID))
|
||||
|
||||
actionsJSON, err := json.Marshal(category.Actions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification category: %w", err)
|
||||
}
|
||||
cActionsJSON := C.CString(string(actionsJSON))
|
||||
defer C.free(unsafe.Pointer(cActionsJSON))
|
||||
|
||||
var cReplyPlaceholder, cReplyButtonTitle *C.char
|
||||
if category.HasReplyField {
|
||||
cReplyPlaceholder = C.CString(category.ReplyPlaceholder)
|
||||
cReplyButtonTitle = C.CString(category.ReplyButtonTitle)
|
||||
defer C.free(unsafe.Pointer(cReplyPlaceholder))
|
||||
defer C.free(unsafe.Pointer(cReplyButtonTitle))
|
||||
}
|
||||
|
||||
id, resultCh := f.registerChannel()
|
||||
C.RegisterNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID, cActionsJSON, C.bool(category.HasReplyField),
|
||||
cReplyPlaceholder, cReplyButtonTitle)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
close(resultCh)
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("category registration failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
f.cleanupChannel(id)
|
||||
return fmt.Errorf("category registration timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory remove a previously registered NotificationCategory.
|
||||
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cCategoryID := C.CString(categoryId)
|
||||
defer C.free(unsafe.Pointer(cCategoryID))
|
||||
|
||||
id, resultCh := f.registerChannel()
|
||||
C.RemoveNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
close(resultCh)
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("category removal failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
f.cleanupChannel(id)
|
||||
return fmt.Errorf("category removal timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAllPendingNotifications removes all pending notifications.
|
||||
func (f *Frontend) RemoveAllPendingNotifications() error {
|
||||
C.RemoveAllPendingNotifications(f.mainWindow.context)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePendingNotification removes a pending notification matching the unique identifier.
|
||||
func (f *Frontend) RemovePendingNotification(identifier string) error {
|
||||
cIdentifier := C.CString(identifier)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
C.RemovePendingNotification(f.mainWindow.context, cIdentifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllDeliveredNotifications removes all delivered notifications.
|
||||
func (f *Frontend) RemoveAllDeliveredNotifications() error {
|
||||
C.RemoveAllDeliveredNotifications(f.mainWindow.context)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDeliveredNotification removes a delivered notification matching the unique identifier.
|
||||
func (f *Frontend) RemoveDeliveredNotification(identifier string) error {
|
||||
cIdentifier := C.CString(identifier)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
C.RemoveDeliveredNotification(f.mainWindow.context, cIdentifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNotification is a macOS stub that always returns nil.
|
||||
// Use one of the following instead:
|
||||
// RemoveAllPendingNotifications
|
||||
// RemovePendingNotification
|
||||
// RemoveAllDeliveredNotifications
|
||||
// RemoveDeliveredNotification
|
||||
// (Linux-specific)
|
||||
func (f *Frontend) RemoveNotification(identifier string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
|
||||
callbackLock.Lock()
|
||||
notificationResultCallback = callback
|
||||
callbackLock.Unlock()
|
||||
}
|
||||
|
||||
//export captureResult
|
||||
func captureResult(channelID C.int, success C.bool, errorMsg *C.char) {
|
||||
f := getCurrentFrontend()
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
resultCh, exists := f.GetChannel(int(channelID))
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if errorMsg != nil {
|
||||
err = fmt.Errorf("%s", C.GoString(errorMsg))
|
||||
}
|
||||
|
||||
resultCh <- notificationChannel{
|
||||
Success: bool(success),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
//export didReceiveNotificationResponse
|
||||
func didReceiveNotificationResponse(jsonPayload *C.char, err *C.char) {
|
||||
result := frontend.NotificationResult{}
|
||||
|
||||
if err != nil {
|
||||
errMsg := C.GoString(err)
|
||||
result.Error = fmt.Errorf("notification response error: %s", errMsg)
|
||||
handleNotificationResult(result)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if jsonPayload == nil {
|
||||
result.Error = fmt.Errorf("received nil JSON payload in notification response")
|
||||
handleNotificationResult(result)
|
||||
return
|
||||
}
|
||||
|
||||
payload := C.GoString(jsonPayload)
|
||||
|
||||
var response frontend.NotificationResponse
|
||||
if err := json.Unmarshal([]byte(payload), &response); err != nil {
|
||||
result.Error = fmt.Errorf("failed to unmarshal notification response: %w", err)
|
||||
handleNotificationResult(result)
|
||||
return
|
||||
}
|
||||
|
||||
if response.ActionIdentifier == AppleDefaultActionIdentifier {
|
||||
response.ActionIdentifier = DefaultActionIdentifier
|
||||
}
|
||||
|
||||
result.Response = response
|
||||
handleNotificationResult(result)
|
||||
}
|
||||
|
||||
func handleNotificationResult(result frontend.NotificationResult) {
|
||||
callbackLock.Lock()
|
||||
callback := notificationResultCallback
|
||||
callbackLock.Unlock()
|
||||
|
||||
if callback != nil {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Log panic but don't crash the app
|
||||
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
|
||||
}
|
||||
}()
|
||||
callback(result)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
func (f *Frontend) registerChannel() (int, chan notificationChannel) {
|
||||
channelsLock.Lock()
|
||||
defer channelsLock.Unlock()
|
||||
|
||||
// Initialize channels map if it's nil
|
||||
if channels == nil {
|
||||
channels = make(map[int]chan notificationChannel)
|
||||
nextChannelID = 0
|
||||
}
|
||||
|
||||
id := nextChannelID
|
||||
nextChannelID++
|
||||
|
||||
resultCh := make(chan notificationChannel, 1)
|
||||
|
||||
channels[id] = resultCh
|
||||
return id, resultCh
|
||||
}
|
||||
|
||||
func (f *Frontend) GetChannel(id int) (chan notificationChannel, bool) {
|
||||
channelsLock.Lock()
|
||||
defer channelsLock.Unlock()
|
||||
|
||||
if channels == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ch, exists := channels[id]
|
||||
if exists {
|
||||
delete(channels, id)
|
||||
}
|
||||
return ch, exists
|
||||
}
|
||||
|
||||
func (f *Frontend) cleanupChannel(id int) {
|
||||
channelsLock.Lock()
|
||||
defer channelsLock.Unlock()
|
||||
|
||||
if channels == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ch, exists := channels[id]; exists {
|
||||
delete(channels, id)
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +73,16 @@ static void install_signal_handlers()
|
|||
#endif
|
||||
}
|
||||
|
||||
static gboolean install_signal_handlers_idle(gpointer data) {
|
||||
(void)data;
|
||||
install_signal_handlers();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void fix_signal_handlers_after_gtk_init() {
|
||||
g_idle_add(install_signal_handlers_idle, NULL);
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
|
@ -204,7 +214,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
|||
|
||||
result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled)
|
||||
|
||||
C.install_signal_handlers()
|
||||
C.fix_signal_handlers_after_gtk_init()
|
||||
|
||||
if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" {
|
||||
prgname := C.CString(appoptions.Linux.ProgramName)
|
||||
|
|
|
|||
|
|
@ -34,8 +34,11 @@ void addAccelerator(GtkWidget* menuItem, GtkAccelGroup* group, guint key, GdkMod
|
|||
}
|
||||
*/
|
||||
import "C"
|
||||
import "github.com/wailsapp/wails/v2/pkg/menu"
|
||||
import "unsafe"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
var menuIdCounter int
|
||||
var menuItemToId map[*menu.MenuItem]int
|
||||
|
|
@ -81,8 +84,10 @@ func (w *Window) SetApplicationMenu(inmenu *menu.Menu) {
|
|||
|
||||
func processMenu(window *Window, menu *menu.Menu) {
|
||||
for _, menuItem := range menu.Items {
|
||||
submenu := processSubmenu(menuItem, window.accels)
|
||||
C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(window.menubar)), submenu)
|
||||
if menuItem.SubMenu != nil {
|
||||
submenu := processSubmenu(menuItem, window.accels)
|
||||
C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(window.menubar)), submenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
594
v2/internal/frontend/desktop/linux/notifications.go
Normal file
594
v2/internal/frontend/desktop/linux/notifications.go
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package linux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||
)
|
||||
|
||||
var (
|
||||
conn *dbus.Conn
|
||||
categories map[string]frontend.NotificationCategory = make(map[string]frontend.NotificationCategory)
|
||||
categoriesLock sync.RWMutex
|
||||
notifications map[uint32]*notificationData = make(map[uint32]*notificationData)
|
||||
notificationsLock sync.RWMutex
|
||||
notificationResultCallback func(result frontend.NotificationResult)
|
||||
callbackLock sync.RWMutex
|
||||
appName string
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
||||
type notificationData struct {
|
||||
ID string
|
||||
Title string
|
||||
Subtitle string
|
||||
Body string
|
||||
CategoryID string
|
||||
Data map[string]interface{}
|
||||
DBusID uint32
|
||||
ActionMap map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
dbusNotificationInterface = "org.freedesktop.Notifications"
|
||||
dbusNotificationPath = "/org/freedesktop/Notifications"
|
||||
DefaultActionIdentifier = "DEFAULT_ACTION"
|
||||
)
|
||||
|
||||
// Creates a new Notifications Service.
|
||||
func (f *Frontend) InitializeNotifications() error {
|
||||
// Clean up any previous initialization
|
||||
f.CleanupNotifications()
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable: %w", err)
|
||||
}
|
||||
appName = filepath.Base(exe)
|
||||
|
||||
_conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to session bus: %w", err)
|
||||
}
|
||||
conn = _conn
|
||||
|
||||
if err := f.loadCategories(); err != nil {
|
||||
f.logger.Warning("Failed to load notification categories: %v", err)
|
||||
}
|
||||
|
||||
var signalCtx context.Context
|
||||
signalCtx, cancel = context.WithCancel(context.Background())
|
||||
|
||||
if err := f.setupSignalHandling(signalCtx); err != nil {
|
||||
return fmt.Errorf("failed to set up notification signal handling: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupNotifications cleans up notification resources
|
||||
func (f *Frontend) CleanupNotifications() {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
cancel = nil
|
||||
}
|
||||
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
conn = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Frontend) IsNotificationAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// RequestNotificationAuthorization is a Linux stub that always returns true, nil.
|
||||
// (authorization is macOS-specific)
|
||||
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckNotificationAuthorization is a Linux stub that always returns true.
|
||||
// (authorization is macOS-specific)
|
||||
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
||||
func (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
|
||||
if conn == nil {
|
||||
return fmt.Errorf("notifications not initialized")
|
||||
}
|
||||
|
||||
hints := map[string]dbus.Variant{}
|
||||
|
||||
body := options.Body
|
||||
if options.Subtitle != "" {
|
||||
body = options.Subtitle + "\n" + body
|
||||
}
|
||||
|
||||
defaultActionID := "default"
|
||||
actions := []string{defaultActionID, "Default"}
|
||||
|
||||
actionMap := map[string]string{
|
||||
defaultActionID: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
hints["x-notification-id"] = dbus.MakeVariant(options.ID)
|
||||
|
||||
if options.Data != nil {
|
||||
userData, err := json.Marshal(options.Data)
|
||||
if err == nil {
|
||||
hints["x-user-data"] = dbus.MakeVariant(string(userData))
|
||||
}
|
||||
}
|
||||
|
||||
// Call the Notify method on the D-Bus interface
|
||||
obj := conn.Object(dbusNotificationInterface, dbusNotificationPath)
|
||||
call := obj.Call(
|
||||
dbusNotificationInterface+".Notify",
|
||||
0,
|
||||
appName,
|
||||
uint32(0),
|
||||
"", // Icon
|
||||
options.Title,
|
||||
body,
|
||||
actions,
|
||||
hints,
|
||||
int32(-1),
|
||||
)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to send notification: %w", call.Err)
|
||||
}
|
||||
|
||||
var dbusID uint32
|
||||
if err := call.Store(&dbusID); err != nil {
|
||||
return fmt.Errorf("failed to store notification ID: %w", err)
|
||||
}
|
||||
|
||||
notification := ¬ificationData{
|
||||
ID: options.ID,
|
||||
Title: options.Title,
|
||||
Subtitle: options.Subtitle,
|
||||
Body: options.Body,
|
||||
Data: options.Data,
|
||||
DBusID: dbusID,
|
||||
ActionMap: actionMap,
|
||||
}
|
||||
|
||||
notificationsLock.Lock()
|
||||
notifications[dbusID] = notification
|
||||
notificationsLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendNotificationWithActions sends a notification with additional actions.
|
||||
func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
|
||||
if conn == nil {
|
||||
return fmt.Errorf("notifications not initialized")
|
||||
}
|
||||
|
||||
categoriesLock.RLock()
|
||||
category, exists := categories[options.CategoryID]
|
||||
categoriesLock.RUnlock()
|
||||
|
||||
if options.CategoryID == "" || !exists {
|
||||
// Fall back to basic notification
|
||||
return f.SendNotification(options)
|
||||
}
|
||||
|
||||
body := options.Body
|
||||
if options.Subtitle != "" {
|
||||
body = options.Subtitle + "\n" + body
|
||||
}
|
||||
|
||||
var actions []string
|
||||
actionMap := make(map[string]string)
|
||||
|
||||
defaultActionID := "default"
|
||||
actions = append(actions, defaultActionID, "Default")
|
||||
actionMap[defaultActionID] = DefaultActionIdentifier
|
||||
|
||||
for _, action := range category.Actions {
|
||||
actions = append(actions, action.ID, action.Title)
|
||||
actionMap[action.ID] = action.ID
|
||||
}
|
||||
|
||||
hints := map[string]dbus.Variant{}
|
||||
|
||||
hints["x-notification-id"] = dbus.MakeVariant(options.ID)
|
||||
|
||||
hints["x-category-id"] = dbus.MakeVariant(options.CategoryID)
|
||||
|
||||
if options.Data != nil {
|
||||
userData, err := json.Marshal(options.Data)
|
||||
if err == nil {
|
||||
hints["x-user-data"] = dbus.MakeVariant(string(userData))
|
||||
}
|
||||
}
|
||||
|
||||
obj := conn.Object(dbusNotificationInterface, dbusNotificationPath)
|
||||
call := obj.Call(
|
||||
dbusNotificationInterface+".Notify",
|
||||
0,
|
||||
appName,
|
||||
uint32(0),
|
||||
"", // Icon
|
||||
options.Title,
|
||||
body,
|
||||
actions,
|
||||
hints,
|
||||
int32(-1),
|
||||
)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to send notification: %w", call.Err)
|
||||
}
|
||||
|
||||
var dbusID uint32
|
||||
if err := call.Store(&dbusID); err != nil {
|
||||
return fmt.Errorf("failed to store notification ID: %w", err)
|
||||
}
|
||||
|
||||
notification := ¬ificationData{
|
||||
ID: options.ID,
|
||||
Title: options.Title,
|
||||
Subtitle: options.Subtitle,
|
||||
Body: options.Body,
|
||||
CategoryID: options.CategoryID,
|
||||
Data: options.Data,
|
||||
DBusID: dbusID,
|
||||
ActionMap: actionMap,
|
||||
}
|
||||
|
||||
notificationsLock.Lock()
|
||||
notifications[dbusID] = notification
|
||||
notificationsLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
|
||||
categoriesLock.Lock()
|
||||
categories[category.ID] = category
|
||||
categoriesLock.Unlock()
|
||||
|
||||
if err := f.saveCategories(); err != nil {
|
||||
f.logger.Warning("Failed to save notification categories: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory removes a previously registered NotificationCategory.
|
||||
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
|
||||
categoriesLock.Lock()
|
||||
delete(categories, categoryId)
|
||||
categoriesLock.Unlock()
|
||||
|
||||
if err := f.saveCategories(); err != nil {
|
||||
f.logger.Warning("Failed to save notification categories: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllPendingNotifications attempts to remove all active notifications.
|
||||
func (f *Frontend) RemoveAllPendingNotifications() error {
|
||||
notificationsLock.Lock()
|
||||
dbusIDs := make([]uint32, 0, len(notifications))
|
||||
for id := range notifications {
|
||||
dbusIDs = append(dbusIDs, id)
|
||||
}
|
||||
notificationsLock.Unlock()
|
||||
|
||||
for _, id := range dbusIDs {
|
||||
f.closeNotification(id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePendingNotification removes a pending notification.
|
||||
func (f *Frontend) RemovePendingNotification(identifier string) error {
|
||||
var dbusID uint32
|
||||
found := false
|
||||
|
||||
notificationsLock.Lock()
|
||||
for id, notif := range notifications {
|
||||
if notif.ID == identifier {
|
||||
dbusID = id
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
notificationsLock.Unlock()
|
||||
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f.closeNotification(dbusID)
|
||||
}
|
||||
|
||||
// RemoveAllDeliveredNotifications functionally equivalent to RemoveAllPendingNotification on Linux.
|
||||
func (f *Frontend) RemoveAllDeliveredNotifications() error {
|
||||
return f.RemoveAllPendingNotifications()
|
||||
}
|
||||
|
||||
// RemoveDeliveredNotification functionally equivalent RemovePendingNotification on Linux.
|
||||
func (f *Frontend) RemoveDeliveredNotification(identifier string) error {
|
||||
return f.RemovePendingNotification(identifier)
|
||||
}
|
||||
|
||||
// RemoveNotification removes a notification by identifier.
|
||||
func (f *Frontend) RemoveNotification(identifier string) error {
|
||||
return f.RemovePendingNotification(identifier)
|
||||
}
|
||||
|
||||
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
|
||||
callbackLock.Lock()
|
||||
defer callbackLock.Unlock()
|
||||
|
||||
notificationResultCallback = callback
|
||||
}
|
||||
|
||||
// Helper method to close a notification.
|
||||
func (f *Frontend) closeNotification(id uint32) error {
|
||||
if conn == nil {
|
||||
return fmt.Errorf("notifications not initialized")
|
||||
}
|
||||
|
||||
obj := conn.Object(dbusNotificationInterface, dbusNotificationPath)
|
||||
call := obj.Call(dbusNotificationInterface+".CloseNotification", 0, id)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to close notification: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Frontend) getConfigDir() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user config directory: %w", err)
|
||||
}
|
||||
|
||||
appConfigDir := filepath.Join(configDir, appName)
|
||||
if err := os.MkdirAll(appConfigDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create app config directory: %w", err)
|
||||
}
|
||||
|
||||
return appConfigDir, nil
|
||||
}
|
||||
|
||||
// Save notification categories.
|
||||
func (f *Frontend) saveCategories() error {
|
||||
configDir, err := f.getConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
categoriesFile := filepath.Join(configDir, "notification-categories.json")
|
||||
|
||||
categoriesLock.RLock()
|
||||
categoriesData, err := json.MarshalIndent(categories, "", " ")
|
||||
categoriesLock.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification categories: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(categoriesFile, categoriesData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write notification categories to disk: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load notification categories.
|
||||
func (f *Frontend) loadCategories() error {
|
||||
configDir, err := f.getConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
categoriesFile := filepath.Join(configDir, "notification-categories.json")
|
||||
|
||||
if _, err := os.Stat(categoriesFile); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
categoriesData, err := os.ReadFile(categoriesFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read notification categories from disk: %w", err)
|
||||
}
|
||||
|
||||
_categories := make(map[string]frontend.NotificationCategory)
|
||||
if err := json.Unmarshal(categoriesData, &_categories); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal notification categories: %w", err)
|
||||
}
|
||||
|
||||
categoriesLock.Lock()
|
||||
categories = _categories
|
||||
categoriesLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup signal handling for notification actions.
|
||||
func (f *Frontend) setupSignalHandling(ctx context.Context) error {
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchInterface(dbusNotificationInterface),
|
||||
dbus.WithMatchMember("ActionInvoked"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchInterface(dbusNotificationInterface),
|
||||
dbus.WithMatchMember("NotificationClosed"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := make(chan *dbus.Signal, 10)
|
||||
conn.Signal(c)
|
||||
|
||||
go f.handleSignals(ctx, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle incoming D-Bus signals.
|
||||
func (f *Frontend) handleSignals(ctx context.Context, c chan *dbus.Signal) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case signal, ok := <-c:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch signal.Name {
|
||||
case dbusNotificationInterface + ".ActionInvoked":
|
||||
f.handleActionInvoked(signal)
|
||||
case dbusNotificationInterface + ".NotificationClosed":
|
||||
f.handleNotificationClosed(signal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ActionInvoked signal.
|
||||
func (f *Frontend) handleActionInvoked(signal *dbus.Signal) {
|
||||
if len(signal.Body) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
dbusID, ok := signal.Body[0].(uint32)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
actionID, ok := signal.Body[1].(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
notificationsLock.Lock()
|
||||
notification, exists := notifications[dbusID]
|
||||
if exists {
|
||||
delete(notifications, dbusID)
|
||||
}
|
||||
notificationsLock.Unlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
appActionID, ok := notification.ActionMap[actionID]
|
||||
if !ok {
|
||||
appActionID = actionID
|
||||
}
|
||||
|
||||
response := frontend.NotificationResponse{
|
||||
ID: notification.ID,
|
||||
ActionIdentifier: appActionID,
|
||||
Title: notification.Title,
|
||||
Subtitle: notification.Subtitle,
|
||||
Body: notification.Body,
|
||||
CategoryID: notification.CategoryID,
|
||||
UserInfo: notification.Data,
|
||||
}
|
||||
|
||||
result := frontend.NotificationResult{
|
||||
Response: response,
|
||||
}
|
||||
|
||||
handleNotificationResult(result)
|
||||
}
|
||||
|
||||
func handleNotificationResult(result frontend.NotificationResult) {
|
||||
callbackLock.Lock()
|
||||
callback := notificationResultCallback
|
||||
callbackLock.Unlock()
|
||||
|
||||
if callback != nil {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Log panic but don't crash the app
|
||||
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
|
||||
}
|
||||
}()
|
||||
callback(result)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle NotificationClosed signal.
|
||||
// Reason codes:
|
||||
// 1 - expired timeout
|
||||
// 2 - dismissed by user (click on X)
|
||||
// 3 - closed by CloseNotification call
|
||||
// 4 - undefined/reserved
|
||||
func (f *Frontend) handleNotificationClosed(signal *dbus.Signal) {
|
||||
if len(signal.Body) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
dbusID, ok := signal.Body[0].(uint32)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
reason, ok := signal.Body[1].(uint32)
|
||||
if !ok {
|
||||
reason = 0 // Unknown reason
|
||||
}
|
||||
|
||||
notificationsLock.Lock()
|
||||
notification, exists := notifications[dbusID]
|
||||
if exists {
|
||||
delete(notifications, dbusID)
|
||||
}
|
||||
notificationsLock.Unlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
if reason == 2 {
|
||||
response := frontend.NotificationResponse{
|
||||
ID: notification.ID,
|
||||
ActionIdentifier: DefaultActionIdentifier,
|
||||
Title: notification.Title,
|
||||
Subtitle: notification.Subtitle,
|
||||
Body: notification.Body,
|
||||
CategoryID: notification.CategoryID,
|
||||
UserInfo: notification.Data,
|
||||
}
|
||||
|
||||
result := frontend.NotificationResult{
|
||||
Response: response,
|
||||
}
|
||||
|
||||
handleNotificationResult(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,11 +28,13 @@ import (
|
|||
"github.com/wailsapp/wails/v2/internal/frontend/originvalidator"
|
||||
wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
w32consts "github.com/wailsapp/wails/v2/internal/platform/win32"
|
||||
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
|
||||
"github.com/wailsapp/wails/v2/pkg/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
w "golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const startURL = "http://wails.localhost/"
|
||||
|
|
@ -75,6 +77,13 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
|||
// Get Windows build number
|
||||
versionInfo, _ := operatingsystem.GetWindowsVersionInfo()
|
||||
|
||||
// Apply DLL search path settings if specified
|
||||
if appoptions.Windows != nil && appoptions.Windows.DLLSearchPaths != 0 {
|
||||
w.SetDefaultDllDirectories(appoptions.Windows.DLLSearchPaths)
|
||||
}
|
||||
// Now initialize packages that load DLLs
|
||||
w32.Init()
|
||||
w32consts.Init()
|
||||
result := &Frontend{
|
||||
frontendOptions: appoptions,
|
||||
logger: myLogger,
|
||||
|
|
|
|||
489
v2/internal/frontend/desktop/windows/notifications.go
Normal file
489
v2/internal/frontend/desktop/windows/notifications.go
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package windows
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
wintoast "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
|
||||
"github.com/google/uuid"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
|
||||
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
categories map[string]frontend.NotificationCategory
|
||||
categoriesLock sync.RWMutex
|
||||
appName string
|
||||
appGUID string
|
||||
iconPath string = ""
|
||||
exePath string
|
||||
iconOnce sync.Once
|
||||
iconErr error
|
||||
|
||||
notificationResultCallback func(result frontend.NotificationResult)
|
||||
callbackLock sync.RWMutex
|
||||
)
|
||||
|
||||
const DefaultActionIdentifier = "DEFAULT_ACTION"
|
||||
|
||||
const (
|
||||
ToastRegistryPath = `Software\Classes\AppUserModelId\`
|
||||
ToastRegistryGuidKey = "CustomActivator"
|
||||
NotificationCategoriesRegistryPath = `SOFTWARE\%s\NotificationCategories`
|
||||
NotificationCategoriesRegistryKey = "Categories"
|
||||
)
|
||||
|
||||
// NotificationPayload combines the action ID and user data into a single structure
|
||||
type NotificationPayload struct {
|
||||
Action string `json:"action"`
|
||||
Options frontend.NotificationOptions `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
func (f *Frontend) InitializeNotifications() error {
|
||||
categoriesLock.Lock()
|
||||
defer categoriesLock.Unlock()
|
||||
categories = make(map[string]frontend.NotificationCategory)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable: %w", err)
|
||||
}
|
||||
exePath = exe
|
||||
appName = filepath.Base(exePath)
|
||||
|
||||
appGUID, err = getGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iconPath = filepath.Join(os.TempDir(), appName+appGUID+".png")
|
||||
|
||||
// Create the registry key for the toast activator
|
||||
key, _, err := registry.CreateKey(registry.CURRENT_USER,
|
||||
`Software\Classes\CLSID\`+appGUID+`\LocalServer32`, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create CLSID key: %w", err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
if err := key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", exePath)); err != nil {
|
||||
return fmt.Errorf("failed to set CLSID server path: %w", err)
|
||||
}
|
||||
|
||||
toast.SetAppData(toast.AppData{
|
||||
AppID: appName,
|
||||
GUID: appGUID,
|
||||
IconPath: iconPath,
|
||||
ActivationExe: exePath,
|
||||
})
|
||||
|
||||
toast.SetActivationCallback(func(args string, data []toast.UserData) {
|
||||
result := frontend.NotificationResult{}
|
||||
|
||||
actionIdentifier, options, err := parseNotificationResponse(args)
|
||||
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
} else {
|
||||
// Subtitle is retained but was not shown with the notification
|
||||
response := frontend.NotificationResponse{
|
||||
ID: options.ID,
|
||||
ActionIdentifier: actionIdentifier,
|
||||
Title: options.Title,
|
||||
Subtitle: options.Subtitle,
|
||||
Body: options.Body,
|
||||
CategoryID: options.CategoryID,
|
||||
UserInfo: options.Data,
|
||||
}
|
||||
|
||||
if userText, found := getUserText(data); found {
|
||||
response.UserText = userText
|
||||
}
|
||||
|
||||
result.Response = response
|
||||
}
|
||||
|
||||
handleNotificationResult(result)
|
||||
})
|
||||
|
||||
// Register the COM class factory for toast activation.
|
||||
// This is required for Windows to activate the app when users interact with notifications.
|
||||
// The go-toast library's SetAppData and SetActivationCallback handle the callback setup,
|
||||
// but the COM class factory registration is not exposed via public APIs, so we use
|
||||
// go:linkname to access the internal registerClassFactory function.
|
||||
if err := registerToastClassFactory(wintoast.ClassFactory); err != nil {
|
||||
return fmt.Errorf("CoRegisterClassObject failed: %w", err)
|
||||
}
|
||||
|
||||
return loadCategoriesFromRegistry()
|
||||
}
|
||||
|
||||
// registerToastClassFactory registers the COM class factory required for Windows toast notification activation.
|
||||
// This function uses go:linkname to access the unexported registerClassFactory function from go-toast.
|
||||
// The class factory is necessary for Windows COM activation when users click notification actions.
|
||||
// Without this registration, notification actions will not activate the application.
|
||||
//
|
||||
// This is a workaround until go-toast exports this functionality via a public API.
|
||||
// See: https://git.sr.ht/~jackmordaunt/go-toast
|
||||
//
|
||||
//go:linkname registerToastClassFactory git.sr.ht/~jackmordaunt/go-toast/v2/wintoast.registerClassFactory
|
||||
func registerToastClassFactory(factory *wintoast.IClassFactory) error
|
||||
|
||||
// CleanupNotifications is a Windows stub that does nothing.
|
||||
// (Linux-specific cleanup)
|
||||
func (f *Frontend) CleanupNotifications() {
|
||||
// No cleanup needed on Windows
|
||||
}
|
||||
|
||||
func (f *Frontend) IsNotificationAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
|
||||
// (subtitle is only available on macOS and Linux)
|
||||
func (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
|
||||
if err := f.saveIconToDir(); err != nil {
|
||||
f.logger.Warning("Error saving icon: %v", err)
|
||||
}
|
||||
|
||||
n := toast.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
ActivationType: toast.Foreground,
|
||||
ActivationArguments: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
encodedPayload, err := encodePayload(DefaultActionIdentifier, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode notification payload: %w", err)
|
||||
}
|
||||
n.ActivationArguments = encodedPayload
|
||||
|
||||
return n.Push()
|
||||
}
|
||||
|
||||
// SendNotificationWithActions sends a notification with additional actions and inputs.
|
||||
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
|
||||
// If a NotificationCategory is not registered a basic notification will be sent.
|
||||
// (subtitle is only available on macOS and Linux)
|
||||
func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
|
||||
if err := f.saveIconToDir(); err != nil {
|
||||
f.logger.Warning("Error saving icon: %v", err)
|
||||
}
|
||||
|
||||
categoriesLock.RLock()
|
||||
nCategory, categoryExists := categories[options.CategoryID]
|
||||
categoriesLock.RUnlock()
|
||||
|
||||
if options.CategoryID == "" || !categoryExists {
|
||||
f.logger.Warning("Category '%s' not found, sending basic notification without actions", options.CategoryID)
|
||||
return f.SendNotification(options)
|
||||
}
|
||||
|
||||
n := toast.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
ActivationType: toast.Foreground,
|
||||
ActivationArguments: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
for _, action := range nCategory.Actions {
|
||||
n.Actions = append(n.Actions, toast.Action{
|
||||
Content: action.Title,
|
||||
Arguments: action.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if nCategory.HasReplyField {
|
||||
n.Inputs = append(n.Inputs, toast.Input{
|
||||
ID: "userText",
|
||||
Placeholder: nCategory.ReplyPlaceholder,
|
||||
})
|
||||
|
||||
n.Actions = append(n.Actions, toast.Action{
|
||||
Content: nCategory.ReplyButtonTitle,
|
||||
Arguments: "TEXT_REPLY",
|
||||
InputID: "userText",
|
||||
})
|
||||
}
|
||||
|
||||
encodedPayload, err := encodePayload(n.ActivationArguments, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode notification payload: %w", err)
|
||||
}
|
||||
n.ActivationArguments = encodedPayload
|
||||
|
||||
for index := range n.Actions {
|
||||
encodedPayload, err := encodePayload(n.Actions[index].Arguments, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode notification payload: %w", err)
|
||||
}
|
||||
n.Actions[index].Arguments = encodedPayload
|
||||
}
|
||||
|
||||
return n.Push()
|
||||
}
|
||||
|
||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
// Registering a category with the same name as a previously registered NotificationCategory will override it.
|
||||
func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
|
||||
categoriesLock.Lock()
|
||||
defer categoriesLock.Unlock()
|
||||
|
||||
categories[category.ID] = frontend.NotificationCategory{
|
||||
ID: category.ID,
|
||||
Actions: category.Actions,
|
||||
HasReplyField: category.HasReplyField,
|
||||
ReplyPlaceholder: category.ReplyPlaceholder,
|
||||
ReplyButtonTitle: category.ReplyButtonTitle,
|
||||
}
|
||||
|
||||
return saveCategoriesToRegistry()
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory removes a previously registered NotificationCategory.
|
||||
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
|
||||
categoriesLock.Lock()
|
||||
defer categoriesLock.Unlock()
|
||||
|
||||
delete(categories, categoryId)
|
||||
|
||||
return saveCategoriesToRegistry()
|
||||
}
|
||||
|
||||
// RemoveAllPendingNotifications is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (f *Frontend) RemoveAllPendingNotifications() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePendingNotification is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (f *Frontend) RemovePendingNotification(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllDeliveredNotifications is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (f *Frontend) RemoveAllDeliveredNotifications() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDeliveredNotification is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (f *Frontend) RemoveDeliveredNotification(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNotification is a Windows stub that always returns nil.
|
||||
// (Linux-specific)
|
||||
func (f *Frontend) RemoveNotification(identifier string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
|
||||
callbackLock.Lock()
|
||||
defer callbackLock.Unlock()
|
||||
|
||||
notificationResultCallback = callback
|
||||
}
|
||||
|
||||
func (f *Frontend) saveIconToDir() error {
|
||||
iconOnce.Do(func() {
|
||||
hIcon := w32.ExtractIcon(exePath, 0)
|
||||
if hIcon == 0 {
|
||||
iconErr = fmt.Errorf("ExtractIcon failed for %s", exePath)
|
||||
return
|
||||
}
|
||||
defer w32.DestroyIcon(hIcon)
|
||||
iconErr = winc.SaveHIconAsPNG(hIcon, iconPath)
|
||||
})
|
||||
return iconErr
|
||||
}
|
||||
|
||||
func saveCategoriesToRegistry() error {
|
||||
// We assume lock is held by caller
|
||||
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName)
|
||||
|
||||
key, _, err := registry.CreateKey(
|
||||
registry.CURRENT_USER,
|
||||
registryPath,
|
||||
registry.ALL_ACCESS,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
data, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return key.SetStringValue(NotificationCategoriesRegistryKey, string(data))
|
||||
}
|
||||
|
||||
func loadCategoriesFromRegistry() error {
|
||||
// We assume lock is held by caller
|
||||
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName)
|
||||
|
||||
key, err := registry.OpenKey(
|
||||
registry.CURRENT_USER,
|
||||
registryPath,
|
||||
registry.QUERY_VALUE,
|
||||
)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
// Not an error, no saved categories
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to open registry key: %w", err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
// No value yet, but key exists
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read categories from registry: %w", err)
|
||||
}
|
||||
|
||||
_categories := make(map[string]frontend.NotificationCategory)
|
||||
if err := json.Unmarshal([]byte(data), &_categories); err != nil {
|
||||
return fmt.Errorf("failed to parse notification categories from registry: %w", err)
|
||||
}
|
||||
|
||||
categories = _categories
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserText(data []toast.UserData) (string, bool) {
|
||||
for _, d := range data {
|
||||
if d.Key == "userText" {
|
||||
return d.Value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// encodePayload combines an action ID and user data into a single encoded string
|
||||
func encodePayload(actionID string, options frontend.NotificationOptions) (string, error) {
|
||||
payload := NotificationPayload{
|
||||
Action: actionID,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return actionID, err
|
||||
}
|
||||
|
||||
encodedPayload := base64.StdEncoding.EncodeToString(jsonData)
|
||||
return encodedPayload, nil
|
||||
}
|
||||
|
||||
// decodePayload extracts the action ID and user data from an encoded payload
|
||||
func decodePayload(encodedString string) (string, frontend.NotificationOptions, error) {
|
||||
jsonData, err := base64.StdEncoding.DecodeString(encodedString)
|
||||
if err != nil {
|
||||
return encodedString, frontend.NotificationOptions{}, fmt.Errorf("failed to decode base64 payload: %w", err)
|
||||
}
|
||||
|
||||
var payload NotificationPayload
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return encodedString, frontend.NotificationOptions{}, fmt.Errorf("failed to unmarshal notification payload: %w", err)
|
||||
}
|
||||
|
||||
return payload.Action, payload.Options, nil
|
||||
}
|
||||
|
||||
// parseNotificationResponse updated to use structured payload decoding
|
||||
func parseNotificationResponse(response string) (action string, options frontend.NotificationOptions, err error) {
|
||||
actionID, options, err := decodePayload(response)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to decode notification response: %v", err)
|
||||
return response, frontend.NotificationOptions{}, err
|
||||
}
|
||||
|
||||
return actionID, options, nil
|
||||
}
|
||||
|
||||
func handleNotificationResult(result frontend.NotificationResult) {
|
||||
callbackLock.RLock()
|
||||
callback := notificationResultCallback
|
||||
callbackLock.RUnlock()
|
||||
|
||||
if callback != nil {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Log panic but don't crash the app
|
||||
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
|
||||
}
|
||||
}()
|
||||
callback(result)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func getGUID() (string, error) {
|
||||
keyPath := ToastRegistryPath + appName
|
||||
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
|
||||
if err == nil {
|
||||
guid, _, err := k.GetStringValue(ToastRegistryGuidKey)
|
||||
k.Close()
|
||||
if err == nil && guid != "" {
|
||||
return guid, nil
|
||||
}
|
||||
}
|
||||
|
||||
guid := generateGUID()
|
||||
|
||||
k, _, err = registry.CreateKey(registry.CURRENT_USER, keyPath, registry.WRITE)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create registry key: %w", err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if err := k.SetStringValue(ToastRegistryGuidKey, guid); err != nil {
|
||||
return "", fmt.Errorf("failed to write GUID to registry: %w", err)
|
||||
}
|
||||
|
||||
return guid, nil
|
||||
}
|
||||
|
||||
func generateGUID() string {
|
||||
guid := uuid.New()
|
||||
return fmt.Sprintf("{%s}", guid.String())
|
||||
}
|
||||
|
|
@ -10,11 +10,86 @@ package winc
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
||||
procGetIconInfo = user32.NewProc("GetIconInfo")
|
||||
procDeleteObject = gdi32.NewProc("DeleteObject")
|
||||
procGetObject = gdi32.NewProc("GetObjectW")
|
||||
procGetDIBits = gdi32.NewProc("GetDIBits")
|
||||
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
|
||||
procSelectObject = gdi32.NewProc("SelectObject")
|
||||
procDeleteDC = gdi32.NewProc("DeleteDC")
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Validate DLL loads at initialization time to surface missing APIs early
|
||||
if err := user32.Load(); err != nil {
|
||||
panic(fmt.Sprintf("failed to load user32.dll: %v", err))
|
||||
}
|
||||
if err := gdi32.Load(); err != nil {
|
||||
panic(fmt.Sprintf("failed to load gdi32.dll: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// ICONINFO mirrors the Win32 ICONINFO struct
|
||||
type ICONINFO struct {
|
||||
FIcon int32
|
||||
XHotspot uint32
|
||||
YHotspot uint32
|
||||
HbmMask uintptr
|
||||
HbmColor uintptr
|
||||
}
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183376.aspx
|
||||
type BITMAPINFOHEADER struct {
|
||||
BiSize uint32
|
||||
BiWidth int32
|
||||
BiHeight int32
|
||||
BiPlanes uint16
|
||||
BiBitCount uint16
|
||||
BiCompression uint32
|
||||
BiSizeImage uint32
|
||||
BiXPelsPerMeter int32
|
||||
BiYPelsPerMeter int32
|
||||
BiClrUsed uint32
|
||||
BiClrImportant uint32
|
||||
}
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162938.aspx
|
||||
type RGBQUAD struct {
|
||||
RgbBlue byte
|
||||
RgbGreen byte
|
||||
RgbRed byte
|
||||
RgbReserved byte
|
||||
}
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183375.aspx
|
||||
type BITMAPINFO struct {
|
||||
BmiHeader BITMAPINFOHEADER
|
||||
BmiColors *RGBQUAD
|
||||
}
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183371.aspx
|
||||
type BITMAP struct {
|
||||
BmType int32
|
||||
BmWidth int32
|
||||
BmHeight int32
|
||||
BmWidthBytes int32
|
||||
BmPlanes uint16
|
||||
BmBitsPixel uint16
|
||||
BmBits unsafe.Pointer
|
||||
}
|
||||
|
||||
type Icon struct {
|
||||
handle w32.HICON
|
||||
}
|
||||
|
|
@ -46,6 +121,95 @@ func ExtractIcon(fileName string, index int) (*Icon, error) {
|
|||
return ico, err
|
||||
}
|
||||
|
||||
func SaveHIconAsPNG(hIcon w32.HICON, filePath string) error {
|
||||
// Get icon info
|
||||
var iconInfo ICONINFO
|
||||
ret, _, err := procGetIconInfo.Call(
|
||||
uintptr(hIcon),
|
||||
uintptr(unsafe.Pointer(&iconInfo)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
defer procDeleteObject.Call(uintptr(iconInfo.HbmMask))
|
||||
defer procDeleteObject.Call(uintptr(iconInfo.HbmColor))
|
||||
|
||||
// Get bitmap info
|
||||
var bmp BITMAP
|
||||
ret, _, err = procGetObject.Call(
|
||||
uintptr(iconInfo.HbmColor),
|
||||
unsafe.Sizeof(bmp),
|
||||
uintptr(unsafe.Pointer(&bmp)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get screen DC for GetDIBits (bitmap must not be selected into a DC)
|
||||
screenDC := w32.GetDC(0)
|
||||
if screenDC == 0 {
|
||||
return fmt.Errorf("failed to get screen DC")
|
||||
}
|
||||
defer w32.ReleaseDC(0, screenDC)
|
||||
|
||||
// Prepare bitmap info header
|
||||
var bi BITMAPINFO
|
||||
bi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bi.BmiHeader))
|
||||
bi.BmiHeader.BiWidth = bmp.BmWidth
|
||||
bi.BmiHeader.BiHeight = bmp.BmHeight
|
||||
bi.BmiHeader.BiPlanes = 1
|
||||
bi.BmiHeader.BiBitCount = 32
|
||||
bi.BmiHeader.BiCompression = w32.BI_RGB
|
||||
|
||||
// Allocate memory for bitmap bits
|
||||
width, height := int(bmp.BmWidth), int(bmp.BmHeight)
|
||||
bufferSize := width * height * 4
|
||||
bits := make([]byte, bufferSize)
|
||||
|
||||
// Get bitmap bits using screen DC (bitmap must not be selected into any DC)
|
||||
ret, _, err = procGetDIBits.Call(
|
||||
uintptr(screenDC),
|
||||
uintptr(iconInfo.HbmColor),
|
||||
0,
|
||||
uintptr(bmp.BmHeight),
|
||||
uintptr(unsafe.Pointer(&bits[0])),
|
||||
uintptr(unsafe.Pointer(&bi)),
|
||||
w32.DIB_RGB_COLORS,
|
||||
)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("failed to get bitmap bits: %w", err)
|
||||
}
|
||||
|
||||
// Create Go image
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// Convert DIB to RGBA
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
// DIB is bottom-up, so we need to invert Y
|
||||
dibIndex := ((height-1-y)*width + x) * 4
|
||||
// RGBA image is top-down
|
||||
imgIndex := (y*width + x) * 4
|
||||
|
||||
// BGRA to RGBA
|
||||
img.Pix[imgIndex] = bits[dibIndex+2] // R
|
||||
img.Pix[imgIndex+1] = bits[dibIndex+1] // G
|
||||
img.Pix[imgIndex+2] = bits[dibIndex] // B
|
||||
img.Pix[imgIndex+3] = bits[dibIndex+3] // A
|
||||
}
|
||||
}
|
||||
|
||||
// Create output file
|
||||
outFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Encode and save the image
|
||||
return png.Encode(outFile, img)
|
||||
}
|
||||
|
||||
func (ic *Icon) Destroy() bool {
|
||||
return w32.DestroyIcon(ic.handle)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ var (
|
|||
setWindowTheme uintptr
|
||||
)
|
||||
|
||||
func init() {
|
||||
func Init() {
|
||||
// Library
|
||||
libuxtheme = MustLoadLibrary("uxtheme.dll")
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,102 @@ func (d *Dispatcher) processSystemCall(payload callMessage, sender frontend.Fron
|
|||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
case "InitializeNotifications":
|
||||
err := sender.InitializeNotifications()
|
||||
return nil, err
|
||||
case "CleanupNotifications":
|
||||
sender.CleanupNotifications()
|
||||
return nil, nil
|
||||
case "IsNotificationAvailable":
|
||||
return sender.IsNotificationAvailable(), nil
|
||||
case "RequestNotificationAuthorization":
|
||||
authorized, err := sender.RequestNotificationAuthorization()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorized, nil
|
||||
case "CheckNotificationAuthorization":
|
||||
authorized, err := sender.CheckNotificationAuthorization()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorized, nil
|
||||
case "SendNotification":
|
||||
if len(payload.Args) < 1 {
|
||||
return nil, errors.New("empty argument, cannot send notification")
|
||||
}
|
||||
var options frontend.NotificationOptions
|
||||
if err := json.Unmarshal(payload.Args[0], &options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := sender.SendNotification(options)
|
||||
return nil, err
|
||||
case "SendNotificationWithActions":
|
||||
if len(payload.Args) < 1 {
|
||||
return nil, errors.New("empty argument, cannot send notification")
|
||||
}
|
||||
var options frontend.NotificationOptions
|
||||
if err := json.Unmarshal(payload.Args[0], &options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := sender.SendNotificationWithActions(options)
|
||||
return nil, err
|
||||
case "RegisterNotificationCategory":
|
||||
if len(payload.Args) < 1 {
|
||||
return nil, errors.New("empty argument, cannot register category")
|
||||
}
|
||||
var category frontend.NotificationCategory
|
||||
if err := json.Unmarshal(payload.Args[0], &category); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := sender.RegisterNotificationCategory(category)
|
||||
return nil, err
|
||||
case "RemoveNotificationCategory":
|
||||
if len(payload.Args) < 1 {
|
||||
return nil, errors.New("empty argument, cannot remove category")
|
||||
}
|
||||
var categoryId string
|
||||
if err := json.Unmarshal(payload.Args[0], &categoryId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := sender.RemoveNotificationCategory(categoryId)
|
||||
return nil, err
|
||||
case "RemoveAllPendingNotifications":
|
||||
err := sender.RemoveAllPendingNotifications()
|
||||
return nil, err
|
||||
case "RemovePendingNotification":
|
||||
if len(payload.Args) < 1 {
|
||||
return nil, errors.New("empty argument, cannot remove notification")
|
||||
}
|
||||
var identifier string
|
||||
if err := json.Unmarshal(payload.Args[0], &identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := sender.RemovePendingNotification(identifier)
|
||||
return nil, err
|
||||
case "RemoveAllDeliveredNotifications":
|
||||
err := sender.RemoveAllDeliveredNotifications()
|
||||
return nil, err
|
||||
case "RemoveDeliveredNotification":
|
||||
if len(payload.Args) < 1 {
|
||||
return nil, errors.New("empty argument, cannot remove notification")
|
||||
}
|
||||
var identifier string
|
||||
if err := json.Unmarshal(payload.Args[0], &identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := sender.RemoveDeliveredNotification(identifier)
|
||||
return nil, err
|
||||
case "RemoveNotification":
|
||||
if len(payload.Args) < 1 {
|
||||
return nil, errors.New("empty argument, cannot remove notification")
|
||||
}
|
||||
var identifier string
|
||||
if err := json.Unmarshal(payload.Args[0], &identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := sender.RemoveNotification(identifier)
|
||||
return nil, err
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown systemcall message: %s", payload.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,51 @@ type MessageDialogOptions struct {
|
|||
Icon []byte
|
||||
}
|
||||
|
||||
// NotificationOptions contains configuration for a notification.
|
||||
type NotificationOptions struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only)
|
||||
Body string `json:"body,omitempty"`
|
||||
CategoryID string `json:"categoryId,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationAction represents an action button for a notification.
|
||||
type NotificationAction struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Destructive bool `json:"destructive,omitempty"` // (macOS-specific)
|
||||
}
|
||||
|
||||
// NotificationCategory groups actions for notifications.
|
||||
type NotificationCategory struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Actions []NotificationAction `json:"actions,omitempty"`
|
||||
HasReplyField bool `json:"hasReplyField,omitempty"`
|
||||
ReplyPlaceholder string `json:"replyPlaceholder,omitempty"`
|
||||
ReplyButtonTitle string `json:"replyButtonTitle,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationResponse represents the response sent by interacting with a notification.
|
||||
type NotificationResponse struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ActionIdentifier string `json:"actionIdentifier,omitempty"`
|
||||
CategoryID string `json:"categoryId,omitempty"` // Consistent with NotificationOptions
|
||||
Title string `json:"title,omitempty"`
|
||||
Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only)
|
||||
Body string `json:"body,omitempty"`
|
||||
UserText string `json:"userText,omitempty"`
|
||||
UserInfo map[string]interface{} `json:"userInfo,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationResult represents the result of a notification response,
|
||||
// returning the response or any errors that occurred.
|
||||
type NotificationResult struct {
|
||||
Response NotificationResponse
|
||||
Error error
|
||||
}
|
||||
|
||||
type Frontend interface {
|
||||
Run(ctx context.Context) error
|
||||
RunMainLoop()
|
||||
|
|
@ -139,4 +184,21 @@ type Frontend interface {
|
|||
// Clipboard
|
||||
ClipboardGetText() (string, error)
|
||||
ClipboardSetText(text string) error
|
||||
|
||||
// Notifications
|
||||
InitializeNotifications() error
|
||||
CleanupNotifications()
|
||||
IsNotificationAvailable() bool
|
||||
RequestNotificationAuthorization() (bool, error)
|
||||
CheckNotificationAuthorization() (bool, error)
|
||||
OnNotificationResponse(callback func(result NotificationResult))
|
||||
SendNotification(options NotificationOptions) error
|
||||
SendNotificationWithActions(options NotificationOptions) error
|
||||
RegisterNotificationCategory(category NotificationCategory) error
|
||||
RemoveNotificationCategory(categoryId string) error
|
||||
RemoveAllPendingNotifications() error
|
||||
RemovePendingNotification(identifier string) error
|
||||
RemoveAllDeliveredNotifications() error
|
||||
RemoveDeliveredNotification(identifier string) error
|
||||
RemoveNotification(identifier string) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,15 +44,27 @@ function checkStyleDropTarget(style) {
|
|||
|
||||
/**
|
||||
* onDragOver is called when the dragover event is emitted.
|
||||
* @param {DragEvent} e
|
||||
* @returns
|
||||
* @param {DragEvent} e
|
||||
* @returns
|
||||
*/
|
||||
function onDragOver(e) {
|
||||
// Check if this is an external file drop or internal HTML drag
|
||||
// External file drops will have "Files" in the types array
|
||||
// Internal HTML drags typically have "text/plain", "text/html" or custom types
|
||||
const isFileDrop = e.dataTransfer.types.includes("Files");
|
||||
|
||||
// Only handle external file drops, let internal HTML5 drag-and-drop work normally
|
||||
if (!isFileDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ALWAYS prevent default for file drops to stop browser navigation
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
|
||||
if (!window.wails.flags.enableWailsDragAndDrop) {
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
e.preventDefault();
|
||||
|
||||
if (!flags.useDropTarget) {
|
||||
return;
|
||||
|
|
@ -80,14 +92,24 @@ function onDragOver(e) {
|
|||
|
||||
/**
|
||||
* onDragLeave is called when the dragleave event is emitted.
|
||||
* @param {DragEvent} e
|
||||
* @returns
|
||||
* @param {DragEvent} e
|
||||
* @returns
|
||||
*/
|
||||
function onDragLeave(e) {
|
||||
// Check if this is an external file drop or internal HTML drag
|
||||
const isFileDrop = e.dataTransfer.types.includes("Files");
|
||||
|
||||
// Only handle external file drops, let internal HTML5 drag-and-drop work normally
|
||||
if (!isFileDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ALWAYS prevent default for file drops to stop browser navigation
|
||||
e.preventDefault();
|
||||
|
||||
if (!window.wails.flags.enableWailsDragAndDrop) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (!flags.useDropTarget) {
|
||||
return;
|
||||
|
|
@ -122,14 +144,24 @@ function onDragLeave(e) {
|
|||
|
||||
/**
|
||||
* onDrop is called when the drop event is emitted.
|
||||
* @param {DragEvent} e
|
||||
* @returns
|
||||
* @param {DragEvent} e
|
||||
* @returns
|
||||
*/
|
||||
function onDrop(e) {
|
||||
// Check if this is an external file drop or internal HTML drag
|
||||
const isFileDrop = e.dataTransfer.types.includes("Files");
|
||||
|
||||
// Only handle external file drops, let internal HTML5 drag-and-drop work normally
|
||||
if (!isFileDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ALWAYS prevent default for file drops to stop browser navigation
|
||||
e.preventDefault();
|
||||
|
||||
if (!window.wails.flags.enableWailsDragAndDrop) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (CanResolveFilePaths()) {
|
||||
// process files
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import * as Browser from "./browser";
|
|||
import * as Clipboard from "./clipboard";
|
||||
import * as DragAndDrop from "./draganddrop";
|
||||
import * as ContextMenu from "./contextmenu";
|
||||
import * as Notifications from "./notifications";
|
||||
|
||||
export function Quit() {
|
||||
window.WailsInvoke('Q');
|
||||
|
|
@ -52,6 +53,7 @@ window.runtime = {
|
|||
...Screen,
|
||||
...Clipboard,
|
||||
...DragAndDrop,
|
||||
...Notifications,
|
||||
EventsOn,
|
||||
EventsOnce,
|
||||
EventsOnMultiple,
|
||||
|
|
|
|||
200
v2/internal/frontend/runtime/desktop/notifications.js
Normal file
200
v2/internal/frontend/runtime/desktop/notifications.js
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 9 */
|
||||
|
||||
import {Call} from "./calls";
|
||||
|
||||
/**
|
||||
* Initialize the notification service for the application.
|
||||
* This must be called before sending any notifications.
|
||||
* On macOS, this also ensures the notification delegate is properly initialized.
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function InitializeNotifications() {
|
||||
return Call(":wails:InitializeNotifications");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up notification resources and release any held connections.
|
||||
* This should be called when shutting down the application to properly release resources
|
||||
* (primarily needed on Linux to close D-Bus connections).
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function CleanupNotifications() {
|
||||
return Call(":wails:CleanupNotifications");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if notifications are available on the current platform.
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<boolean>} True if notifications are available, false otherwise
|
||||
*/
|
||||
export function IsNotificationAvailable() {
|
||||
return Call(":wails:IsNotificationAvailable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification authorization from the user.
|
||||
* On macOS, this prompts the user to allow notifications.
|
||||
* On other platforms, this always returns true.
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<boolean>} True if authorization was granted, false otherwise
|
||||
*/
|
||||
export function RequestNotificationAuthorization() {
|
||||
return Call(":wails:RequestNotificationAuthorization");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the current notification authorization status.
|
||||
* On macOS, this checks if the app has notification permissions.
|
||||
* On other platforms, this always returns true.
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<boolean>} True if authorized, false otherwise
|
||||
*/
|
||||
export function CheckNotificationAuthorization() {
|
||||
return Call(":wails:CheckNotificationAuthorization");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a basic notification with the given options.
|
||||
* The notification will display with the provided title, subtitle (if supported), and body text.
|
||||
*
|
||||
* @export
|
||||
* @param {Object} options - Notification options
|
||||
* @param {string} options.id - Unique identifier for the notification
|
||||
* @param {string} options.title - Notification title
|
||||
* @param {string} [options.subtitle] - Notification subtitle (macOS and Linux only)
|
||||
* @param {string} [options.body] - Notification body text
|
||||
* @param {string} [options.categoryId] - Category ID for action buttons (requires SendNotificationWithActions)
|
||||
* @param {Object<string, any>} [options.data] - Additional user data to attach to the notification
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function SendNotification(options) {
|
||||
return Call(":wails:SendNotification", [options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification with action buttons.
|
||||
* A NotificationCategory must be registered first using RegisterNotificationCategory.
|
||||
* The options.categoryId must match a previously registered category ID.
|
||||
* If the category is not found, a basic notification will be sent instead.
|
||||
*
|
||||
* @export
|
||||
* @param {Object} options - Notification options
|
||||
* @param {string} options.id - Unique identifier for the notification
|
||||
* @param {string} options.title - Notification title
|
||||
* @param {string} [options.subtitle] - Notification subtitle (macOS and Linux only)
|
||||
* @param {string} [options.body] - Notification body text
|
||||
* @param {string} options.categoryId - Category ID that matches a registered category
|
||||
* @param {Object<string, any>} [options.data] - Additional user data to attach to the notification
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function SendNotificationWithActions(options) {
|
||||
return Call(":wails:SendNotificationWithActions", [options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a notification category that can be used with SendNotificationWithActions.
|
||||
* Categories define the action buttons and optional reply fields that will appear on notifications.
|
||||
* Registering a category with the same ID as a previously registered category will override it.
|
||||
*
|
||||
* @export
|
||||
* @param {Object} category - Notification category definition
|
||||
* @param {string} category.id - Unique identifier for the category
|
||||
* @param {Array<Object>} [category.actions] - Array of action buttons
|
||||
* @param {string} category.actions[].id - Unique identifier for the action
|
||||
* @param {string} category.actions[].title - Display title for the action button
|
||||
* @param {boolean} [category.actions[].destructive] - Whether the action is destructive (macOS-specific)
|
||||
* @param {boolean} [category.hasReplyField] - Whether to include a text input field for replies
|
||||
* @param {string} [category.replyPlaceholder] - Placeholder text for the reply field (required if hasReplyField is true)
|
||||
* @param {string} [category.replyButtonTitle] - Title for the reply button (required if hasReplyField is true)
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return Call(":wails:RegisterNotificationCategory", [category]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a previously registered notification category.
|
||||
*
|
||||
* @export
|
||||
* @param {string} categoryId - The ID of the category to remove
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return Call(":wails:RemoveNotificationCategory", [categoryId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all pending notifications from the notification center.
|
||||
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return Call(":wails:RemoveAllPendingNotifications");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific pending notification by its identifier.
|
||||
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
|
||||
*
|
||||
* @export
|
||||
* @param {string} identifier - The ID of the notification to remove
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return Call(":wails:RemovePendingNotification", [identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all delivered notifications from the notification center.
|
||||
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return Call(":wails:RemoveAllDeliveredNotifications");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific delivered notification by its identifier.
|
||||
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
|
||||
*
|
||||
* @export
|
||||
* @param {string} identifier - The ID of the notification to remove
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return Call(":wails:RemoveDeliveredNotification", [identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a notification by its identifier.
|
||||
* This is a convenience function that works across platforms.
|
||||
* On macOS, use the more specific RemovePendingNotification or RemoveDeliveredNotification functions.
|
||||
*
|
||||
* @export
|
||||
* @param {string} identifier - The ID of the notification to remove
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export function RemoveNotification(identifier) {
|
||||
return Call(":wails:RemoveNotification", [identifier]);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -246,4 +246,85 @@ export function OnFileDropOff() :void
|
|||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue