From 75f9c5d7d41a8dd24557285809b04bb89f0ad0ec Mon Sep 17 00:00:00 2001 From: arhat0307 Date: Fri, 26 Jun 2026 00:06:07 +0900 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 7 + .../checksum/build-frontend--DEV--RUNNER-npm- | 1 + .../checksum/windows-common-generate-bindings | 1 + .task/checksum/windows-common-generate-icons | 1 + .../windows-common-install-frontend-deps-npm | 1 + README.md | 2 + Taskfile.yml | 61 + build/Taskfile.yml | 366 ++++ build/android/Taskfile.yml | 290 +++ build/android/app/build.gradle | 83 + build/android/app/proguard-rules.pro | 12 + .../android/app/src/main/AndroidManifest.xml | 63 + .../main/java/com/wails/app/MainActivity.java | 806 +++++++++ .../main/java/com/wails/app/WailsBridge.java | 1317 ++++++++++++++ .../com/wails/app/WailsForegroundService.java | 74 + .../java/com/wails/app/WailsJSBridge.java | 151 ++ .../java/com/wails/app/WailsPathHandler.java | 68 + .../app/src/main/res/layout/activity_main.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2338 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2338 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1652 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1652 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3276 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3276 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5202 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 5202 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7198 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 7198 bytes .../app/src/main/res/values/colors.xml | 8 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/themes.xml | 14 + .../app/src/main/res/xml/file_paths.xml | 6 + build/android/build.gradle | 4 + build/android/gradle.properties | 26 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + build/android/gradlew | 248 +++ build/android/gradlew.bat | 93 + build/android/main_android.go | 11 + build/android/scripts/deps/install_deps.go | 151 ++ build/android/settings.gradle | 18 + .../appicon.icon/Assets/wails_icon_vector.svg | 9 + build/appicon.icon/icon.json | 51 + build/appicon.png | Bin 0 -> 28176 bytes build/config.yml | 79 + build/darwin/Assets.car | Bin 0 -> 1631800 bytes build/darwin/Info.dev.plist | 35 + build/darwin/Info.plist | 30 + build/darwin/Taskfile.yml | 193 ++ build/darwin/icons.icns | Bin 0 -> 76609 bytes build/docker/Dockerfile.cross | 220 +++ build/docker/Dockerfile.server | 41 + build/ios/Assets.xcassets | 116 ++ build/ios/Info.dev.plist | 56 + build/ios/Info.plist | 54 + build/ios/LaunchScreen.storyboard | 53 + build/ios/Taskfile.yml | 365 ++++ build/ios/app_options_default.go | 10 + build/ios/app_options_ios.go | 11 + build/ios/entitlements.plist | 8 + build/ios/icon.png | 3 + build/ios/main.m | 22 + build/ios/main_ios.go | 23 + build/ios/project.pbxproj | 222 +++ build/ios/scripts/deps/install_deps.go | 319 ++++ build/linux/Taskfile.yml | 220 +++ build/linux/appimage/build.sh | 35 + build/linux/desktop | 13 + build/linux/nfpm/nfpm.yaml | 80 + build/linux/nfpm/scripts/postinstall.sh | 21 + build/linux/nfpm/scripts/postremove.sh | 1 + build/linux/nfpm/scripts/preinstall.sh | 1 + build/linux/nfpm/scripts/preremove.sh | 1 + build/windows/Taskfile.yml | 195 +++ build/windows/icon.ico | Bin 0 -> 13021 bytes build/windows/info.json | 15 + build/windows/msix/app_manifest.xml | 55 + build/windows/msix/template.xml | 54 + build/windows/nsis/project.nsi | 119 ++ build/windows/nsis/wails_tools.nsh | 261 +++ build/windows/wails.exe.manifest | 22 + docs/PLAN.md | 132 ++ .../wailsapp/wails/v3/internal/eventcreate.ts | 9 + .../wailsapp/wails/v3/internal/eventdata.d.ts | 2 + frontend/bindings/opiccoach/appservice.ts | 34 + frontend/bindings/opiccoach/index.ts | 21 + frontend/bindings/opiccoach/models.ts | 92 + frontend/bindings/time/index.ts | 6 + frontend/bindings/time/models.ts | 47 + frontend/index.html | 14 + frontend/package-lock.json | 1556 +++++++++++++++++ frontend/package.json | 22 + frontend/src/App.vue | 437 +++++ frontend/src/api.ts | 24 + frontend/src/main.ts | 6 + frontend/src/style.css | 176 ++ frontend/src/types.ts | 45 + frontend/src/vite-env.d.ts | 2 + frontend/tsconfig.json | 17 + frontend/vite.config.ts | 13 + go.mod | 17 + go.sum | 37 + main.go | 43 + models.go | 101 ++ opiccoach.exe | Bin 0 -> 16172032 bytes questions.go | 61 + service.go | 421 +++++ 108 files changed, 10256 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .task/checksum/build-frontend--DEV--RUNNER-npm- create mode 100644 .task/checksum/windows-common-generate-bindings create mode 100644 .task/checksum/windows-common-generate-icons create mode 100644 .task/checksum/windows-common-install-frontend-deps-npm create mode 100644 README.md create mode 100644 Taskfile.yml create mode 100644 build/Taskfile.yml create mode 100644 build/android/Taskfile.yml create mode 100644 build/android/app/build.gradle create mode 100644 build/android/app/proguard-rules.pro create mode 100644 build/android/app/src/main/AndroidManifest.xml create mode 100644 build/android/app/src/main/java/com/wails/app/MainActivity.java create mode 100644 build/android/app/src/main/java/com/wails/app/WailsBridge.java create mode 100644 build/android/app/src/main/java/com/wails/app/WailsForegroundService.java create mode 100644 build/android/app/src/main/java/com/wails/app/WailsJSBridge.java create mode 100644 build/android/app/src/main/java/com/wails/app/WailsPathHandler.java create mode 100644 build/android/app/src/main/res/layout/activity_main.xml create mode 100644 build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 build/android/app/src/main/res/values/colors.xml create mode 100644 build/android/app/src/main/res/values/strings.xml create mode 100644 build/android/app/src/main/res/values/themes.xml create mode 100644 build/android/app/src/main/res/xml/file_paths.xml create mode 100644 build/android/build.gradle create mode 100644 build/android/gradle.properties create mode 100644 build/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 build/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 build/android/gradlew create mode 100644 build/android/gradlew.bat create mode 100644 build/android/main_android.go create mode 100644 build/android/scripts/deps/install_deps.go create mode 100644 build/android/settings.gradle create mode 100644 build/appicon.icon/Assets/wails_icon_vector.svg create mode 100644 build/appicon.icon/icon.json create mode 100644 build/appicon.png create mode 100644 build/config.yml create mode 100644 build/darwin/Assets.car create mode 100644 build/darwin/Info.dev.plist create mode 100644 build/darwin/Info.plist create mode 100644 build/darwin/Taskfile.yml create mode 100644 build/darwin/icons.icns create mode 100644 build/docker/Dockerfile.cross create mode 100644 build/docker/Dockerfile.server create mode 100644 build/ios/Assets.xcassets create mode 100644 build/ios/Info.dev.plist create mode 100644 build/ios/Info.plist create mode 100644 build/ios/LaunchScreen.storyboard create mode 100644 build/ios/Taskfile.yml create mode 100644 build/ios/app_options_default.go create mode 100644 build/ios/app_options_ios.go create mode 100644 build/ios/entitlements.plist create mode 100644 build/ios/icon.png create mode 100644 build/ios/main.m create mode 100644 build/ios/main_ios.go create mode 100644 build/ios/project.pbxproj create mode 100644 build/ios/scripts/deps/install_deps.go create mode 100644 build/linux/Taskfile.yml create mode 100644 build/linux/appimage/build.sh create mode 100644 build/linux/desktop create mode 100644 build/linux/nfpm/nfpm.yaml create mode 100644 build/linux/nfpm/scripts/postinstall.sh create mode 100644 build/linux/nfpm/scripts/postremove.sh create mode 100644 build/linux/nfpm/scripts/preinstall.sh create mode 100644 build/linux/nfpm/scripts/preremove.sh create mode 100644 build/windows/Taskfile.yml create mode 100644 build/windows/icon.ico create mode 100644 build/windows/info.json create mode 100644 build/windows/msix/app_manifest.xml create mode 100644 build/windows/msix/template.xml create mode 100644 build/windows/nsis/project.nsi create mode 100644 build/windows/nsis/wails_tools.nsh create mode 100644 build/windows/wails.exe.manifest create mode 100644 docs/PLAN.md create mode 100644 frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts create mode 100644 frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts create mode 100644 frontend/bindings/opiccoach/appservice.ts create mode 100644 frontend/bindings/opiccoach/index.ts create mode 100644 frontend/bindings/opiccoach/models.ts create mode 100644 frontend/bindings/time/index.ts create mode 100644 frontend/bindings/time/models.ts create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/style.css create mode 100644 frontend/src/types.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 models.go create mode 100644 opiccoach.exe create mode 100644 questions.go create mode 100644 service.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c835b7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +frontend/dist/ +frontend/node_modules/ +.idea/ +.vscode/ +*.log + diff --git a/.task/checksum/build-frontend--DEV--RUNNER-npm- b/.task/checksum/build-frontend--DEV--RUNNER-npm- new file mode 100644 index 0000000..dacfa89 --- /dev/null +++ b/.task/checksum/build-frontend--DEV--RUNNER-npm- @@ -0,0 +1 @@ +d098e6fd836c808a8a51f1d4f66e26d9 diff --git a/.task/checksum/windows-common-generate-bindings b/.task/checksum/windows-common-generate-bindings new file mode 100644 index 0000000..c1583a3 --- /dev/null +++ b/.task/checksum/windows-common-generate-bindings @@ -0,0 +1 @@ +595353d17d943f79f25c683007ba302c diff --git a/.task/checksum/windows-common-generate-icons b/.task/checksum/windows-common-generate-icons new file mode 100644 index 0000000..b3361d4 --- /dev/null +++ b/.task/checksum/windows-common-generate-icons @@ -0,0 +1 @@ +a78891e5df81dd5285725ac05c28aadc diff --git a/.task/checksum/windows-common-install-frontend-deps-npm b/.task/checksum/windows-common-install-frontend-deps-npm new file mode 100644 index 0000000..11cd4f8 --- /dev/null +++ b/.task/checksum/windows-common-install-frontend-deps-npm @@ -0,0 +1 @@ +2a6c359f91e7e7c95aa4b1f4064d40c diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b1454e --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# opic + diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..fc322ec --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,61 @@ +version: '3' + +vars: + APP_NAME: "opic-flow" + BIN_DIR: "bin" + PACKAGE_MANAGER: '{{.PACKAGE_MANAGER | default "npm"}}' + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +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 + +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 + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + cmds: + - task: common:build:server + + run:server: + summary: Runs the application in server mode + cmds: + - task: common:run:server + + build:docker: + summary: Builds a Docker image for server mode deployment + cmds: + - task: common:build:docker + + run:docker: + summary: Builds and runs the Docker image + cmds: + - task: common:run:docker diff --git a/build/Taskfile.yml b/build/Taskfile.yml new file mode 100644 index 0000000..eeb2f37 --- /dev/null +++ b/build/Taskfile.yml @@ -0,0 +1,366 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + # Universal/multi-arch builds invoke this concurrently from parallel deps; + # two `go mod tidy` processes racing on go.mod can corrupt it (#4637). + run: once + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + run: once + cmds: + - task: install:frontend:deps:{{.PACKAGE_MANAGER}} + + install:frontend:deps:npm: + 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 + + install:frontend:deps:bun: + dir: frontend + sources: + - package.json + - bun.lock + - bun.lockb + generates: + - node_modules + preconditions: + - sh: bun --version + msg: "bun not found" + cmds: + - bun install + + install:frontend:deps:pnpm: + dir: frontend + sources: + - package.json + - pnpm-lock.yaml + generates: + - node_modules + preconditions: + - sh: pnpm --version + msg: "pnpm not found" + cmds: + - pnpm install + + install:frontend:deps:yarn: + dir: frontend + sources: + - package.json + - yarn.lock + status: + - test -d node_modules || test -f .pnp.cjs + preconditions: + - sh: yarn --version + msg: "yarn not found" + cmds: + - yarn install + + build:frontend: + label: build:frontend (DEV={{.DEV}} RUNNER={{.PACKAGE_MANAGER}}) + summary: Build the frontend project + # darwin:build:universal runs its per-arch builds as parallel deps, each of + # which depends on this task. Without run:once the two executions race: + # one regenerates frontend/bindings (-clean deletes it first) while the + # other's bundler is reading it, failing intermittently with + # 'Could not resolve "./bindings/"' (#4637). + run: once + dir: frontend + sources: + - "**/*" + - exclude: node_modules/**/* + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + OBFUSCATED: + ref: .OBFUSCATED + cmds: + - task: frontend:run + vars: + SCRIPT: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}' + env: + PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}' + + frontend:run: + summary: Run a frontend script with selected runner + cmds: + - task: frontend:run:{{.PACKAGE_MANAGER}} + vars: + SCRIPT: "{{.SCRIPT}}" + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:npm: + dir: frontend + cmds: + - npm run {{.SCRIPT}} -q + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:yarn: + dir: frontend + cmds: + - yarn {{.SCRIPT}} + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:pnpm: + dir: frontend + cmds: + - pnpm run {{.SCRIPT}} + vars: + SCRIPT: "{{.SCRIPT}}" + + frontend:run:bun: + dir: frontend + cmds: + - bun run {{.SCRIPT}} + vars: + SCRIPT: "{{.SCRIPT}}" + + 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 " "; 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: + summary: Generates bindings for the frontend + run: once + 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{{if eq .OBFUSCATED "true"}} -obfuscated{{end}} -ts -i + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms). + run: once + dir: build + sources: + - "appicon.png" + - "appicon.icon" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin + + dev:frontend: + summary: Runs the frontend in development mode + deps: + - task: install:frontend:deps + cmds: + - task: frontend:dev:{{.PACKAGE_MANAGER}} + + frontend:dev:npm: + dir: frontend + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + frontend:dev:yarn: + dir: frontend + cmds: + - yarn dev --port {{.VITE_PORT}} --strictPort + + frontend:dev:pnpm: + dir: frontend + cmds: + - pnpm dev --port {{.VITE_PORT}} --strictPort + + frontend:dev:bun: + dir: frontend + cmds: + - bun 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 . + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + desc: | + Builds the application with the server build tag enabled. + Server mode runs as a pure HTTP server without native GUI dependencies. + Usage: task build:server + deps: + - task: build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - go build -tags server {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + vars: + BUILD_FLAGS: "{{.BUILD_FLAGS}}" + + run:server: + summary: Builds and runs the application in server mode + deps: + - task: build:server + cmds: + - ./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + + build:docker: + summary: Builds a Docker image for server mode deployment + desc: | + Creates a minimal Docker image containing the server mode binary. + The image is based on distroless for security and small size. + Usage: task build:docker [TAG=myapp:latest] + cmds: + - docker build -t {{.TAG | default (printf "%s:latest" .APP_NAME)}} -f build/docker/Dockerfile.server . + vars: + TAG: "{{.TAG}}" + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + - sh: test -f build/docker/Dockerfile.server + msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it." + + run:docker: + summary: Builds and runs the Docker image + desc: | + Builds the Docker image and runs it, exposing port 8080. + Usage: task run:docker [TAG=myapp:latest] [PORT=8080] + Note: The internal container port is always 8080. The PORT variable + only changes the host port mapping. Ensure your app uses port 8080 + or modify the Dockerfile to match your ServerOptions.Port setting. + deps: + - task: build:docker + vars: + TAG: + ref: .TAG + cmds: + - docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}} + vars: + TAG: "{{.TAG}}" + PORT: "{{.PORT}}" + + 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/.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}}" diff --git a/build/android/Taskfile.yml b/build/android/Taskfile.yml new file mode 100644 index 0000000..800acd8 --- /dev/null +++ b/build/android/Taskfile.yml @@ -0,0 +1,290 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + APP_ID: '{{.APP_ID | default "com.wails.app"}}' + MIN_SDK: '21' + TARGET_SDK: '34' + # The emulator runs the host architecture; physical devices are arm64 + HOST_ARCH: + sh: '[ "$(uname -m)" = "x86_64" ] && echo "amd64" || echo "arm64"' + # System-image ABI for the host, used in the "create an AVD" hint below. + ANDROID_ABI: + sh: '[ "$(uname -m)" = "arm64" ] && echo "arm64-v8a" || echo "x86_64"' + # SDK location: $ANDROID_HOME / $ANDROID_SDK_ROOT, else the per-OS default + # (macOS: ~/Library/Android/sdk, Linux/other: ~/Android/Sdk) + SDK_ROOT: + sh: 'echo "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$([ -d "$HOME/Library/Android/sdk" ] && echo "$HOME/Library/Android/sdk" || echo "$HOME/Android/Sdk")}}"' + ADB: + sh: 'command -v adb || echo "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$([ -d "$HOME/Library/Android/sdk" ] && echo "$HOME/Library/Android/sdk" || echo "$HOME/Android/Sdk")}}/platform-tools/adb"' + EMULATOR: + sh: 'command -v emulator || echo "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$([ -d "$HOME/Library/Android/sdk" ] && echo "$HOME/Library/Android/sdk" || echo "$HOME/Android/Sdk")}}/emulator/emulator"' + +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 debug build of the application for Android + deps: + - task: common:go:mod:tidy + - task: generate:android:overlay + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + cmds: + - echo "Building Android app {{.APP_NAME}}..." + - task: compile:go:shared + vars: + ARCH: '{{.ARCH | default .HOST_ARCH}}' + 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: + - | + # Locate the NDK: $ANDROID_NDK_HOME, or the newest installed NDK + NDK_ROOT="$ANDROID_NDK_HOME" + if [ -z "$NDK_ROOT" ]; then + SDK_ROOT="{{.SDK_ROOT}}" + NDK_ROOT=$(ls -d "$SDK_ROOT"/ndk/* 2>/dev/null | sort -V | tail -1) + fi + if [ -z "$NDK_ROOT" ] || [ ! -d "$NDK_ROOT" ]; then + echo "Error: Android NDK not found" + echo "Install one with: sdkmanager 'ndk;26.3.11579264' (or set ANDROID_NDK_HOME)" + 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 -overlay build/android/overlay.json {{.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 a signed release APK + deps: + - task: build + vars: + PRODUCTION: "true" + ARCH: '{{.ARCH | default .HOST_ARCH}}' + cmds: + - task: assemble:apk:release + + package:fat: + summary: Packages a production build for all architectures (fat APK) + deps: + - task: build + vars: + PRODUCTION: "true" + ARCH: arm64 + cmds: + - task: compile:go:shared + vars: + ARCH: amd64 + PRODUCTION: "true" + - task: assemble:apk:release + + assemble:apk: + summary: Assembles a debug APK using Gradle + preconditions: + - sh: 'command -v java >/dev/null || [ -n "$JAVA_HOME" ]' + msg: "Java not found. Install a JDK (e.g. brew install openjdk@21) and/or set JAVA_HOME" + cmds: + - | + cd build/android + # The exec bit is lost when gradlew is extracted from the embedded + # build assets, so restore it before invoking the wrapper. + chmod +x ./gradlew + ./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 (signed with the debug keystore unless ANDROID_KEYSTORE_FILE is set) + preconditions: + - sh: 'command -v java >/dev/null || [ -n "$JAVA_HOME" ]' + msg: "Java not found. Install a JDK (e.g. brew install openjdk@21) and/or set JAVA_HOME" + cmds: + - | + cd build/android + # The exec bit is lost when gradlew is extracted from the embedded + # build assets, so restore it before invoking the wrapper. + chmod +x ./gradlew + ./gradlew assembleRelease + cp app/build/outputs/apk/release/app-release.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk" + echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk" + + generate:android:overlay: + internal: true + summary: Generate Go build overlay that registers the Android main + sources: + - build/config.yml + generates: + - build/android/overlay.json + - build/android/gen/main_android.gen.go + cmds: + - wails3 android overlay:gen -out build/android/overlay.json -config build/config.yml + + generate:android:bindings: + internal: true + summary: Generates bindings for Android + sources: + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + # Bindings are generated from the Go AST; CGO is disabled so the NDK + # is not required for this step + - wails3 generate bindings -f '-tags android' -clean=true + env: + GOOS: android + CGO_ENABLED: 0 + + 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 | tail -1) + if [ -z "$AVD_NAME" ]; then + echo "No Android Virtual Devices found. Create one with:" + echo " avdmanager create avd --name wails --package 'system-images;android-34;google_apis;{{.ANDROID_ABI}}' --device pixel_7" + exit 1 + fi + + echo "Starting emulator: $AVD_NAME" + # Start the emulator daemonized so it outlives this task step. go-task's + # shell tracks background jobs by PID and reaps them when the command's + # interpreter finishes (which nohup/setsid alone don't prevent — the kill + # is direct), so a bare `emulator &` is gone before the later + # install/launch steps run. Launch it from a short-lived child shell that + # backgrounds the emulator and exits immediately: the emulator is then + # reparented to init/launchd and go-task's shell has no handle to reap it. + nohup sh -c "'{{.EMULATOR}}' -avd '$AVD_NAME' -no-snapshot-load /dev/null 2>&1 &" >/dev/null 2>&1 + + # Wait for emulator to boot (max 120 seconds) + echo "Waiting for emulator to boot..." + "{{.ADB}}" wait-for-device + + for i in $(seq 1 120); 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: '[ -x "{{.ADB}}" ] || command -v adb' + msg: "adb not found. Install the Android SDK platform-tools (or set ANDROID_HOME)" + - sh: '[ -x "{{.EMULATOR}}" ] || command -v emulator' + msg: "emulator not found. Install the Android SDK emulator package (or set ANDROID_HOME)" + + deploy-emulator: + summary: Deploy the packaged release APK to the Android Emulator + deps: [package] + cmds: + - task: ensure-emulator + - '"{{.ADB}}" uninstall {{.APP_ID}} 2>/dev/null || true' + - '"{{.ADB}}" install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"' + - '"{{.ADB}}" shell am start -n {{.APP_ID}}/com.wails.app.MainActivity' + + run: + summary: Build, install and launch a debug build in the Android Emulator + deps: + - task: ensure-emulator + - task: build + 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}}/com.wails.app.MainActivity' + + logs: + summary: Stream Android logcat filtered to this app + cmds: + - '"{{.ADB}}" logcat -v time | grep -E "(Wails|{{.APP_NAME}})" || true' + + 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 diff --git a/build/android/app/build.gradle b/build/android/app/build.gradle new file mode 100644 index 0000000..630f03e --- /dev/null +++ b/build/android/app/build.gradle @@ -0,0 +1,83 @@ +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' + } + } + + def keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE") + def hasKeystore = keystoreFile != null && !keystoreFile.trim().isEmpty() + + signingConfigs { + // A real keystore can be provided via environment variables; without + // one, release builds are signed with the debug keystore so they can + // be installed for testing (not suitable for Play Store uploads). + release { + if (hasKeystore) { + storeFile file(keystoreFile) + storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD") + keyAlias System.getenv("ANDROID_KEY_ALIAS") + keyPassword System.getenv("ANDROID_KEY_PASSWORD") + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig hasKeystore ? signingConfigs.release : signingConfigs.debug + } + 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' + implementation 'androidx.biometric:biometric:1.1.0' + implementation 'androidx.security:security-crypto:1.1.0-alpha06' +} diff --git a/build/android/app/proguard-rules.pro b/build/android/app/proguard-rules.pro new file mode 100644 index 0000000..8b88c3d --- /dev/null +++ b/build/android/app/proguard-rules.pro @@ -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 ; +} + +# Keep Wails bridge classes +-keep class com.wails.app.WailsBridge { *; } +-keep class com.wails.app.WailsJSBridge { *; } diff --git a/build/android/app/src/main/AndroidManifest.xml b/build/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..03d343a --- /dev/null +++ b/build/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/android/app/src/main/java/com/wails/app/MainActivity.java b/build/android/app/src/main/java/com/wails/app/MainActivity.java new file mode 100644 index 0000000..4d7b204 --- /dev/null +++ b/build/android/app/src/main/java/com/wails/app/MainActivity.java @@ -0,0 +1,806 @@ +package com.wails.app; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.Uri; +import android.os.BatteryManager; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.util.Base64; +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.core.content.FileProvider; +import androidx.webkit.WebViewAssetLoader; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 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 boolean DEBUG = BuildConfig.DEBUG; + private static final String WAILS_SCHEME = "https"; + private static final String WAILS_HOST = "wails.localhost"; + private static final int FILE_PICKER_REQUEST = 7001; + + private WebView webView; + private WailsBridge bridge; + // Battery: system-event receivers are registered only while the activity is + // in the foreground (onStart) and torn down in onStop, so background battery/ + // network/screen broadcasts don't wake the app. + private boolean systemReceiversRegistered = false; + private WebViewAssetLoader assetLoader; + + // The Go-side dialog ID of the in-flight file picker (-1 when idle) + private int pendingFilePickerCallbackID = -1; + private static final int PHOTO_CAPTURE_REQUEST = 7002; + private static final int VIDEO_CAPTURE_REQUEST = 7003; + private static final int CAMERA_PERMISSION_REQUEST = 7010; + private File pendingCaptureFile; + private boolean pendingCaptureIsVideo; + + // System-event sources (battery/power, screen lock, network). Registered in + // onCreate, torn down in onDestroy. Each forwards a "system:*" event to JS + // via the bridge. + private BroadcastReceiver batteryReceiver; + private BroadcastReceiver screenReceiver; + private BroadcastReceiver powerSaveReceiver; + private ConnectivityManager connectivityManager; + private ConnectivityManager.NetworkCallback networkCallback; + + @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); + bridge.setWebView(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 (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) { + // Handle wails.localhost requests + if (request.getUrl().getHost() != null && + request.getUrl().getHost().equals(WAILS_HOST)) { + + // For wails API calls (runtime, capabilities, etc.) pass the + // full URL including the query string, because + // WebViewAssetLoader.PathHandler strips query params + String path = request.getUrl().getPath(); + if (path != null && path.startsWith("/wails/")) { + String fullPath = path; + String query = request.getUrl().getQuery(); + if (query != null && !query.isEmpty()) { + fullPath = path + "?" + query; + } + if (DEBUG) Log.d(TAG, "Wails API call: " + fullPath); + + 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 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()) + ); + } + + // Stream captured photos/videos from the cache with HTTP Range + // support so