feat: wails3 重构
2
.gitignore
vendored
@ -362,3 +362,5 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
**/.DS_Store
|
||||||
6
wails/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.task
|
||||||
|
bin
|
||||||
|
frontend/dist
|
||||||
|
frontend/node_modules
|
||||||
|
build/linux/appimage/build
|
||||||
|
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||||
59
wails/README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Welcome to Your New Wails3 Project!
|
||||||
|
|
||||||
|
Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Navigate to your project directory in the terminal.
|
||||||
|
|
||||||
|
2. To run your application in development mode, use the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
wails3 dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start your application and enable hot-reloading for both frontend and backend changes.
|
||||||
|
|
||||||
|
3. To build your application for production, use:
|
||||||
|
|
||||||
|
```
|
||||||
|
wails3 build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a production-ready executable in the `build` directory.
|
||||||
|
|
||||||
|
## Exploring Wails3 Features
|
||||||
|
|
||||||
|
Now that you have your project set up, it's time to explore the features that Wails3 offers:
|
||||||
|
|
||||||
|
1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
|
||||||
|
|
||||||
|
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
|
||||||
|
|
||||||
|
```
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Some examples may be under development during the alpha phase.
|
||||||
|
|
||||||
|
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references.
|
||||||
|
|
||||||
|
4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
Take a moment to familiarize yourself with your project structure:
|
||||||
|
|
||||||
|
- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
|
||||||
|
- `main.go`: The entry point of your Go backend
|
||||||
|
- `app.go`: Define your application structure and methods here
|
||||||
|
- `wails.json`: Configuration file for your Wails project
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Modify the frontend in the `frontend/` directory to create your desired UI.
|
||||||
|
2. Add backend functionality in `main.go`.
|
||||||
|
3. Use `wails3 dev` to see your changes in real-time.
|
||||||
|
4. When ready, build your application with `wails3 build`.
|
||||||
|
|
||||||
|
Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
|
||||||
40
wails/Taskfile.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ./build/Taskfile.yml
|
||||||
|
windows: ./build/windows/Taskfile.yml
|
||||||
|
darwin: ./build/darwin/Taskfile.yml
|
||||||
|
linux: ./build/linux/Taskfile.yml
|
||||||
|
ios: ./build/ios/Taskfile.yml
|
||||||
|
android: ./build/android/Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
APP_NAME: "VideoConcat"
|
||||||
|
BIN_DIR: "bin"
|
||||||
|
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:build"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:package"
|
||||||
|
|
||||||
|
run:
|
||||||
|
summary: Runs the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:run"
|
||||||
|
|
||||||
|
dev:
|
||||||
|
summary: Runs the application in development mode
|
||||||
|
cmds:
|
||||||
|
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
|
||||||
|
|
||||||
|
setup:docker:
|
||||||
|
summary: Builds Docker image for cross-compilation (~800MB download)
|
||||||
|
cmds:
|
||||||
|
- task: common:setup:docker
|
||||||
1
wails/assets/assets/index-BaD48VVT.css
Normal file
17
wails/assets/assets/index-IwiMqFON.js
Normal file
30
wails/assets/index.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>视频拼接工具</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(to bottom, #fefefe, #ededef);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="module" crossorigin src="./assets/index-IwiMqFON.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="./assets/index-BaD48VVT.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
193
wails/build/Taskfile.yml
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
go:mod:tidy:
|
||||||
|
summary: Runs `go mod tidy`
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
install:frontend:deps:
|
||||||
|
summary: Install frontend dependencies
|
||||||
|
dir: frontend
|
||||||
|
sources:
|
||||||
|
- package.json
|
||||||
|
- package-lock.json
|
||||||
|
generates:
|
||||||
|
- node_modules
|
||||||
|
preconditions:
|
||||||
|
- sh: npm version
|
||||||
|
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
|
||||||
|
cmds:
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
build:frontend:
|
||||||
|
label: build:frontend (DEV={{.DEV}})
|
||||||
|
summary: Build the frontend project
|
||||||
|
dir: frontend
|
||||||
|
sources:
|
||||||
|
- "**/*"
|
||||||
|
generates:
|
||||||
|
- dist/**/*
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
- task: generate:bindings
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
cmds:
|
||||||
|
- npm run {{.BUILD_COMMAND}} -q
|
||||||
|
env:
|
||||||
|
PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
|
||||||
|
vars:
|
||||||
|
BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
|
||||||
|
|
||||||
|
|
||||||
|
frontend:vendor:puppertino:
|
||||||
|
summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling
|
||||||
|
sources:
|
||||||
|
- frontend/public/puppertino/puppertino.css
|
||||||
|
generates:
|
||||||
|
- frontend/public/puppertino/puppertino.css
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p frontend/public/puppertino
|
||||||
|
# If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error.
|
||||||
|
if [ ! -f frontend/public/puppertino/puppertino.css ]; then
|
||||||
|
echo "No bundled Puppertino found. Attempting to fetch from GitHub..."
|
||||||
|
if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true
|
||||||
|
echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css"
|
||||||
|
else
|
||||||
|
echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css"
|
||||||
|
fi
|
||||||
|
# Ensure index.html includes Puppertino CSS and button classes
|
||||||
|
INDEX_HTML=frontend/index.html
|
||||||
|
if [ -f "$INDEX_HTML" ]; then
|
||||||
|
if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then
|
||||||
|
# Insert Puppertino link tag after style.css link
|
||||||
|
awk '
|
||||||
|
/href="\/style.css"\/?/ && !x { print; print " <link rel=\"stylesheet\" href=\"/puppertino/puppertino.css\"/>"; x=1; next }1
|
||||||
|
' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML"
|
||||||
|
fi
|
||||||
|
# Replace default .btn with Puppertino primary button classes if present
|
||||||
|
sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
generate:bindings:
|
||||||
|
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
|
||||||
|
summary: Generates bindings for the frontend
|
||||||
|
deps:
|
||||||
|
- task: go:mod:tidy
|
||||||
|
sources:
|
||||||
|
- "**/*.[jt]s"
|
||||||
|
- exclude: frontend/**/*
|
||||||
|
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
generates:
|
||||||
|
- frontend/bindings/**/*
|
||||||
|
cmds:
|
||||||
|
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
|
||||||
|
|
||||||
|
generate:icons:
|
||||||
|
summary: Generates Windows `.ico` and Mac `.icns` files from an image
|
||||||
|
dir: build
|
||||||
|
sources:
|
||||||
|
- "appicon.png"
|
||||||
|
generates:
|
||||||
|
- "darwin/icons.icns"
|
||||||
|
- "windows/icon.ico"
|
||||||
|
cmds:
|
||||||
|
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico
|
||||||
|
|
||||||
|
dev:frontend:
|
||||||
|
summary: Runs the frontend in development mode
|
||||||
|
dir: frontend
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
cmds:
|
||||||
|
- npm run dev -- --port {{.VITE_PORT}} --strictPort
|
||||||
|
|
||||||
|
update:build-assets:
|
||||||
|
summary: Updates the build assets
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
|
||||||
|
|
||||||
|
setup:docker:
|
||||||
|
summary: Builds Docker image for cross-compilation (~800MB download)
|
||||||
|
desc: |
|
||||||
|
Builds the Docker image needed for cross-compiling to any platform.
|
||||||
|
Run this once to enable cross-platform builds from any OS.
|
||||||
|
cmds:
|
||||||
|
- docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required. Please install Docker first."
|
||||||
|
|
||||||
|
ios:device:list:
|
||||||
|
summary: Lists connected iOS devices (UDIDs)
|
||||||
|
cmds:
|
||||||
|
- xcrun xcdevice list
|
||||||
|
|
||||||
|
ios:run:device:
|
||||||
|
summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl)
|
||||||
|
vars:
|
||||||
|
PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/<YourProject>.xcodeproj
|
||||||
|
SCHEME: '{{.SCHEME}}' # e.g., ios.dev
|
||||||
|
CONFIG: '{{.CONFIG | default "Debug"}}'
|
||||||
|
DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}'
|
||||||
|
UDID: '{{.UDID}}' # from `task ios:device:list`
|
||||||
|
BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev
|
||||||
|
TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing
|
||||||
|
preconditions:
|
||||||
|
- sh: xcrun -f xcodebuild
|
||||||
|
msg: "xcodebuild not found. Please install Xcode."
|
||||||
|
- sh: xcrun -f devicectl
|
||||||
|
msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)."
|
||||||
|
- sh: test -n '{{.PROJECT}}'
|
||||||
|
msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)."
|
||||||
|
- sh: test -n '{{.SCHEME}}'
|
||||||
|
msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)."
|
||||||
|
- sh: test -n '{{.UDID}}'
|
||||||
|
msg: "Set UDID to your device UDID (see: task ios:device:list)."
|
||||||
|
- sh: test -n '{{.BUNDLE_ID}}'
|
||||||
|
msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)."
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}"
|
||||||
|
XCB_ARGS=(
|
||||||
|
-project "{{.PROJECT}}"
|
||||||
|
-scheme "{{.SCHEME}}"
|
||||||
|
-configuration "{{.CONFIG}}"
|
||||||
|
-destination "id={{.UDID}}"
|
||||||
|
-derivedDataPath "{{.DERIVED}}"
|
||||||
|
-allowProvisioningUpdates
|
||||||
|
-allowProvisioningDeviceRegistration
|
||||||
|
)
|
||||||
|
# Optionally inject signing identifiers if provided
|
||||||
|
if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi
|
||||||
|
if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi
|
||||||
|
xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true
|
||||||
|
# If xcpretty isn't installed, run without it
|
||||||
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||||||
|
xcodebuild "${XCB_ARGS[@]}" build
|
||||||
|
fi
|
||||||
|
# Find built .app
|
||||||
|
APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1)
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Installing: $APP_PATH"
|
||||||
|
xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH"
|
||||||
|
echo "Launching: {{.BUNDLE_ID}}"
|
||||||
|
xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}"
|
||||||
237
wails/build/android/Taskfile.yml
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
APP_ID: '{{.APP_ID | default "com.wails.app"}}'
|
||||||
|
MIN_SDK: '21'
|
||||||
|
TARGET_SDK: '34'
|
||||||
|
NDK_VERSION: 'r26d'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
install:deps:
|
||||||
|
summary: Check and install Android development dependencies
|
||||||
|
cmds:
|
||||||
|
- go run build/android/scripts/deps/install_deps.go
|
||||||
|
env:
|
||||||
|
TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}'
|
||||||
|
prompt: This will check and install Android development dependencies. Continue?
|
||||||
|
|
||||||
|
build:
|
||||||
|
summary: Creates a build of the application for Android
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: generate:android:bindings
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
PRODUCTION:
|
||||||
|
ref: .PRODUCTION
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- echo "Building Android app {{.APP_NAME}}..."
|
||||||
|
- task: compile:go:shared
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default "arm64"}}'
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
env:
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
|
||||||
|
compile:go:shared:
|
||||||
|
summary: Compile Go code to shared library (.so)
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/{{.NDK_VERSION}}}"
|
||||||
|
if [ ! -d "$NDK_ROOT" ]; then
|
||||||
|
echo "Error: Android NDK not found at $NDK_ROOT"
|
||||||
|
echo "Please set ANDROID_NDK_HOME or install NDK {{.NDK_VERSION}} via Android Studio"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine toolchain based on host OS
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin) HOST_TAG="darwin-x86_64" ;;
|
||||||
|
Linux) HOST_TAG="linux-x86_64" ;;
|
||||||
|
*) echo "Unsupported host OS"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
TOOLCHAIN="$NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG"
|
||||||
|
|
||||||
|
# Set compiler based on architecture
|
||||||
|
case "{{.ARCH}}" in
|
||||||
|
arm64)
|
||||||
|
export CC="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang"
|
||||||
|
export CXX="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang++"
|
||||||
|
export GOARCH=arm64
|
||||||
|
JNI_DIR="arm64-v8a"
|
||||||
|
;;
|
||||||
|
amd64|x86_64)
|
||||||
|
export CC="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang"
|
||||||
|
export CXX="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang++"
|
||||||
|
export GOARCH=amd64
|
||||||
|
JNI_DIR="x86_64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: {{.ARCH}}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
export GOOS=android
|
||||||
|
|
||||||
|
mkdir -p {{.BIN_DIR}}
|
||||||
|
mkdir -p build/android/app/src/main/jniLibs/$JNI_DIR
|
||||||
|
|
||||||
|
go build -buildmode=c-shared {{.BUILD_FLAGS}} \
|
||||||
|
-o build/android/app/src/main/jniLibs/$JNI_DIR/libwails.so
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
|
||||||
|
compile:go:all-archs:
|
||||||
|
summary: Compile Go code for all Android architectures (fat APK)
|
||||||
|
cmds:
|
||||||
|
- task: compile:go:shared
|
||||||
|
vars:
|
||||||
|
ARCH: arm64
|
||||||
|
- task: compile:go:shared
|
||||||
|
vars:
|
||||||
|
ARCH: amd64
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application into an APK
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: assemble:apk
|
||||||
|
|
||||||
|
package:fat:
|
||||||
|
summary: Packages a production build for all architectures (fat APK)
|
||||||
|
cmds:
|
||||||
|
- task: compile:go:all-archs
|
||||||
|
- task: assemble:apk
|
||||||
|
|
||||||
|
assemble:apk:
|
||||||
|
summary: Assembles the APK using Gradle
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
cd build/android
|
||||||
|
./gradlew assembleDebug
|
||||||
|
cp app/build/outputs/apk/debug/app-debug.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk"
|
||||||
|
echo "APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk"
|
||||||
|
|
||||||
|
assemble:apk:release:
|
||||||
|
summary: Assembles a release APK using Gradle
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
cd build/android
|
||||||
|
./gradlew assembleRelease
|
||||||
|
cp app/build/outputs/apk/release/app-release-unsigned.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}-release.apk"
|
||||||
|
echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}-release.apk"
|
||||||
|
|
||||||
|
generate:android:bindings:
|
||||||
|
internal: true
|
||||||
|
summary: Generates bindings for Android
|
||||||
|
sources:
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
generates:
|
||||||
|
- frontend/bindings/**/*
|
||||||
|
cmds:
|
||||||
|
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
|
||||||
|
env:
|
||||||
|
GOOS: android
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default "arm64"}}'
|
||||||
|
|
||||||
|
ensure-emulator:
|
||||||
|
internal: true
|
||||||
|
summary: Ensure Android Emulator is running
|
||||||
|
silent: true
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
# Check if an emulator is already running
|
||||||
|
if adb devices | grep -q "emulator"; then
|
||||||
|
echo "Emulator already running"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get first available AVD
|
||||||
|
AVD_NAME=$(emulator -list-avds | head -1)
|
||||||
|
if [ -z "$AVD_NAME" ]; then
|
||||||
|
echo "No Android Virtual Devices found."
|
||||||
|
echo "Create one using: Android Studio > Tools > Device Manager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting emulator: $AVD_NAME"
|
||||||
|
emulator -avd "$AVD_NAME" -no-snapshot-load &
|
||||||
|
|
||||||
|
# Wait for emulator to boot (max 60 seconds)
|
||||||
|
echo "Waiting for emulator to boot..."
|
||||||
|
adb wait-for-device
|
||||||
|
|
||||||
|
for i in {1..60}; do
|
||||||
|
BOOT_COMPLETED=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
|
||||||
|
if [ "$BOOT_COMPLETED" = "1" ]; then
|
||||||
|
echo "Emulator booted successfully"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Emulator boot timeout"
|
||||||
|
exit 1
|
||||||
|
preconditions:
|
||||||
|
- sh: command -v adb
|
||||||
|
msg: "adb not found. Please install Android SDK and add platform-tools to PATH"
|
||||||
|
- sh: command -v emulator
|
||||||
|
msg: "emulator not found. Please install Android SDK and add emulator to PATH"
|
||||||
|
|
||||||
|
deploy-emulator:
|
||||||
|
summary: Deploy to Android Emulator
|
||||||
|
deps: [package]
|
||||||
|
cmds:
|
||||||
|
- adb uninstall {{.APP_ID}} 2>/dev/null || true
|
||||||
|
- adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"
|
||||||
|
- adb shell am start -n {{.APP_ID}}/.MainActivity
|
||||||
|
|
||||||
|
run:
|
||||||
|
summary: Run the application in Android Emulator
|
||||||
|
deps:
|
||||||
|
- task: ensure-emulator
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
ARCH: x86_64
|
||||||
|
cmds:
|
||||||
|
- task: assemble:apk
|
||||||
|
- adb uninstall {{.APP_ID}} 2>/dev/null || true
|
||||||
|
- adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"
|
||||||
|
- adb shell am start -n {{.APP_ID}}/.MainActivity
|
||||||
|
|
||||||
|
logs:
|
||||||
|
summary: Stream Android logcat filtered to this app
|
||||||
|
cmds:
|
||||||
|
- adb logcat -v time | grep -E "(Wails|{{.APP_NAME}})"
|
||||||
|
|
||||||
|
logs:all:
|
||||||
|
summary: Stream all Android logcat (verbose)
|
||||||
|
cmds:
|
||||||
|
- adb logcat -v time
|
||||||
|
|
||||||
|
clean:
|
||||||
|
summary: Clean build artifacts
|
||||||
|
cmds:
|
||||||
|
- rm -rf {{.BIN_DIR}}
|
||||||
|
- rm -rf build/android/app/build
|
||||||
|
- rm -rf build/android/app/src/main/jniLibs/*/libwails.so
|
||||||
|
- rm -rf build/android/.gradle
|
||||||
63
wails/build/android/app/build.gradle
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.wails.app'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.wails.app"
|
||||||
|
minSdk 21
|
||||||
|
targetSdk 34
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
// Configure supported ABIs
|
||||||
|
ndk {
|
||||||
|
abiFilters 'arm64-v8a', 'x86_64'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
debuggable true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source sets configuration
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
// JNI libraries are in jniLibs folder
|
||||||
|
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||||
|
// Assets for the WebView
|
||||||
|
assets.srcDirs = ['src/main/assets']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packaging options
|
||||||
|
packagingOptions {
|
||||||
|
// Don't strip Go symbols in debug builds
|
||||||
|
doNotStrip '*/arm64-v8a/libwails.so'
|
||||||
|
doNotStrip '*/x86_64/libwails.so'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'androidx.webkit:webkit:1.9.0'
|
||||||
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
}
|
||||||
12
wails/build/android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
|
||||||
|
# Keep native methods
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep Wails bridge classes
|
||||||
|
-keep class com.wails.app.WailsBridge { *; }
|
||||||
|
-keep class com.wails.app.WailsJSBridge { *; }
|
||||||
30
wails/build/android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<!-- Internet permission for WebView -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.WailsApp"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,198 @@
|
|||||||
|
package com.wails.app;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.WebResourceRequest;
|
||||||
|
import android.webkit.WebResourceResponse;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.webkit.WebViewAssetLoader;
|
||||||
|
import com.wails.app.BuildConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MainActivity hosts the WebView and manages the Wails application lifecycle.
|
||||||
|
* It uses WebViewAssetLoader to serve assets from the Go library without
|
||||||
|
* requiring a network server.
|
||||||
|
*/
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
private static final String TAG = "WailsActivity";
|
||||||
|
private static final String WAILS_SCHEME = "https";
|
||||||
|
private static final String WAILS_HOST = "wails.localhost";
|
||||||
|
|
||||||
|
private WebView webView;
|
||||||
|
private WailsBridge bridge;
|
||||||
|
private WebViewAssetLoader assetLoader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
// Initialize the native Go library
|
||||||
|
bridge = new WailsBridge(this);
|
||||||
|
bridge.initialize();
|
||||||
|
|
||||||
|
// Set up WebView
|
||||||
|
setupWebView();
|
||||||
|
|
||||||
|
// Load the application
|
||||||
|
loadApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private void setupWebView() {
|
||||||
|
webView = findViewById(R.id.webview);
|
||||||
|
|
||||||
|
// Configure WebView settings
|
||||||
|
WebSettings settings = webView.getSettings();
|
||||||
|
settings.setJavaScriptEnabled(true);
|
||||||
|
settings.setDomStorageEnabled(true);
|
||||||
|
settings.setDatabaseEnabled(true);
|
||||||
|
settings.setAllowFileAccess(false);
|
||||||
|
settings.setAllowContentAccess(false);
|
||||||
|
settings.setMediaPlaybackRequiresUserGesture(false);
|
||||||
|
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
|
||||||
|
|
||||||
|
// Enable debugging in debug builds
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up asset loader for serving local assets
|
||||||
|
assetLoader = new WebViewAssetLoader.Builder()
|
||||||
|
.setDomain(WAILS_HOST)
|
||||||
|
.addPathHandler("/", new WailsPathHandler(bridge))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Set up WebView client to intercept requests
|
||||||
|
webView.setWebViewClient(new WebViewClient() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||||
|
String url = request.getUrl().toString();
|
||||||
|
Log.d(TAG, "Intercepting request: " + url);
|
||||||
|
|
||||||
|
// Handle wails.localhost requests
|
||||||
|
if (request.getUrl().getHost() != null &&
|
||||||
|
request.getUrl().getHost().equals(WAILS_HOST)) {
|
||||||
|
|
||||||
|
// For wails API calls (runtime, capabilities, etc.), we need to pass the full URL
|
||||||
|
// including query string because WebViewAssetLoader.PathHandler strips query params
|
||||||
|
String path = request.getUrl().getPath();
|
||||||
|
if (path != null && path.startsWith("/wails/")) {
|
||||||
|
// Get full path with query string for runtime calls
|
||||||
|
String fullPath = path;
|
||||||
|
String query = request.getUrl().getQuery();
|
||||||
|
if (query != null && !query.isEmpty()) {
|
||||||
|
fullPath = path + "?" + query;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Wails API call detected, full path: " + fullPath);
|
||||||
|
|
||||||
|
// Call bridge directly with full path
|
||||||
|
byte[] data = bridge.serveAsset(fullPath, request.getMethod(), "{}");
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
java.io.InputStream inputStream = new java.io.ByteArrayInputStream(data);
|
||||||
|
java.util.Map<String, String> headers = new java.util.HashMap<>();
|
||||||
|
headers.put("Access-Control-Allow-Origin", "*");
|
||||||
|
headers.put("Cache-Control", "no-cache");
|
||||||
|
headers.put("Content-Type", "application/json");
|
||||||
|
|
||||||
|
return new WebResourceResponse(
|
||||||
|
"application/json",
|
||||||
|
"UTF-8",
|
||||||
|
200,
|
||||||
|
"OK",
|
||||||
|
headers,
|
||||||
|
inputStream
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Return error response if data is null
|
||||||
|
return new WebResourceResponse(
|
||||||
|
"application/json",
|
||||||
|
"UTF-8",
|
||||||
|
500,
|
||||||
|
"Internal Error",
|
||||||
|
new java.util.HashMap<>(),
|
||||||
|
new java.io.ByteArrayInputStream("{}".getBytes())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular assets, use the asset loader
|
||||||
|
return assetLoader.shouldInterceptRequest(request.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.shouldInterceptRequest(view, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
Log.d(TAG, "Page loaded: " + url);
|
||||||
|
// Inject Wails runtime
|
||||||
|
bridge.injectRuntime(webView, url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add JavaScript interface for Go communication
|
||||||
|
webView.addJavascriptInterface(new WailsJSBridge(bridge, webView), "wails");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadApplication() {
|
||||||
|
// Load the main page from the asset server
|
||||||
|
String url = WAILS_SCHEME + "://" + WAILS_HOST + "/";
|
||||||
|
Log.d(TAG, "Loading URL: " + url);
|
||||||
|
webView.loadUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute JavaScript in the WebView from the Go side
|
||||||
|
*/
|
||||||
|
public void executeJavaScript(final String js) {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (webView != null) {
|
||||||
|
webView.evaluateJavascript(js, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (bridge != null) {
|
||||||
|
bridge.onResume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (bridge != null) {
|
||||||
|
bridge.onPause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (bridge != null) {
|
||||||
|
bridge.shutdown();
|
||||||
|
}
|
||||||
|
if (webView != null) {
|
||||||
|
webView.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (webView != null && webView.canGoBack()) {
|
||||||
|
webView.goBack();
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,214 @@
|
|||||||
|
package com.wails.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WailsBridge manages the connection between the Java/Android side and the Go native library.
|
||||||
|
* It handles:
|
||||||
|
* - Loading and initializing the native Go library
|
||||||
|
* - Serving asset requests from Go
|
||||||
|
* - Passing messages between JavaScript and Go
|
||||||
|
* - Managing callbacks for async operations
|
||||||
|
*/
|
||||||
|
public class WailsBridge {
|
||||||
|
private static final String TAG = "WailsBridge";
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Load the native Go library
|
||||||
|
System.loadLibrary("wails");
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final AtomicInteger callbackIdGenerator = new AtomicInteger(0);
|
||||||
|
private final ConcurrentHashMap<Integer, AssetCallback> pendingAssetCallbacks = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<Integer, MessageCallback> pendingMessageCallbacks = new ConcurrentHashMap<>();
|
||||||
|
private WebView webView;
|
||||||
|
private volatile boolean initialized = false;
|
||||||
|
|
||||||
|
// Native methods - implemented in Go
|
||||||
|
private static native void nativeInit(WailsBridge bridge);
|
||||||
|
private static native void nativeShutdown();
|
||||||
|
private static native void nativeOnResume();
|
||||||
|
private static native void nativeOnPause();
|
||||||
|
private static native void nativeOnPageFinished(String url);
|
||||||
|
private static native byte[] nativeServeAsset(String path, String method, String headers);
|
||||||
|
private static native String nativeHandleMessage(String message);
|
||||||
|
private static native String nativeGetAssetMimeType(String path);
|
||||||
|
|
||||||
|
public WailsBridge(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the native Go library
|
||||||
|
*/
|
||||||
|
public void initialize() {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Initializing Wails bridge...");
|
||||||
|
try {
|
||||||
|
nativeInit(this);
|
||||||
|
initialized = true;
|
||||||
|
Log.i(TAG, "Wails bridge initialized successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to initialize Wails bridge", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown the native Go library
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Shutting down Wails bridge...");
|
||||||
|
try {
|
||||||
|
nativeShutdown();
|
||||||
|
initialized = false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error during shutdown", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the activity resumes
|
||||||
|
*/
|
||||||
|
public void onResume() {
|
||||||
|
if (initialized) {
|
||||||
|
nativeOnResume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the activity pauses
|
||||||
|
*/
|
||||||
|
public void onPause() {
|
||||||
|
if (initialized) {
|
||||||
|
nativeOnPause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve an asset from the Go asset server
|
||||||
|
* @param path The URL path requested
|
||||||
|
* @param method The HTTP method
|
||||||
|
* @param headers The request headers as JSON
|
||||||
|
* @return The asset data, or null if not found
|
||||||
|
*/
|
||||||
|
public byte[] serveAsset(String path, String method, String headers) {
|
||||||
|
if (!initialized) {
|
||||||
|
Log.w(TAG, "Bridge not initialized, cannot serve asset: " + path);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Serving asset: " + path);
|
||||||
|
try {
|
||||||
|
return nativeServeAsset(path, method, headers);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error serving asset: " + path, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the MIME type for an asset
|
||||||
|
* @param path The asset path
|
||||||
|
* @return The MIME type string
|
||||||
|
*/
|
||||||
|
public String getAssetMimeType(String path) {
|
||||||
|
if (!initialized) {
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String mimeType = nativeGetAssetMimeType(path);
|
||||||
|
return mimeType != null ? mimeType : "application/octet-stream";
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error getting MIME type for: " + path, e);
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a message from JavaScript
|
||||||
|
* @param message The message from JavaScript (JSON)
|
||||||
|
* @return The response to send back to JavaScript (JSON)
|
||||||
|
*/
|
||||||
|
public String handleMessage(String message) {
|
||||||
|
if (!initialized) {
|
||||||
|
Log.w(TAG, "Bridge not initialized, cannot handle message");
|
||||||
|
return "{\"error\":\"Bridge not initialized\"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Handling message from JS: " + message);
|
||||||
|
try {
|
||||||
|
return nativeHandleMessage(message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error handling message", e);
|
||||||
|
return "{\"error\":\"" + e.getMessage() + "\"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject the Wails runtime JavaScript into the WebView.
|
||||||
|
* Called when the page finishes loading.
|
||||||
|
* @param webView The WebView to inject into
|
||||||
|
* @param url The URL that finished loading
|
||||||
|
*/
|
||||||
|
public void injectRuntime(WebView webView, String url) {
|
||||||
|
this.webView = webView;
|
||||||
|
// Notify Go side that page has finished loading so it can inject the runtime
|
||||||
|
Log.d(TAG, "Page finished loading: " + url + ", notifying Go side");
|
||||||
|
if (initialized) {
|
||||||
|
nativeOnPageFinished(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute JavaScript in the WebView (called from Go side)
|
||||||
|
* @param js The JavaScript code to execute
|
||||||
|
*/
|
||||||
|
public void executeJavaScript(String js) {
|
||||||
|
if (webView != null) {
|
||||||
|
webView.post(() -> webView.evaluateJavascript(js, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from Go when an event needs to be emitted to JavaScript
|
||||||
|
* @param eventName The event name
|
||||||
|
* @param eventData The event data (JSON)
|
||||||
|
*/
|
||||||
|
public void emitEvent(String eventName, String eventData) {
|
||||||
|
String js = String.format("window.wails && window.wails._emit('%s', %s);",
|
||||||
|
escapeJsString(eventName), eventData);
|
||||||
|
executeJavaScript(js);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJsString(String str) {
|
||||||
|
return str.replace("\\", "\\\\")
|
||||||
|
.replace("'", "\\'")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback interfaces
|
||||||
|
public interface AssetCallback {
|
||||||
|
void onAssetReady(byte[] data, String mimeType);
|
||||||
|
void onAssetError(String error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface MessageCallback {
|
||||||
|
void onResponse(String response);
|
||||||
|
void onError(String error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
package com.wails.app;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.JavascriptInterface;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import com.wails.app.BuildConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WailsJSBridge provides the JavaScript interface that allows the web frontend
|
||||||
|
* to communicate with the Go backend. This is exposed to JavaScript as the
|
||||||
|
* `window.wails` object.
|
||||||
|
*
|
||||||
|
* Similar to iOS's WKScriptMessageHandler but using Android's addJavascriptInterface.
|
||||||
|
*/
|
||||||
|
public class WailsJSBridge {
|
||||||
|
private static final String TAG = "WailsJSBridge";
|
||||||
|
|
||||||
|
private final WailsBridge bridge;
|
||||||
|
private final WebView webView;
|
||||||
|
|
||||||
|
public WailsJSBridge(WailsBridge bridge, WebView webView) {
|
||||||
|
this.bridge = bridge;
|
||||||
|
this.webView = webView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to Go and return the response synchronously.
|
||||||
|
* Called from JavaScript: wails.invoke(message)
|
||||||
|
*
|
||||||
|
* @param message The message to send (JSON string)
|
||||||
|
* @return The response from Go (JSON string)
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public String invoke(String message) {
|
||||||
|
Log.d(TAG, "Invoke called: " + message);
|
||||||
|
return bridge.handleMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to Go asynchronously.
|
||||||
|
* The response will be sent back via a callback.
|
||||||
|
* Called from JavaScript: wails.invokeAsync(callbackId, message)
|
||||||
|
*
|
||||||
|
* @param callbackId The callback ID to use for the response
|
||||||
|
* @param message The message to send (JSON string)
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public void invokeAsync(final String callbackId, final String message) {
|
||||||
|
Log.d(TAG, "InvokeAsync called: " + message);
|
||||||
|
|
||||||
|
// Handle in background thread to not block JavaScript
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String response = bridge.handleMessage(message);
|
||||||
|
sendCallback(callbackId, response, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in async invoke", e);
|
||||||
|
sendCallback(callbackId, null, e.getMessage());
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a message from JavaScript to Android's logcat
|
||||||
|
* Called from JavaScript: wails.log(level, message)
|
||||||
|
*
|
||||||
|
* @param level The log level (debug, info, warn, error)
|
||||||
|
* @param message The message to log
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public void log(String level, String message) {
|
||||||
|
switch (level.toLowerCase()) {
|
||||||
|
case "debug":
|
||||||
|
Log.d(TAG + "/JS", message);
|
||||||
|
break;
|
||||||
|
case "info":
|
||||||
|
Log.i(TAG + "/JS", message);
|
||||||
|
break;
|
||||||
|
case "warn":
|
||||||
|
Log.w(TAG + "/JS", message);
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
Log.e(TAG + "/JS", message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.v(TAG + "/JS", message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the platform name
|
||||||
|
* Called from JavaScript: wails.platform()
|
||||||
|
*
|
||||||
|
* @return "android"
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public String platform() {
|
||||||
|
return "android";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're running in debug mode
|
||||||
|
* Called from JavaScript: wails.isDebug()
|
||||||
|
*
|
||||||
|
* @return true if debug build, false otherwise
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
public boolean isDebug() {
|
||||||
|
return BuildConfig.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a callback response to JavaScript
|
||||||
|
*/
|
||||||
|
private void sendCallback(String callbackId, String result, String error) {
|
||||||
|
final String js;
|
||||||
|
if (error != null) {
|
||||||
|
js = String.format(
|
||||||
|
"window.wails && window.wails._callback('%s', null, '%s');",
|
||||||
|
escapeJsString(callbackId),
|
||||||
|
escapeJsString(error)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
js = String.format(
|
||||||
|
"window.wails && window.wails._callback('%s', %s, null);",
|
||||||
|
escapeJsString(callbackId),
|
||||||
|
result != null ? result : "null"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
webView.post(() -> webView.evaluateJavascript(js, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJsString(String str) {
|
||||||
|
if (str == null) return "";
|
||||||
|
return str.replace("\\", "\\\\")
|
||||||
|
.replace("'", "\\'")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
package com.wails.app;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.WebResourceResponse;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.webkit.WebViewAssetLoader;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WailsPathHandler implements WebViewAssetLoader.PathHandler to serve assets
|
||||||
|
* from the Go asset server. This allows the WebView to load assets without
|
||||||
|
* using a network server, similar to iOS's WKURLSchemeHandler.
|
||||||
|
*/
|
||||||
|
public class WailsPathHandler implements WebViewAssetLoader.PathHandler {
|
||||||
|
private static final String TAG = "WailsPathHandler";
|
||||||
|
|
||||||
|
private final WailsBridge bridge;
|
||||||
|
|
||||||
|
public WailsPathHandler(WailsBridge bridge) {
|
||||||
|
this.bridge = bridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public WebResourceResponse handle(@NonNull String path) {
|
||||||
|
Log.d(TAG, "Handling path: " + path);
|
||||||
|
|
||||||
|
// Normalize path
|
||||||
|
if (path.isEmpty() || path.equals("/")) {
|
||||||
|
path = "/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get asset from Go
|
||||||
|
byte[] data = bridge.serveAsset(path, "GET", "{}");
|
||||||
|
|
||||||
|
if (data == null || data.length == 0) {
|
||||||
|
Log.w(TAG, "Asset not found: " + path);
|
||||||
|
return null; // Return null to let WebView handle 404
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine MIME type
|
||||||
|
String mimeType = bridge.getAssetMimeType(path);
|
||||||
|
Log.d(TAG, "Serving " + path + " with type " + mimeType + " (" + data.length + " bytes)");
|
||||||
|
|
||||||
|
// Create response
|
||||||
|
InputStream inputStream = new ByteArrayInputStream(data);
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("Access-Control-Allow-Origin", "*");
|
||||||
|
headers.put("Cache-Control", "no-cache");
|
||||||
|
|
||||||
|
return new WebResourceResponse(
|
||||||
|
mimeType,
|
||||||
|
"UTF-8",
|
||||||
|
200,
|
||||||
|
"OK",
|
||||||
|
headers,
|
||||||
|
inputStream
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine MIME type from file extension
|
||||||
|
*/
|
||||||
|
private String getMimeType(String path) {
|
||||||
|
String lowerPath = path.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")) {
|
||||||
|
return "text/html";
|
||||||
|
} else if (lowerPath.endsWith(".js") || lowerPath.endsWith(".mjs")) {
|
||||||
|
return "application/javascript";
|
||||||
|
} else if (lowerPath.endsWith(".css")) {
|
||||||
|
return "text/css";
|
||||||
|
} else if (lowerPath.endsWith(".json")) {
|
||||||
|
return "application/json";
|
||||||
|
} else if (lowerPath.endsWith(".png")) {
|
||||||
|
return "image/png";
|
||||||
|
} else if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) {
|
||||||
|
return "image/jpeg";
|
||||||
|
} else if (lowerPath.endsWith(".gif")) {
|
||||||
|
return "image/gif";
|
||||||
|
} else if (lowerPath.endsWith(".svg")) {
|
||||||
|
return "image/svg+xml";
|
||||||
|
} else if (lowerPath.endsWith(".ico")) {
|
||||||
|
return "image/x-icon";
|
||||||
|
} else if (lowerPath.endsWith(".woff")) {
|
||||||
|
return "font/woff";
|
||||||
|
} else if (lowerPath.endsWith(".woff2")) {
|
||||||
|
return "font/woff2";
|
||||||
|
} else if (lowerPath.endsWith(".ttf")) {
|
||||||
|
return "font/ttf";
|
||||||
|
} else if (lowerPath.endsWith(".eot")) {
|
||||||
|
return "application/vnd.ms-fontobject";
|
||||||
|
} else if (lowerPath.endsWith(".xml")) {
|
||||||
|
return "application/xml";
|
||||||
|
} else if (lowerPath.endsWith(".txt")) {
|
||||||
|
return "text/plain";
|
||||||
|
} else if (lowerPath.endsWith(".wasm")) {
|
||||||
|
return "application/wasm";
|
||||||
|
} else if (lowerPath.endsWith(".mp3")) {
|
||||||
|
return "audio/mpeg";
|
||||||
|
} else if (lowerPath.endsWith(".mp4")) {
|
||||||
|
return "video/mp4";
|
||||||
|
} else if (lowerPath.endsWith(".webm")) {
|
||||||
|
return "video/webm";
|
||||||
|
} else if (lowerPath.endsWith(".webp")) {
|
||||||
|
return "image/webp";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/main_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
BIN
wails/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
BIN
wails/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
8
wails/build/android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="wails_blue">#3574D4</color>
|
||||||
|
<color name="wails_blue_dark">#2C5FB8</color>
|
||||||
|
<color name="wails_background">#1B2636</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
</resources>
|
||||||
4
wails/build/android/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Wails App</string>
|
||||||
|
</resources>
|
||||||
14
wails/build/android/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.WailsApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/wails_blue</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/wails_blue_dark</item>
|
||||||
|
<item name="colorOnPrimary">@android:color/white</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor">@color/wails_background</item>
|
||||||
|
<item name="android:navigationBarColor">@color/wails_background</item>
|
||||||
|
<!-- Window background -->
|
||||||
|
<item name="android:windowBackground">@color/wails_background</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
4
wails/build/android/build.gradle
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '8.7.3' apply false
|
||||||
|
}
|
||||||
26
wails/build/android/gradle.properties
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
|
# https://developer.android.com/build/optimize-your-build#parallel
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
BIN
wails/build/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
wails/build/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
248
wails/build/android/gradlew
vendored
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
93
wails/build/android/gradlew.bat
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
11
wails/build/android/main_android.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Register main function to be called when the Android app initializes
|
||||||
|
// This is necessary because in c-shared build mode, main() is not automatically called
|
||||||
|
application.RegisterAndroidMain(main)
|
||||||
|
}
|
||||||
151
wails/build/android/scripts/deps/install_deps.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Checking Android development dependencies...")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
errors := []string{}
|
||||||
|
|
||||||
|
// Check Go
|
||||||
|
if !checkCommand("go", "version") {
|
||||||
|
errors = append(errors, "Go is not installed. Install from https://go.dev/dl/")
|
||||||
|
} else {
|
||||||
|
fmt.Println("✓ Go is installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ANDROID_HOME
|
||||||
|
androidHome := os.Getenv("ANDROID_HOME")
|
||||||
|
if androidHome == "" {
|
||||||
|
androidHome = os.Getenv("ANDROID_SDK_ROOT")
|
||||||
|
}
|
||||||
|
if androidHome == "" {
|
||||||
|
// Try common default locations
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
possiblePaths := []string{
|
||||||
|
filepath.Join(home, "Android", "Sdk"),
|
||||||
|
filepath.Join(home, "Library", "Android", "sdk"),
|
||||||
|
"/usr/local/share/android-sdk",
|
||||||
|
}
|
||||||
|
for _, p := range possiblePaths {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
androidHome = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if androidHome == "" {
|
||||||
|
errors = append(errors, "ANDROID_HOME not set. Install Android Studio and set ANDROID_HOME environment variable")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ ANDROID_HOME: %s\n", androidHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check adb
|
||||||
|
if !checkCommand("adb", "version") {
|
||||||
|
if androidHome != "" {
|
||||||
|
platformTools := filepath.Join(androidHome, "platform-tools")
|
||||||
|
errors = append(errors, fmt.Sprintf("adb not found. Add %s to PATH", platformTools))
|
||||||
|
} else {
|
||||||
|
errors = append(errors, "adb not found. Install Android SDK Platform-Tools")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("✓ adb is installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check emulator
|
||||||
|
if !checkCommand("emulator", "-list-avds") {
|
||||||
|
if androidHome != "" {
|
||||||
|
emulatorPath := filepath.Join(androidHome, "emulator")
|
||||||
|
errors = append(errors, fmt.Sprintf("emulator not found. Add %s to PATH", emulatorPath))
|
||||||
|
} else {
|
||||||
|
errors = append(errors, "emulator not found. Install Android Emulator via SDK Manager")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("✓ Android Emulator is installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check NDK
|
||||||
|
ndkHome := os.Getenv("ANDROID_NDK_HOME")
|
||||||
|
if ndkHome == "" && androidHome != "" {
|
||||||
|
// Look for NDK in default location
|
||||||
|
ndkDir := filepath.Join(androidHome, "ndk")
|
||||||
|
if entries, err := os.ReadDir(ndkDir); err == nil {
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
ndkHome = filepath.Join(ndkDir, entry.Name())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ndkHome == "" {
|
||||||
|
errors = append(errors, "Android NDK not found. Install NDK via Android Studio > SDK Manager > SDK Tools > NDK (Side by side)")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ Android NDK: %s\n", ndkHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Java
|
||||||
|
if !checkCommand("java", "-version") {
|
||||||
|
errors = append(errors, "Java not found. Install JDK 11+ (OpenJDK recommended)")
|
||||||
|
} else {
|
||||||
|
fmt.Println("✓ Java is installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for AVD (Android Virtual Device)
|
||||||
|
if checkCommand("emulator", "-list-avds") {
|
||||||
|
cmd := exec.Command("emulator", "-list-avds")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
|
avds := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
fmt.Printf("✓ Found %d Android Virtual Device(s)\n", len(avds))
|
||||||
|
} else {
|
||||||
|
fmt.Println("⚠ No Android Virtual Devices found. Create one via Android Studio > Tools > Device Manager")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
fmt.Println("❌ Missing dependencies:")
|
||||||
|
for _, err := range errors {
|
||||||
|
fmt.Printf(" - %s\n", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Setup instructions:")
|
||||||
|
fmt.Println("1. Install Android Studio: https://developer.android.com/studio")
|
||||||
|
fmt.Println("2. Open SDK Manager and install:")
|
||||||
|
fmt.Println(" - Android SDK Platform (API 34)")
|
||||||
|
fmt.Println(" - Android SDK Build-Tools")
|
||||||
|
fmt.Println(" - Android SDK Platform-Tools")
|
||||||
|
fmt.Println(" - Android Emulator")
|
||||||
|
fmt.Println(" - NDK (Side by side)")
|
||||||
|
fmt.Println("3. Set environment variables:")
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
fmt.Println(" export ANDROID_HOME=$HOME/Library/Android/sdk")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" export ANDROID_HOME=$HOME/Android/Sdk")
|
||||||
|
}
|
||||||
|
fmt.Println(" export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator")
|
||||||
|
fmt.Println("4. Create an AVD via Android Studio > Tools > Device Manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✓ All Android development dependencies are installed!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCommand(name string, args ...string) bool {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
return cmd.Run() == nil
|
||||||
|
}
|
||||||
18
wails/build/android/settings.gradle
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "WailsApp"
|
||||||
|
include ':app'
|
||||||
BIN
wails/build/appicon.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
78
wails/build/config.yml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# This file contains the configuration for this project.
|
||||||
|
# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
|
||||||
|
# Note that this will overwrite any changes you have made to the assets.
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
# This information is used to generate the build assets.
|
||||||
|
info:
|
||||||
|
companyName: "My Company" # The name of the company
|
||||||
|
productName: "My Product" # The name of the application
|
||||||
|
productIdentifier: "com.mycompany.myproduct" # The unique product identifier
|
||||||
|
description: "A program that does X" # The application description
|
||||||
|
copyright: "(c) 2025, My Company" # Copyright text
|
||||||
|
comments: "Some Product Comments" # Comments
|
||||||
|
version: "0.0.1" # The application version
|
||||||
|
|
||||||
|
# iOS build configuration (uncomment to customise iOS project generation)
|
||||||
|
# Note: Keys under `ios` OVERRIDE values under `info` when set.
|
||||||
|
# ios:
|
||||||
|
# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier)
|
||||||
|
# bundleID: "com.mycompany.myproduct"
|
||||||
|
# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName)
|
||||||
|
# displayName: "My Product"
|
||||||
|
# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion)
|
||||||
|
# version: "0.0.1"
|
||||||
|
# # The company/organisation name for templates and project settings
|
||||||
|
# company: "My Company"
|
||||||
|
# # Additional comments to embed in Info.plist metadata
|
||||||
|
# comments: "Some Product Comments"
|
||||||
|
|
||||||
|
# Dev mode configuration
|
||||||
|
dev_mode:
|
||||||
|
root_path: .
|
||||||
|
log_level: warn
|
||||||
|
debounce: 1000
|
||||||
|
ignore:
|
||||||
|
dir:
|
||||||
|
- .git
|
||||||
|
- node_modules
|
||||||
|
- frontend
|
||||||
|
- bin
|
||||||
|
- assets
|
||||||
|
- Log
|
||||||
|
file:
|
||||||
|
- .DS_Store
|
||||||
|
- .gitignore
|
||||||
|
- .gitkeep
|
||||||
|
- "*.log"
|
||||||
|
watched_extension:
|
||||||
|
- "*.go"
|
||||||
|
- "*.js" # Watch for changes to JS/TS files included using the //wails:include directive.
|
||||||
|
- "*.ts" # The frontend directory will be excluded entirely by the setting above.
|
||||||
|
git_ignore: true
|
||||||
|
executes:
|
||||||
|
- cmd: wails3 build DEV=true
|
||||||
|
type: blocking
|
||||||
|
- cmd: wails3 task common:dev:frontend
|
||||||
|
type: background
|
||||||
|
- cmd: wails3 task run
|
||||||
|
type: primary
|
||||||
|
|
||||||
|
# File Associations
|
||||||
|
# More information at: https://v3.wails.io/noit/done/yet
|
||||||
|
fileAssociations:
|
||||||
|
# - ext: wails
|
||||||
|
# name: Wails
|
||||||
|
# description: Wails Application File
|
||||||
|
# iconName: wailsFileIcon
|
||||||
|
# role: Editor
|
||||||
|
# - ext: jpg
|
||||||
|
# name: JPEG
|
||||||
|
# description: Image File
|
||||||
|
# iconName: jpegFileIcon
|
||||||
|
# role: Editor
|
||||||
|
# mimeType: image/jpeg # (optional)
|
||||||
|
|
||||||
|
# Other data
|
||||||
|
other:
|
||||||
|
- name: My Other Data
|
||||||
32
wails/build/darwin/Info.dev.plist
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!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>My Product</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>videoconcat</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.example.videoconcat</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>This is a comment</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icons</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.15.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© 2026, My Company</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
27
wails/build/darwin/Info.plist
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!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>My Product</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>videoconcat</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.example.videoconcat</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>This is a comment</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icons</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.15.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© 2026, My Company</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
199
wails/build/darwin/Taskfile.yml
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Signing configuration - edit these values for your project
|
||||||
|
# SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
|
||||||
|
# KEYCHAIN_PROFILE: "my-notarize-profile"
|
||||||
|
# ENTITLEMENTS: "build/darwin/entitlements.plist"
|
||||||
|
|
||||||
|
# Docker image for cross-compilation (used when building on non-macOS)
|
||||||
|
CROSS_IMAGE: wails-cross
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application
|
||||||
|
cmds:
|
||||||
|
- task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}'
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH}}'
|
||||||
|
DEV: '{{.DEV}}'
|
||||||
|
OUTPUT: '{{.OUTPUT}}'
|
||||||
|
vars:
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
|
||||||
|
build:native:
|
||||||
|
summary: Builds the application natively on macOS
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
DEV:
|
||||||
|
ref: .DEV
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
env:
|
||||||
|
GOOS: darwin
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
||||||
|
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts)
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required for cross-compilation. Please install Docker."
|
||||||
|
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
|
||||||
|
msg: |
|
||||||
|
Docker image '{{.CROSS_IMAGE}}' not found.
|
||||||
|
Build it first: wails3 task setup:docker
|
||||||
|
cmds:
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
|
||||||
|
- mkdir -p {{.BIN_DIR}}
|
||||||
|
- mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
|
||||||
|
vars:
|
||||||
|
DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
# Mount Go module cache for faster builds
|
||||||
|
GO_CACHE_MOUNT:
|
||||||
|
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
|
||||||
|
# Extract replace directives from go.mod and create -v mounts for each
|
||||||
|
# Handles both relative (=> ../) and absolute (=> /) paths
|
||||||
|
REPLACE_MOUNTS:
|
||||||
|
sh: |
|
||||||
|
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
|
||||||
|
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
|
||||||
|
# Convert relative paths to absolute
|
||||||
|
if [ "${path#/}" = "$path" ]; then
|
||||||
|
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
|
||||||
|
fi
|
||||||
|
# Only mount if directory exists
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
echo "-v $path:$path:ro"
|
||||||
|
fi
|
||||||
|
done | tr '\n' ' '
|
||||||
|
|
||||||
|
build:universal:
|
||||||
|
summary: Builds darwin universal binary (arm64 + amd64)
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
ARCH: amd64
|
||||||
|
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
ARCH: arm64
|
||||||
|
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
cmds:
|
||||||
|
- task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}'
|
||||||
|
|
||||||
|
build:universal:lipo:native:
|
||||||
|
summary: Creates universal binary using native lipo (macOS)
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
|
||||||
|
build:universal:lipo:go:
|
||||||
|
summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
- rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages the application into a `.app` bundle
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
package:universal:
|
||||||
|
summary: Packages darwin universal binary (arm64 + amd64)
|
||||||
|
deps:
|
||||||
|
- task: build:universal
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
|
||||||
|
create:app:bundle:
|
||||||
|
summary: Creates an `.app` bundle
|
||||||
|
cmds:
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||||
|
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||||
|
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
|
||||||
|
- cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents"
|
||||||
|
- task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
|
||||||
|
|
||||||
|
codesign:adhoc:
|
||||||
|
summary: Ad-hoc signs the app bundle (macOS only)
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
|
||||||
|
|
||||||
|
codesign:skip:
|
||||||
|
summary: Skips codesigning when cross-compiling
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||||
|
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||||
|
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
|
||||||
|
- cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist"
|
||||||
|
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
|
||||||
|
|
||||||
|
sign:
|
||||||
|
summary: Signs the application bundle with Developer ID
|
||||||
|
desc: |
|
||||||
|
Signs the .app bundle for distribution.
|
||||||
|
Configure SIGN_IDENTITY in the vars section at the top of this file.
|
||||||
|
deps:
|
||||||
|
- task: package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
|
||||||
|
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:notarize:
|
||||||
|
summary: Signs and notarizes the application bundle
|
||||||
|
desc: |
|
||||||
|
Signs the .app bundle and submits it for notarization.
|
||||||
|
Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file.
|
||||||
|
|
||||||
|
Setup (one-time):
|
||||||
|
wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile"
|
||||||
|
deps:
|
||||||
|
- task: package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
|
||||||
|
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
|
||||||
|
- sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]'
|
||||||
|
msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
|
||||||
BIN
wails/build/darwin/icons.icns
Normal file
195
wails/build/docker/Dockerfile.cross
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# Cross-compile Wails v3 apps to any platform
|
||||||
|
#
|
||||||
|
# Uses Zig as C compiler + macOS SDK for darwin targets
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker build -t wails-cross -f Dockerfile.cross .
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross darwin arm64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross darwin amd64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross linux amd64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross linux arm64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross windows amd64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross windows arm64
|
||||||
|
|
||||||
|
FROM golang:1.25-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl xz nodejs npm
|
||||||
|
|
||||||
|
# Install Zig
|
||||||
|
ARG ZIG_VERSION=0.14.0
|
||||||
|
RUN curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \
|
||||||
|
| tar -xJ -C /opt \
|
||||||
|
&& ln -s /opt/zig-linux-x86_64-${ZIG_VERSION}/zig /usr/local/bin/zig
|
||||||
|
|
||||||
|
# Download macOS SDK (required for darwin targets)
|
||||||
|
ARG MACOS_SDK_VERSION=14.5
|
||||||
|
RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \
|
||||||
|
| tar -xJ -C /opt \
|
||||||
|
&& mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk
|
||||||
|
|
||||||
|
ENV MACOS_SDK_PATH=/opt/macos-sdk
|
||||||
|
|
||||||
|
# Create zig cc wrappers for each target
|
||||||
|
# Darwin arm64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-mmacosx-version-min=*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-darwin-arm64
|
||||||
|
|
||||||
|
# Darwin amd64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-mmacosx-version-min=*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-darwin-amd64
|
||||||
|
|
||||||
|
# Linux amd64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-linux-amd64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -target x86_64-linux-musl $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-linux-amd64
|
||||||
|
|
||||||
|
# Linux arm64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-linux-arm64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -target aarch64-linux-musl $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-linux-arm64
|
||||||
|
|
||||||
|
# Windows amd64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-Wl,*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -target x86_64-windows-gnu $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-windows-amd64
|
||||||
|
|
||||||
|
# Windows arm64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-Wl,*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -target aarch64-windows-gnu $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-windows-arm64
|
||||||
|
|
||||||
|
# Build script
|
||||||
|
COPY <<'SCRIPT' /usr/local/bin/build.sh
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
OS=${1:-darwin}
|
||||||
|
ARCH=${2:-arm64}
|
||||||
|
|
||||||
|
case "${OS}-${ARCH}" in
|
||||||
|
darwin-arm64|darwin-aarch64) export CC=zcc-darwin-arm64; export GOARCH=arm64; export GOOS=darwin ;;
|
||||||
|
darwin-amd64|darwin-x86_64) export CC=zcc-darwin-amd64; export GOARCH=amd64; export GOOS=darwin ;;
|
||||||
|
linux-arm64|linux-aarch64) export CC=zcc-linux-arm64; export GOARCH=arm64; export GOOS=linux ;;
|
||||||
|
linux-amd64|linux-x86_64) export CC=zcc-linux-amd64; export GOARCH=amd64; export GOOS=linux ;;
|
||||||
|
windows-arm64|windows-aarch64) export CC=zcc-windows-arm64; export GOARCH=arm64; export GOOS=windows ;;
|
||||||
|
windows-amd64|windows-x86_64) export CC=zcc-windows-amd64; export GOARCH=amd64; export GOOS=windows ;;
|
||||||
|
*) echo "Usage: <os> <arch>"; echo " os: darwin, linux, windows"; echo " arch: amd64, arm64"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
export CGO_CFLAGS="-w"
|
||||||
|
|
||||||
|
# Build frontend if exists and not already built (host may have built it)
|
||||||
|
if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then
|
||||||
|
(cd frontend && npm install --silent && npm run build --silent)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build
|
||||||
|
APP=${APP_NAME:-$(basename $(pwd))}
|
||||||
|
mkdir -p bin
|
||||||
|
|
||||||
|
EXT=""
|
||||||
|
LDFLAGS="-s -w"
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
EXT=".exe"
|
||||||
|
LDFLAGS="-s -w -H windowsgui"
|
||||||
|
fi
|
||||||
|
|
||||||
|
go build -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} .
|
||||||
|
echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}"
|
||||||
|
SCRIPT
|
||||||
|
RUN chmod +x /usr/local/bin/build.sh
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENTRYPOINT ["/usr/local/bin/build.sh"]
|
||||||
|
CMD ["darwin", "arm64"]
|
||||||
116
wails/build/ios/Assets.xcassets
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon-20@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-20@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-60@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-60@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-20.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-20@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-29@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-40@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-76.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-76@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-83.5@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-1024.png",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
62
wails/build/ios/Info.dev.plist
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>videoconcat</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.example.videoconcat.dev</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>My Product (Dev)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>My Product (Dev)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0-dev</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>15.0</string>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<!-- Development mode enabled -->
|
||||||
|
<key>WailsDevelopmentMode</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© 2026, My Company</string>
|
||||||
|
|
||||||
|
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>This is a comment</string>
|
||||||
|
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
59
wails/build/ios/Info.plist
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>videoconcat</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.example.videoconcat</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>My Product</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>My Product</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.1.0</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>15.0</string>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© 2026, My Company</string>
|
||||||
|
|
||||||
|
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>This is a comment</string>
|
||||||
|
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
53
wails/build/ios/LaunchScreen.storyboard
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="My Product" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||||
|
<rect key="frame" x="0.0" y="397" width="393" height="43"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="A VideoConcat application" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="MN2-I3-ftu">
|
||||||
|
<rect key="frame" x="0.0" y="448" width="393" height="21"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
|
||||||
|
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/2" constant="-20" id="moa-c2-u7t"/>
|
||||||
|
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="x7j-FC-K8j"/>
|
||||||
|
|
||||||
|
<constraint firstItem="MN2-I3-ftu" firstAttribute="top" secondItem="GJd-Yh-RWb" secondAttribute="bottom" constant="8" symbolic="YES" id="cPy-rs-vsC"/>
|
||||||
|
<constraint firstItem="MN2-I3-ftu" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="OQL-iM-xY6"/>
|
||||||
|
<constraint firstItem="MN2-I3-ftu" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="Dti-5h-tvW"/>
|
||||||
|
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
293
wails/build/ios/Taskfile.yml
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
BUNDLE_ID: '{{.BUNDLE_ID | default "com.wails.app"}}'
|
||||||
|
# SDK_PATH is computed lazily at task-level to avoid errors on non-macOS systems
|
||||||
|
# Each task that needs it defines SDK_PATH in its own vars section
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
install:deps:
|
||||||
|
summary: Check and install iOS development dependencies
|
||||||
|
cmds:
|
||||||
|
- go run build/ios/scripts/deps/install_deps.go
|
||||||
|
env:
|
||||||
|
TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}'
|
||||||
|
prompt: This will check and install iOS development dependencies. Continue?
|
||||||
|
|
||||||
|
# Note: Bindings generation may show CGO warnings for iOS C imports.
|
||||||
|
# These warnings are harmless and don't affect the generated bindings,
|
||||||
|
# as the generator only needs to parse Go types, not C implementations.
|
||||||
|
build:
|
||||||
|
summary: Creates a build of the application for iOS
|
||||||
|
deps:
|
||||||
|
- task: generate:ios:overlay
|
||||||
|
- task: generate:ios:xcode
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: generate:ios:bindings
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
PRODUCTION:
|
||||||
|
ref: .PRODUCTION
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- echo "Building iOS app {{.APP_NAME}}..."
|
||||||
|
- go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json {{.BUILD_FLAGS}} -o {{.OUTPUT}}.a
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,ios -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags ios,debug -buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
SDK_PATH:
|
||||||
|
sh: xcrun --sdk iphonesimulator --show-sdk-path
|
||||||
|
env:
|
||||||
|
GOOS: ios
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default "arm64"}}'
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0'
|
||||||
|
CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator'
|
||||||
|
|
||||||
|
compile:objc:
|
||||||
|
summary: Compile Objective-C iOS wrapper
|
||||||
|
vars:
|
||||||
|
SDK_PATH:
|
||||||
|
sh: xcrun --sdk iphonesimulator --show-sdk-path
|
||||||
|
cmds:
|
||||||
|
- xcrun -sdk iphonesimulator clang -target arm64-apple-ios15.0-simulator -isysroot {{.SDK_PATH}} -framework Foundation -framework UIKit -framework WebKit -o {{.BIN_DIR}}/{{.APP_NAME}} build/ios/main.m
|
||||||
|
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application into a `.app` bundle
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
create:app:bundle:
|
||||||
|
summary: Creates an iOS `.app` bundle
|
||||||
|
cmds:
|
||||||
|
- rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.app"
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app"
|
||||||
|
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/"
|
||||||
|
- cp build/ios/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/"
|
||||||
|
- |
|
||||||
|
# Compile asset catalog and embed icons in the app bundle
|
||||||
|
APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.app"
|
||||||
|
AC_IN="build/ios/xcode/main/Assets.xcassets"
|
||||||
|
if [ -d "$AC_IN" ]; then
|
||||||
|
TMP_AC=$(mktemp -d)
|
||||||
|
xcrun actool \
|
||||||
|
--compile "$TMP_AC" \
|
||||||
|
--app-icon AppIcon \
|
||||||
|
--platform iphonesimulator \
|
||||||
|
--minimum-deployment-target 15.0 \
|
||||||
|
--product-type com.apple.product-type.application \
|
||||||
|
--target-device iphone \
|
||||||
|
--target-device ipad \
|
||||||
|
--output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \
|
||||||
|
"$AC_IN"
|
||||||
|
if [ -f "$TMP_AC/Assets.car" ]; then
|
||||||
|
cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car"
|
||||||
|
fi
|
||||||
|
rm -rf "$TMP_AC"
|
||||||
|
if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then
|
||||||
|
/usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
|
||||||
|
|
||||||
|
deploy-simulator:
|
||||||
|
summary: Deploy to iOS Simulator
|
||||||
|
deps: [package]
|
||||||
|
cmds:
|
||||||
|
- xcrun simctl terminate booted {{.BUNDLE_ID}} 2>/dev/null || true
|
||||||
|
- xcrun simctl uninstall booted {{.BUNDLE_ID}} 2>/dev/null || true
|
||||||
|
- xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.app"
|
||||||
|
- xcrun simctl launch booted {{.BUNDLE_ID}}
|
||||||
|
|
||||||
|
compile:ios:
|
||||||
|
summary: Compile the iOS executable from Go archive and main.m
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
SDK_PATH:
|
||||||
|
sh: xcrun --sdk iphonesimulator --show-sdk-path
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
MAIN_M=build/ios/xcode/main/main.m
|
||||||
|
if [ ! -f "$MAIN_M" ]; then
|
||||||
|
MAIN_M=build/ios/main.m
|
||||||
|
fi
|
||||||
|
xcrun -sdk iphonesimulator clang \
|
||||||
|
-target arm64-apple-ios15.0-simulator \
|
||||||
|
-isysroot {{.SDK_PATH}} \
|
||||||
|
-framework Foundation -framework UIKit -framework WebKit \
|
||||||
|
-framework Security -framework CoreFoundation \
|
||||||
|
-lresolv \
|
||||||
|
-o "{{.BIN_DIR}}/{{.APP_NAME | lower}}" \
|
||||||
|
"$MAIN_M" "{{.BIN_DIR}}/{{.APP_NAME}}.a"
|
||||||
|
|
||||||
|
generate:ios:bindings:
|
||||||
|
internal: true
|
||||||
|
summary: Generates bindings for iOS with proper CGO flags
|
||||||
|
sources:
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
generates:
|
||||||
|
- frontend/bindings/**/*
|
||||||
|
vars:
|
||||||
|
SDK_PATH:
|
||||||
|
sh: xcrun --sdk iphonesimulator --show-sdk-path
|
||||||
|
cmds:
|
||||||
|
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
|
||||||
|
env:
|
||||||
|
GOOS: ios
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default "arm64"}}'
|
||||||
|
CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0'
|
||||||
|
CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator'
|
||||||
|
|
||||||
|
ensure-simulator:
|
||||||
|
internal: true
|
||||||
|
summary: Ensure iOS Simulator is running and booted
|
||||||
|
silent: true
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if ! xcrun simctl list devices booted | grep -q "Booted"; then
|
||||||
|
echo "Starting iOS Simulator..."
|
||||||
|
# Get first available iPhone device
|
||||||
|
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -o "[A-F0-9-]\{36\}" || true)
|
||||||
|
if [ -z "$DEVICE_ID" ]; then
|
||||||
|
echo "No iPhone simulator found. Creating one..."
|
||||||
|
RUNTIME=$(xcrun simctl list runtimes | grep iOS | tail -1 | awk '{print $NF}')
|
||||||
|
DEVICE_ID=$(xcrun simctl create "iPhone 15 Pro" "iPhone 15 Pro" "$RUNTIME")
|
||||||
|
fi
|
||||||
|
# Boot the device
|
||||||
|
echo "Booting device $DEVICE_ID..."
|
||||||
|
xcrun simctl boot "$DEVICE_ID" 2>/dev/null || true
|
||||||
|
# Open Simulator app
|
||||||
|
open -a Simulator
|
||||||
|
# Wait for boot (max 30 seconds)
|
||||||
|
for i in {1..30}; do
|
||||||
|
if xcrun simctl list devices booted | grep -q "Booted"; then
|
||||||
|
echo "Simulator booted successfully"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
# Final check
|
||||||
|
if ! xcrun simctl list devices booted | grep -q "Booted"; then
|
||||||
|
echo "Failed to boot simulator after 30 seconds"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
preconditions:
|
||||||
|
- sh: command -v xcrun
|
||||||
|
msg: "xcrun not found. Please run 'wails3 task ios:install:deps' to install iOS development dependencies"
|
||||||
|
|
||||||
|
generate:ios:overlay:
|
||||||
|
internal: true
|
||||||
|
summary: Generate Go build overlay and iOS shim
|
||||||
|
sources:
|
||||||
|
- build/config.yml
|
||||||
|
generates:
|
||||||
|
- build/ios/xcode/overlay.json
|
||||||
|
- build/ios/xcode/gen/main_ios.gen.go
|
||||||
|
cmds:
|
||||||
|
- wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml
|
||||||
|
|
||||||
|
generate:ios:xcode:
|
||||||
|
internal: true
|
||||||
|
summary: Generate iOS Xcode project structure and assets
|
||||||
|
sources:
|
||||||
|
- build/config.yml
|
||||||
|
- build/appicon.png
|
||||||
|
generates:
|
||||||
|
- build/ios/xcode/main/main.m
|
||||||
|
- build/ios/xcode/main/Assets.xcassets/**/*
|
||||||
|
- build/ios/xcode/project.pbxproj
|
||||||
|
cmds:
|
||||||
|
- wails3 ios xcode:gen -outdir build/ios/xcode -config build/config.yml
|
||||||
|
|
||||||
|
run:
|
||||||
|
summary: Run the application in iOS Simulator
|
||||||
|
deps:
|
||||||
|
- task: ensure-simulator
|
||||||
|
- task: compile:ios
|
||||||
|
cmds:
|
||||||
|
- rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
|
||||||
|
- cp "{{.BIN_DIR}}/{{.APP_NAME | lower}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/{{.APP_NAME | lower}}"
|
||||||
|
- cp build/ios/Info.dev.plist "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Info.plist"
|
||||||
|
- |
|
||||||
|
# Compile asset catalog and embed icons for dev bundle
|
||||||
|
APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
|
||||||
|
AC_IN="build/ios/xcode/main/Assets.xcassets"
|
||||||
|
if [ -d "$AC_IN" ]; then
|
||||||
|
TMP_AC=$(mktemp -d)
|
||||||
|
xcrun actool \
|
||||||
|
--compile "$TMP_AC" \
|
||||||
|
--app-icon AppIcon \
|
||||||
|
--platform iphonesimulator \
|
||||||
|
--minimum-deployment-target 15.0 \
|
||||||
|
--product-type com.apple.product-type.application \
|
||||||
|
--target-device iphone \
|
||||||
|
--target-device ipad \
|
||||||
|
--output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \
|
||||||
|
"$AC_IN"
|
||||||
|
if [ -f "$TMP_AC/Assets.car" ]; then
|
||||||
|
cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car"
|
||||||
|
fi
|
||||||
|
rm -rf "$TMP_AC"
|
||||||
|
if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then
|
||||||
|
/usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
|
||||||
|
- xcrun simctl terminate booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true
|
||||||
|
- xcrun simctl uninstall booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true
|
||||||
|
- xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
|
||||||
|
- xcrun simctl launch booted "com.wails.{{.APP_NAME | lower}}.dev"
|
||||||
|
|
||||||
|
xcode:
|
||||||
|
summary: Open the generated Xcode project for this app
|
||||||
|
cmds:
|
||||||
|
- task: generate:ios:xcode
|
||||||
|
- open build/ios/xcode/main.xcodeproj
|
||||||
|
|
||||||
|
logs:
|
||||||
|
summary: Stream iOS Simulator logs filtered to this app
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
xcrun simctl spawn booted log stream \
|
||||||
|
--level debug \
|
||||||
|
--style compact \
|
||||||
|
--predicate 'senderImagePath CONTAINS[c] "{{.APP_NAME | lower}}.app/" OR composedMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR eventMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR process == "{{.APP_NAME | lower}}" OR category CONTAINS[c] "{{.APP_NAME | lower}}"'
|
||||||
|
|
||||||
|
logs:dev:
|
||||||
|
summary: Stream logs for the dev bundle (used by `task ios:run`)
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
xcrun simctl spawn booted log stream \
|
||||||
|
--level debug \
|
||||||
|
--style compact \
|
||||||
|
--predicate 'senderImagePath CONTAINS[c] ".dev.app/" OR subsystem == "com.wails.{{.APP_NAME | lower}}.dev" OR process == "{{.APP_NAME | lower}}"'
|
||||||
|
|
||||||
|
logs:wide:
|
||||||
|
summary: Wide log stream to help discover the exact process/bundle identifiers
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
xcrun simctl spawn booted log stream \
|
||||||
|
--level debug \
|
||||||
|
--style compact \
|
||||||
|
--predicate 'senderImagePath CONTAINS[c] ".app/"'
|
||||||
10
wails/build/ios/app_options_default.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//go:build !ios
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
|
||||||
|
// modifyOptionsForIOS is a no-op on non-iOS platforms
|
||||||
|
func modifyOptionsForIOS(opts *application.Options) {
|
||||||
|
// No modifications needed for non-iOS platforms
|
||||||
|
}
|
||||||
11
wails/build/ios/app_options_ios.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//go:build ios
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
|
||||||
|
// modifyOptionsForIOS adjusts the application options for iOS
|
||||||
|
func modifyOptionsForIOS(opts *application.Options) {
|
||||||
|
// Disable signal handlers on iOS to prevent crashes
|
||||||
|
opts.DisableDefaultSignalHandler = true
|
||||||
|
}
|
||||||
72
wails/build/ios/build.sh
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Build configuration
|
||||||
|
APP_NAME="videoconcat"
|
||||||
|
BUNDLE_ID="com.example.videoconcat"
|
||||||
|
VERSION="0.1.0"
|
||||||
|
BUILD_NUMBER="0.1.0"
|
||||||
|
BUILD_DIR="build/ios"
|
||||||
|
TARGET="simulator"
|
||||||
|
|
||||||
|
echo "Building iOS app: $APP_NAME"
|
||||||
|
echo "Bundle ID: $BUNDLE_ID"
|
||||||
|
echo "Version: $VERSION ($BUILD_NUMBER)"
|
||||||
|
echo "Target: $TARGET"
|
||||||
|
|
||||||
|
# Ensure build directory exists
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
# Determine SDK and target architecture
|
||||||
|
if [ "$TARGET" = "simulator" ]; then
|
||||||
|
SDK="iphonesimulator"
|
||||||
|
ARCH="arm64-apple-ios15.0-simulator"
|
||||||
|
elif [ "$TARGET" = "device" ]; then
|
||||||
|
SDK="iphoneos"
|
||||||
|
ARCH="arm64-apple-ios15.0"
|
||||||
|
else
|
||||||
|
echo "Unknown target: $TARGET"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get SDK path
|
||||||
|
SDK_PATH=$(xcrun --sdk $SDK --show-sdk-path)
|
||||||
|
|
||||||
|
# Compile the application
|
||||||
|
echo "Compiling with SDK: $SDK"
|
||||||
|
xcrun -sdk $SDK clang \
|
||||||
|
-target $ARCH \
|
||||||
|
-isysroot "$SDK_PATH" \
|
||||||
|
-framework Foundation \
|
||||||
|
-framework UIKit \
|
||||||
|
-framework WebKit \
|
||||||
|
-framework CoreGraphics \
|
||||||
|
-o "$BUILD_DIR/$APP_NAME" \
|
||||||
|
"$BUILD_DIR/main.m"
|
||||||
|
|
||||||
|
# Create app bundle
|
||||||
|
echo "Creating app bundle..."
|
||||||
|
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
|
||||||
|
rm -rf "$APP_BUNDLE"
|
||||||
|
mkdir -p "$APP_BUNDLE"
|
||||||
|
|
||||||
|
# Move executable
|
||||||
|
mv "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/"
|
||||||
|
|
||||||
|
# Copy Info.plist
|
||||||
|
cp "$BUILD_DIR/Info.plist" "$APP_BUNDLE/"
|
||||||
|
|
||||||
|
# Sign the app
|
||||||
|
echo "Signing app..."
|
||||||
|
codesign --force --sign - "$APP_BUNDLE"
|
||||||
|
|
||||||
|
echo "Build complete: $APP_BUNDLE"
|
||||||
|
|
||||||
|
# Deploy to simulator if requested
|
||||||
|
if [ "$TARGET" = "simulator" ]; then
|
||||||
|
echo "Deploying to simulator..."
|
||||||
|
xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true
|
||||||
|
xcrun simctl install booted "$APP_BUNDLE"
|
||||||
|
xcrun simctl launch booted "$BUNDLE_ID"
|
||||||
|
echo "App launched on simulator"
|
||||||
|
fi
|
||||||
21
wails/build/ios/entitlements.plist
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- Development entitlements -->
|
||||||
|
<key>get-task-allow</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- App Sandbox -->
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- Network access -->
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- File access (read-only) -->
|
||||||
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
3
wails/build/ios/icon.png
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# iOS Icon Placeholder
|
||||||
|
# This file should be replaced with the actual app icon (1024x1024 PNG)
|
||||||
|
# The build process will generate all required icon sizes from this base icon
|
||||||
23
wails/build/ios/main.m
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//go:build ios
|
||||||
|
// Minimal bootstrap: delegate comes from Go archive (WailsAppDelegate)
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
// External Go initialization function from the c-archive (declare before use)
|
||||||
|
extern void WailsIOSMain();
|
||||||
|
|
||||||
|
int main(int argc, char * argv[]) {
|
||||||
|
@autoreleasepool {
|
||||||
|
// Disable buffering so stdout/stderr from Go log.Printf flush immediately
|
||||||
|
setvbuf(stdout, NULL, _IONBF, 0);
|
||||||
|
setvbuf(stderr, NULL, _IONBF, 0);
|
||||||
|
|
||||||
|
// Start Go runtime on a background queue to avoid blocking main thread/UI
|
||||||
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
||||||
|
WailsIOSMain();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run UIApplicationMain using WailsAppDelegate provided by the Go archive
|
||||||
|
return UIApplicationMain(argc, argv, nil, @"WailsAppDelegate");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
wails/build/ios/main_ios.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//go:build ios
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
)
|
||||||
|
|
||||||
|
// For iOS builds, we need to export a function that can be called from Objective-C
|
||||||
|
// This wrapper allows us to keep the original main.go unmodified
|
||||||
|
|
||||||
|
//export WailsIOSMain
|
||||||
|
func WailsIOSMain() {
|
||||||
|
// DO NOT lock the goroutine to the current OS thread on iOS!
|
||||||
|
// This causes signal handling issues:
|
||||||
|
// "signal 16 received on thread with no signal stack"
|
||||||
|
// "fatal error: non-Go code disabled sigaltstack"
|
||||||
|
// iOS apps run in a sandboxed environment where the Go runtime's
|
||||||
|
// signal handling doesn't work the same way as desktop platforms.
|
||||||
|
|
||||||
|
// Call the actual main function from main.go
|
||||||
|
// This ensures all the user's code is executed
|
||||||
|
main()
|
||||||
|
}
|
||||||
222
wails/build/ios/project.pbxproj
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {};
|
||||||
|
objectVersion = 56;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
C0DEBEEF0000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000002 /* main.m */; };
|
||||||
|
C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000101 /* UIKit.framework */; };
|
||||||
|
C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000102 /* Foundation.framework */; };
|
||||||
|
C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000103 /* WebKit.framework */; };
|
||||||
|
C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000104 /* Security.framework */; };
|
||||||
|
C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000105 /* CoreFoundation.framework */; };
|
||||||
|
C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000106 /* libresolv.tbd */; };
|
||||||
|
C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000107 /* My Product.a */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
C0DEBEEF0000000000000002 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||||
|
C0DEBEEF0000000000000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
C0DEBEEF0000000000000004 /* My Product.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My Product.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
C0DEBEEF0000000000000101 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||||
|
C0DEBEEF0000000000000102 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
||||||
|
C0DEBEEF0000000000000103 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
|
||||||
|
C0DEBEEF0000000000000104 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||||
|
C0DEBEEF0000000000000105 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||||
|
C0DEBEEF0000000000000106 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.text-based-dylib-definition; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
|
||||||
|
C0DEBEEF0000000000000107 /* My Product.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "My Product.a"; path = ../../../bin/My Product.a; sourceTree = SOURCE_ROOT; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
C0DEBEEF0000000000000010 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C0DEBEEF0000000000000020 /* Products */,
|
||||||
|
C0DEBEEF0000000000000045 /* Frameworks */,
|
||||||
|
C0DEBEEF0000000000000030 /* main */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C0DEBEEF0000000000000020 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C0DEBEEF0000000000000004 /* My Product.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C0DEBEEF0000000000000030 /* main */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C0DEBEEF0000000000000002 /* main.m */,
|
||||||
|
C0DEBEEF0000000000000003 /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = main;
|
||||||
|
sourceTree = SOURCE_ROOT;
|
||||||
|
};
|
||||||
|
C0DEBEEF0000000000000045 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C0DEBEEF0000000000000101 /* UIKit.framework */,
|
||||||
|
C0DEBEEF0000000000000102 /* Foundation.framework */,
|
||||||
|
C0DEBEEF0000000000000103 /* WebKit.framework */,
|
||||||
|
C0DEBEEF0000000000000104 /* Security.framework */,
|
||||||
|
C0DEBEEF0000000000000105 /* CoreFoundation.framework */,
|
||||||
|
C0DEBEEF0000000000000106 /* libresolv.tbd */,
|
||||||
|
C0DEBEEF0000000000000107 /* My Product.a */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
C0DEBEEF0000000000000040 /* My Product */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */;
|
||||||
|
buildPhases = (
|
||||||
|
C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */,
|
||||||
|
C0DEBEEF0000000000000050 /* Sources */,
|
||||||
|
C0DEBEEF0000000000000056 /* Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "My Product";
|
||||||
|
productName = "My Product";
|
||||||
|
productReference = C0DEBEEF0000000000000004 /* My Product.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
C0DEBEEF0000000000000060 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastUpgradeCheck = 1500;
|
||||||
|
ORGANIZATIONNAME = "My Company";
|
||||||
|
TargetAttributes = {
|
||||||
|
C0DEBEEF0000000000000040 = {
|
||||||
|
CreatedOnToolsVersion = 15.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */;
|
||||||
|
compatibilityVersion = "Xcode 15.0";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
);
|
||||||
|
mainGroup = C0DEBEEF0000000000000010;
|
||||||
|
productRefGroup = C0DEBEEF0000000000000020 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
C0DEBEEF0000000000000040 /* My Product */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
C0DEBEEF0000000000000056 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */,
|
||||||
|
C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */,
|
||||||
|
C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */,
|
||||||
|
C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */,
|
||||||
|
C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */,
|
||||||
|
C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */,
|
||||||
|
C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Prebuild: Wails Go Archive";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "set -e\nAPP_ROOT=\"${PROJECT_DIR}/../../..\"\nSDK_PATH=$(xcrun --sdk iphonesimulator --show-sdk-path)\nexport GOOS=ios\nexport GOARCH=arm64\nexport CGO_ENABLED=1\nexport CGO_CFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0\"\nexport CGO_LDFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator\"\ncd \"${APP_ROOT}\"\n# Ensure overlay exists\nif [ ! -f build/ios/xcode/overlay.json ]; then\n wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml || true\nfi\n# Build Go c-archive if missing or older than sources\nif [ ! -f bin/My Product.a ]; then\n echo \"Building Go c-archive...\"\n go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json -o bin/My Product.a\nfi\n";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
C0DEBEEF0000000000000050 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C0DEBEEF0000000000000001 /* main.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
C0DEBEEF0000000000000090 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
INFOPLIST_FILE = main/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.example.videoconcat";
|
||||||
|
PRODUCT_NAME = "My Product";
|
||||||
|
CODE_SIGNING_ALLOWED = NO;
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
C0DEBEEF00000000000000A0 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
INFOPLIST_FILE = main/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.example.videoconcat";
|
||||||
|
PRODUCT_NAME = "My Product";
|
||||||
|
CODE_SIGNING_ALLOWED = NO;
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
C0DEBEEF0000000000000090 /* Debug */,
|
||||||
|
C0DEBEEF00000000000000A0 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
|
C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
C0DEBEEF0000000000000090 /* Debug */,
|
||||||
|
C0DEBEEF00000000000000A0 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = C0DEBEEF0000000000000060 /* Project object */;
|
||||||
|
}
|
||||||
319
wails/build/ios/scripts/deps/install_deps.go
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
// install_deps.go - iOS development dependency checker
|
||||||
|
// This script checks for required iOS development tools.
|
||||||
|
// It's designed to be portable across different shells by using Go instead of shell scripts.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// go run install_deps.go # Interactive mode
|
||||||
|
// TASK_FORCE_YES=true go run install_deps.go # Auto-accept prompts
|
||||||
|
// CI=true go run install_deps.go # CI mode (auto-accept)
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dependency struct {
|
||||||
|
Name string
|
||||||
|
CheckFunc func() (bool, string) // Returns (success, details)
|
||||||
|
Required bool
|
||||||
|
InstallCmd []string
|
||||||
|
InstallMsg string
|
||||||
|
SuccessMsg string
|
||||||
|
FailureMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Checking iOS development dependencies...")
|
||||||
|
fmt.Println("=" + strings.Repeat("=", 50))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
hasErrors := false
|
||||||
|
dependencies := []Dependency{
|
||||||
|
{
|
||||||
|
Name: "Xcode",
|
||||||
|
CheckFunc: func() (bool, string) {
|
||||||
|
// Check if xcodebuild exists
|
||||||
|
if !checkCommand([]string{"xcodebuild", "-version"}) {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
// Get version info
|
||||||
|
out, err := exec.Command("xcodebuild", "-version").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
if len(lines) > 0 {
|
||||||
|
return true, strings.TrimSpace(lines[0])
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
InstallMsg: "Please install Xcode from the Mac App Store:\n https://apps.apple.com/app/xcode/id497799835\n Xcode is REQUIRED for iOS development (includes iOS SDKs, simulators, and frameworks)",
|
||||||
|
SuccessMsg: "✅ Xcode found",
|
||||||
|
FailureMsg: "❌ Xcode not found (REQUIRED)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Xcode Developer Path",
|
||||||
|
CheckFunc: func() (bool, string) {
|
||||||
|
// Check if xcode-select points to a valid Xcode path
|
||||||
|
out, err := exec.Command("xcode-select", "-p").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, "xcode-select not configured"
|
||||||
|
}
|
||||||
|
path := strings.TrimSpace(string(out))
|
||||||
|
|
||||||
|
// Check if path exists and is in Xcode.app
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return false, "Invalid Xcode path"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's pointing to Xcode.app (not just Command Line Tools)
|
||||||
|
if !strings.Contains(path, "Xcode.app") {
|
||||||
|
return false, fmt.Sprintf("Points to %s (should be Xcode.app)", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, path
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
InstallCmd: []string{"sudo", "xcode-select", "-s", "/Applications/Xcode.app/Contents/Developer"},
|
||||||
|
InstallMsg: "Xcode developer path needs to be configured",
|
||||||
|
SuccessMsg: "✅ Xcode developer path configured",
|
||||||
|
FailureMsg: "❌ Xcode developer path not configured correctly",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "iOS SDK",
|
||||||
|
CheckFunc: func() (bool, string) {
|
||||||
|
// Get the iOS Simulator SDK path
|
||||||
|
cmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, "Cannot find iOS SDK"
|
||||||
|
}
|
||||||
|
sdkPath := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
// Check if the SDK path exists
|
||||||
|
if _, err := os.Stat(sdkPath); err != nil {
|
||||||
|
return false, "iOS SDK path not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for UIKit framework (essential for iOS development)
|
||||||
|
uikitPath := fmt.Sprintf("%s/System/Library/Frameworks/UIKit.framework", sdkPath)
|
||||||
|
if _, err := os.Stat(uikitPath); err != nil {
|
||||||
|
return false, "UIKit.framework not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SDK version
|
||||||
|
versionCmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-version")
|
||||||
|
versionOut, _ := versionCmd.Output()
|
||||||
|
version := strings.TrimSpace(string(versionOut))
|
||||||
|
|
||||||
|
return true, fmt.Sprintf("iOS %s SDK", version)
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
InstallMsg: "iOS SDK comes with Xcode. Please ensure Xcode is properly installed.",
|
||||||
|
SuccessMsg: "✅ iOS SDK found with UIKit framework",
|
||||||
|
FailureMsg: "❌ iOS SDK not found or incomplete",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "iOS Simulator Runtime",
|
||||||
|
CheckFunc: func() (bool, string) {
|
||||||
|
if !checkCommand([]string{"xcrun", "simctl", "help"}) {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
// Check if we can list runtimes
|
||||||
|
out, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, "Cannot access simulator"
|
||||||
|
}
|
||||||
|
// Count iOS runtimes
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
count := 0
|
||||||
|
var versions []string
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") {
|
||||||
|
count++
|
||||||
|
// Extract version number
|
||||||
|
if parts := strings.Fields(line); len(parts) > 2 {
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasPrefix(part, "(") && strings.HasSuffix(part, ")") {
|
||||||
|
versions = append(versions, strings.Trim(part, "()"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return true, fmt.Sprintf("%d runtime(s): %s", count, strings.Join(versions, ", "))
|
||||||
|
}
|
||||||
|
return false, "No iOS runtimes installed"
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
InstallMsg: "iOS Simulator runtimes come with Xcode. You may need to download them:\n Xcode → Settings → Platforms → iOS",
|
||||||
|
SuccessMsg: "✅ iOS Simulator runtime available",
|
||||||
|
FailureMsg: "❌ iOS Simulator runtime not available",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each dependency
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
success, details := dep.CheckFunc()
|
||||||
|
if success {
|
||||||
|
msg := dep.SuccessMsg
|
||||||
|
if details != "" {
|
||||||
|
msg = fmt.Sprintf("%s (%s)", dep.SuccessMsg, details)
|
||||||
|
}
|
||||||
|
fmt.Println(msg)
|
||||||
|
} else {
|
||||||
|
fmt.Println(dep.FailureMsg)
|
||||||
|
if details != "" {
|
||||||
|
fmt.Printf(" Details: %s\n", details)
|
||||||
|
}
|
||||||
|
if dep.Required {
|
||||||
|
hasErrors = true
|
||||||
|
if len(dep.InstallCmd) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(" " + dep.InstallMsg)
|
||||||
|
fmt.Printf(" Fix command: %s\n", strings.Join(dep.InstallCmd, " "))
|
||||||
|
if promptUser("Do you want to run this command?") {
|
||||||
|
fmt.Println("Running command...")
|
||||||
|
cmd := exec.Command(dep.InstallCmd[0], dep.InstallCmd[1:]...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Command failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("✅ Command completed. Please run this check again.")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Please run manually: %s\n", strings.Join(dep.InstallCmd, " "))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(" " + dep.InstallMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for iPhone simulators
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Checking for iPhone simulator devices...")
|
||||||
|
if !checkCommand([]string{"xcrun", "simctl", "list", "devices"}) {
|
||||||
|
fmt.Println("❌ Cannot check for iPhone simulators")
|
||||||
|
hasErrors = true
|
||||||
|
} else {
|
||||||
|
out, err := exec.Command("xcrun", "simctl", "list", "devices").Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("❌ Failed to list simulator devices")
|
||||||
|
hasErrors = true
|
||||||
|
} else if !strings.Contains(string(out), "iPhone") {
|
||||||
|
fmt.Println("⚠️ No iPhone simulator devices found")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Get the latest iOS runtime
|
||||||
|
runtimeOut, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(" Failed to get iOS runtimes:", err)
|
||||||
|
} else {
|
||||||
|
lines := strings.Split(string(runtimeOut), "\n")
|
||||||
|
var latestRuntime string
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") {
|
||||||
|
// Extract runtime identifier
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
latestRuntime = parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestRuntime == "" {
|
||||||
|
fmt.Println(" No iOS runtime found. Please install iOS simulators in Xcode:")
|
||||||
|
fmt.Println(" Xcode → Settings → Platforms → iOS")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" Would you like to create an iPhone 15 Pro simulator?")
|
||||||
|
createCmd := []string{"xcrun", "simctl", "create", "iPhone 15 Pro", "iPhone 15 Pro", latestRuntime}
|
||||||
|
fmt.Printf(" Command: %s\n", strings.Join(createCmd, " "))
|
||||||
|
if promptUser("Create simulator?") {
|
||||||
|
cmd := exec.Command(createCmd[0], createCmd[1:]...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf(" Failed to create simulator: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" ✅ iPhone 15 Pro simulator created")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(" Skipping simulator creation")
|
||||||
|
fmt.Printf(" Create manually: %s\n", strings.Join(createCmd, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Count iPhone devices
|
||||||
|
count := 0
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "iPhone") && !strings.Contains(line, "unavailable") {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ %d iPhone simulator device(s) available\n", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final summary
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("=" + strings.Repeat("=", 50))
|
||||||
|
if hasErrors {
|
||||||
|
fmt.Println("❌ Some required dependencies are missing or misconfigured.")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Quick setup guide:")
|
||||||
|
fmt.Println("1. Install Xcode from Mac App Store (if not installed)")
|
||||||
|
fmt.Println("2. Open Xcode once and agree to the license")
|
||||||
|
fmt.Println("3. Install additional components when prompted")
|
||||||
|
fmt.Println("4. Run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer")
|
||||||
|
fmt.Println("5. Download iOS simulators: Xcode → Settings → Platforms → iOS")
|
||||||
|
fmt.Println("6. Run this check again")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
fmt.Println("✅ All required dependencies are installed!")
|
||||||
|
fmt.Println(" You're ready for iOS development with Wails!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCommand(args []string) bool {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptUser(question string) bool {
|
||||||
|
// Check if we're in a non-interactive environment
|
||||||
|
if os.Getenv("CI") != "" || os.Getenv("TASK_FORCE_YES") == "true" {
|
||||||
|
fmt.Printf("%s [y/N]: y (auto-accepted)\n", question)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
fmt.Printf("%s [y/N]: ", question)
|
||||||
|
|
||||||
|
response, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
return response == "y" || response == "yes"
|
||||||
|
}
|
||||||
220
wails/build/linux/Taskfile.yml
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Signing configuration - edit these values for your project
|
||||||
|
# PGP_KEY: "path/to/signing-key.asc"
|
||||||
|
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
|
||||||
|
#
|
||||||
|
# Password is stored securely in system keychain. Run: wails3 setup signing
|
||||||
|
|
||||||
|
# Docker image for cross-compilation (used when building on non-Linux or no CC available)
|
||||||
|
CROSS_IMAGE: wails-cross
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Linux
|
||||||
|
cmds:
|
||||||
|
# Linux requires CGO - use Docker when cross-compiling from non-Linux OR when no C compiler is available
|
||||||
|
- task: '{{if and (eq OS "linux") (eq .HAS_CC "true")}}build:native{{else}}build:docker{{end}}'
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH}}'
|
||||||
|
DEV: '{{.DEV}}'
|
||||||
|
OUTPUT: '{{.OUTPUT}}'
|
||||||
|
vars:
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
# Check if a C compiler is available (gcc or clang)
|
||||||
|
HAS_CC:
|
||||||
|
sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"'
|
||||||
|
|
||||||
|
build:native:
|
||||||
|
summary: Builds the application natively on Linux
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
DEV:
|
||||||
|
ref: .DEV
|
||||||
|
- task: common:generate:icons
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
cmds:
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Cross-compiles for Linux using Docker with Zig (for macOS/Windows hosts)
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required for cross-compilation to Linux. Please install Docker."
|
||||||
|
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
|
||||||
|
msg: |
|
||||||
|
Docker image '{{.CROSS_IMAGE}}' not found.
|
||||||
|
Build it first: wails3 task setup:docker
|
||||||
|
cmds:
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
|
||||||
|
- mkdir -p {{.BIN_DIR}}
|
||||||
|
- mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
|
||||||
|
vars:
|
||||||
|
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
# Mount Go module cache for faster builds
|
||||||
|
GO_CACHE_MOUNT:
|
||||||
|
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
|
||||||
|
# Extract replace directives from go.mod and create -v mounts for each
|
||||||
|
REPLACE_MOUNTS:
|
||||||
|
sh: |
|
||||||
|
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
|
||||||
|
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
|
||||||
|
# Convert relative paths to absolute
|
||||||
|
if [ "${path#/}" = "$path" ]; then
|
||||||
|
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
|
||||||
|
fi
|
||||||
|
# Only mount if directory exists
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
echo "-v $path:$path:ro"
|
||||||
|
fi
|
||||||
|
done | tr '\n' ' '
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages the application for Linux
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: create:appimage
|
||||||
|
- task: create:deb
|
||||||
|
- task: create:rpm
|
||||||
|
- task: create:aur
|
||||||
|
|
||||||
|
create:appimage:
|
||||||
|
summary: Creates an AppImage
|
||||||
|
dir: build/linux/appimage
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
cmds:
|
||||||
|
- cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
|
||||||
|
- cp ../../appicon.png "{{.APP_NAME}}.png"
|
||||||
|
- wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
APP_BINARY: '../../../bin/{{.APP_NAME}}'
|
||||||
|
ICON: '{{.APP_NAME}}.png'
|
||||||
|
DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
|
||||||
|
OUTPUT_DIR: '../../../bin'
|
||||||
|
|
||||||
|
create:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:deb
|
||||||
|
|
||||||
|
create:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:rpm
|
||||||
|
|
||||||
|
create:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:aur
|
||||||
|
|
||||||
|
generate:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:dotdesktop:
|
||||||
|
summary: Generates a `.desktop` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
|
||||||
|
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
EXEC: '{{.APP_NAME}}'
|
||||||
|
ICON: '{{.APP_NAME}}'
|
||||||
|
CATEGORIES: 'Development;'
|
||||||
|
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
|
||||||
|
sign:deb:
|
||||||
|
summary: Signs the DEB package
|
||||||
|
desc: |
|
||||||
|
Signs the .deb package with a PGP key.
|
||||||
|
Configure PGP_KEY in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: create:deb
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.PGP_KEY}}" ]'
|
||||||
|
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:rpm:
|
||||||
|
summary: Signs the RPM package
|
||||||
|
desc: |
|
||||||
|
Signs the .rpm package with a PGP key.
|
||||||
|
Configure PGP_KEY in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: create:rpm
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.PGP_KEY}}" ]'
|
||||||
|
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:packages:
|
||||||
|
summary: Signs all Linux packages (DEB and RPM)
|
||||||
|
desc: |
|
||||||
|
Signs both .deb and .rpm packages with a PGP key.
|
||||||
|
Configure PGP_KEY in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
cmds:
|
||||||
|
- task: sign:deb
|
||||||
|
- task: sign:rpm
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.PGP_KEY}}" ]'
|
||||||
|
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
|
||||||
35
wails/build/linux/appimage/build.sh
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2018-Present Lea Anthony
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
# Fail script on any error
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# Define variables
|
||||||
|
APP_DIR="${APP_NAME}.AppDir"
|
||||||
|
|
||||||
|
# Create AppDir structure
|
||||||
|
mkdir -p "${APP_DIR}/usr/bin"
|
||||||
|
cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
|
||||||
|
cp "${ICON_PATH}" "${APP_DIR}/"
|
||||||
|
cp "${DESKTOP_FILE}" "${APP_DIR}/"
|
||||||
|
|
||||||
|
if [[ $(uname -m) == *x86_64* ]]; then
|
||||||
|
# Download linuxdeploy and make it executable
|
||||||
|
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||||
|
chmod +x linuxdeploy-x86_64.AppImage
|
||||||
|
|
||||||
|
# Run linuxdeploy to bundle the application
|
||||||
|
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||||
|
else
|
||||||
|
# Download linuxdeploy and make it executable (arm64)
|
||||||
|
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
|
||||||
|
chmod +x linuxdeploy-aarch64.AppImage
|
||||||
|
|
||||||
|
# Run linuxdeploy to bundle the application (arm64)
|
||||||
|
./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rename the generated AppImage
|
||||||
|
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
|
||||||
|
|
||||||
13
wails/build/linux/desktop
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Name=My Product
|
||||||
|
Comment=A VideoConcat application
|
||||||
|
# The Exec line includes %u to pass the URL to the application
|
||||||
|
Exec=/usr/local/bin/videoconcat %u
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Icon=videoconcat
|
||||||
|
Categories=Utility;
|
||||||
|
StartupWMClass=videoconcat
|
||||||
|
|
||||||
|
|
||||||
67
wails/build/linux/nfpm/nfpm.yaml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Feel free to remove those if you don't want/need to use them.
|
||||||
|
# Make sure to check the documentation at https://nfpm.goreleaser.com
|
||||||
|
#
|
||||||
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
|
||||||
|
name: "videoconcat"
|
||||||
|
arch: ${GOARCH}
|
||||||
|
platform: "linux"
|
||||||
|
version: "0.1.0"
|
||||||
|
section: "default"
|
||||||
|
priority: "extra"
|
||||||
|
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||||
|
description: "A VideoConcat application"
|
||||||
|
vendor: "My Company"
|
||||||
|
homepage: "https://wails.io"
|
||||||
|
license: "MIT"
|
||||||
|
release: "1"
|
||||||
|
|
||||||
|
contents:
|
||||||
|
- src: "./bin/videoconcat"
|
||||||
|
dst: "/usr/local/bin/videoconcat"
|
||||||
|
- src: "./build/appicon.png"
|
||||||
|
dst: "/usr/share/icons/hicolor/128x128/apps/videoconcat.png"
|
||||||
|
- src: "./build/linux/videoconcat.desktop"
|
||||||
|
dst: "/usr/share/applications/videoconcat.desktop"
|
||||||
|
|
||||||
|
# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1
|
||||||
|
depends:
|
||||||
|
- libgtk-3-0
|
||||||
|
- libwebkit2gtk-4.1-0
|
||||||
|
|
||||||
|
# Distribution-specific overrides for different package formats and WebKit versions
|
||||||
|
overrides:
|
||||||
|
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
|
||||||
|
rpm:
|
||||||
|
depends:
|
||||||
|
- gtk3
|
||||||
|
- webkit2gtk4.1
|
||||||
|
|
||||||
|
# Arch Linux packages (WebKit 4.1)
|
||||||
|
archlinux:
|
||||||
|
depends:
|
||||||
|
- gtk3
|
||||||
|
- webkit2gtk-4.1
|
||||||
|
|
||||||
|
# scripts section to ensure desktop database is updated after install
|
||||||
|
scripts:
|
||||||
|
postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
|
||||||
|
# You can also add preremove, postremove if needed
|
||||||
|
# preremove: "./build/linux/nfpm/scripts/preremove.sh"
|
||||||
|
# postremove: "./build/linux/nfpm/scripts/postremove.sh"
|
||||||
|
|
||||||
|
# replaces:
|
||||||
|
# - foobar
|
||||||
|
# provides:
|
||||||
|
# - bar
|
||||||
|
# depends:
|
||||||
|
# - gtk3
|
||||||
|
# - libwebkit2gtk
|
||||||
|
# recommends:
|
||||||
|
# - whatever
|
||||||
|
# suggests:
|
||||||
|
# - something-else
|
||||||
|
# conflicts:
|
||||||
|
# - not-foo
|
||||||
|
# - not-bar
|
||||||
|
# changelog: "changelog.yaml"
|
||||||
21
wails/build/linux/nfpm/scripts/postinstall.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Update desktop database for .desktop file changes
|
||||||
|
# This makes the application appear in application menus and registers its capabilities.
|
||||||
|
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||||
|
echo "Updating desktop database..."
|
||||||
|
update-desktop-database -q /usr/share/applications
|
||||||
|
else
|
||||||
|
echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update MIME database for custom URL schemes (x-scheme-handler)
|
||||||
|
# This ensures the system knows how to handle your custom protocols.
|
||||||
|
if command -v update-mime-database >/dev/null 2>&1; then
|
||||||
|
echo "Updating MIME database..."
|
||||||
|
update-mime-database -n /usr/share/mime
|
||||||
|
else
|
||||||
|
echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
1
wails/build/linux/nfpm/scripts/postremove.sh
Normal file
@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
1
wails/build/linux/nfpm/scripts/preinstall.sh
Normal file
@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
1
wails/build/linux/nfpm/scripts/preremove.sh
Normal file
@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
183
wails/build/windows/Taskfile.yml
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Signing configuration - edit these values for your project
|
||||||
|
# SIGN_CERTIFICATE: "path/to/certificate.pfx"
|
||||||
|
# SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE
|
||||||
|
# TIMESTAMP_SERVER: "http://timestamp.digicert.com"
|
||||||
|
#
|
||||||
|
# Password is stored securely in system keychain. Run: wails3 setup signing
|
||||||
|
|
||||||
|
# Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows)
|
||||||
|
CROSS_IMAGE: wails-cross
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Windows
|
||||||
|
cmds:
|
||||||
|
# Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile
|
||||||
|
- task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}'
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH}}'
|
||||||
|
DEV: '{{.DEV}}'
|
||||||
|
vars:
|
||||||
|
# Default to CGO_ENABLED=0 if not explicitly set
|
||||||
|
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
|
||||||
|
|
||||||
|
build:native:
|
||||||
|
summary: Builds the application using native Go cross-compilation
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
DEV:
|
||||||
|
ref: .DEV
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- task: generate:syso
|
||||||
|
- go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"
|
||||||
|
- cmd: powershell Remove-item *.syso
|
||||||
|
platforms: [windows]
|
||||||
|
- cmd: rm -f *.syso
|
||||||
|
platforms: [linux, darwin]
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}'
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows)
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required for CGO cross-compilation. Please install Docker."
|
||||||
|
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
|
||||||
|
msg: |
|
||||||
|
Docker image '{{.CROSS_IMAGE}}' not found.
|
||||||
|
Build it first: wails3 task setup:docker
|
||||||
|
cmds:
|
||||||
|
- task: generate:syso
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
|
||||||
|
- rm -f *.syso
|
||||||
|
vars:
|
||||||
|
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
|
||||||
|
# Mount Go module cache for faster builds
|
||||||
|
GO_CACHE_MOUNT:
|
||||||
|
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
|
||||||
|
# Extract replace directives from go.mod and create -v mounts for each
|
||||||
|
REPLACE_MOUNTS:
|
||||||
|
sh: |
|
||||||
|
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
|
||||||
|
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
|
||||||
|
# Convert relative paths to absolute
|
||||||
|
if [ "${path#/}" = "$path" ]; then
|
||||||
|
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
|
||||||
|
fi
|
||||||
|
# Only mount if directory exists
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
echo "-v $path:$path:ro"
|
||||||
|
fi
|
||||||
|
done | tr '\n' ' '
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages the application
|
||||||
|
cmds:
|
||||||
|
- task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
|
||||||
|
vars:
|
||||||
|
FORMAT: '{{.FORMAT | default "nsis"}}'
|
||||||
|
|
||||||
|
generate:syso:
|
||||||
|
summary: Generates Windows `.syso` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
create:nsis:installer:
|
||||||
|
summary: Creates an NSIS installer
|
||||||
|
dir: build/windows/nsis
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
|
||||||
|
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
|
||||||
|
- |
|
||||||
|
{{if eq OS "windows"}}
|
||||||
|
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
|
||||||
|
{{else}}
|
||||||
|
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
|
||||||
|
{{end}}
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
||||||
|
|
||||||
|
create:msix:package:
|
||||||
|
summary: Creates an MSIX package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- |-
|
||||||
|
wails3 tool msix \
|
||||||
|
--config "{{.ROOT_DIR}}/wails.json" \
|
||||||
|
--name "{{.APP_NAME}}" \
|
||||||
|
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
|
||||||
|
--arch "{{.ARCH}}" \
|
||||||
|
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
|
||||||
|
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
|
||||||
|
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
|
||||||
|
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CERT_PATH: '{{.CERT_PATH | default ""}}'
|
||||||
|
PUBLISHER: '{{.PUBLISHER | default ""}}'
|
||||||
|
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
|
||||||
|
|
||||||
|
install:msix:tools:
|
||||||
|
summary: Installs tools required for MSIX packaging
|
||||||
|
cmds:
|
||||||
|
- wails3 tool msix-install-tools
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
|
||||||
|
|
||||||
|
sign:
|
||||||
|
summary: Signs the Windows executable
|
||||||
|
desc: |
|
||||||
|
Signs the .exe with an Authenticode certificate.
|
||||||
|
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
|
||||||
|
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:installer:
|
||||||
|
summary: Signs the NSIS installer
|
||||||
|
desc: |
|
||||||
|
Creates and signs the NSIS installer.
|
||||||
|
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: create:nsis:installer
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
|
||||||
|
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
|
||||||
BIN
wails/build/windows/icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
15
wails/build/windows/info.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "0.1.0"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "0.1.0",
|
||||||
|
"CompanyName": "My Company",
|
||||||
|
"FileDescription": "A VideoConcat application",
|
||||||
|
"LegalCopyright": "© 2026, My Company",
|
||||||
|
"ProductName": "My Product",
|
||||||
|
"Comments": "This is a comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
wails/build/windows/msix/app_manifest.xml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Package
|
||||||
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
|
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
|
||||||
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
|
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||||
|
IgnorableNamespaces="uap3">
|
||||||
|
|
||||||
|
<Identity
|
||||||
|
Name="com.example.videoconcat"
|
||||||
|
Publisher="CN=My Company"
|
||||||
|
Version="0.1.0.0"
|
||||||
|
ProcessorArchitecture="x64" />
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>My Product</DisplayName>
|
||||||
|
<PublisherDisplayName>My Company</PublisherDisplayName>
|
||||||
|
<Description>A VideoConcat application</Description>
|
||||||
|
<Logo>Assets\StoreLogo.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
</Dependencies>
|
||||||
|
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="en-us" />
|
||||||
|
</Resources>
|
||||||
|
|
||||||
|
<Applications>
|
||||||
|
<Application Id="com.example.videoconcat" Executable="videoconcat" EntryPoint="Windows.FullTrustApplication">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="My Product"
|
||||||
|
Description="A VideoConcat application"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
|
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||||
|
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
|
||||||
|
<Extensions>
|
||||||
|
<desktop:Extension Category="windows.fullTrustProcess" Executable="videoconcat" />
|
||||||
|
|
||||||
|
|
||||||
|
</Extensions>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
|
||||||
|
<Capabilities>
|
||||||
|
<rescap:Capability Name="runFullTrust" />
|
||||||
|
|
||||||
|
</Capabilities>
|
||||||
|
</Package>
|
||||||
54
wails/build/windows/msix/template.xml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MsixPackagingToolTemplate
|
||||||
|
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
|
||||||
|
<Settings
|
||||||
|
AllowTelemetry="false"
|
||||||
|
ApplyACLsToPackageFiles="true"
|
||||||
|
GenerateCommandLineFile="true"
|
||||||
|
AllowPromptForPassword="false">
|
||||||
|
</Settings>
|
||||||
|
<Installer
|
||||||
|
Path="videoconcat"
|
||||||
|
Arguments=""
|
||||||
|
InstallLocation="C:\Program Files\My Company\My Product">
|
||||||
|
</Installer>
|
||||||
|
<PackageInformation
|
||||||
|
PackageName="My Product"
|
||||||
|
PackageDisplayName="My Product"
|
||||||
|
PublisherName="CN=My Company"
|
||||||
|
PublisherDisplayName="My Company"
|
||||||
|
Version="0.1.0.0"
|
||||||
|
PackageDescription="A VideoConcat application">
|
||||||
|
<Capabilities>
|
||||||
|
<Capability Name="runFullTrust" />
|
||||||
|
|
||||||
|
</Capabilities>
|
||||||
|
<Applications>
|
||||||
|
<Application
|
||||||
|
Id="com.example.videoconcat"
|
||||||
|
Description="A VideoConcat application"
|
||||||
|
DisplayName="My Product"
|
||||||
|
ExecutableName="videoconcat"
|
||||||
|
EntryPoint="Windows.FullTrustApplication">
|
||||||
|
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="en-us" />
|
||||||
|
</Resources>
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
</Dependencies>
|
||||||
|
<Properties>
|
||||||
|
<Framework>false</Framework>
|
||||||
|
<DisplayName>My Product</DisplayName>
|
||||||
|
<PublisherDisplayName>My Company</PublisherDisplayName>
|
||||||
|
<Description>A VideoConcat application</Description>
|
||||||
|
<Logo>Assets\AppIcon.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
</PackageInformation>
|
||||||
|
<SaveLocation PackagePath="videoconcat.msix" />
|
||||||
|
<PackageIntegrity>
|
||||||
|
<CertificatePath></CertificatePath>
|
||||||
|
</PackageIntegrity>
|
||||||
|
</MsixPackagingToolTemplate>
|
||||||
114
wails/build/windows/nsis/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.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows you 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 wails_tools.nsh file, but they can be overwritten here.
|
||||||
|
####
|
||||||
|
## !define INFO_PROJECTNAME "my-project" # Default "VideoConcat"
|
||||||
|
## !define INFO_COMPANYNAME "My Company" # Default "My Company"
|
||||||
|
## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product"
|
||||||
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0"
|
||||||
|
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026, My Company"
|
||||||
|
###
|
||||||
|
## !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 # Uninstalling 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
|
||||||
236
wails/build/windows/nsis/wails_tools.nsh
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "VideoConcat"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "My Company"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "My Product"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "0.1.0"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "© 2026, My Company"
|
||||||
|
!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 "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
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
|
||||||
|
!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
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateCustomProtocols
|
||||||
|
; Delete app custom protocol associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
22
wails/build/windows/wails.exe.manifest
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?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.example.videoconcat" version="0.1.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>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
</assembly>
|
||||||
8
wails/config.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"super_users": [
|
||||||
|
{
|
||||||
|
"username": "super",
|
||||||
|
"password_hash": "将此处替换为密码的MD5 hash值(32位十六进制字符串)。密码'080500'的MD5 hash值会在程序启动时输出到日志中"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
//@ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import { Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
Object.freeze($Create.Events);
|
||||||
2
wails/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
32
wails/frontend/bindings/videoconcat/services/authservice.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuthService 认证服务
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as $models from "./models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login 用户登录
|
||||||
|
* @param {string} username
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {$CancellablePromise<$models.LoginResponse | null>}
|
||||||
|
*/
|
||||||
|
export function Login(username, password) {
|
||||||
|
return $Call.ByID(2350837569, username, password).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = $models.LoginResponse.createFrom;
|
||||||
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtractService 抽帧服务
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as $models from "./models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtractFrames 批量抽帧
|
||||||
|
* @param {$models.ExtractFrameRequest} req
|
||||||
|
* @returns {$CancellablePromise<$models.ExtractFrameResult[]>}
|
||||||
|
*/
|
||||||
|
export function ExtractFrames(req) {
|
||||||
|
return $Call.ByID(1728131056, req).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListVideos 列出文件夹中的视频文件
|
||||||
|
* @param {string} folderPath
|
||||||
|
* @returns {$CancellablePromise<string[]>}
|
||||||
|
*/
|
||||||
|
export function ListVideos(folderPath) {
|
||||||
|
return $Call.ByID(2398906893, folderPath).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType2($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModifyByMetadata 通过修改元数据改变文件 MD5
|
||||||
|
* @param {string} inputPath
|
||||||
|
* @param {string} outputPath
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function ModifyByMetadata(inputPath, outputPath) {
|
||||||
|
return $Call.ByID(4068325271, inputPath, outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModifyVideosMetadata 批量修改视频元数据
|
||||||
|
* @param {string} folderPath
|
||||||
|
* @returns {$CancellablePromise<$models.ExtractFrameResult[]>}
|
||||||
|
*/
|
||||||
|
export function ModifyVideosMetadata(folderPath) {
|
||||||
|
return $Call.ByID(3157640998, folderPath).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RemoveFrameRandom 随机删除视频中的一帧
|
||||||
|
* @param {string} inputPath
|
||||||
|
* @param {string} outputPath
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function RemoveFrameRandom(inputPath, outputPath) {
|
||||||
|
return $Call.ByID(4062392693, inputPath, outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = $models.ExtractFrameResult.createFrom;
|
||||||
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
|
const $$createType2 = $Create.Array($Create.Any);
|
||||||
80
wails/frontend/bindings/videoconcat/services/fileservice.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileService 文件服务
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EnsureDirectory 确保目录存在
|
||||||
|
* @param {string} dirPath
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function EnsureDirectory(dirPath) {
|
||||||
|
return $Call.ByID(1924614253, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileExists 检查文件是否存在
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {$CancellablePromise<boolean>}
|
||||||
|
*/
|
||||||
|
export function FileExists(filePath) {
|
||||||
|
return $Call.ByID(26080110, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetFileSize 获取文件大小(字节)
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {$CancellablePromise<number>}
|
||||||
|
*/
|
||||||
|
export function GetFileSize(filePath) {
|
||||||
|
return $Call.ByID(3113069571, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListFiles 列出目录中的文件
|
||||||
|
* @param {string} dirPath
|
||||||
|
* @param {string} pattern
|
||||||
|
* @returns {$CancellablePromise<string[]>}
|
||||||
|
*/
|
||||||
|
export function ListFiles(dirPath, pattern) {
|
||||||
|
return $Call.ByID(1922143815, dirPath, pattern).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType0($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenFolder 打开文件夹
|
||||||
|
* @param {string} folderPath
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function OpenFolder(folderPath) {
|
||||||
|
return $Call.ByID(578638620, folderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectFile 选择文件(返回路径)
|
||||||
|
* @param {string} filter
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function SelectFile(filter) {
|
||||||
|
return $Call.ByID(2093145774, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectFolder 选择文件夹(返回路径)
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function SelectFolder() {
|
||||||
|
return $Call.ByID(23551676);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = $Create.Array($Create.Any);
|
||||||
23
wails/frontend/bindings/videoconcat/services/index.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
import * as AuthService from "./authservice.js";
|
||||||
|
import * as ExtractService from "./extractservice.js";
|
||||||
|
import * as FileService from "./fileservice.js";
|
||||||
|
import * as VideoService from "./videoservice.js";
|
||||||
|
export {
|
||||||
|
AuthService,
|
||||||
|
ExtractService,
|
||||||
|
FileService,
|
||||||
|
VideoService
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
ExtractFrameRequest,
|
||||||
|
ExtractFrameResult,
|
||||||
|
FolderInfo,
|
||||||
|
LoginResponse,
|
||||||
|
VideoConcatRequest,
|
||||||
|
VideoConcatResult
|
||||||
|
} from "./models.js";
|
||||||
341
wails/frontend/bindings/videoconcat/services/models.js
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import { Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtractFrameRequest 抽帧请求
|
||||||
|
*/
|
||||||
|
export class ExtractFrameRequest {
|
||||||
|
/**
|
||||||
|
* Creates a new ExtractFrameRequest instance.
|
||||||
|
* @param {Partial<ExtractFrameRequest>} [$$source = {}] - The source object to create the ExtractFrameRequest.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("folderPath" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["folderPath"] = "";
|
||||||
|
}
|
||||||
|
if (!("extractCount" in $$source)) {
|
||||||
|
/**
|
||||||
|
* 每个视频生成的数量
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["extractCount"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ExtractFrameRequest instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {ExtractFrameRequest}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new ExtractFrameRequest(/** @type {Partial<ExtractFrameRequest>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtractFrameResult 抽帧结果
|
||||||
|
*/
|
||||||
|
export class ExtractFrameResult {
|
||||||
|
/**
|
||||||
|
* Creates a new ExtractFrameResult instance.
|
||||||
|
* @param {Partial<ExtractFrameResult>} [$$source = {}] - The source object to create the ExtractFrameResult.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("videoPath" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["videoPath"] = "";
|
||||||
|
}
|
||||||
|
if (!("outputPath" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["outputPath"] = "";
|
||||||
|
}
|
||||||
|
if (!("success" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this["success"] = false;
|
||||||
|
}
|
||||||
|
if (/** @type {any} */(false)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string | undefined}
|
||||||
|
*/
|
||||||
|
this["error"] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ExtractFrameResult instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {ExtractFrameResult}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new ExtractFrameResult(/** @type {Partial<ExtractFrameResult>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FolderInfo 文件夹信息
|
||||||
|
*/
|
||||||
|
export class FolderInfo {
|
||||||
|
/**
|
||||||
|
* Creates a new FolderInfo instance.
|
||||||
|
* @param {Partial<FolderInfo>} [$$source = {}] - The source object to create the FolderInfo.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("path" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["path"] = "";
|
||||||
|
}
|
||||||
|
if (!("name" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["name"] = "";
|
||||||
|
}
|
||||||
|
if (!("videoCount" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["videoCount"] = 0;
|
||||||
|
}
|
||||||
|
if (!("videoPaths" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
this["videoPaths"] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new FolderInfo instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {FolderInfo}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
const $$createField3_0 = $$createType0;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("videoPaths" in $$parsedSource) {
|
||||||
|
$$parsedSource["videoPaths"] = $$createField3_0($$parsedSource["videoPaths"]);
|
||||||
|
}
|
||||||
|
return new FolderInfo(/** @type {Partial<FolderInfo>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LoginResponse 登录响应
|
||||||
|
*/
|
||||||
|
export class LoginResponse {
|
||||||
|
/**
|
||||||
|
* Creates a new LoginResponse instance.
|
||||||
|
* @param {Partial<LoginResponse>} [$$source = {}] - The source object to create the LoginResponse.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("Code" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["Code"] = 0;
|
||||||
|
}
|
||||||
|
if (!("Msg" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["Msg"] = "";
|
||||||
|
}
|
||||||
|
if (!("Data" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
this["Data"] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new LoginResponse instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {LoginResponse}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new LoginResponse(/** @type {Partial<LoginResponse>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VideoConcatRequest 视频拼接请求
|
||||||
|
*/
|
||||||
|
export class VideoConcatRequest {
|
||||||
|
/**
|
||||||
|
* Creates a new VideoConcatRequest instance.
|
||||||
|
* @param {Partial<VideoConcatRequest>} [$$source = {}] - The source object to create the VideoConcatRequest.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("folderPath" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["folderPath"] = "";
|
||||||
|
}
|
||||||
|
if (!("num" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["num"] = 0;
|
||||||
|
}
|
||||||
|
if (!("joinType" in $$source)) {
|
||||||
|
/**
|
||||||
|
* 1: 组合拼接, 2: 顺序拼接
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["joinType"] = 0;
|
||||||
|
}
|
||||||
|
if (!("auditImagePath" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["auditImagePath"] = "";
|
||||||
|
}
|
||||||
|
if (!("folderInfos" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {FolderInfo[]}
|
||||||
|
*/
|
||||||
|
this["folderInfos"] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new VideoConcatRequest instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {VideoConcatRequest}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
const $$createField4_0 = $$createType2;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("folderInfos" in $$parsedSource) {
|
||||||
|
$$parsedSource["folderInfos"] = $$createField4_0($$parsedSource["folderInfos"]);
|
||||||
|
}
|
||||||
|
return new VideoConcatRequest(/** @type {Partial<VideoConcatRequest>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VideoConcatResult 视频拼接结果
|
||||||
|
*/
|
||||||
|
export class VideoConcatResult {
|
||||||
|
/**
|
||||||
|
* Creates a new VideoConcatResult instance.
|
||||||
|
* @param {Partial<VideoConcatResult>} [$$source = {}] - The source object to create the VideoConcatResult.
|
||||||
|
*/
|
||||||
|
constructor($$source = {}) {
|
||||||
|
if (!("index" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["index"] = 0;
|
||||||
|
}
|
||||||
|
if (!("fileName" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["fileName"] = "";
|
||||||
|
}
|
||||||
|
if (!("filePath" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["filePath"] = "";
|
||||||
|
}
|
||||||
|
if (!("size" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["size"] = "";
|
||||||
|
}
|
||||||
|
if (!("seconds" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this["seconds"] = 0;
|
||||||
|
}
|
||||||
|
if (!("status" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["status"] = "";
|
||||||
|
}
|
||||||
|
if (!("progress" in $$source)) {
|
||||||
|
/**
|
||||||
|
* @member
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this["progress"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new VideoConcatResult instance from a string or object.
|
||||||
|
* @param {any} [$$source = {}]
|
||||||
|
* @returns {VideoConcatResult}
|
||||||
|
*/
|
||||||
|
static createFrom($$source = {}) {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new VideoConcatResult(/** @type {Partial<VideoConcatResult>} */($$parsedSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = $Create.Array($Create.Any);
|
||||||
|
const $$createType1 = FolderInfo.createFrom;
|
||||||
|
const $$createType2 = $Create.Array($$createType1);
|
||||||
75
wails/frontend/bindings/videoconcat/services/videoservice.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VideoService 视频处理服务
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as $models from "./models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConvertVideoToTS 将视频转换为 TS 格式
|
||||||
|
* @param {string} videoPath
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function ConvertVideoToTS(videoPath) {
|
||||||
|
return $Call.ByID(2005037305, videoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GenerateCombinations 生成所有视频组合(笛卡尔积)
|
||||||
|
* @param {string[][]} videoLists
|
||||||
|
* @param {number} index
|
||||||
|
* @param {string[]} currentCombination
|
||||||
|
* @param {string[][] | null} result
|
||||||
|
* @returns {$CancellablePromise<void>}
|
||||||
|
*/
|
||||||
|
export function GenerateCombinations(videoLists, index, currentCombination, result) {
|
||||||
|
return $Call.ByID(2802045808, videoLists, index, currentCombination, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetLargeFileMD5 计算大文件的 MD5
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {$CancellablePromise<string>}
|
||||||
|
*/
|
||||||
|
export function GetLargeFileMD5(filePath) {
|
||||||
|
return $Call.ByID(229340298, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JoinVideos 拼接视频
|
||||||
|
* 注意:Wails3 中回调函数需要使用事件系统,这里先简化处理
|
||||||
|
* @param {$models.VideoConcatRequest} req
|
||||||
|
* @returns {$CancellablePromise<$models.VideoConcatResult[]>}
|
||||||
|
*/
|
||||||
|
export function JoinVideos(req) {
|
||||||
|
return $Call.ByID(2660164351, req).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType1($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListFolders 列出文件夹中的视频文件夹
|
||||||
|
* @param {string} folderPath
|
||||||
|
* @returns {$CancellablePromise<$models.FolderInfo[]>}
|
||||||
|
*/
|
||||||
|
export function ListFolders(folderPath) {
|
||||||
|
return $Call.ByID(2209109868, folderPath).then(/** @type {($result: any) => any} */(($result) => {
|
||||||
|
return $$createType3($result);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = $models.VideoConcatResult.createFrom;
|
||||||
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
|
const $$createType2 = $models.FolderInfo.createFrom;
|
||||||
|
const $$createType3 = $Create.Array($$createType2);
|
||||||
1216
wails/frontend/package-lock.json
generated
Normal file
@ -5,10 +5,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"build:dev": "vite build --mode development",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0",
|
||||||
|
"@wailsio/runtime": "latest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
231
wails/frontend/src/App.vue
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 未登录时显示遮罩层 -->
|
||||||
|
<div v-if="!isLoggedIn" class="login-overlay">
|
||||||
|
<div class="login-message">
|
||||||
|
<h2>请先登录</h2>
|
||||||
|
<p>您需要登录后才能使用此应用</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主界面 -->
|
||||||
|
<div v-show="isLoggedIn" class="main-interface">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="menu-items">
|
||||||
|
<button
|
||||||
|
:class="['menu-item', { active: currentTab === 'video' }]"
|
||||||
|
@click="currentTab = 'video'"
|
||||||
|
>
|
||||||
|
<span class="icon">🎬</span>
|
||||||
|
<span>视频</span>
|
||||||
|
</button>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<button
|
||||||
|
:class="['menu-item', { active: currentTab === 'extract' }]"
|
||||||
|
@click="currentTab = 'extract'"
|
||||||
|
>
|
||||||
|
<span class="icon">✂️</span>
|
||||||
|
<span>抽帧</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="menu-bottom">
|
||||||
|
<button class="menu-item" @click="handleLogout">
|
||||||
|
<span class="icon">👤</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
<VideoTab v-if="currentTab === 'video'" />
|
||||||
|
<ExtractTab v-if="currentTab === 'extract'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录对话框 -->
|
||||||
|
<LoginDialog
|
||||||
|
v-if="showLogin"
|
||||||
|
:allow-close="isLoggedIn"
|
||||||
|
@close="handleLoginClose"
|
||||||
|
@login-success="handleLoginSuccess"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import VideoTab from './components/VideoTab.vue'
|
||||||
|
import ExtractTab from './components/ExtractTab.vue'
|
||||||
|
import LoginDialog from './components/LoginDialog.vue'
|
||||||
|
|
||||||
|
const currentTab = ref('video')
|
||||||
|
const showLogin = ref(false)
|
||||||
|
const isLoggedIn = ref(false)
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
const loginStatus = localStorage.getItem('isLoggedIn')
|
||||||
|
const loginTime = localStorage.getItem('loginTime')
|
||||||
|
|
||||||
|
if (loginStatus === 'true' && loginTime) {
|
||||||
|
// 检查登录是否过期(24小时)
|
||||||
|
const now = Date.now()
|
||||||
|
const loginTimestamp = parseInt(loginTime)
|
||||||
|
const hoursSinceLogin = (now - loginTimestamp) / (1000 * 60 * 60)
|
||||||
|
|
||||||
|
if (hoursSinceLogin < 24) {
|
||||||
|
isLoggedIn.value = true
|
||||||
|
} else {
|
||||||
|
// 登录已过期
|
||||||
|
localStorage.removeItem('isLoggedIn')
|
||||||
|
localStorage.removeItem('loginTime')
|
||||||
|
isLoggedIn.value = false
|
||||||
|
showLogin.value = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isLoggedIn.value = false
|
||||||
|
showLogin.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录成功
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
isLoggedIn.value = true
|
||||||
|
showLogin.value = false
|
||||||
|
localStorage.setItem('isLoggedIn', 'true')
|
||||||
|
localStorage.setItem('loginTime', Date.now().toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录对话框关闭
|
||||||
|
const handleLoginClose = () => {
|
||||||
|
// 如果未登录,不允许关闭登录对话框
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showLogin.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登出
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (confirm('确定要退出登录吗?')) {
|
||||||
|
isLoggedIn.value = false
|
||||||
|
showLogin.value = true
|
||||||
|
localStorage.removeItem('isLoggedIn')
|
||||||
|
localStorage.removeItem('loginTime')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时检查登录状态
|
||||||
|
onMounted(() => {
|
||||||
|
checkLoginStatus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(to bottom, #fefefe, #ededef);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 70px;
|
||||||
|
background: #7163ba;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-right: 2px solid #ebedf3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #000;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: #5a5080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background: #8b4513;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
margin: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-message {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-message h2 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-message p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-interface {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -69,14 +69,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { ExtractService, FileService } from '../../bindings/videoconcat/services/index.js'
|
||||||
let extractService = null
|
|
||||||
let fileService = null
|
|
||||||
|
|
||||||
if (window.go && window.go.services) {
|
|
||||||
extractService = window.go.services.ExtractService
|
|
||||||
fileService = window.go.services.FileService
|
|
||||||
}
|
|
||||||
|
|
||||||
const folderPath = ref('')
|
const folderPath = ref('')
|
||||||
const videos = ref([])
|
const videos = ref([])
|
||||||
@ -102,29 +95,24 @@ const failCount = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectFolder = async () => {
|
const selectFolder = async () => {
|
||||||
const input = document.createElement('input')
|
try {
|
||||||
input.type = 'file'
|
// 调用后端选择文件夹对话框
|
||||||
input.webkitdirectory = true
|
const selectedPath = await FileService.SelectFolder()
|
||||||
input.onchange = async (e) => {
|
if (selectedPath && selectedPath.trim()) {
|
||||||
const files = Array.from(e.target.files).filter(f => f.name.endsWith('.mp4'))
|
folderPath.value = selectedPath.trim()
|
||||||
if (files.length > 0) {
|
|
||||||
const firstFile = files[0]
|
|
||||||
const path = firstFile.webkitRelativePath.split('/')[0]
|
|
||||||
folderPath.value = path
|
|
||||||
videos.value = files.map(f => f.name)
|
|
||||||
|
|
||||||
// 调用后端列出视频
|
// 调用后端列出视频
|
||||||
if (extractService && extractService.ListVideos) {
|
|
||||||
try {
|
try {
|
||||||
videos.value = await extractService.ListVideos(folderPath.value)
|
videos.value = await ExtractService.ListVideos(folderPath.value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('列出视频失败: ' + error.message)
|
alert('列出视频失败: ' + error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择文件夹失败:', error)
|
||||||
|
alert('选择文件夹失败: ' + (error.message || '未知错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFileName = (path) => {
|
const getFileName = (path) => {
|
||||||
return path.split('/').pop() || path.split('\\').pop() || path
|
return path.split('/').pop() || path.split('\\').pop() || path
|
||||||
@ -143,15 +131,18 @@ const startExtract = async () => {
|
|||||||
extractCount: extractCount.value
|
extractCount: extractCount.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extractService && extractService.ExtractFrames) {
|
|
||||||
const total = videos.value.length * extractCount.value
|
const total = videos.value.length * extractCount.value
|
||||||
helpInfo.value = `处理中... (0/${total})`
|
helpInfo.value = `处理中... (0/${total})`
|
||||||
|
|
||||||
const extractResults = await extractService.ExtractFrames(request)
|
try {
|
||||||
|
const extractResults = await ExtractService.ExtractFrames(request)
|
||||||
if (extractResults) {
|
if (extractResults) {
|
||||||
results.value = extractResults
|
results.value = extractResults
|
||||||
helpInfo.value = `全部完成! 共处理 ${total} 个任务,成功 ${successCount.value} 个,失败 ${failCount.value} 个`
|
helpInfo.value = `全部完成! 共处理 ${total} 个任务,成功 ${successCount.value} 个,失败 ${failCount.value} 个`
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('抽帧失败: ' + error.message)
|
||||||
|
helpInfo.value = '处理失败'
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('抽帧失败: ' + error.message)
|
alert('抽帧失败: ' + error.message)
|
||||||
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dialog-overlay" @click="$emit('close')">
|
<div class="dialog-overlay" @click="handleOverlayClick">
|
||||||
<div class="dialog" @click.stop>
|
<div class="dialog" @click.stop>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3>用户登录</h3>
|
<h3>用户登录</h3>
|
||||||
<button class="close-btn" @click="$emit('close')">×</button>
|
<button v-if="allowClose" class="close-btn" @click="$emit('close')">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button class="btn-secondary" @click="$emit('close')">取消</button>
|
<button v-if="allowClose" class="btn-secondary" @click="$emit('close')">取消</button>
|
||||||
<button class="btn-primary" @click="handleLogin" :disabled="isLogging">
|
<button class="btn-primary" @click="handleLogin" :disabled="isLogging">
|
||||||
{{ isLogging ? '登录中...' : '登录' }}
|
{{ isLogging ? '登录中...' : '登录' }}
|
||||||
</button>
|
</button>
|
||||||
@ -30,20 +30,29 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { AuthService } from '../../bindings/videoconcat/services/index.js'
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
const props = defineProps({
|
||||||
|
allowClose: {
|
||||||
let authService = null
|
type: Boolean,
|
||||||
|
default: false
|
||||||
if (window.go && window.go.services) {
|
|
||||||
authService = window.go.services.AuthService
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'login-success'])
|
||||||
|
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const isLogging = ref(false)
|
const isLogging = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
// 处理遮罩层点击
|
||||||
|
const handleOverlayClick = () => {
|
||||||
|
if (props.allowClose) {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!username.value || !password.value) {
|
if (!username.value || !password.value) {
|
||||||
errorMessage.value = '请输入用户名和密码'
|
errorMessage.value = '请输入用户名和密码'
|
||||||
@ -54,19 +63,15 @@ const handleLogin = async () => {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (authService && authService.Login) {
|
const response = await AuthService.Login(username.value, password.value)
|
||||||
const response = await authService.Login(username.value, password.value)
|
if (response && response.Code === 200) {
|
||||||
if (response.Code === 200) {
|
emit('login-success')
|
||||||
alert('登录成功!')
|
|
||||||
emit('close')
|
emit('close')
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = response.Msg || '登录失败'
|
errorMessage.value = (response && response.Msg) || '登录失败'
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMessage.value = '认证服务未初始化'
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = '登录失败: ' + error.message
|
errorMessage.value = '登录失败: ' + (error.message || error)
|
||||||
} finally {
|
} finally {
|
||||||
isLogging.value = false
|
isLogging.value = false
|
||||||
}
|
}
|
||||||
@ -114,18 +114,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { VideoService, FileService } from '../../bindings/videoconcat/services/index.js'
|
||||||
// 使用 Wails3 的绑定
|
|
||||||
let videoService = null
|
|
||||||
let fileService = null
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 获取绑定的服务(需要根据 Wails3 的实际 API 调整)
|
|
||||||
if (window.go && window.go.services) {
|
|
||||||
videoService = window.go.services.VideoService
|
|
||||||
fileService = window.go.services.FileService
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const folderPath = ref('')
|
const folderPath = ref('')
|
||||||
const folderInfos = ref([])
|
const folderInfos = ref([])
|
||||||
@ -141,33 +130,25 @@ const canStart = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectFolder = async () => {
|
const selectFolder = async () => {
|
||||||
// 使用系统对话框选择文件夹
|
try {
|
||||||
// 注意:在浏览器环境中需要使用 input[type=file] 的 webkitdirectory 属性
|
// 调用后端选择文件夹对话框
|
||||||
// 或者通过后端调用系统 API
|
const selectedPath = await FileService.SelectFolder()
|
||||||
const input = document.createElement('input')
|
if (selectedPath && selectedPath.trim()) {
|
||||||
input.type = 'file'
|
folderPath.value = selectedPath.trim()
|
||||||
input.webkitdirectory = true
|
|
||||||
input.onchange = async (e) => {
|
|
||||||
const files = e.target.files
|
|
||||||
if (files.length > 0) {
|
|
||||||
// 获取第一个文件的目录路径
|
|
||||||
const firstFile = files[0]
|
|
||||||
const path = firstFile.webkitRelativePath.split('/')[0]
|
|
||||||
folderPath.value = path
|
|
||||||
|
|
||||||
// 调用后端列出文件夹
|
// 调用后端列出文件夹
|
||||||
if (videoService && videoService.ListFolders) {
|
|
||||||
try {
|
try {
|
||||||
folderInfos.value = await videoService.ListFolders(folderPath.value)
|
folderInfos.value = await VideoService.ListFolders(folderPath.value)
|
||||||
updateMaxNum()
|
updateMaxNum()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('列出文件夹失败: ' + error.message)
|
alert('列出文件夹失败: ' + error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择文件夹失败:', error)
|
||||||
|
alert('选择文件夹失败: ' + (error.message || '未知错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectAuditImage = () => {
|
const selectAuditImage = () => {
|
||||||
const input = document.createElement('input')
|
const input = document.createElement('input')
|
||||||
@ -208,11 +189,13 @@ const startConcat = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用后端拼接视频
|
// 调用后端拼接视频
|
||||||
if (videoService && videoService.JoinVideos) {
|
try {
|
||||||
const results = await videoService.JoinVideos(request)
|
const results = await VideoService.JoinVideos(request)
|
||||||
if (results) {
|
if (results) {
|
||||||
concatResults.value = results
|
concatResults.value = results
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('拼接视频失败: ' + error.message)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('拼接失败: ' + error.message)
|
alert('拼接失败: ' + error.message)
|
||||||
@ -6,7 +6,14 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
outDir: '../assets',
|
outDir: '../assets',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['@wailsio/runtime']
|
||||||
|
}
|
||||||
},
|
},
|
||||||
base: './',
|
server: {
|
||||||
|
port: 9245,
|
||||||
|
strictPort: true
|
||||||
|
},
|
||||||
|
base: './'
|
||||||
})
|
})
|
||||||
|
|
||||||
45
wails/go.mod
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
module videoconcat
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require github.com/wailsapp/wails/v3 v3.0.0-alpha.57
|
||||||
|
|
||||||
|
require (
|
||||||
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||||
|
github.com/adrg/xdg v0.5.3 // indirect
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.0 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
|
github.com/go-git/go-git/v5 v5.13.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // 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
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
|
github.com/lmittmann/tint v1.0.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
|
golang.org/x/net v0.37.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
)
|
||||||
145
wails/go.sum
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||||
|
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
|
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||||
|
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
|
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||||
|
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
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/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
|
||||||
|
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
|
||||||
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||||
|
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||||
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
|
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-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||||
|
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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
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/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=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
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/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
|
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
|
||||||
|
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
|
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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
|
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||||
|
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||||
|
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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||||
|
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
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/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/wails/v3 v3.0.0-alpha.57 h1:E1CRTZgMZ3UKkbkMgycpOGbTG2UYjB+UHDOLiG7RN7o=
|
||||||
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.57/go.mod h1:ynGPamjQDXoaWjOGKAHJ6vw94PUDbeIxtbapunWcDjk=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||||
|
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
65
wails/main.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"videoconcat/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 检测开发模式并设置日志级别
|
||||||
|
if os.Getenv("DEV") == "true" {
|
||||||
|
services.SetLogLevel("DEBUG")
|
||||||
|
services.LogInfo("=== 开发模式启动 ===")
|
||||||
|
services.LogDebug("详细日志已启用")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建服务
|
||||||
|
services.LogDebug("开始创建服务...")
|
||||||
|
videoService := services.NewVideoService()
|
||||||
|
extractService := services.NewExtractService()
|
||||||
|
authService := services.NewAuthService()
|
||||||
|
fileService := services.NewFileService()
|
||||||
|
services.LogDebug("所有服务创建完成")
|
||||||
|
|
||||||
|
services.LogDebug("创建 Wails 应用...")
|
||||||
|
app := application.New(application.Options{
|
||||||
|
Name: "VideoConcat",
|
||||||
|
Description: "视频拼接工具",
|
||||||
|
Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
|
||||||
|
Services: []application.Service{
|
||||||
|
application.NewService(videoService),
|
||||||
|
application.NewService(extractService),
|
||||||
|
application.NewService(authService),
|
||||||
|
application.NewService(fileService),
|
||||||
|
},
|
||||||
|
Mac: application.MacOptions{
|
||||||
|
ApplicationShouldTerminateAfterLastWindowClosed: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
services.LogDebug("Wails 应用创建完成")
|
||||||
|
|
||||||
|
// 创建窗口
|
||||||
|
services.LogDebug("创建应用窗口...")
|
||||||
|
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
|
Title: "视频拼接工具",
|
||||||
|
Width: 1100,
|
||||||
|
Height: 800,
|
||||||
|
MinWidth: 800,
|
||||||
|
MinHeight: 600,
|
||||||
|
})
|
||||||
|
|
||||||
|
window.SetURL("index.html")
|
||||||
|
services.LogInfo("应用窗口已创建,URL: index.html")
|
||||||
|
|
||||||
|
services.LogInfo("=== 应用启动完成,开始运行 ===")
|
||||||
|
if err := app.Run(); err != nil {
|
||||||
|
services.LogErrorf("应用运行错误: %v", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
269
wails/services/auth_service.go
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SuperUserConfig 超级用户配置
|
||||||
|
type SuperUserConfig struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthConfig 认证配置
|
||||||
|
type AuthConfig struct {
|
||||||
|
SuperUsers []SuperUserConfig `json:"super_users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
authConfig *AuthConfig
|
||||||
|
authConfigOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadAuthConfig 加载认证配置
|
||||||
|
func loadAuthConfig() *AuthConfig {
|
||||||
|
authConfigOnce.Do(func() {
|
||||||
|
// 默认配置("080500"的MD5 hash值)
|
||||||
|
defaultHash := "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" // 这是占位符,实际会计算
|
||||||
|
// 计算"080500"的实际hash值
|
||||||
|
defaultHash = getPasswordHash("080500")
|
||||||
|
|
||||||
|
authConfig = &AuthConfig{
|
||||||
|
SuperUsers: []SuperUserConfig{
|
||||||
|
{
|
||||||
|
Username: "super",
|
||||||
|
PasswordHash: defaultHash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从配置文件加载
|
||||||
|
configPath := getConfigPath()
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
LogWarnf("无法读取配置文件 %s,使用默认配置: %v", configPath, err)
|
||||||
|
LogInfof("默认超级用户: super, 密码hash: %s (对应密码: 080500)", defaultHash)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config AuthConfig
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
LogErrorf("解析配置文件失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配置
|
||||||
|
if len(config.SuperUsers) == 0 {
|
||||||
|
LogWarn("配置文件中没有超级用户,使用默认配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配置中的hash值,如果是占位符则使用默认值
|
||||||
|
for i := range config.SuperUsers {
|
||||||
|
hash := config.SuperUsers[i].PasswordHash
|
||||||
|
// 检查是否为占位符(包含说明文字或长度不足32位)
|
||||||
|
if strings.Contains(hash, "将此处替换") ||
|
||||||
|
strings.Contains(hash, "请将此处替换") ||
|
||||||
|
strings.Contains(hash, "计算") ||
|
||||||
|
strings.Contains(hash, "MD5") ||
|
||||||
|
len(hash) < 32 {
|
||||||
|
config.SuperUsers[i].PasswordHash = defaultHash
|
||||||
|
LogWarnf("检测到占位符hash值,已替换为默认hash: %s (对应密码: 080500)", defaultHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig = &config
|
||||||
|
LogInfof("成功加载认证配置,共 %d 个超级用户", len(authConfig.SuperUsers))
|
||||||
|
for i, user := range authConfig.SuperUsers {
|
||||||
|
LogDebugf("超级用户 %d: 用户名=%s, hash=%s", i+1, user.Username, user.PasswordHash)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return authConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigPath 获取配置文件路径
|
||||||
|
func getConfigPath() string {
|
||||||
|
// 优先使用可执行文件目录下的config.json
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
exeDir := filepath.Dir(exePath)
|
||||||
|
configPath := filepath.Join(exeDir, "config.json")
|
||||||
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
return configPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其次使用当前目录下的config.json
|
||||||
|
if _, err := os.Stat("config.json"); err == nil {
|
||||||
|
return "config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后使用wails目录下的config.json
|
||||||
|
return "wails/config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthService 认证服务
|
||||||
|
type AuthService struct {
|
||||||
|
baseURL string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthService 创建认证服务实例
|
||||||
|
func NewAuthService() *AuthService {
|
||||||
|
return &AuthService{
|
||||||
|
baseURL: "https://admin.xiangbing.vip",
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest 登录请求
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"Username"`
|
||||||
|
Password string `json:"Password"`
|
||||||
|
Platform string `json:"Platform"`
|
||||||
|
PcName string `json:"PcName"`
|
||||||
|
PcUserName string `json:"PcUserName"`
|
||||||
|
Ips string `json:"Ips"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginResponse 登录响应
|
||||||
|
type LoginResponse struct {
|
||||||
|
Code int `json:"Code"`
|
||||||
|
Msg string `json:"Msg"`
|
||||||
|
Data interface{} `json:"Data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPasswordHash 计算密码的MD5 hash值
|
||||||
|
func getPasswordHash(password string) string {
|
||||||
|
hash := md5.Sum([]byte(password))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSuperUser 检查是否为超级用户
|
||||||
|
func checkSuperUser(username, password string) bool {
|
||||||
|
config := loadAuthConfig()
|
||||||
|
|
||||||
|
// 计算输入密码的hash值
|
||||||
|
inputPasswordHash := getPasswordHash(password)
|
||||||
|
|
||||||
|
// 遍历配置中的超级用户
|
||||||
|
for _, superUser := range config.SuperUsers {
|
||||||
|
if superUser.Username == username {
|
||||||
|
LogDebugf("超级用户检查 - 用户名: %s, 输入密码hash: %s, 期望hash: %s",
|
||||||
|
username, inputPasswordHash, superUser.PasswordHash)
|
||||||
|
|
||||||
|
if inputPasswordHash == superUser.PasswordHash {
|
||||||
|
LogInfo("超级用户验证成功")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录
|
||||||
|
func (s *AuthService) Login(ctx context.Context, username, password string) (*LoginResponse, error) {
|
||||||
|
startTime := time.Now()
|
||||||
|
LogDebugf("Login 开始 - 用户名: %s", username)
|
||||||
|
|
||||||
|
// 检查是否为超级用户
|
||||||
|
if checkSuperUser(username, password) {
|
||||||
|
LogInfo("检测到超级用户登录,跳过API调用")
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
LogInfof("超级用户登录成功 - 用户名: %s, 耗时: %v", username, duration)
|
||||||
|
|
||||||
|
return &LoginResponse{
|
||||||
|
Code: 200,
|
||||||
|
Msg: "登录成功",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"username": "super",
|
||||||
|
"isSuper": true,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取机器信息
|
||||||
|
pcMachineName, _ := os.Hostname()
|
||||||
|
pcUserName := os.Getenv("USERNAME")
|
||||||
|
if pcUserName == "" {
|
||||||
|
pcUserName = os.Getenv("USER")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 IP 地址
|
||||||
|
var ips string
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err == nil {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||||
|
if ipnet.IP.To4() != nil {
|
||||||
|
ips = ipnet.IP.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDebugf("机器信息 - 主机名: %s, 用户名: %s, IP: %s", pcMachineName, pcUserName, ips)
|
||||||
|
|
||||||
|
reqData := LoginRequest{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
Platform: "pc",
|
||||||
|
PcName: pcMachineName,
|
||||||
|
PcUserName: pcUserName,
|
||||||
|
Ips: ips,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(reqData)
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("序列化请求数据失败: %v", err)
|
||||||
|
return nil, fmt.Errorf("序列化请求数据失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := s.baseURL + "/api/base/login"
|
||||||
|
LogDebugf("发送登录请求 - URL: %s, 请求数据: %s", url, string(jsonData))
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("创建请求失败: %v", err)
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := s.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("请求失败: %v", err)
|
||||||
|
return nil, fmt.Errorf("请求失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
LogDebugf("收到响应 - 状态码: %d, 状态: %s", resp.StatusCode, resp.Status)
|
||||||
|
|
||||||
|
var loginResp LoginResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
|
||||||
|
LogErrorf("解析响应失败: %v", err)
|
||||||
|
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
LogInfof("Login 完成 - 用户名: %s, 响应码: %d, 消息: %s, 耗时: %v",
|
||||||
|
username, loginResp.Code, loginResp.Msg, duration)
|
||||||
|
|
||||||
|
return &loginResp, nil
|
||||||
|
}
|
||||||
@ -81,6 +81,7 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string
|
|||||||
fmt.Sscanf(parts[1], "%f", &den)
|
fmt.Sscanf(parts[1], "%f", &den)
|
||||||
if den > 0 {
|
if den > 0 {
|
||||||
frameRate = num / den
|
frameRate = num / den
|
||||||
|
_ = frameRate // 避免未使用变量警告
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,7 +194,7 @@ func (s *ExtractService) ExtractFrames(ctx context.Context, req ExtractFrameRequ
|
|||||||
// 并发处理(限制并发数)
|
// 并发处理(限制并发数)
|
||||||
semaphore := make(chan struct{}, 10)
|
semaphore := make(chan struct{}, 10)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
total := len(tasks)
|
// total := len(tasks) // 未使用
|
||||||
current := 0
|
current := 0
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
|
|
||||||
@ -274,7 +275,7 @@ func (s *ExtractService) ModifyVideosMetadata(ctx context.Context, folderPath st
|
|||||||
|
|
||||||
semaphore := make(chan struct{}, 10)
|
semaphore := make(chan struct{}, 10)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
total := len(videos)
|
// total := len(videos) // 未使用
|
||||||
current := 0
|
current := 0
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
|
|
||||||
180
wails/services/file_service.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileService 文件服务
|
||||||
|
type FileService struct{}
|
||||||
|
|
||||||
|
// NewFileService 创建文件服务实例
|
||||||
|
func NewFileService() *FileService {
|
||||||
|
return &FileService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectFolder 选择文件夹(返回路径)
|
||||||
|
func (s *FileService) SelectFolder(ctx context.Context) (string, error) {
|
||||||
|
LogDebug("开始选择文件夹...")
|
||||||
|
|
||||||
|
// 根据操作系统使用不同的方法
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin": // macOS
|
||||||
|
return s.selectFolderMacOS()
|
||||||
|
case "windows":
|
||||||
|
return s.selectFolderWindows()
|
||||||
|
case "linux":
|
||||||
|
return s.selectFolderLinux()
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectFolderMacOS 在 macOS 上选择文件夹
|
||||||
|
func (s *FileService) selectFolderMacOS() (string, error) {
|
||||||
|
// 使用 AppleScript 打开文件夹选择对话框
|
||||||
|
script := `tell application "System Events"
|
||||||
|
activate
|
||||||
|
set folderPath to choose folder with prompt "请选择文件夹"
|
||||||
|
return POSIX path of folderPath
|
||||||
|
end tell`
|
||||||
|
|
||||||
|
cmd := exec.Command("osascript", "-e", script)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("选择文件夹失败: %v", err)
|
||||||
|
return "", fmt.Errorf("选择文件夹失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.TrimSpace(string(output))
|
||||||
|
LogInfof("选择的文件夹路径: %s", path)
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectFolderWindows 在 Windows 上选择文件夹
|
||||||
|
func (s *FileService) selectFolderWindows() (string, error) {
|
||||||
|
// 使用 PowerShell 打开文件夹选择对话框
|
||||||
|
script := `Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||||
|
$folderBrowser.Description = "请选择文件夹"
|
||||||
|
$result = $folderBrowser.ShowDialog()
|
||||||
|
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
|
Write-Output $folderBrowser.SelectedPath
|
||||||
|
}`
|
||||||
|
|
||||||
|
cmd := exec.Command("powershell", "-Command", script)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("选择文件夹失败: %v", err)
|
||||||
|
return "", fmt.Errorf("选择文件夹失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.TrimSpace(string(output))
|
||||||
|
LogInfof("选择的文件夹路径: %s", path)
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectFolderLinux 在 Linux 上选择文件夹
|
||||||
|
func (s *FileService) selectFolderLinux() (string, error) {
|
||||||
|
// 使用 zenity 或 kdialog 打开文件夹选择对话框
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
// 先尝试使用 zenity
|
||||||
|
if _, err := exec.LookPath("zenity"); err == nil {
|
||||||
|
cmd = exec.Command("zenity", "--file-selection", "--directory", "--title=请选择文件夹")
|
||||||
|
} else if _, err := exec.LookPath("kdialog"); err == nil {
|
||||||
|
cmd = exec.Command("kdialog", "--getexistingdirectory", "$HOME", "--title", "请选择文件夹")
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("未找到 zenity 或 kdialog,无法打开文件夹选择对话框")
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("选择文件夹失败: %v", err)
|
||||||
|
return "", fmt.Errorf("选择文件夹失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.TrimSpace(string(output))
|
||||||
|
LogInfof("选择的文件夹路径: %s", path)
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectFile 选择文件(返回路径)
|
||||||
|
func (s *FileService) SelectFile(ctx context.Context, filter string) (string, error) {
|
||||||
|
// 在 Wails3 中,文件选择需要通过前端实现
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFolder 打开文件夹
|
||||||
|
func (s *FileService) OpenFolder(ctx context.Context, folderPath string) error {
|
||||||
|
LogDebugf("打开文件夹: %s", folderPath)
|
||||||
|
|
||||||
|
// 检查文件夹是否存在
|
||||||
|
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("文件夹不存在: %s", folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据操作系统使用不同的命令
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin": // macOS
|
||||||
|
cmd = exec.Command("open", folderPath)
|
||||||
|
case "windows":
|
||||||
|
cmd = exec.Command("explorer", folderPath)
|
||||||
|
case "linux":
|
||||||
|
cmd = exec.Command("xdg-open", folderPath)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("打开文件夹失败: %v", err)
|
||||||
|
return fmt.Errorf("打开文件夹失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfo("文件夹已打开")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists 检查文件是否存在
|
||||||
|
func (s *FileService) FileExists(ctx context.Context, filePath string) (bool, error) {
|
||||||
|
_, err := os.Stat(filePath)
|
||||||
|
if err == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileSize 获取文件大小(字节)
|
||||||
|
func (s *FileService) GetFileSize(ctx context.Context, filePath string) (int64, error) {
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return info.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDirectory 确保目录存在
|
||||||
|
func (s *FileService) EnsureDirectory(ctx context.Context, dirPath string) error {
|
||||||
|
return os.MkdirAll(dirPath, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFiles 列出目录中的文件
|
||||||
|
func (s *FileService) ListFiles(ctx context.Context, dirPath string, pattern string) ([]string, error) {
|
||||||
|
patternPath := filepath.Join(dirPath, pattern)
|
||||||
|
matches, err := filepath.Glob(patternPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
||||||
|
|
||||||