diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d5363ab9..2a076ad3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,15 +9,6 @@ updates: go-modules: patterns: ["*"] - # apps/tlmst is a deliberately separate Go module. - - package-ecosystem: gomod - directory: /apps/tlmst - schedule: - interval: weekly - groups: - go-modules: - patterns: ["*"] - # Keep the GitHub Actions used in workflows up to date. - package-ecosystem: github-actions directory: / diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42339f9e..18d086d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,43 +24,6 @@ jobs: go-version-file: go.mod - run: go build ./... - # apps/tlmst is a deliberately separate module (CGO/WebKit/Wails deps kept out - # of the root build), so the jobs above never touch it. This job guards it - # against breakage — e.g. a stale module path in the regenerated frontend - # bindings after a rename. It builds the SvelteKit frontend (the Go binary - # embeds frontend/dist) and then compiles the desktop binary. - tlmst: - name: Build (apps/tlmst) - runs-on: ubuntu-latest - defaults: - run: - working-directory: apps/tlmst - steps: - - uses: actions/checkout@v7 - # Wails3 links against GTK4 + WebKitGTK 6.0 on Linux via CGO (its default; - # pkg-config resolves gtk4 and webkitgtk-6.0). - - name: Install GUI toolkits - run: | - sudo apt-get update - sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev - - uses: actions/setup-go@v6 - with: - go-version-file: apps/tlmst/go.mod - cache-dependency-path: apps/tlmst/go.sum - - uses: actions/setup-node@v6 - with: - node-version: lts/* - cache: npm - cache-dependency-path: apps/tlmst/frontend/package-lock.json - - name: Build frontend - working-directory: apps/tlmst/frontend - run: | - npm ci - npm run build - # `go build ./...` fails in build/ios (iOS scaffolding, ios-tag only), so - # build the root package, which covers the whole backend. - - run: go build . - test: name: Test runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index f9f21163..4e38910a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,12 +33,6 @@ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -errorsastype ./... # add -fix to apply ``` -`go test ./...` from the root does **not** include `apps/tlmst`: that is a -deliberately separate Go module (Wails3 desktop app) so its CGO/WebKit deps stay -out of the root build. There is no committed `go.work` (it's `.gitignore`d); -`apps/tlmst` resolves the parent module via a `replace` directive, so plain `go` -commands work without a workspace. - Run the stack locally (each in its own terminal): ```sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76f7e149..92a5544f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,10 +19,6 @@ The packages form a strict bottom-up dependency stack at the edges. `CLAUDE.md` has a fuller map of the architecture and where changes belong; `README.md` is the example-driven guide to the public API. -`apps/tlmst` is a deliberately separate Go module (a Wails3 desktop app) so its -CGO/WebKit dependencies stay out of the root build. `go test ./...` from the root -does **not** cover it. - ## Development workflow Run these before opening a pull request: diff --git a/README.md b/README.md index 0a8dcfe2..dc1a23c4 100644 --- a/README.md +++ b/README.md @@ -175,9 +175,7 @@ go test -race ./pkg/moqt/session/... # race detector for goroutine/stream code golangci-lint run # lint + format check (.golangci.yml) ``` -`go test ./...` from the root does not include `apps/tlmst` (a separate module -with CGO/WebKit deps). For the benchmark suite and the `benchstat` -regression-comparison workflow, see +For the benchmark suite and the `benchstat` regression-comparison workflow, see [`benchmarks/README.md`](benchmarks/README.md). ### Interoperability tests diff --git a/apps/tlmst/.gitignore b/apps/tlmst/.gitignore deleted file mode 100644 index 3993d794..00000000 --- a/apps/tlmst/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Wails v3 Specifics -bin/ -build/bin/ -build/devmode.config.yaml - -# Frontend dependencies and builds -frontend/node_modules/ -frontend/dist/ -frontend/.build/ - -.svelte-kit/ -.task/ -bin/ - -tlmst diff --git a/apps/tlmst/README.md b/apps/tlmst/README.md deleted file mode 100644 index e52cfe8b..00000000 --- a/apps/tlmst/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# tlmst — Media-over-QUIC video conferencing demo - -`tlmst` is a small [Wails3](https://v3.wails.io/) desktop application that -demonstrates the relay's **multi-party video-conferencing** capabilities. It is a -test / demo client for the parent [`moq-go`](../../README.md) library, not a -production app: each participant joins a room on a relay, publishes their camera -and microphone, and subscribes to everyone else in the room — all carried over -Media over QUIC Transport (MOQT). - -It exists to exercise the library end-to-end the way a real client would — -SETUP, publish, subscribe, subgroup data streams, and live teardown — against a -running [`cmd/relay`](../../cmd/relay). - -## What it does - -- **Join a room.** Establishes a QUIC + MOQT session against a relay address and - announces the local participant. -- **Publish local media.** Captures camera/microphone in the webview frontend, - encodes it, and publishes it as MOQT tracks through the relay. -- **Subscribe to peers.** Discovers other participants in the room and subscribes - to their tracks; remote frames are decoded and rendered in the UI. One - goroutine per subgroup means frames from adjacent GOPs can arrive concurrently. -- **Show live stats.** A debug panel surfaces connection stats and a stream of - backend log records (proxied to the frontend via Wails events). - -### How it maps to the library - -The Go backend is the interesting part for library users: - -- `sessionservice.go` — the Wails service bound to the frontend. Opens the - `session.Session` (`pkg/moqt/session`) over `quicconn`, and owns join/leave. -- `publisher.go` — publishes local media tracks to the relay. -- `subscriber.go` — subscribes to remote participants and forwards decoded - `MediaChunk`s to the UI. -- `stats.go` — collects per-connection statistics for the debug panel. -- `main.go` — Wails app wiring; registers the typed events (`moq:log`, - `moq:participant-joined`, `moq:participant-left`, `moq:media-chunk`). - -The frontend (`frontend/`, SvelteKit) has two screens: a join screen (`/`) and -the call screen (`/call`). It talks to the backend exclusively through the -generated bindings in `frontend/bindings/`. - -## Why it is a separate module - -`apps/tlmst` is a **deliberately separate Go module** (see `CLAUDE.md` at the -repo root) so that its CGO/WebKit/Wails dependencies stay out of the parent -module's hermetic, pure-Go build and test suite. It resolves the parent library -via a `replace github.com/floatdrop/moq-go => ../..` directive, so plain `go` -commands work without a `go.work` file. Consequently `go test ./...` from the -repo root does **not** build or test this app — it has its own CI job (see -[Continuous integration](#continuous-integration) below). - -## Prerequisites - -- Go (matching the version in `go.mod`). -- Node.js + npm (for the SvelteKit frontend). -- The [Wails3 CLI](https://v3.wails.io/getting-started/installation/): - `go install github.com/wailsapp/wails/v3/cmd/wails3@latest` -- Platform GUI toolkits required by Wails3: - - **macOS** — Xcode command-line tools. - - **Linux** — `libgtk-4-dev` and `libwebkitgtk-6.0-dev` (Wails3 defaults to - GTK4 + WebKitGTK 6.0). -- A running relay to connect to: - - ```sh - go run ./cmd/relay # from the repo root; self-signed cert on :4433 - ``` - -## Running - -From this directory (`apps/tlmst`): - -```sh -wails3 dev # development mode with hot-reload (frontend + backend) -wails3 build # production build into ./bin -``` - -Then point the app at your relay address (default `localhost:4433`) on the join -screen. Launch a second instance to see two participants in the same room. - -## Building manually - -The Go binary embeds `frontend/dist` via `//go:embed`, so the frontend must be -built before the Go build: - -```sh -cd frontend && npm ci && npm run build && cd .. -go build . # builds the desktop binary (the root package) -``` - -> Note: `go build ./...` will fail in `build/ios/` — that directory is Wails iOS -> scaffolding that only compiles under the `ios` build tag (via Xcode / -> `build/ios/build.sh`). Build the root package (`go build .`) instead. - -## Continuous integration - -Because this module is excluded from the root suite, the repository's CI has a -dedicated `tlmst` job that guards against breakage (e.g. a stale module path in -the regenerated frontend bindings). It builds the frontend and then compiles the -Go binary, which is enough to catch import/binding regressions. See -`.github/workflows/ci.yml`. - -## Project structure - -- `main.go`, `sessionservice.go`, `publisher.go`, `subscriber.go`, `stats.go` — - the Go backend (`package main`). -- `frontend/` — SvelteKit frontend (`src/`), generated Wails bindings - (`frontend/bindings/`), and the built assets (`frontend/dist/`, gitignored). -- `build/` — per-platform Wails build scaffolding and `Taskfile.yml` includes. -- `Taskfile.yml` — Task targets (`build`, `dev`, `run`, server/Docker modes). diff --git a/apps/tlmst/Taskfile.yml b/apps/tlmst/Taskfile.yml deleted file mode 100644 index b423cef7..00000000 --- a/apps/tlmst/Taskfile.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: '3' - -vars: - APP_NAME: "tlmst" - 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/apps/tlmst/build/Taskfile.yml b/apps/tlmst/build/Taskfile.yml deleted file mode 100644 index fdb0d17f..00000000 --- a/apps/tlmst/build/Taskfile.yml +++ /dev/null @@ -1,355 +0,0 @@ -version: '3' - -tasks: - go:mod:tidy: - summary: Runs `go mod tidy` - internal: true - cmds: - - go mod tidy - - install:frontend:deps: - summary: Install frontend dependencies - 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 - 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: - 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{{if eq .OBFUSCATED "true"}} -obfuscated{{end}} - - 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). - 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/apps/tlmst/build/android/Taskfile.yml b/apps/tlmst/build/android/Taskfile.yml deleted file mode 100644 index aca62e4f..00000000 --- a/apps/tlmst/build/android/Taskfile.yml +++ /dev/null @@ -1,237 +0,0 @@ -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 diff --git a/apps/tlmst/build/android/app/build.gradle b/apps/tlmst/build/android/app/build.gradle deleted file mode 100644 index 78fdbf7d..00000000 --- a/apps/tlmst/build/android/app/build.gradle +++ /dev/null @@ -1,63 +0,0 @@ -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' -} diff --git a/apps/tlmst/build/android/app/proguard-rules.pro b/apps/tlmst/build/android/app/proguard-rules.pro deleted file mode 100644 index 8b88c3df..00000000 --- a/apps/tlmst/build/android/app/proguard-rules.pro +++ /dev/null @@ -1,12 +0,0 @@ -# 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/apps/tlmst/build/android/app/src/main/AndroidManifest.xml b/apps/tlmst/build/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 6c7982af..00000000 --- a/apps/tlmst/build/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/tlmst/build/android/app/src/main/java/com/wails/app/MainActivity.java b/apps/tlmst/build/android/app/src/main/java/com/wails/app/MainActivity.java deleted file mode 100644 index 3067fee0..00000000 --- a/apps/tlmst/build/android/app/src/main/java/com/wails/app/MainActivity.java +++ /dev/null @@ -1,198 +0,0 @@ -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 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(); - } - } -} diff --git a/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsBridge.java b/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsBridge.java deleted file mode 100644 index 3dab6524..00000000 --- a/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsBridge.java +++ /dev/null @@ -1,214 +0,0 @@ -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 pendingAssetCallbacks = new ConcurrentHashMap<>(); - private final ConcurrentHashMap 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); - } -} diff --git a/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java b/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java deleted file mode 100644 index 98ae5b24..00000000 --- a/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java +++ /dev/null @@ -1,142 +0,0 @@ -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"); - } -} diff --git a/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java b/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java deleted file mode 100644 index 326fa9b4..00000000 --- a/apps/tlmst/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java +++ /dev/null @@ -1,118 +0,0 @@ -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 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"; - } -} diff --git a/apps/tlmst/build/android/app/src/main/res/layout/activity_main.xml b/apps/tlmst/build/android/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index f278384c..00000000 --- a/apps/tlmst/build/android/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/tlmst/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 9409abeb..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/apps/tlmst/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 9409abeb..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/tlmst/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 5b6acc04..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/apps/tlmst/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 5b6acc04..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/tlmst/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 1c2c6645..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/apps/tlmst/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 1c2c6645..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/tlmst/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index be557d89..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/tlmst/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index be557d89..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/tlmst/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4507f32a..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/tlmst/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 4507f32a..00000000 Binary files a/apps/tlmst/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/apps/tlmst/build/android/app/src/main/res/values/colors.xml b/apps/tlmst/build/android/app/src/main/res/values/colors.xml deleted file mode 100644 index dd33f3b7..00000000 --- a/apps/tlmst/build/android/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - #3574D4 - #2C5FB8 - #1B2636 - #FFFFFFFF - #FF000000 - diff --git a/apps/tlmst/build/android/app/src/main/res/values/strings.xml b/apps/tlmst/build/android/app/src/main/res/values/strings.xml deleted file mode 100644 index 3ed9e471..00000000 --- a/apps/tlmst/build/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Wails App - diff --git a/apps/tlmst/build/android/app/src/main/res/values/themes.xml b/apps/tlmst/build/android/app/src/main/res/values/themes.xml deleted file mode 100644 index be8a282b..00000000 --- a/apps/tlmst/build/android/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/apps/tlmst/build/android/build.gradle b/apps/tlmst/build/android/build.gradle deleted file mode 100644 index d7fbab39..00000000 --- a/apps/tlmst/build/android/build.gradle +++ /dev/null @@ -1,4 +0,0 @@ -// 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 -} diff --git a/apps/tlmst/build/android/gradle.properties b/apps/tlmst/build/android/gradle.properties deleted file mode 100644 index b9d4426d..00000000 --- a/apps/tlmst/build/android/gradle.properties +++ /dev/null @@ -1,26 +0,0 @@ -# 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 diff --git a/apps/tlmst/build/android/gradle/wrapper/gradle-wrapper.jar b/apps/tlmst/build/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index f8e1ee31..00000000 Binary files a/apps/tlmst/build/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/apps/tlmst/build/android/gradle/wrapper/gradle-wrapper.properties b/apps/tlmst/build/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 23449a2b..00000000 --- a/apps/tlmst/build/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -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 diff --git a/apps/tlmst/build/android/gradlew b/apps/tlmst/build/android/gradlew deleted file mode 100644 index adff685a..00000000 --- a/apps/tlmst/build/android/gradlew +++ /dev/null @@ -1,248 +0,0 @@ -#!/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" "$@" diff --git a/apps/tlmst/build/android/gradlew.bat b/apps/tlmst/build/android/gradlew.bat deleted file mode 100644 index e509b2dd..00000000 --- a/apps/tlmst/build/android/gradlew.bat +++ /dev/null @@ -1,93 +0,0 @@ -@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 diff --git a/apps/tlmst/build/android/main_android.go b/apps/tlmst/build/android/main_android.go deleted file mode 100644 index 70a71647..00000000 --- a/apps/tlmst/build/android/main_android.go +++ /dev/null @@ -1,11 +0,0 @@ -//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) -} diff --git a/apps/tlmst/build/android/scripts/deps/install_deps.go b/apps/tlmst/build/android/scripts/deps/install_deps.go deleted file mode 100644 index d9dfedf8..00000000 --- a/apps/tlmst/build/android/scripts/deps/install_deps.go +++ /dev/null @@ -1,151 +0,0 @@ -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 -} diff --git a/apps/tlmst/build/android/settings.gradle b/apps/tlmst/build/android/settings.gradle deleted file mode 100644 index a3f3ec3d..00000000 --- a/apps/tlmst/build/android/settings.gradle +++ /dev/null @@ -1,18 +0,0 @@ -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "WailsApp" -include ':app' diff --git a/apps/tlmst/build/appicon.icon/Assets/wails_icon_vector.svg b/apps/tlmst/build/appicon.icon/Assets/wails_icon_vector.svg deleted file mode 100644 index b099222f..00000000 --- a/apps/tlmst/build/appicon.icon/Assets/wails_icon_vector.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/apps/tlmst/build/appicon.icon/icon.json b/apps/tlmst/build/appicon.icon/icon.json deleted file mode 100644 index ecf18497..00000000 --- a/apps/tlmst/build/appicon.icon/icon.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "fill" : { - "automatic-gradient" : "extended-gray:1.00000,1.00000" - }, - "groups" : [ - { - "layers" : [ - { - "fill-specializations" : [ - { - "appearance" : "dark", - "value" : { - "solid" : "srgb:0.92143,0.92145,0.92144,1.00000" - } - }, - { - "appearance" : "tinted", - "value" : { - "solid" : "srgb:0.83742,0.83744,0.83743,1.00000" - } - } - ], - "image-name" : "wails_icon_vector.svg", - "name" : "wails_icon_vector", - "position" : { - "scale" : 1.25, - "translation-in-points" : [ - 36.890625, - 4.96875 - ] - } - } - ], - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 - }, - "specular" : true, - "translucency" : { - "enabled" : true, - "value" : 0.5 - } - } - ], - "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" - } -} \ No newline at end of file diff --git a/apps/tlmst/build/appicon.png b/apps/tlmst/build/appicon.png deleted file mode 100644 index 63617fe4..00000000 Binary files a/apps/tlmst/build/appicon.png and /dev/null differ diff --git a/apps/tlmst/build/config.yml b/apps/tlmst/build/config.yml deleted file mode 100644 index 9d49291f..00000000 --- a/apps/tlmst/build/config.yml +++ /dev/null @@ -1,79 +0,0 @@ -# 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 - # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) - # # Should match the name of your .icon file without the extension - # # If not set and Assets.car exists, defaults to "appicon" - -# 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 - file: - - .DS_Store - - .gitignore - - .gitkeep - - "*_test.go" - 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 \ No newline at end of file diff --git a/apps/tlmst/build/darwin/Assets.car b/apps/tlmst/build/darwin/Assets.car deleted file mode 100644 index 4def9c32..00000000 Binary files a/apps/tlmst/build/darwin/Assets.car and /dev/null differ diff --git a/apps/tlmst/build/darwin/Info.dev.plist b/apps/tlmst/build/darwin/Info.dev.plist deleted file mode 100644 index d6f7d6bb..00000000 --- a/apps/tlmst/build/darwin/Info.dev.plist +++ /dev/null @@ -1,38 +0,0 @@ - - - - CFBundlePackageType - APPL - CFBundleName - My Product - CFBundleExecutable - tlmst - CFBundleIdentifier - com.example.tlmst - CFBundleVersion - 0.1.0 - CFBundleGetInfoString - This is a comment - CFBundleShortVersionString - 0.1.0 - CFBundleIconFile - icons - CFBundleIconName - appicon - LSMinimumSystemVersion - 12.0.0 - NSHighResolutionCapable - true - NSHumanReadableCopyright - © 2026, My Company - NSAppTransportSecurity - - NSAllowsLocalNetworking - - - NSMicrophoneUsageDescription - This app requires access to the microphone - NSCameraUsageDescription - This app requires access to the camera - - \ No newline at end of file diff --git a/apps/tlmst/build/darwin/Info.plist b/apps/tlmst/build/darwin/Info.plist deleted file mode 100644 index d3449337..00000000 --- a/apps/tlmst/build/darwin/Info.plist +++ /dev/null @@ -1,33 +0,0 @@ - - - - CFBundlePackageType - APPL - CFBundleName - My Product - CFBundleExecutable - tlmst - CFBundleIdentifier - com.example.tlmst - CFBundleVersion - 0.1.0 - CFBundleGetInfoString - This is a comment - CFBundleShortVersionString - 0.1.0 - CFBundleIconFile - icons - CFBundleIconName - appicon - LSMinimumSystemVersion - 12.0.0 - NSHighResolutionCapable - true - NSHumanReadableCopyright - © 2026, My Company - NSMicrophoneUsageDescription - This app requires access to the microphone - NSCameraUsageDescription - This app requires access to the camera - - \ No newline at end of file diff --git a/apps/tlmst/build/darwin/Taskfile.yml b/apps/tlmst/build/darwin/Taskfile.yml deleted file mode 100644 index 1af4303e..00000000 --- a/apps/tlmst/build/darwin/Taskfile.yml +++ /dev/null @@ -1,205 +0,0 @@ -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}}' - EXTRA_TAGS: '{{.EXTRA_TAGS}}' - OBFUSCATED: '{{.OBFUSCATED}}' - GARBLE_ARGS: '{{.GARBLE_ARGS}}' - 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 - OBFUSCATED: - ref: .OBFUSCATED - DEV: - ref: .DEV - - task: common:generate:icons - preconditions: - - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}' - msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility." - cmds: - - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o "{{.OUTPUT}}"' - vars: - BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -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=12.0" - CGO_LDFLAGS: "-mmacosx-version-min=12.0" - MACOSX_DEPLOYMENT_TARGET: "12.0" - - build:docker: - summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts) - internal: true - deps: - - task: common:build:frontend - vars: - OBFUSCATED: - ref: .OBFUSCATED - - 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" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} - - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin - platforms: [linux, darwin] - - 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 }}' - # Generate Docker volume mounts: Go module cache + go.mod replace directives - # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS) - DOCKER_MOUNTS: - sh: 'wails3 tool docker-mounts' - - 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" - - | - if [ -f build/darwin/Assets.car ]; then - cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" - fi - - 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" - - | - if [ -f build/darwin/Assets.car ]; then - cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" - fi - - 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" diff --git a/apps/tlmst/build/darwin/icons.icns b/apps/tlmst/build/darwin/icons.icns deleted file mode 100644 index 1b5bd4c8..00000000 Binary files a/apps/tlmst/build/darwin/icons.icns and /dev/null differ diff --git a/apps/tlmst/build/docker/Dockerfile.cross b/apps/tlmst/build/docker/Dockerfile.cross deleted file mode 100644 index 46c274f4..00000000 --- a/apps/tlmst/build/docker/Dockerfile.cross +++ /dev/null @@ -1,212 +0,0 @@ -# Cross-compile Wails v3 apps to any platform -# -# Darwin: Zig + macOS SDK -# Linux: Native GCC when host matches target, Zig for cross-arch -# Windows: Zig + bundled mingw -# -# 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.26-bookworm - -ARG TARGETARCH -ARG GARBLE_VERSION=v0.16.0 - -# Install base tools, GCC, and GTK/WebKit dev packages -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl xz-utils nodejs npm pkg-config gcc libc6-dev \ - libgtk-3-dev libwebkit2gtk-4.1-dev \ - libgtk-4-dev libwebkitgtk-6.0-dev \ - && rm -rf /var/lib/apt/lists/* - -RUN go install mvdan.cc/garble@${GARBLE_VERSION} - -# Install Zig - automatically selects correct binary for host architecture -ARG ZIG_VERSION=0.14.0 -RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ - curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \ - | tar -xJ -C /opt \ - && ln -s /opt/zig-linux-${ZIG_ARCH}-${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 cross-compilation targets -# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch) - -# 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 - -# Windows amd64 - uses Zig's bundled mingw -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 - uses Zig's bundled mingw -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=gcc - export GOARCH=arm64 - export GOOS=linux - ;; - linux-amd64|linux-x86_64) - export CC=gcc - 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: " - 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 - -TAGS="production" -if [ -n "$EXTRA_TAGS" ]; then - TAGS="${TAGS},${EXTRA_TAGS}" -fi - -COMPILER="go build" -if [ "$OBFUSCATED" = "true" ]; then - COMPILER="garble ${GARBLE_ARGS} build" - TAGS="${TAGS},wails_obfuscated" -fi - -${COMPILER} -tags "$TAGS" -trimpath -buildvcs=false -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"] diff --git a/apps/tlmst/build/docker/Dockerfile.server b/apps/tlmst/build/docker/Dockerfile.server deleted file mode 100644 index 58fb64f7..00000000 --- a/apps/tlmst/build/docker/Dockerfile.server +++ /dev/null @@ -1,41 +0,0 @@ -# Wails Server Mode Dockerfile -# Multi-stage build for minimal image size - -# Build stage -FROM golang:alpine AS builder - -WORKDIR /app - -# Install build dependencies -RUN apk add --no-cache git - -# Copy source code -COPY . . - -# Remove local replace directive if present (for production builds) -RUN sed -i '/^replace/d' go.mod || true - -# Download dependencies -RUN go mod tidy - -# Build the server binary -RUN go build -tags server -ldflags="-s -w" -o server . - -# Runtime stage - minimal image -FROM gcr.io/distroless/static-debian12 - -# Copy the binary -COPY --from=builder /app/server /server - -# Copy frontend assets -COPY --from=builder /app/frontend/dist /frontend/dist - -# Expose the default port -EXPOSE 8080 - -# Bind to all interfaces (required for Docker) -# Can be overridden at runtime with -e WAILS_SERVER_HOST=... -ENV WAILS_SERVER_HOST=0.0.0.0 - -# Run the server -ENTRYPOINT ["/server"] diff --git a/apps/tlmst/build/ios/Assets.xcassets b/apps/tlmst/build/ios/Assets.xcassets deleted file mode 100644 index 46fbb878..00000000 --- a/apps/tlmst/build/ios/Assets.xcassets +++ /dev/null @@ -1,116 +0,0 @@ -{ - "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" - } - ] -} \ No newline at end of file diff --git a/apps/tlmst/build/ios/Info.dev.plist b/apps/tlmst/build/ios/Info.dev.plist deleted file mode 100644 index 6399a34b..00000000 --- a/apps/tlmst/build/ios/Info.dev.plist +++ /dev/null @@ -1,62 +0,0 @@ - - - - - CFBundleExecutable - tlmst - CFBundleIdentifier - com.example.tlmst.dev - CFBundleName - My Product (Dev) - CFBundleDisplayName - My Product (Dev) - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.1.0-dev - CFBundleVersion - 0.1.0 - LSRequiresIPhoneOS - - MinimumOSVersion - 15.0 - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAllowsLocalNetworking - - - - WailsDevelopmentMode - - - NSHumanReadableCopyright - © 2026, My Company - - - CFBundleGetInfoString - This is a comment - - - \ No newline at end of file diff --git a/apps/tlmst/build/ios/Info.plist b/apps/tlmst/build/ios/Info.plist deleted file mode 100644 index b674010a..00000000 --- a/apps/tlmst/build/ios/Info.plist +++ /dev/null @@ -1,59 +0,0 @@ - - - - - CFBundleExecutable - tlmst - CFBundleIdentifier - com.example.tlmst - CFBundleName - My Product - CFBundleDisplayName - My Product - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.1.0 - CFBundleVersion - 0.1.0 - LSRequiresIPhoneOS - - MinimumOSVersion - 15.0 - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAllowsLocalNetworking - - - - NSHumanReadableCopyright - © 2026, My Company - - - CFBundleGetInfoString - This is a comment - - - \ No newline at end of file diff --git a/apps/tlmst/build/ios/LaunchScreen.storyboard b/apps/tlmst/build/ios/LaunchScreen.storyboard deleted file mode 100644 index 1ca6c18f..00000000 --- a/apps/tlmst/build/ios/LaunchScreen.storyboard +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/tlmst/build/ios/Taskfile.yml b/apps/tlmst/build/ios/Taskfile.yml deleted file mode 100644 index 8c27f089..00000000 --- a/apps/tlmst/build/ios/Taskfile.yml +++ /dev/null @@ -1,293 +0,0 @@ -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/"' \ No newline at end of file diff --git a/apps/tlmst/build/ios/app_options_default.go b/apps/tlmst/build/ios/app_options_default.go deleted file mode 100644 index 1f136502..00000000 --- a/apps/tlmst/build/ios/app_options_default.go +++ /dev/null @@ -1,10 +0,0 @@ -//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 -} diff --git a/apps/tlmst/build/ios/app_options_ios.go b/apps/tlmst/build/ios/app_options_ios.go deleted file mode 100644 index a7cc7fca..00000000 --- a/apps/tlmst/build/ios/app_options_ios.go +++ /dev/null @@ -1,11 +0,0 @@ -//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 -} diff --git a/apps/tlmst/build/ios/build.sh b/apps/tlmst/build/ios/build.sh deleted file mode 100644 index c1b1b7bf..00000000 --- a/apps/tlmst/build/ios/build.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash -set -e - -# Build configuration -APP_NAME="tlmst" -BUNDLE_ID="com.example.tlmst" -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 \ No newline at end of file diff --git a/apps/tlmst/build/ios/entitlements.plist b/apps/tlmst/build/ios/entitlements.plist deleted file mode 100644 index cc5d9582..00000000 --- a/apps/tlmst/build/ios/entitlements.plist +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - get-task-allow - - - - com.apple.security.app-sandbox - - - - com.apple.security.network.client - - - - com.apple.security.files.user-selected.read-only - - - \ No newline at end of file diff --git a/apps/tlmst/build/ios/icon.png b/apps/tlmst/build/ios/icon.png deleted file mode 100644 index be7d5917..00000000 --- a/apps/tlmst/build/ios/icon.png +++ /dev/null @@ -1,3 +0,0 @@ -# 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 \ No newline at end of file diff --git a/apps/tlmst/build/ios/main.m b/apps/tlmst/build/ios/main.m deleted file mode 100644 index 366767a6..00000000 --- a/apps/tlmst/build/ios/main.m +++ /dev/null @@ -1,23 +0,0 @@ -//go:build ios -// Minimal bootstrap: delegate comes from Go archive (WailsAppDelegate) -#import -#include - -// 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"); - } -} \ No newline at end of file diff --git a/apps/tlmst/build/ios/main_ios.go b/apps/tlmst/build/ios/main_ios.go deleted file mode 100644 index 44e8e251..00000000 --- a/apps/tlmst/build/ios/main_ios.go +++ /dev/null @@ -1,24 +0,0 @@ -//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() -} diff --git a/apps/tlmst/build/ios/project.pbxproj b/apps/tlmst/build/ios/project.pbxproj deleted file mode 100644 index 33a6c407..00000000 --- a/apps/tlmst/build/ios/project.pbxproj +++ /dev/null @@ -1,222 +0,0 @@ -// !$*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 = ""; }; - C0DEBEEF0000000000000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 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 = ""; - }; - C0DEBEEF0000000000000020 /* Products */ = { - isa = PBXGroup; - children = ( - C0DEBEEF0000000000000004 /* My Product.app */, - ); - name = Products; - sourceTree = ""; - }; - 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 = ""; - }; -/* 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.tlmst"; - 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.tlmst"; - 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 */; -} diff --git a/apps/tlmst/build/ios/scripts/deps/install_deps.go b/apps/tlmst/build/ios/scripts/deps/install_deps.go deleted file mode 100644 index 7f7e745e..00000000 --- a/apps/tlmst/build/ios/scripts/deps/install_deps.go +++ /dev/null @@ -1,319 +0,0 @@ -// 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" -} diff --git a/apps/tlmst/build/linux/Taskfile.yml b/apps/tlmst/build/linux/Taskfile.yml deleted file mode 100644 index 7508e300..00000000 --- a/apps/tlmst/build/linux/Taskfile.yml +++ /dev/null @@ -1,224 +0,0 @@ -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: - # 1. Cross-compiling from non-Linux, OR - # 2. No C compiler is available, OR - # 3. Target architecture differs from host architecture (cross-arch compilation) - - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}' - vars: - ARCH: '{{.ARCH}}' - DEV: '{{.DEV}}' - OUTPUT: '{{.OUTPUT}}' - EXTRA_TAGS: '{{.EXTRA_TAGS}}' - OBFUSCATED: '{{.OBFUSCATED}}' - GARBLE_ARGS: '{{.GARBLE_ARGS}}' - vars: - DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' - OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' - # Determine target architecture (defaults to host ARCH if not specified) - TARGET_ARCH: '{{.ARCH | default ARCH}}' - # Check if a C compiler is available (gcc or clang) — cross-platform via wails3 tool - HAS_CC: - sh: 'wails3 tool has-cc' - - 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 - OBFUSCATED: - ref: .OBFUSCATED - DEV: - ref: .DEV - - task: common:generate:icons - - task: generate:dotdesktop - preconditions: - - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}' - msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility." - cmds: - - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.BUILD_FLAGS}} -o {{.OUTPUT}}' - vars: - BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -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: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available) - internal: true - deps: - - task: common:build:frontend - vars: - OBFUSCATED: - ref: .OBFUSCATED - - 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" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} - - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin - platforms: [linux, darwin] - - 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 }}' - # Generate Docker volume mounts: Go module cache + go.mod replace directives - # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS) - DOCKER_MOUNTS: - sh: 'wails3 tool docker-mounts' - - 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" diff --git a/apps/tlmst/build/linux/appimage/build.sh b/apps/tlmst/build/linux/appimage/build.sh deleted file mode 100644 index 85901c34..00000000 --- a/apps/tlmst/build/linux/appimage/build.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/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" - diff --git a/apps/tlmst/build/linux/desktop b/apps/tlmst/build/linux/desktop deleted file mode 100644 index b36f33c1..00000000 --- a/apps/tlmst/build/linux/desktop +++ /dev/null @@ -1,13 +0,0 @@ -[Desktop Entry] -Version=1.0 -Name=My Product -Comment=A tlmst application -# The Exec line includes %u to pass the URL to the application -Exec=/usr/local/bin/tlmst %u -Terminal=false -Type=Application -Icon=tlmst -Categories=Utility; -StartupWMClass=tlmst - - diff --git a/apps/tlmst/build/linux/nfpm/nfpm.yaml b/apps/tlmst/build/linux/nfpm/nfpm.yaml deleted file mode 100644 index 470abf48..00000000 --- a/apps/tlmst/build/linux/nfpm/nfpm.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# 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: "tlmst" -arch: ${GOARCH} -platform: "linux" -version: "0.1.0" -section: "default" -priority: "extra" -maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> -description: "A tlmst application" -vendor: "My Company" -homepage: "https://wails.io" -license: "MIT" -release: "1" - -contents: - - src: "./bin/tlmst" - dst: "/usr/local/bin/tlmst" - - src: "./build/appicon.png" - dst: "/usr/share/icons/hicolor/128x128/apps/tlmst.png" - - src: "./build/linux/tlmst.desktop" - dst: "/usr/share/applications/tlmst.desktop" - -# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+) -depends: - - libgtk-4-1 - - libwebkitgtk-6.0-4 - -# Distribution-specific overrides for different package formats -overrides: - # RPM packages for Fedora / RHEL / AlmaLinux / Rocky Linux - rpm: - depends: - - gtk4 - - webkitgtk6.0 - - # Arch Linux packages - archlinux: - depends: - - gtk4 - - webkitgtk-6.0 - -# 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" - -# If you build your app with -tags gtk3 (legacy WebKit2GTK 4.1 stack — supported through v3.0.x, removed in v3.1), -# replace the depends/overrides above with these: -# -# depends: -# - libgtk-3-0 -# - libwebkit2gtk-4.1-0 -# overrides: -# rpm: -# depends: -# - gtk3 -# - webkit2gtk4.1 -# archlinux: -# depends: -# - gtk3 -# - webkit2gtk-4.1 -# -# replaces: -# - foobar -# provides: -# - bar -# recommends: -# - whatever -# suggests: -# - something-else -# conflicts: -# - not-foo -# - not-bar -# changelog: "changelog.yaml" diff --git a/apps/tlmst/build/linux/nfpm/scripts/postinstall.sh b/apps/tlmst/build/linux/nfpm/scripts/postinstall.sh deleted file mode 100644 index 4bbb815a..00000000 --- a/apps/tlmst/build/linux/nfpm/scripts/postinstall.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/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 diff --git a/apps/tlmst/build/linux/nfpm/scripts/postremove.sh b/apps/tlmst/build/linux/nfpm/scripts/postremove.sh deleted file mode 100644 index a9bf588e..00000000 --- a/apps/tlmst/build/linux/nfpm/scripts/postremove.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/apps/tlmst/build/linux/nfpm/scripts/preinstall.sh b/apps/tlmst/build/linux/nfpm/scripts/preinstall.sh deleted file mode 100644 index a9bf588e..00000000 --- a/apps/tlmst/build/linux/nfpm/scripts/preinstall.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/apps/tlmst/build/linux/nfpm/scripts/preremove.sh b/apps/tlmst/build/linux/nfpm/scripts/preremove.sh deleted file mode 100644 index a9bf588e..00000000 --- a/apps/tlmst/build/linux/nfpm/scripts/preremove.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/apps/tlmst/build/windows/Taskfile.yml b/apps/tlmst/build/windows/Taskfile.yml deleted file mode 100644 index f2bbe235..00000000 --- a/apps/tlmst/build/windows/Taskfile.yml +++ /dev/null @@ -1,197 +0,0 @@ -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}}' - EXTRA_TAGS: '{{.EXTRA_TAGS}}' - OBFUSCATED: '{{.OBFUSCATED}}' - GARBLE_ARGS: '{{.GARBLE_ARGS}}' - 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 - OBFUSCATED: - ref: .OBFUSCATED - DEV: - ref: .DEV - - task: common:generate:icons - preconditions: - - sh: '{{if eq .OBFUSCATED "true"}}command -v garble >/dev/null 2>&1{{else}}true{{end}}' - msg: "garble is required for obfuscated builds. Install it with: go install mvdan.cc/garble@v0.16.0 (requires Go 1.24+). See https://github.com/burrowers/garble/releases for version/toolchain compatibility." - cmds: - - task: generate:syso - vars: - ARCH: '{{.ARCH}}' - - '{{if eq .OBFUSCATED "true"}}garble {{.GARBLE_ARGS}} build{{else}}go build{{end}} {{.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"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -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 - vars: - OBFUSCATED: - ref: .OBFUSCATED - - 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 - vars: - ARCH: '{{.ARCH}}' - - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.DOCKER_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{if eq .OBFUSCATED "true"}}-e OBFUSCATED=true{{end}} {{if .GARBLE_ARGS}}-e GARBLE_ARGS="{{.GARBLE_ARGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} - - cmd: docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin - platforms: [linux, darwin] - - rm -f *.syso - vars: - DOCKER_ARCH: '{{.ARCH | default "amd64"}}' - # Generate Docker volume mounts: Go module cache + go.mod replace directives - # Uses wails3 tool docker-mounts for cross-platform compatibility (Windows/Linux/macOS) - DOCKER_MOUNTS: - sh: 'wails3 tool docker-mounts' - - package: - summary: Packages the application - cmds: - - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}' - vars: - INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' - vars: - FORMAT: '{{.FORMAT | default "nsis"}}' - INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' - - 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 - preconditions: - - sh: '[ "{{.INSTALL_SCOPE}}" = "user" ] || [ "{{.INSTALL_SCOPE}}" = "machine" ]' - msg: "INSTALL_SCOPE must be 'user' or 'machine', got '{{.INSTALL_SCOPE}}'" - 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 {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi - {{else}} - makensis {{if eq .INSTALL_SCOPE "user"}}-DWAILS_INSTALL_SCOPE=user -DREQUEST_EXECUTION_LEVEL=user {{end}}-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}}' - INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' - - 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 - vars: - INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' - vars: - INSTALL_SCOPE: '{{.INSTALL_SCOPE | default "machine"}}' - 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" diff --git a/apps/tlmst/build/windows/icon.ico b/apps/tlmst/build/windows/icon.ico deleted file mode 100644 index bfa0690b..00000000 Binary files a/apps/tlmst/build/windows/icon.ico and /dev/null differ diff --git a/apps/tlmst/build/windows/info.json b/apps/tlmst/build/windows/info.json deleted file mode 100644 index cd4da298..00000000 --- a/apps/tlmst/build/windows/info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "fixed": { - "file_version": "0.1.0" - }, - "info": { - "0000": { - "ProductVersion": "0.1.0", - "CompanyName": "My Company", - "FileDescription": "A tlmst application", - "LegalCopyright": "© 2026, My Company", - "ProductName": "My Product", - "Comments": "This is a comment" - } - } -} \ No newline at end of file diff --git a/apps/tlmst/build/windows/msix/app_manifest.xml b/apps/tlmst/build/windows/msix/app_manifest.xml deleted file mode 100644 index d99e251a..00000000 --- a/apps/tlmst/build/windows/msix/app_manifest.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - My Product - My Company - A tlmst application - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/tlmst/build/windows/msix/template.xml b/apps/tlmst/build/windows/msix/template.xml deleted file mode 100644 index ce35e804..00000000 --- a/apps/tlmst/build/windows/msix/template.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - false - My Product - My Company - A tlmst application - Assets\AppIcon.png - - - - - - - diff --git a/apps/tlmst/build/windows/nsis/project.nsi b/apps/tlmst/build/windows/nsis/project.nsi deleted file mode 100644 index 0b115d91..00000000 --- a/apps/tlmst/build/windows/nsis/project.nsi +++ /dev/null @@ -1,119 +0,0 @@ -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 "tlmst" -## !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 -## !define WAILS_INSTALL_SCOPE "user" # Default "machine" - set to "user" for per-user install ($LOCALAPPDATA) without UAC prompt -#### -## 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. -!if "${WAILS_INSTALL_SCOPE}" == "user" - InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}" -!else - InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" -!endif -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 diff --git a/apps/tlmst/build/windows/nsis/wails_tools.nsh b/apps/tlmst/build/windows/nsis/wails_tools.nsh deleted file mode 100644 index a3a61526..00000000 --- a/apps/tlmst/build/windows/nsis/wails_tools.nsh +++ /dev/null @@ -1,261 +0,0 @@ -# DO NOT EDIT - Generated automatically by `wails build` - -!include "x64.nsh" -!include "WinVer.nsh" -!include "FileFunc.nsh" - -!ifndef INFO_PROJECTNAME - !define INFO_PROJECTNAME "tlmst" -!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 WAILS_INSTALL_SCOPE - !define WAILS_INSTALL_SCOPE "machine" -!endif - -!ifndef REQUEST_EXECUTION_LEVEL - !if "${WAILS_INSTALL_SCOPE}" == "user" - !define REQUEST_EXECUTION_LEVEL "user" - !else - !define REQUEST_EXECUTION_LEVEL "admin" - !endif -!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 - !if "${WAILS_INSTALL_SCOPE}" == "user" - WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" - WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" - - ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 - IntFmt $0 "0x%08X" $0 - WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" - !else - 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" - !endif -!macroend - -!macro wails.deleteUninstaller - Delete "$INSTDIR\uninstall.exe" - - SetRegView 64 - !if "${WAILS_INSTALL_SCOPE}" == "user" - DeleteRegKey HKCU "${UNINST_KEY}" - !else - DeleteRegKey HKLM "${UNINST_KEY}" - !endif -!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 \ No newline at end of file diff --git a/apps/tlmst/build/windows/wails.exe.manifest b/apps/tlmst/build/windows/wails.exe.manifest deleted file mode 100644 index 461b4f85..00000000 --- a/apps/tlmst/build/windows/wails.exe.manifest +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - true/pm - permonitorv2,permonitor - - - - - - - - - - \ No newline at end of file diff --git a/apps/tlmst/frontend/Inter Font License.txt b/apps/tlmst/frontend/Inter Font License.txt deleted file mode 100644 index b525cbf3..00000000 --- a/apps/tlmst/frontend/Inter Font License.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/tlmst/frontend/README.md b/apps/tlmst/frontend/README.md deleted file mode 100644 index 5ce67661..00000000 --- a/apps/tlmst/frontend/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# create-svelte - -Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npm create svelte@latest - -# create a new project in my-app -npm create svelte@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/index.js b/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/index.js deleted file mode 100644 index 60211c3b..00000000 --- a/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/index.js +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -import * as SessionService from "./sessionservice.js"; -export { - SessionService -}; - -export { - AudioConfig, - ConnStats, - LogEntry, - MediaChunk, - RemoteLeft, - RemoteParticipant, - VideoConfig -} from "./models.js"; diff --git a/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/models.js b/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/models.js deleted file mode 100644 index a42ef691..00000000 --- a/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/models.js +++ /dev/null @@ -1,460 +0,0 @@ -// @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"; - -/** - * AudioConfig describes the Opus audio track. - */ -export class AudioConfig { - /** - * Creates a new AudioConfig instance. - * @param {Partial} [$$source = {}] - The source object to create the AudioConfig. - */ - constructor($$source = {}) { - if (!("codec" in $$source)) { - /** - * "opus" - * @member - * @type {string} - */ - this["codec"] = ""; - } - if (!("samplerate" in $$source)) { - /** - * 48000 - * @member - * @type {number} - */ - this["samplerate"] = 0; - } - if (!("channelConfig" in $$source)) { - /** - * "1" or "2" - * @member - * @type {string} - */ - this["channelConfig"] = ""; - } - if (!("bitrate" in $$source)) { - /** - * @member - * @type {number} - */ - this["bitrate"] = 0; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new AudioConfig instance from a string or object. - * @param {any} [$$source = {}] - * @returns {AudioConfig} - */ - static createFrom($$source = {}) { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new AudioConfig(/** @type {Partial} */($$parsedSource)); - } -} - -/** - * ConnStats is a snapshot of QUIC transport metrics for the active session, - * polled by the debug panel via SessionService.Stats. RTTs are milliseconds; - * byte/packet counters are cumulative since the connection opened. The panel - * diffs successive snapshots to derive rates (throughput, loss %). - */ -export class ConnStats { - /** - * Creates a new ConnStats instance. - * @param {Partial} [$$source = {}] - The source object to create the ConnStats. - */ - constructor($$source = {}) { - if (!("connected" in $$source)) { - /** - * @member - * @type {boolean} - */ - this["connected"] = false; - } - if (!("smoothedRttMs" in $$source)) { - /** - * @member - * @type {number} - */ - this["smoothedRttMs"] = 0; - } - if (!("latestRttMs" in $$source)) { - /** - * @member - * @type {number} - */ - this["latestRttMs"] = 0; - } - if (!("minRttMs" in $$source)) { - /** - * @member - * @type {number} - */ - this["minRttMs"] = 0; - } - if (!("congestionBytes" in $$source)) { - /** - * @member - * @type {number} - */ - this["congestionBytes"] = 0; - } - if (!("bytesInFlight" in $$source)) { - /** - * @member - * @type {number} - */ - this["bytesInFlight"] = 0; - } - if (!("packetsSent" in $$source)) { - /** - * @member - * @type {number} - */ - this["packetsSent"] = 0; - } - if (!("packetsReceived" in $$source)) { - /** - * @member - * @type {number} - */ - this["packetsReceived"] = 0; - } - if (!("packetsLost" in $$source)) { - /** - * @member - * @type {number} - */ - this["packetsLost"] = 0; - } - if (!("bytesSent" in $$source)) { - /** - * @member - * @type {number} - */ - this["bytesSent"] = 0; - } - if (!("bytesReceived" in $$source)) { - /** - * @member - * @type {number} - */ - this["bytesReceived"] = 0; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new ConnStats instance from a string or object. - * @param {any} [$$source = {}] - * @returns {ConnStats} - */ - static createFrom($$source = {}) { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new ConnStats(/** @type {Partial} */($$parsedSource)); - } -} - -/** - * LogEntry is one log record forwarded from the Go backend to the frontend. - * The binding generator turns this into a typed event payload. - */ -export class LogEntry { - /** - * Creates a new LogEntry instance. - * @param {Partial} [$$source = {}] - The source object to create the LogEntry. - */ - constructor($$source = {}) { - if (!("time" in $$source)) { - /** - * @member - * @type {string} - */ - this["time"] = ""; - } - if (!("level" in $$source)) { - /** - * @member - * @type {string} - */ - this["level"] = ""; - } - if (!("message" in $$source)) { - /** - * @member - * @type {string} - */ - this["message"] = ""; - } - if (!("attrs" in $$source)) { - /** - * @member - * @type {{ [_ in string]?: string }} - */ - this["attrs"] = {}; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new LogEntry instance from a string or object. - * @param {any} [$$source = {}] - * @returns {LogEntry} - */ - static createFrom($$source = {}) { - const $$createField3_0 = $$createType0; - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("attrs" in $$parsedSource) { - $$parsedSource["attrs"] = $$createField3_0($$parsedSource["attrs"]); - } - return new LogEntry(/** @type {Partial} */($$parsedSource)); - } -} - -/** - * MediaChunk carries one encoded frame from a remote peer to the frontend, - * where WebCodecs decodes and renders it. - * - * GroupID/ObjectID are the absolute MoQ coordinates of the object (§11.4.2). - * The frontend needs them to restore decode order: each video GOP is its own - * subgroup = its own QUIC stream, and the subscriber reads streams - * concurrently (one goroutine per subgroup), so chunks from adjacent GOPs can - * reach the frontend out of order over a lossy link. The player feeds frames - * to the decoder in (GroupID, ObjectID) order and jumps to the newest keyframe - * rather than decoding stale deltas against a reset reference — which is what - * produced the severe smearing seen against a remote relay. - */ -export class MediaChunk { - /** - * Creates a new MediaChunk instance. - * @param {Partial} [$$source = {}] - The source object to create the MediaChunk. - */ - constructor($$source = {}) { - if (!("participantId" in $$source)) { - /** - * @member - * @type {string} - */ - this["participantId"] = ""; - } - if (!("kind" in $$source)) { - /** - * "video" | "audio" - * @member - * @type {string} - */ - this["kind"] = ""; - } - if (!("data" in $$source)) { - /** - * base64-encoded codec bytes - * @member - * @type {string} - */ - this["data"] = ""; - } - if (!("timestampMicros" in $$source)) { - /** - * @member - * @type {number} - */ - this["timestampMicros"] = 0; - } - if (!("keyframe" in $$source)) { - /** - * @member - * @type {boolean} - */ - this["keyframe"] = false; - } - if (!("groupId" in $$source)) { - /** - * @member - * @type {number} - */ - this["groupId"] = 0; - } - if (!("objectId" in $$source)) { - /** - * @member - * @type {number} - */ - this["objectId"] = 0; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new MediaChunk instance from a string or object. - * @param {any} [$$source = {}] - * @returns {MediaChunk} - */ - static createFrom($$source = {}) { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new MediaChunk(/** @type {Partial} */($$parsedSource)); - } -} - -/** - * RemoteLeft signals a peer withdrew its namespace. - */ -export class RemoteLeft { - /** - * Creates a new RemoteLeft instance. - * @param {Partial} [$$source = {}] - The source object to create the RemoteLeft. - */ - constructor($$source = {}) { - if (!("id" in $$source)) { - /** - * @member - * @type {string} - */ - this["id"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new RemoteLeft instance from a string or object. - * @param {any} [$$source = {}] - * @returns {RemoteLeft} - */ - static createFrom($$source = {}) { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new RemoteLeft(/** @type {Partial} */($$parsedSource)); - } -} - -/** - * RemoteParticipant announces a discovered peer and its track configuration. - */ -export class RemoteParticipant { - /** - * Creates a new RemoteParticipant instance. - * @param {Partial} [$$source = {}] - The source object to create the RemoteParticipant. - */ - constructor($$source = {}) { - if (!("id" in $$source)) { - /** - * @member - * @type {string} - */ - this["id"] = ""; - } - if (/** @type {any} */(false)) { - /** - * @member - * @type {VideoConfig | null | undefined} - */ - this["video"] = undefined; - } - if (/** @type {any} */(false)) { - /** - * @member - * @type {AudioConfig | null | undefined} - */ - this["audio"] = undefined; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new RemoteParticipant instance from a string or object. - * @param {any} [$$source = {}] - * @returns {RemoteParticipant} - */ - static createFrom($$source = {}) { - const $$createField1_0 = $$createType2; - const $$createField2_0 = $$createType4; - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("video" in $$parsedSource) { - $$parsedSource["video"] = $$createField1_0($$parsedSource["video"]); - } - if ("audio" in $$parsedSource) { - $$parsedSource["audio"] = $$createField2_0($$parsedSource["audio"]); - } - return new RemoteParticipant(/** @type {Partial} */($$parsedSource)); - } -} - -/** - * VideoConfig describes the H.264 video track. The frontend fills it once its - * WebCodecs VideoEncoder is configured. - */ -export class VideoConfig { - /** - * Creates a new VideoConfig instance. - * @param {Partial} [$$source = {}] - The source object to create the VideoConfig. - */ - constructor($$source = {}) { - if (!("codec" in $$source)) { - /** - * e.g. "avc1.42E01F" - * @member - * @type {string} - */ - this["codec"] = ""; - } - if (!("width" in $$source)) { - /** - * @member - * @type {number} - */ - this["width"] = 0; - } - if (!("height" in $$source)) { - /** - * @member - * @type {number} - */ - this["height"] = 0; - } - if (!("framerate" in $$source)) { - /** - * @member - * @type {number} - */ - this["framerate"] = 0; - } - if (!("bitrate" in $$source)) { - /** - * @member - * @type {number} - */ - this["bitrate"] = 0; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new VideoConfig instance from a string or object. - * @param {any} [$$source = {}] - * @returns {VideoConfig} - */ - static createFrom($$source = {}) { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new VideoConfig(/** @type {Partial} */($$parsedSource)); - } -} - -// Private type creation functions -const $$createType0 = $Create.Map($Create.Any, $Create.Any); -const $$createType1 = VideoConfig.createFrom; -const $$createType2 = $Create.Nullable($$createType1); -const $$createType3 = AudioConfig.createFrom; -const $$createType4 = $Create.Nullable($$createType3); diff --git a/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/sessionservice.js b/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/sessionservice.js deleted file mode 100644 index ecb80f15..00000000 --- a/apps/tlmst/frontend/bindings/github.com/floatdrop/moq-go/apps/tlmst/sessionservice.js +++ /dev/null @@ -1,101 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -/** - * SessionService establishes and holds a MoQ (Media over QUIC Transport) - * client session on behalf of the frontend. - * @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"; - -/** - * Join establishes a MoQ session against the relay at addr. It blocks until - * the QUIC connection and MOQT SETUP handshake complete (or fail). Throughout - * establishment, every backend log line is proxied to the frontend as a - * "moq:log" event via the context logger, so the UI can render progress live. - * @param {string} addr - * @returns {$CancellablePromise} - */ -export function Join(addr) { - return $Call.ByID(1641025405, addr); -} - -/** - * Leave tears down the current session, returning the UI to its initial state. - * It is safe to call when no session is open. - * @returns {$CancellablePromise} - */ -export function Leave() { - return $Call.ByID(4266125548); -} - -/** - * PublishAudioChunk forwards one Opus frame (the bytes of a WebCodecs - * EncodedAudioChunk) to the audio track. The data arrives as a base64 string - * on the wire; Wails (via encoding/json) decodes it into the []byte for us. - * @param {string} data - * @param {number} timestampMicros - * @returns {$CancellablePromise} - */ -export function PublishAudioChunk(data, timestampMicros) { - return $Call.ByID(2935103691, data, timestampMicros); -} - -/** - * PublishVideoChunk forwards one H.264 access unit (the bytes of a WebCodecs - * EncodedVideoChunk) to the video track. timestampMicros is the chunk's - * presentation timestamp in microseconds. The data arrives as a base64 string - * on the wire; Wails (via encoding/json) decodes it into the []byte for us. - * @param {string} data - * @param {number} timestampMicros - * @param {boolean} keyframe - * @returns {$CancellablePromise} - */ -export function PublishVideoChunk(data, timestampMicros, keyframe) { - return $Call.ByID(3509541092, data, timestampMicros, keyframe); -} - -/** - * StartPublishing publishes the catalog, video, and audio tracks for the - * announced room. The frontend calls this once its WebCodecs encoders are - * configured, before sending any media chunks. - * @param {$models.VideoConfig} video - * @param {$models.AudioConfig} audio - * @returns {$CancellablePromise} - */ -export function StartPublishing(video, audio) { - return $Call.ByID(648899046, video, audio); -} - -/** - * StartSubscribing begins discovering other participants via SUBSCRIBE_NAMESPACE - * and subscribing to their tracks. The frontend calls this from the call screen - * once its remote-media event listeners are attached. - * @returns {$CancellablePromise} - */ -export function StartSubscribing() { - return $Call.ByID(2704777838); -} - -/** - * Stats returns a snapshot of the current QUIC connection's transport metrics - * for the debug panel. It is safe to call with no session open (it reports - * Connected=false and zeroed counters). - * @returns {$CancellablePromise<$models.ConnStats>} - */ -export function Stats() { - return $Call.ByID(3717796760).then(/** @type {($result: any) => any} */(($result) => { - return $$createType0($result); - })); -} - -// Private type creation functions -const $$createType0 = $models.ConnStats.createFrom; diff --git a/apps/tlmst/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js b/apps/tlmst/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js deleted file mode 100644 index 7cef1d97..00000000 --- a/apps/tlmst/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js +++ /dev/null @@ -1,28 +0,0 @@ -//@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"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as main$0 from "../../../../floatdrop/moq-go/apps/tlmst/models.js"; - -function configure() { - Object.freeze(Object.assign($Create.Events, { - "moq:log": $$createType0, - "moq:media-chunk": $$createType1, - "moq:participant-joined": $$createType2, - "moq:participant-left": $$createType3, - })); -} - -// Private type creation functions -const $$createType0 = main$0.LogEntry.createFrom; -const $$createType1 = main$0.MediaChunk.createFrom; -const $$createType2 = main$0.RemoteParticipant.createFrom; -const $$createType3 = main$0.RemoteLeft.createFrom; - -configure(); diff --git a/apps/tlmst/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/apps/tlmst/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts deleted file mode 100644 index 8ec571f5..00000000 --- a/apps/tlmst/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// 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 type { Events } from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import type * as main$0 from "../../../../floatdrop/moq-go/apps/tlmst/models.js"; - -declare module "@wailsio/runtime" { - namespace Events { - interface CustomEvents { - "moq:log": main$0.LogEntry; - "moq:media-chunk": main$0.MediaChunk; - "moq:participant-joined": main$0.RemoteParticipant; - "moq:participant-left": main$0.RemoteLeft; - } - } -} diff --git a/apps/tlmst/frontend/components.json b/apps/tlmst/frontend/components.json deleted file mode 100644 index 3ec01ba7..00000000 --- a/apps/tlmst/frontend/components.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://shadcn-svelte.com/schema.json", - "tailwind": { - "css": "src/routes/layout.css", - "baseColor": "zinc" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils", - "ui": "$lib/components/ui", - "hooks": "$lib/hooks", - "lib": "$lib" - }, - "typescript": true, - "registry": "https://shadcn-svelte.com/registry", - "style": "vega", - "iconLibrary": "lucide", - "menuColor": "default", - "menuAccent": "subtle" -} diff --git a/apps/tlmst/frontend/frontend/style.css b/apps/tlmst/frontend/frontend/style.css deleted file mode 100644 index 0b9c5827..00000000 --- a/apps/tlmst/frontend/frontend/style.css +++ /dev/null @@ -1,157 +0,0 @@ -:root { - font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", - "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: rgba(27, 38, 54, 1); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 400; - src: local(""), - url("./Inter-Medium.ttf") format("truetype"); -} - -h3 { - font-size: 3em; - line-height: 1.1; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -button { - width: 60px; - height: 30px; - line-height: 30px; - border-radius: 3px; - border: none; - margin: 0 0 0 20px; - padding: 0 8px; - cursor: pointer; -} - -.result { - height: 20px; - line-height: 20px; -} - -body { - margin: 0; - display: flex; - place-items: center; - place-content: center; - min-width: 320px; - min-height: 100vh; -} - -.container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; -} - -.logo:hover { - filter: drop-shadow(0 0 2em #e80000aa); -} - -.logo.vanilla:hover { - filter: drop-shadow(0 0 2em #f7df1eaa); -} - -.result { - height: 20px; - line-height: 20px; - margin: 1.5rem auto; - text-align: center; -} - -.footer { - margin-top: 1rem; - align-content: center; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} - - -.input-box .btn:hover { - background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); - color: #333333; -} - -.input-box .input { - border: none; - border-radius: 3px; - outline: none; - height: 30px; - line-height: 30px; - padding: 0 10px; - color: black; - background-color: rgba(240, 240, 240, 1); - -webkit-font-smoothing: antialiased; -} - -.input-box .input:hover { - border: none; - background-color: rgba(255, 255, 255, 1); -} - -.input-box .input:focus { - border: none; - background-color: rgba(255, 255, 255, 1); -} \ No newline at end of file diff --git a/apps/tlmst/frontend/package-lock.json b/apps/tlmst/frontend/package-lock.json deleted file mode 100644 index 0d29c9cf..00000000 --- a/apps/tlmst/frontend/package-lock.json +++ /dev/null @@ -1,2039 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.1", - "dependencies": { - "@wailsio/runtime": "latest" - }, - "devDependencies": { - "@fontsource-variable/inter": "^5.2.8", - "@internationalized/date": "^3.12.2", - "@lucide/svelte": "^1.17.0", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.53.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.2.2", - "@types/node": "^25.9.1", - "bits-ui": "^2.18.1", - "clsx": "^2.1.1", - "shadcn-svelte": "^1.3.0", - "svelte": "^5.46.4", - "svelte-check": "^4.4.8", - "tailwind-merge": "^3.6.0", - "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.2.2", - "tw-animate-css": "^1.4.0", - "vite": "^8.0.5" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@fontsource-variable/inter": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz", - "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==", - "dev": true, - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@internationalized/date": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", - "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lucide/svelte": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.17.0.tgz", - "integrity": "sha512-q06YCFBN5CO8cd1ADmLCxWRVMVb7xxvHzqC0lvNoxGa+FLW6Cd1Y1AOxgbQk4Iwe68vkAMCRveNHint4WoaVKg==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "svelte": "^5" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", - "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", - "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.62.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.62.0.tgz", - "integrity": "sha512-4JlkXGRJ3kW15dL4LCHV3Mu5aSTTtmH8EBNE4QjJl+KLY77dClgAsZg8aebpwFcDXemNP1z9az8EatD2UNWAcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.9", - "@types/cookie": "^0.6.0", - "acorn": "^8.16.0", - "cookie": "^0.6.0", - "devalue": "^5.8.1", - "esm-env": "^1.2.2", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", - "sirv": "^3.0.0" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3 || ^6.0.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", - "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "deepmerge": "^4.3.1", - "magic-string": "^0.30.21", - "obug": "^2.1.0", - "vitefu": "^1.1.2" - }, - "engines": { - "node": "^20.19 || ^22.12 || >=24" - }, - "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", - "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.21.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.0" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", - "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-arm64": "4.3.0", - "@tailwindcss/oxide-darwin-x64": "4.3.0", - "@tailwindcss/oxide-freebsd-x64": "4.3.0", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", - "@tailwindcss/oxide-linux-x64-musl": "4.3.0", - "@tailwindcss/oxide-wasm32-wasi": "4.3.0", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", - "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", - "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", - "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", - "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", - "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", - "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", - "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", - "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", - "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", - "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.10.0", - "@emnapi/runtime": "^1.10.0", - "@emnapi/wasi-threads": "^1.2.1", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", - "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", - "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", - "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.0", - "@tailwindcss/oxide": "4.3.0", - "tailwindcss": "4.3.0" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@wailsio/runtime": { - "version": "3.0.0-alpha.79", - "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.79.tgz", - "integrity": "sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA==", - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/bits-ui": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", - "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.1", - "@floating-ui/dom": "^1.7.1", - "esm-env": "^1.1.2", - "runed": "^0.35.1", - "svelte-toolbelt": "^0.10.6", - "tabbable": "^6.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/huntabyte" - }, - "peerDependencies": { - "@internationalized/date": "^3.8.1", - "svelte": "^5.33.0" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devalue": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", - "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.22.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", - "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esrap": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.10.tgz", - "integrity": "sha512-HUTyxhhAQBl1hhsyLlHD1sh9xF6o6vaejzLxK5sge+LzrdEflQPQaNhC+n98d+OVB8v3LCCF+y80x/4bACjjJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "peerDependencies": { - "@typescript-eslint/types": "^8.2.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/types": { - "optional": true - } - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/runed": { - "version": "0.35.1", - "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", - "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", - "dev": true, - "funding": [ - "https://github.com/sponsors/huntabyte", - "https://github.com/sponsors/tglide" - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "esm-env": "^1.0.0", - "lz-string": "^1.5.0" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.21.0", - "svelte": "^5.7.0" - }, - "peerDependenciesMeta": { - "@sveltejs/kit": { - "optional": true - } - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/shadcn-svelte": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/shadcn-svelte/-/shadcn-svelte-1.3.0.tgz", - "integrity": "sha512-Pd4ICWTkTks/b2YU4c9vF2XsX1x5HFPRl5bKszS1LcnWS83x+7T4WiIvbWz8Qh9knkcGZ+SCz1+Dmhdq+AYooA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^14.0.0", - "node-fetch-native": "^1.6.4", - "postcss": "^8.5.5", - "tailwind-merge": "^3.0.0" - }, - "bin": { - "shadcn-svelte": "dist/index.mjs" - }, - "peerDependencies": { - "svelte": "^5.0.0" - } - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/svelte": { - "version": "5.56.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.1.tgz", - "integrity": "sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.10", - "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", - "acorn": "^8.12.1", - "aria-query": "5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "devalue": "^5.8.1", - "esm-env": "^1.2.1", - "esrap": "^2.2.9", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/svelte-check": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.5.0.tgz", - "integrity": "sha512-9lNwPxCLWniFvQIcEv1LFqjIxcFtO3smb5+5BKbRJ3ttL4o2lXCej5rLF4DAnfLPI66oaA81vAxw6ILdIWI7kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/svelte-toolbelt": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", - "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/huntabyte" - ], - "dependencies": { - "clsx": "^2.1.1", - "runed": "^0.35.1", - "style-to-object": "^1.0.8" - }, - "engines": { - "node": ">=18", - "pnpm": ">=8.7.0" - }, - "peerDependencies": { - "svelte": "^5.30.2" - } - }, - "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tailwind-merge": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", - "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwind-variants": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", - "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.x", - "pnpm": ">=7.x" - }, - "peerDependencies": { - "tailwind-merge": ">=3.0.0", - "tailwindcss": "*" - }, - "peerDependenciesMeta": { - "tailwind-merge": { - "optional": true - } - } - }, - "node_modules/tailwindcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", - "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tw-animate-css": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", - "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Wombosvideo" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", - "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/zimmerframe": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/apps/tlmst/frontend/package.json b/apps/tlmst/frontend/package.json deleted file mode 100644 index 1de7b9ca..00000000 --- a/apps/tlmst/frontend/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.1", - "private": true, - "type": "module", - "scripts": { - "dev": "vite dev", - "build:dev": "vite build --minify false --mode development", - "build": "vite build --mode production", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" - }, - "dependencies": { - "@wailsio/runtime": "latest" - }, - "devDependencies": { - "@fontsource-variable/inter": "^5.2.8", - "@internationalized/date": "^3.12.2", - "@lucide/svelte": "^1.17.0", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.53.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.2.2", - "@types/node": "^25.9.1", - "bits-ui": "^2.18.1", - "clsx": "^2.1.1", - "shadcn-svelte": "^1.3.0", - "svelte": "^5.46.4", - "svelte-check": "^4.4.8", - "tailwind-merge": "^3.6.0", - "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.2.2", - "tw-animate-css": "^1.4.0", - "vite": "^8.0.5" - } -} diff --git a/apps/tlmst/frontend/src/app.html b/apps/tlmst/frontend/src/app.html deleted file mode 100644 index 2b3908e2..00000000 --- a/apps/tlmst/frontend/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/tlmst/frontend/src/lib/components/DebugPanel.svelte b/apps/tlmst/frontend/src/lib/components/DebugPanel.svelte deleted file mode 100644 index 0d50fae9..00000000 --- a/apps/tlmst/frontend/src/lib/components/DebugPanel.svelte +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - Connection stats - - QUIC transport and codec metrics for {addr}. - - - -
- -
-
-
- RTT (smoothed) - {fmt(stat.rtt, 1)} ms -
- -
- latest {fmt(stat.rttLatest, 1)} · min {fmt(stat.rttMin, 1)} ms -
-
- -
-
- Packet loss - {fmt(stat.lossPct, 2)} % -
- -
- {stat.packetsLost} lost / {stat.packetsSent} sent -
-
- -
-
- Upload - {fmt(stat.upKbps)} kbps -
- -
- -
-
- Download - {fmt(stat.downKbps)} kbps -
- -
-
- - -
-
cwnd {fmt(stat.cwndKB, 1)} KB
-
in-flight {fmt(stat.inFlightKB, 1)} KB
-
recv {stat.packetsReceived} pkts
-
- - -
-

Publishing

- {#if codecStats.publish.active} -
-
resolution {codecStats.publish.width}×{codecStats.publish.height}
-
fps {fmt(stat.pubFps, 1)}
-
bitrate {fmt(stat.pubKbps)} kbps
-
encode queue {codecStats.publish.encodeQueue}
-
keyframes {codecStats.publish.keyframes}
-
frames dropped {codecStats.publish.framesDropped}
-
- {:else} -

Not publishing.

- {/if} -
- - -
-

Remote decoders

- {#if remoteEntries.length === 0} -

No remote participants.

- {:else} -
- {#each remoteEntries as [id, r] (id)} -
-
{id}
-
{r.width}×{r.height}
-
{fmt(remoteFps[id] ?? 0, 1)} fps
-
decoded {r.framesDecoded}
-
dropped {r.framesDropped}
-
errors {r.decodeErrors}
-
- {/each} -
- {/if} -
-
-
-
diff --git a/apps/tlmst/frontend/src/lib/components/RemoteTile.svelte b/apps/tlmst/frontend/src/lib/components/RemoteTile.svelte deleted file mode 100644 index c25c18e7..00000000 --- a/apps/tlmst/frontend/src/lib/components/RemoteTile.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
- - - {label || id} - -
diff --git a/apps/tlmst/frontend/src/lib/components/Sparkline.svelte b/apps/tlmst/frontend/src/lib/components/Sparkline.svelte deleted file mode 100644 index a31e901f..00000000 --- a/apps/tlmst/frontend/src/lib/components/Sparkline.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/VideoTile.svelte b/apps/tlmst/frontend/src/lib/components/VideoTile.svelte deleted file mode 100644 index 941eb0e0..00000000 --- a/apps/tlmst/frontend/src/lib/components/VideoTile.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -
- - - - - {#if stream} - - {:else} -
- No video -
- {/if} - - {#if label} - - {label} - - {/if} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/button/button.svelte b/apps/tlmst/frontend/src/lib/components/ui/button/button.svelte deleted file mode 100644 index c38862ed..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/button/button.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - -{#if href} - - {@render children?.()} - -{:else} - -{/if} diff --git a/apps/tlmst/frontend/src/lib/components/ui/button/index.ts b/apps/tlmst/frontend/src/lib/components/ui/button/index.ts deleted file mode 100644 index fb585d76..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/button/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Root, { - type ButtonProps, - type ButtonSize, - type ButtonVariant, - buttonVariants, -} from "./button.svelte"; - -export { - Root, - type ButtonProps as Props, - // - Root as Button, - buttonVariants, - type ButtonProps, - type ButtonSize, - type ButtonVariant, -}; diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/card-action.svelte b/apps/tlmst/frontend/src/lib/components/ui/card/card-action.svelte deleted file mode 100644 index 7c48844a..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/card-action.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/card-content.svelte b/apps/tlmst/frontend/src/lib/components/ui/card/card-content.svelte deleted file mode 100644 index 082a7861..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/card-content.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/card-description.svelte b/apps/tlmst/frontend/src/lib/components/ui/card/card-description.svelte deleted file mode 100644 index 9b20ac70..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/card-description.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -

- {@render children?.()} -

diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/card-footer.svelte b/apps/tlmst/frontend/src/lib/components/ui/card/card-footer.svelte deleted file mode 100644 index 591c3f71..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/card-footer.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/card-header.svelte b/apps/tlmst/frontend/src/lib/components/ui/card/card-header.svelte deleted file mode 100644 index 21e9a170..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/card-header.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/card-title.svelte b/apps/tlmst/frontend/src/lib/components/ui/card/card-title.svelte deleted file mode 100644 index 7d20243b..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/card-title.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/card.svelte b/apps/tlmst/frontend/src/lib/components/ui/card/card.svelte deleted file mode 100644 index 329a6c72..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/card.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)} - {...restProps} -> - {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/card/index.ts b/apps/tlmst/frontend/src/lib/components/ui/card/index.ts deleted file mode 100644 index 4d3fce48..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/card/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Root from "./card.svelte"; -import Content from "./card-content.svelte"; -import Description from "./card-description.svelte"; -import Footer from "./card-footer.svelte"; -import Header from "./card-header.svelte"; -import Title from "./card-title.svelte"; -import Action from "./card-action.svelte"; - -export { - Root, - Content, - Description, - Footer, - Header, - Title, - Action, - // - Root as Card, - Content as CardContent, - Description as CardDescription, - Footer as CardFooter, - Header as CardHeader, - Title as CardTitle, - Action as CardAction, -}; diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-close.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-close.svelte deleted file mode 100644 index de68f2f0..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-close.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-content.svelte deleted file mode 100644 index a4663c93..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-content.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - {@render children?.()} - {#if showCloseButton} - - {#snippet child({ props })} - - {/snippet} - - {/if} - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-description.svelte deleted file mode 100644 index 0102d910..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-description.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-footer.svelte deleted file mode 100644 index 56858957..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-footer.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
- {@render children?.()} - {#if showCloseButton} - - {#snippet child({ props })} - - {/snippet} - - {/if} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-header.svelte deleted file mode 100644 index c3ce8a24..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-header.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte deleted file mode 100644 index 19f69f05..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-portal.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-portal.svelte deleted file mode 100644 index ccfa79ca..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-portal.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-title.svelte deleted file mode 100644 index 3f1618b6..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-title.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte deleted file mode 100644 index 589ee0c3..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog.svelte b/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog.svelte deleted file mode 100644 index 211672c6..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/dialog.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/dialog/index.ts b/apps/tlmst/frontend/src/lib/components/ui/dialog/index.ts deleted file mode 100644 index 076cef52..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/dialog/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Root from "./dialog.svelte"; -import Portal from "./dialog-portal.svelte"; -import Title from "./dialog-title.svelte"; -import Footer from "./dialog-footer.svelte"; -import Header from "./dialog-header.svelte"; -import Overlay from "./dialog-overlay.svelte"; -import Content from "./dialog-content.svelte"; -import Description from "./dialog-description.svelte"; -import Trigger from "./dialog-trigger.svelte"; -import Close from "./dialog-close.svelte"; - -export { - Root, - Title, - Portal, - Footer, - Header, - Trigger, - Overlay, - Content, - Description, - Close, - // - Root as Dialog, - Title as DialogTitle, - Portal as DialogPortal, - Footer as DialogFooter, - Header as DialogHeader, - Trigger as DialogTrigger, - Overlay as DialogOverlay, - Content as DialogContent, - Description as DialogDescription, - Close as DialogClose, -}; diff --git a/apps/tlmst/frontend/src/lib/components/ui/input/index.ts b/apps/tlmst/frontend/src/lib/components/ui/input/index.ts deleted file mode 100644 index f47b6d3f..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/input/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Root from "./input.svelte"; - -export { - Root, - // - Root as Input, -}; diff --git a/apps/tlmst/frontend/src/lib/components/ui/input/input.svelte b/apps/tlmst/frontend/src/lib/components/ui/input/input.svelte deleted file mode 100644 index 9978bcbd..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/input/input.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - -{#if type === "file"} - -{:else} - -{/if} diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/index.ts b/apps/tlmst/frontend/src/lib/components/ui/select/index.ts deleted file mode 100644 index 4dec358b..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Root from "./select.svelte"; -import Group from "./select-group.svelte"; -import Label from "./select-label.svelte"; -import Item from "./select-item.svelte"; -import Content from "./select-content.svelte"; -import Trigger from "./select-trigger.svelte"; -import Separator from "./select-separator.svelte"; -import ScrollDownButton from "./select-scroll-down-button.svelte"; -import ScrollUpButton from "./select-scroll-up-button.svelte"; -import GroupHeading from "./select-group-heading.svelte"; -import Portal from "./select-portal.svelte"; - -export { - Root, - Group, - Label, - Item, - Content, - Trigger, - Separator, - ScrollDownButton, - ScrollUpButton, - GroupHeading, - Portal, - // - Root as Select, - Group as SelectGroup, - Label as SelectLabel, - Item as SelectItem, - Content as SelectContent, - Trigger as SelectTrigger, - Separator as SelectSeparator, - ScrollDownButton as SelectScrollDownButton, - ScrollUpButton as SelectScrollUpButton, - GroupHeading as SelectGroupHeading, - Portal as SelectPortal, -}; diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-content.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-content.svelte deleted file mode 100644 index 887afcd0..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-content.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - {@render children?.()} - - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-group-heading.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-group-heading.svelte deleted file mode 100644 index 1fab5f00..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-group-heading.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - {@render children?.()} - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-group.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-group.svelte deleted file mode 100644 index f666cb22..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-group.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-item.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-item.svelte deleted file mode 100644 index 5acb6b1c..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-item.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - - {#snippet children({ selected, highlighted })} - - {#if selected} - - {/if} - - {#if childrenProp} - {@render childrenProp({ selected, highlighted })} - {:else} - {label || value} - {/if} - {/snippet} - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-label.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-label.svelte deleted file mode 100644 index 46960259..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-label.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-portal.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-portal.svelte deleted file mode 100644 index 424bcddc..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-portal.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte deleted file mode 100644 index 94f41cdd..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte deleted file mode 100644 index 035ea094..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-separator.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-separator.svelte deleted file mode 100644 index 3b24bab6..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-separator.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select-trigger.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select-trigger.svelte deleted file mode 100644 index 6ad07c78..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select-trigger.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - {@render children?.()} - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/select/select.svelte b/apps/tlmst/frontend/src/lib/components/ui/select/select.svelte deleted file mode 100644 index 05eb6634..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/select/select.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/components/ui/separator/index.ts b/apps/tlmst/frontend/src/lib/components/ui/separator/index.ts deleted file mode 100644 index 82442d2c..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/separator/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Root from "./separator.svelte"; - -export { - Root, - // - Root as Separator, -}; diff --git a/apps/tlmst/frontend/src/lib/components/ui/separator/separator.svelte b/apps/tlmst/frontend/src/lib/components/ui/separator/separator.svelte deleted file mode 100644 index 5fd8a429..00000000 --- a/apps/tlmst/frontend/src/lib/components/ui/separator/separator.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/apps/tlmst/frontend/src/lib/index.js b/apps/tlmst/frontend/src/lib/index.js deleted file mode 100644 index 856f2b6c..00000000 --- a/apps/tlmst/frontend/src/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/apps/tlmst/frontend/src/lib/mediaPublisher.js b/apps/tlmst/frontend/src/lib/mediaPublisher.js deleted file mode 100644 index cb79d8a4..00000000 --- a/apps/tlmst/frontend/src/lib/mediaPublisher.js +++ /dev/null @@ -1,390 +0,0 @@ -import { SessionService } from "../../bindings/github.com/floatdrop/moq-go/apps/tlmst/index.js"; -import { - publishStarted, - publishStopped, - recordEncodedFrame, - recordEncodeDrop, -} from "./stores/stats.svelte.js"; - -// Tunables for the local encode. -const VIDEO_CODEC = "avc1.42E01F"; // H.264 Baseline 3.1 -const AUDIO_CODEC = "opus"; -const VIDEO_BITRATE = 1_500_000; -const AUDIO_BITRATE = 64_000; -const KEYFRAME_EVERY = 30; // force a keyframe ~every 1s at 30fps - -/** - * MediaPublisher encodes a local MediaStream with WebCodecs (H.264 + Opus) and - * streams the encoded chunks to the Go backend, which packages them as LOC - * objects and publishes them over MoQ. - * - * Frame delivery uses an HTMLVideoElement + requestVideoFrameCallback because - * MediaStreamTrackProcessor is not available in WebKit/WKWebView. Audio capture - * still relies on MediaStreamTrackProcessor; when it's missing we publish video - * only and report it via onStatus. - */ -export class MediaPublisher { - /** - * @param {MediaStream} stream - * @param {(msg: string) => void} [onStatus] - */ - constructor(stream, onStatus = () => {}) { - this.stream = stream; - this.onStatus = onStatus; - this.running = false; - this.frameCount = 0; - /** @type {VideoEncoder | null} */ - this.videoEncoder = null; - /** @type {AudioEncoder | null} */ - this.audioEncoder = null; - /** @type {HTMLVideoElement | null} */ - this.videoEl = null; - /** @type {ReadableStreamDefaultReader | null} */ - this.audioReader = null; - /** @type {AudioContext | null} */ - this.audioContext = null; - /** @type {AudioWorkletNode | null} */ - this.audioNode = null; - /** @type {MediaStreamAudioSourceNode | null} */ - this.audioSource = null; - // Running frame count for AudioData timestamps on the worklet path. - this.audioSampleCursor = 0; - // Encoder config, captured at start and kept stable across device switches - // so re-captured audio keeps matching the published catalog. - this.sampleRate = 48000; - this.channels = 1; - // Per-track promise chains keep backend calls ordered without blocking the - // encoder output callbacks. - this.videoChain = Promise.resolve(); - this.audioChain = Promise.resolve(); - } - - async start() { - if (!("VideoEncoder" in window)) { - throw new Error("WebCodecs VideoEncoder is not available in this webview"); - } - - const videoTrack = this.stream.getVideoTracks()[0]; - const audioTrack = this.stream.getAudioTracks()[0]; - if (!videoTrack) { - throw new Error("no video track to publish"); - } - - const vs = videoTrack.getSettings(); - const width = vs.width ?? 1280; - const height = vs.height ?? 720; - const framerate = vs.frameRate ?? 30; - - const as = audioTrack?.getSettings() ?? {}; - this.sampleRate = as.sampleRate ?? 48000; - this.channels = as.channelCount ?? 1; - const sampleRate = this.sampleRate; - const channels = this.channels; - - // 1. Tell the backend to announce the catalog + open the media tracks. - await SessionService.StartPublishing( - { - codec: VIDEO_CODEC, - width, - height, - framerate, - bitrate: VIDEO_BITRATE, - }, - { - codec: AUDIO_CODEC, - samplerate: sampleRate, - channelConfig: String(channels), - bitrate: AUDIO_BITRATE, - }, - ); - - this.running = true; - publishStarted(width, height); - - // 2. Configure the video encoder. "annexb" makes each keyframe carry its - // SPS/PPS inline, so the stream is self-describing with no separate config. - this.videoEncoder = new VideoEncoder({ - output: (chunk) => this.#onVideoChunk(chunk), - error: (e) => this.onStatus(`video encoder error: ${e.message}`), - }); - this.videoEncoder.configure({ - codec: VIDEO_CODEC, - width, - height, - framerate, - bitrate: VIDEO_BITRATE, - latencyMode: "realtime", - avc: { format: "annexb" }, - }); - - // 3. Pump camera frames into the encoder. - this.videoEl = document.createElement("video"); - this.videoEl.srcObject = new MediaStream([videoTrack]); - this.videoEl.muted = true; - this.videoEl.playsInline = true; - await this.videoEl.play(); - - const pump = (_now, metadata) => { - if (!this.running || !this.videoEncoder || !this.videoEl) return; - try { - const seconds = metadata?.mediaTime ?? this.videoEl.currentTime; - const frame = new VideoFrame(this.videoEl, { - timestamp: Math.round(seconds * 1e6), - }); - if (this.videoEncoder.encodeQueueSize < 2) { - this.videoEncoder.encode(frame, { - keyFrame: this.frameCount % KEYFRAME_EVERY === 0, - }); - this.frameCount++; - } else { - // Encoder is backed up (CPU-bound capture): skip this frame rather - // than queue it. Counted so the panel can surface capture pressure. - recordEncodeDrop(this.videoEncoder.encodeQueueSize); - } - frame.close(); - } catch (e) { - this.onStatus(`video frame error: ${e.message ?? e}`); - } - this.videoEl.requestVideoFrameCallback(pump); - }; - this.videoEl.requestVideoFrameCallback(pump); - - // 4. Audio. Configure the Opus encoder once, then start capturing PCM. - if (audioTrack && "AudioEncoder" in window) { - this.audioEncoder = new AudioEncoder({ - output: (chunk) => this.#onAudioChunk(chunk), - error: (e) => this.onStatus(`audio encoder error: ${e.message}`), - }); - this.audioEncoder.configure({ - codec: AUDIO_CODEC, - sampleRate, - numberOfChannels: channels, - bitrate: AUDIO_BITRATE, - }); - await this.#startAudioCapture(audioTrack); - } else { - this.onStatus("publishing video only (audio capture unavailable)"); - } - } - - /** - * Hot-swaps the camera the encoder reads from, without disturbing audio or - * restarting the encoder, so remote video never stalls on a device change. - * @param {MediaStreamTrack} track - */ - async switchVideoTrack(track) { - if (!this.videoEl) return; - this.videoEl.srcObject = new MediaStream([track]); - try { - await this.videoEl.play(); - } catch { - // autoplay/play race — frames resume on the next rVFC tick - } - } - - /** - * Hot-swaps the microphone feeding the (unchanged) Opus encoder. The capture - * is re-pointed at the new track while the encoder, its config, and the - * timestamp cursor stay put, so the published audio stays decodable. - * @param {MediaStreamTrack} track - */ - async switchAudioTrack(track) { - if (!this.audioEncoder) return; - // Worklet path (WebKit): just re-point the source node at the new track, - // keeping the same AudioContext (and thus the same sample rate). - if (this.audioContext && this.audioNode) { - try { - this.audioSource?.disconnect(); - } catch { - // already disconnected - } - this.audioSource = this.audioContext.createMediaStreamSource(new MediaStream([track])); - this.audioSource.connect(this.audioNode); - return; - } - // MediaStreamTrackProcessor path (Chromium): restart the reader. - if ("MediaStreamTrackProcessor" in window) { - try { - await this.audioReader?.cancel(); - } catch { - // reader already closed - } - // eslint-disable-next-line no-undef - const proc = new MediaStreamTrackProcessor({ track }); - this.audioReader = proc.readable.getReader(); - this.#pumpAudio(); - } - } - - // #startAudioCapture begins PCM capture via whichever API the webview - // supports: MediaStreamTrackProcessor (Chromium) or an AudioWorklet - // (WebKit/WKWebView, which lacks the former). - /** @param {MediaStreamTrack} audioTrack */ - async #startAudioCapture(audioTrack) { - if ("MediaStreamTrackProcessor" in window) { - // eslint-disable-next-line no-undef - const proc = new MediaStreamTrackProcessor({ track: audioTrack }); - this.audioReader = proc.readable.getReader(); - this.#pumpAudio(); - this.onStatus("publishing video + audio"); - } else if ("AudioWorkletNode" in window && "AudioData" in window) { - await this.#startAudioWorklet(audioTrack); - this.onStatus("publishing video + audio"); - } else { - this.audioEncoder?.close(); - this.audioEncoder = null; - this.onStatus("publishing video only (audio capture unavailable)"); - } - } - - // #pumpAudio drains the MediaStreamTrackProcessor reader (Chromium path). - async #pumpAudio() { - try { - while (this.running && this.audioReader && this.audioEncoder) { - const { value, done } = await this.audioReader.read(); - if (done) break; - if (this.audioEncoder.encodeQueueSize < 10) { - this.audioEncoder.encode(value); - } - value.close(); - } - } catch (e) { - this.onStatus(`audio pump error: ${e.message ?? e}`); - } - } - - /** - * Captures mic PCM via an AudioWorklet — the WebKit-compatible path — and - * feeds each render quantum to the Opus encoder as AudioData. The encoder - * buffers internally into 20ms Opus frames, so forwarding raw quanta is fine. - * @param {MediaStreamTrack} audioTrack - */ - async #startAudioWorklet(audioTrack) { - const sampleRate = this.sampleRate; - const channels = this.channels; - const ctx = new AudioContext({ sampleRate }); - this.audioContext = ctx; - // Resume in case the context starts suspended (no direct user gesture here). - await ctx.resume().catch(() => {}); - await ctx.audioWorklet.addModule("/pcmWorklet.js"); - - const source = ctx.createMediaStreamSource(new MediaStream([audioTrack])); - this.audioSource = source; - const node = new AudioWorkletNode(ctx, "pcm-capture"); - this.audioNode = node; - - node.port.onmessage = (ev) => { - if (!this.running || !this.audioEncoder) return; - const chs = ev.data.channels; - if (!chs || chs.length === 0) return; - const frames = chs[0].length; - // Pack the per-channel Float32 buffers into one planar buffer. - const planar = new Float32Array(frames * channels); - for (let c = 0; c < channels; c++) { - planar.set(chs[c] ?? chs[0], c * frames); - } - try { - const audioData = new AudioData({ - format: "f32-planar", - sampleRate, - numberOfFrames: frames, - numberOfChannels: channels, - timestamp: Math.round((this.audioSampleCursor / sampleRate) * 1e6), - data: planar, - }); - if (this.audioEncoder.encodeQueueSize < 20) { - this.audioEncoder.encode(audioData); - } - audioData.close(); - } catch (e) { - this.onStatus(`audio data error: ${e.message ?? e}`); - } - this.audioSampleCursor += frames; - }; - - // Drive the graph: the worklet writes no output, so connecting it to the - // destination pulls PCM through with no audible playback (no echo). - source.connect(node); - node.connect(ctx.destination); - } - - /** @param {EncodedVideoChunk} chunk */ - #onVideoChunk(chunk) { - const b64 = chunkToBase64(chunk); - const ts = chunk.timestamp; - const key = chunk.type === "key"; - recordEncodedFrame(chunk.byteLength, key, this.videoEncoder?.encodeQueueSize ?? 0); - this.videoChain = this.videoChain - .then(() => SessionService.PublishVideoChunk(b64, ts, key)) - .catch((e) => this.onStatus(`publish video chunk: ${e.message ?? e}`)); - } - - /** @param {EncodedAudioChunk} chunk */ - #onAudioChunk(chunk) { - const b64 = chunkToBase64(chunk); - const ts = chunk.timestamp; - this.audioChain = this.audioChain - .then(() => SessionService.PublishAudioChunk(b64, ts)) - .catch((e) => this.onStatus(`publish audio chunk: ${e.message ?? e}`)); - } - - async stop() { - this.running = false; - publishStopped(); - try { - await this.audioReader?.cancel(); - } catch { - // reader already closed - } - try { - this.audioSource?.disconnect(); - } catch { - // already disconnected - } - try { - this.audioNode?.disconnect(); - } catch { - // node already disconnected - } - try { - await this.audioContext?.close(); - } catch { - // context already closed - } - try { - this.videoEncoder?.close(); - } catch { - // already closed - } - try { - this.audioEncoder?.close(); - } catch { - // already closed - } - if (this.videoEl) { - this.videoEl.srcObject = null; - this.videoEl = null; - } - this.videoEncoder = null; - this.audioEncoder = null; - this.audioReader = null; - this.audioNode = null; - this.audioSource = null; - this.audioContext = null; - } -} - -/** - * Copies a WebCodecs EncodedVideoChunk/EncodedAudioChunk into a base64 string - * suitable for transit to Go (where it's base64-decoded back to bytes). - * @param {EncodedVideoChunk | EncodedAudioChunk} chunk - */ -function chunkToBase64(chunk) { - const buf = new Uint8Array(chunk.byteLength); - chunk.copyTo(buf); - let binary = ""; - for (let i = 0; i < buf.length; i++) { - binary += String.fromCharCode(buf[i]); - } - return btoa(binary); -} diff --git a/apps/tlmst/frontend/src/lib/remote.svelte.js b/apps/tlmst/frontend/src/lib/remote.svelte.js deleted file mode 100644 index aad6299a..00000000 --- a/apps/tlmst/frontend/src/lib/remote.svelte.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Events } from "@wailsio/runtime"; -import { RemotePlayer } from "./remotePlayer.js"; - -// Reactive list of remote participants, consumed by the call grid. -export const remotes = $state({ - /** @type {{ id: string }[]} */ - list: [], -}); - -/** @type {Map} */ -const players = new Map(); -/** @type {AudioContext | null} */ -let audioCtx = null; -/** @type {(() => void)[]} */ -let unsub = []; - -// startRemotes wires the backend's remote-media events to per-participant -// decoders. Call it from the call screen before StartSubscribing so no early -// announcements are missed. -export function startRemotes() { - if (unsub.length) return; - audioCtx = "AudioContext" in window ? new AudioContext() : null; - // May start suspended without a direct gesture; resume for playback. - audioCtx?.resume?.().catch(() => {}); - unsub.push(Events.On("moq:participant-joined", (e) => onJoined(e.data))); - unsub.push(Events.On("moq:participant-left", (e) => onLeft(e.data))); - unsub.push(Events.On("moq:media-chunk", (e) => onChunk(e.data))); -} - -export function stopRemotes() { - unsub.forEach((off) => off()); - unsub = []; - players.forEach((p) => p.close()); - players.clear(); - remotes.list = []; - audioCtx?.close().catch(() => {}); - audioCtx = null; -} - -// attachCanvas connects a participant's tile canvas to its decoder. -/** @param {string} id @param {HTMLCanvasElement} canvas */ -export function attachCanvas(id, canvas) { - players.get(id)?.setCanvas(canvas); -} - -/** @param {{id: string, video?: any, audio?: any}} p */ -function onJoined(p) { - if (players.has(p.id)) return; - players.set(p.id, new RemotePlayer(p.id, p.video ?? null, p.audio ?? null, audioCtx)); - remotes.list = [...remotes.list, { id: p.id }]; -} - -/** @param {{id: string}} p */ -function onLeft(p) { - players.get(p.id)?.close(); - players.delete(p.id); - remotes.list = remotes.list.filter((r) => r.id !== p.id); -} - -/** @param {{participantId: string, kind: "video"|"audio", data: string, timestampMicros: number, keyframe: boolean, groupId: number, objectId: number}} c */ -function onChunk(c) { - players - .get(c.participantId) - ?.pushChunk(c.kind, c.data, c.timestampMicros, c.keyframe, c.groupId, c.objectId); -} diff --git a/apps/tlmst/frontend/src/lib/remotePlayer.js b/apps/tlmst/frontend/src/lib/remotePlayer.js deleted file mode 100644 index 5d3713ad..00000000 --- a/apps/tlmst/frontend/src/lib/remotePlayer.js +++ /dev/null @@ -1,270 +0,0 @@ -import { pushLog } from "./stores/logs.svelte.js"; -import { ensureRemoteStat, removeRemoteStat } from "./stores/stats.svelte.js"; - -// How far a video frame may lag the live edge (newest timestamp received) -// before we treat it as stale. Past this, a fresher frame is already waiting, -// so decoding the lagging one only accrues latency ("slow motion"); we drop -// the rest of its GOP and jump to the next keyframe instead. ~250ms ≈ 7-8 -// frames of slack at 30fps — enough to ride out normal jitter without -// over-trimming, while keeping end-to-end latency bounded. Catch-up costs at -// most one GOP (KEYFRAME_EVERY frames, ~1s). -const MAX_LAG_US = 250_000; - -/** - * RemotePlayer decodes one remote participant's H.264 + Opus streams with - * WebCodecs. Decoded video frames are drawn onto a canvas; decoded audio is - * scheduled for playback through a shared AudioContext. - */ -export class RemotePlayer { - /** - * @param {string} id - * @param {{codec: string, width: number, height: number} | null} videoConfig - * @param {{codec: string, samplerate: number, channelConfig: string} | null} audioConfig - * @param {AudioContext | null} audioCtx - */ - constructor(id, videoConfig, audioConfig, audioCtx) { - this.id = id; - this.audioCtx = audioCtx; - /** @type {HTMLCanvasElement | null} */ - this.canvas = null; - this.sawKeyframe = false; - this.nextPlayTime = 0; - this.renderedFrame = false; - // Codec stats for the debug panel; mutated as frames flow. - this.stat = ensureRemoteStat(id); - - // Decode-order gating state. Each video GOP is its own MoQ group/subgroup - // = its own QUIC stream, and the backend reads streams concurrently, so - // chunks from adjacent GOPs can arrive out of order on a lossy link. We - // only ever feed the decoder objects belonging to the group we're anchored - // on, in ascending ObjectID, and jump forward to a newer keyframe instead - // of decoding stale deltas (which is what smeared the picture against a - // remote relay). - /** @type {number | undefined} */ - this.curGroup = undefined; - this.curObject = -1; - // Newest video timestamp received (the live edge), used to skip stale - // frames and converge playback to live. - this.liveEdgeTs = 0; - - /** @type {VideoDecoder | null} */ - this.videoDecoder = null; - /** @type {AudioDecoder | null} */ - this.audioDecoder = null; - - if (videoConfig && "VideoDecoder" in window) { - this.videoDecoder = new VideoDecoder({ - output: (frame) => this.#drawFrame(frame), - error: (e) => { - this.stat.decodeErrors++; - pushLog("ERROR", "video decoder error", { user: id, err: e.message ?? String(e) }); - }, - }); - try { - this.videoDecoder.configure({ - codec: videoConfig.codec, - codedWidth: videoConfig.width || undefined, - codedHeight: videoConfig.height || undefined, - optimizeForLatency: true, - }); - pushLog("DEBUG", "video decoder configured", { user: id, codec: videoConfig.codec }); - } catch (e) { - pushLog("ERROR", "video configure failed", { user: id, codec: videoConfig.codec, err: e.message ?? String(e) }); - this.videoDecoder = null; - } - } - - if (audioConfig && audioCtx && "AudioDecoder" in window) { - this.audioDecoder = new AudioDecoder({ - output: (data) => this.#playAudio(data), - error: (e) => pushLog("ERROR", "audio decoder error", { user: id, err: e.message ?? String(e) }), - }); - try { - this.audioDecoder.configure({ - codec: audioConfig.codec, - sampleRate: audioConfig.samplerate, - numberOfChannels: Number(audioConfig.channelConfig) || 1, - }); - pushLog("DEBUG", "audio decoder configured", { user: id, codec: audioConfig.codec }); - } catch (e) { - pushLog("ERROR", "audio configure failed", { user: id, err: e.message ?? String(e) }); - this.audioDecoder = null; - } - } - } - - /** @param {HTMLCanvasElement} canvas */ - setCanvas(canvas) { - this.canvas = canvas; - } - - /** - * @param {"video" | "audio"} kind - * @param {string} b64 - * @param {number} timestampMicros - * @param {boolean} keyframe - * @param {number} groupId - * @param {number} objectId - */ - pushChunk(kind, b64, timestampMicros, keyframe, groupId, objectId) { - const data = base64ToBytes(b64); - if (kind === "video") { - this.#pushVideo(data, timestampMicros, keyframe, groupId, objectId); - } else { - if (!this.audioDecoder || this.audioDecoder.state !== "configured") return; - // Opus frames are independently decodable, so audio needs no group - // gating; the player schedules them by timestamp (see #playAudio). - this.audioDecoder.decode( - new EncodedAudioChunk({ type: "key", timestamp: timestampMicros, data }), - ); - } - } - - /** - * Feed one video object to the decoder in strict decode order. The rules: - * - A keyframe (object 0 of a group) is an IDR — independently decodable - * and a reference-buffer reset. Jump to it whenever it belongs to a - * group at least as new as the current one. This bootstraps the first - * frame and recovers from any loss/reorder in the previous GOP. - * - A delta is decoded only if it's the next object of the current group. - * Stragglers from an older group, or a newer group whose keyframe hasn't - * arrived, are dropped. A hole inside the current group abandons it and - * waits for the next keyframe rather than smearing forward. - * @param {Uint8Array} data - * @param {number} ts - * @param {boolean} keyframe - * @param {number} groupId - * @param {number} objectId - */ - #pushVideo(data, ts, keyframe, groupId, objectId) { - if (!this.videoDecoder || this.videoDecoder.state !== "configured") return; - - // Advance the live edge. `behind` means a fresher frame already arrived - // (reordering/backlog on the link), so this one is stale. - if (ts > this.liveEdgeTs) this.liveEdgeTs = ts; - const behind = this.liveEdgeTs - ts > MAX_LAG_US; - - if (keyframe) { - // Keyframes are the catch-up points: always take the newest one. Even if - // it lags the live edge, decoding it re-anchors us nearer to live than - // staying on the older group would. - if (this.curGroup === undefined || groupId >= this.curGroup) { - this.curGroup = groupId; - this.curObject = objectId; // 0 - this.sawKeyframe = true; - this.#decodeVideo("key", ts, data); - } else { - this.stat.framesDropped++; // late keyframe from a group we're already past - } - return; - } - - if (!this.sawKeyframe) return; // not yet anchored on a keyframe - if (groupId !== this.curGroup || objectId !== this.curObject + 1) { - // Out-of-group straggler, a future group missing its keyframe, or a hole - // in the current GOP. Stop decoding this group; the next keyframe - // re-anchors us cleanly. - if (groupId === this.curGroup) this.sawKeyframe = false; - this.stat.framesDropped++; - return; - } - if (behind) { - // We're lagging the live edge. Abandon the rest of this GOP and wait for - // the next keyframe to jump forward, rather than playing out the backlog - // in slow motion. - this.sawKeyframe = false; - this.stat.framesDropped++; - return; - } - this.curObject = objectId; - this.#decodeVideo("delta", ts, data); - } - - /** @param {"key"|"delta"} type @param {number} ts @param {Uint8Array} data */ - #decodeVideo(type, ts, data) { - try { - this.videoDecoder.decode(new EncodedVideoChunk({ type, timestamp: ts, data })); - this.stat.decodeQueue = this.videoDecoder.decodeQueueSize; - } catch (e) { - this.stat.decodeErrors++; - pushLog("ERROR", "video decode threw", { user: this.id, err: e.message ?? String(e) }); - } - } - - /** @param {VideoFrame} frame */ - #drawFrame(frame) { - const canvas = this.canvas; - if (!canvas) { - frame.close(); - return; - } - const w = frame.displayWidth; - const h = frame.displayHeight; - if (canvas.width !== w) canvas.width = w; - if (canvas.height !== h) canvas.height = h; - const ctx = canvas.getContext("2d"); - if (ctx) ctx.drawImage(frame, 0, 0, w, h); - frame.close(); - this.stat.framesDecoded++; - this.stat.width = w; - this.stat.height = h; - this.stat.decodeQueue = this.videoDecoder?.decodeQueueSize ?? 0; - if (!this.renderedFrame) { - this.renderedFrame = true; - pushLog("INFO", "rendering remote video", { user: this.id, w, h }); - } - } - - /** @param {AudioData} data */ - #playAudio(data) { - const ctx = this.audioCtx; - if (!ctx) { - data.close(); - return; - } - const channels = data.numberOfChannels; - const frames = data.numberOfFrames; - const buffer = ctx.createBuffer(channels, frames, data.sampleRate); - for (let c = 0; c < channels; c++) { - const plane = new Float32Array(frames); - data.copyTo(plane, { planeIndex: c, format: "f32-planar" }); - buffer.copyToChannel(plane, c); - } - data.close(); - - const src = ctx.createBufferSource(); - src.buffer = buffer; - src.connect(ctx.destination); - // Schedule back-to-back; if we've fallen behind, resync to now. - const start = Math.max(ctx.currentTime, this.nextPlayTime); - src.start(start); - this.nextPlayTime = start + buffer.duration; - } - - close() { - try { - this.videoDecoder?.close(); - } catch { - // already closed - } - try { - this.audioDecoder?.close(); - } catch { - // already closed - } - this.videoDecoder = null; - this.audioDecoder = null; - this.canvas = null; - removeRemoteStat(this.id); - } -} - -/** @param {string} b64 */ -function base64ToBytes(b64) { - const bin = atob(b64); - const bytes = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) { - bytes[i] = bin.charCodeAt(i); - } - return bytes; -} diff --git a/apps/tlmst/frontend/src/lib/stores/logs.svelte.js b/apps/tlmst/frontend/src/lib/stores/logs.svelte.js deleted file mode 100644 index c3eac2e4..00000000 --- a/apps/tlmst/frontend/src/lib/stores/logs.svelte.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Events } from "@wailsio/runtime"; - -// Shared, app-wide capture of backend log records (the "moq:log" event). Lives -// at module scope so it persists across navigation and accumulates everything -// from app start — session handshake, publisher, and subscriber alike. -export const logStore = $state({ - /** @type {{time: string, level: string, message: string, attrs: Record}[]} */ - entries: [], -}); - -const MAX_ENTRIES = 1000; -let started = false; - -// startLogCapture subscribes to backend logs exactly once. Safe to call from -// multiple places; subsequent calls are no-ops. -export function startLogCapture() { - if (started) return; - started = true; - Events.On("moq:log", (e) => { - const next = [...logStore.entries, e.data]; - if (next.length > MAX_ENTRIES) { - next.splice(0, next.length - MAX_ENTRIES); - } - logStore.entries = next; - }); -} - -export function clearLogs() { - logStore.entries = []; -} - -// pushLog appends a frontend-originated entry to the same panel, so decode/ -// render diagnostics sit alongside the backend logs. -/** @param {string} level @param {string} message @param {Record} [attrs] */ -export function pushLog(level, message, attrs = {}) { - const stringAttrs = {}; - for (const [k, v] of Object.entries(attrs)) stringAttrs[k] = String(v); - const next = [...logStore.entries, { - time: new Date().toISOString(), - level, - message: `[web] ${message}`, - attrs: stringAttrs, - }]; - if (next.length > MAX_ENTRIES) next.splice(0, next.length - MAX_ENTRIES); - logStore.entries = next; -} diff --git a/apps/tlmst/frontend/src/lib/stores/session.svelte.js b/apps/tlmst/frontend/src/lib/stores/session.svelte.js deleted file mode 100644 index 6150171f..00000000 --- a/apps/tlmst/frontend/src/lib/stores/session.svelte.js +++ /dev/null @@ -1,10 +0,0 @@ -// Shared session state. This is a plain reactive object that survives -// client-side navigation (the app is an SPA, so the module is not re-evaluated -// when moving between routes). The Go backend owns the real MoQ session; this -// just tracks enough for the UI to gate the /call route and show the relay. -export const session = $state({ - /** @type {boolean} whether a MoQ session is currently established */ - connected: false, - /** @type {string} the relay address that was joined */ - addr: "", -}); diff --git a/apps/tlmst/frontend/src/lib/stores/stats.svelte.js b/apps/tlmst/frontend/src/lib/stores/stats.svelte.js deleted file mode 100644 index 4ced4cc1..00000000 --- a/apps/tlmst/frontend/src/lib/stores/stats.svelte.js +++ /dev/null @@ -1,93 +0,0 @@ -// Frontend-side codec counters for the debug panel. The WebCodecs encoder and -// decoders live in the browser, so their stats can't come from the Go -// SessionService.Stats() call — they're collected here instead. -// -// Producers (MediaPublisher, RemotePlayer) only bump cumulative counters and -// gauges; the debug panel diffs successive samples on its poll tick to derive -// rates (fps, bitrate). Keeping all rate math in the panel means the producers -// stay timer-free and there's a single, consistent sampling interval shared -// with the QUIC stats. - -/** - * @typedef {Object} RemoteStat - * @property {number} framesDecoded frames handed to the canvas - * @property {number} framesDropped frames dropped by decode-order gating - * @property {number} decodeErrors decoder error / throw count - * @property {number} decodeQueue current VideoDecoder.decodeQueueSize - * @property {number} width last decoded frame width - * @property {number} height last decoded frame height - */ - -export const codecStats = $state({ - publish: { - active: false, - framesEncoded: 0, - framesDropped: 0, - keyframes: 0, - bytesEncoded: 0, - encodeQueue: 0, - width: 0, - height: 0, - }, - /** @type {Record} */ - remotes: {}, -}); - -/** Reset publish counters at the start of a publishing session. */ -export function publishStarted(width, height) { - Object.assign(codecStats.publish, { - active: true, - framesEncoded: 0, - framesDropped: 0, - keyframes: 0, - bytesEncoded: 0, - encodeQueue: 0, - width, - height, - }); -} - -export function publishStopped() { - codecStats.publish.active = false; -} - -/** @param {number} bytes @param {boolean} keyframe @param {number} queue */ -export function recordEncodedFrame(bytes, keyframe, queue) { - const p = codecStats.publish; - p.framesEncoded++; - p.bytesEncoded += bytes; - if (keyframe) p.keyframes++; - p.encodeQueue = queue; -} - -/** @param {number} queue */ -export function recordEncodeDrop(queue) { - codecStats.publish.framesDropped++; - codecStats.publish.encodeQueue = queue; -} - -/** - * Returns the (reactive) stat entry for a remote participant, creating it on - * first use. The caller mutates the returned object directly; it's part of the - * $state proxy, so the panel re-renders. - * @param {string} id - * @returns {RemoteStat} - */ -export function ensureRemoteStat(id) { - if (!codecStats.remotes[id]) { - codecStats.remotes[id] = { - framesDecoded: 0, - framesDropped: 0, - decodeErrors: 0, - decodeQueue: 0, - width: 0, - height: 0, - }; - } - return codecStats.remotes[id]; -} - -/** @param {string} id */ -export function removeRemoteStat(id) { - delete codecStats.remotes[id]; -} diff --git a/apps/tlmst/frontend/src/lib/utils.ts b/apps/tlmst/frontend/src/lib/utils.ts deleted file mode 100644 index 55b3a918..00000000 --- a/apps/tlmst/frontend/src/lib/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChild = T extends { child?: any } ? Omit : T; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChildren = T extends { children?: any } ? Omit : T; -export type WithoutChildrenOrChild = WithoutChildren>; -export type WithElementRef = T & { ref?: U | null }; diff --git a/apps/tlmst/frontend/src/routes/+layout.js b/apps/tlmst/frontend/src/routes/+layout.js deleted file mode 100644 index ceccaaf6..00000000 --- a/apps/tlmst/frontend/src/routes/+layout.js +++ /dev/null @@ -1,2 +0,0 @@ -export const prerender = true; -export const ssr = false; diff --git a/apps/tlmst/frontend/src/routes/+layout.svelte b/apps/tlmst/frontend/src/routes/+layout.svelte deleted file mode 100644 index a5f3da4d..00000000 --- a/apps/tlmst/frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -{@render children()} diff --git a/apps/tlmst/frontend/src/routes/+page.svelte b/apps/tlmst/frontend/src/routes/+page.svelte deleted file mode 100644 index 6c86e82b..00000000 --- a/apps/tlmst/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,158 +0,0 @@ - - -
-

tlmst

- - - - Join a relay - - Establish a MoQ session and watch the handshake unfold. - - - - -
- e.key === "Enter" && join()} - /> - -
- -
- - {statusLabel} - {#if error} - — {error} - {/if} -
- - {#if logs.length !== 0} -
- {#each logs as log, i (i)} -
- {fmtTime(log.time)} - {log.level.padEnd(5)} - - {log.message} - {#each Object.entries(log.attrs ?? {}) as [k, v]} - {k}={v}  - {/each} - -
- {/each} -
- {/if} -
-
-
diff --git a/apps/tlmst/frontend/src/routes/call/+page.svelte b/apps/tlmst/frontend/src/routes/call/+page.svelte deleted file mode 100644 index 41da30d3..00000000 --- a/apps/tlmst/frontend/src/routes/call/+page.svelte +++ /dev/null @@ -1,444 +0,0 @@ - - -
- -
- {#each tiles as tile (tile.id)} - -
-
- {#if tile.remote} - - {:else} - - {/if} -
-
- {/each} -
- - {#if mediaError} -

{mediaError}

- {/if} - {#if publishStatus} -

{publishStatus}

- {/if} - - -
-
- - - - - {#each videoDevices as device (device.deviceId)} - - {device.label || "Camera"} - - {/each} - - - - - - - {audioLabel} - - - {#each audioDevices as device (device.deviceId)} - - {device.label || "Microphone"} - - {/each} - - -
- -
- - - - - -
-
-
- - - - - - - Settings - - Connected to {session.addr}. - - - -
-

Debug logs

- -
- -
- {#if logStore.entries.length === 0} -

No logs yet.

- {:else} - {#each logStore.entries as log, i (i)} -
- {fmtLogTime(log.time)} - {log.level.padEnd(5)} - - {log.message} - {#each Object.entries(log.attrs ?? {}) as [k, v]} - {k}={v} - {/each} - -
- {/each} - {/if} -
-
-
diff --git a/apps/tlmst/frontend/src/routes/layout.css b/apps/tlmst/frontend/src/routes/layout.css deleted file mode 100644 index f8b6ac29..00000000 --- a/apps/tlmst/frontend/src/routes/layout.css +++ /dev/null @@ -1,129 +0,0 @@ -@import 'tailwindcss'; -@import "tw-animate-css"; -@import "shadcn-svelte/tailwind.css"; -@import "@fontsource-variable/inter"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.141 0.005 285.823); - --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.005 285.823); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.21 0.006 285.885); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); - --accent: oklch(0.967 0.001 286.375); - --accent-foreground: oklch(0.21 0.006 285.885); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.92 0.004 286.32); - --input: oklch(0.92 0.004 286.32); - --ring: oklch(0.705 0.015 286.067); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.141 0.005 285.823); - --sidebar-primary: oklch(0.21 0.006 285.885); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.967 0.001 286.375); - --sidebar-accent-foreground: oklch(0.21 0.006 285.885); - --sidebar-border: oklch(0.92 0.004 286.32); - --sidebar-ring: oklch(0.705 0.015 286.067); -} - -.dark { - --background: oklch(0.141 0.005 285.823); - --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0.006 285.885); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0.006 285.885); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.92 0.004 286.32); - --primary-foreground: oklch(0.21 0.006 285.885); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.274 0.006 286.033); - --muted-foreground: oklch(0.705 0.015 286.067); - --accent: oklch(0.274 0.006 286.033); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.552 0.016 285.938); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --sidebar: oklch(0.21 0.006 285.885); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.274 0.006 286.033); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.552 0.016 285.938); -} - -@theme inline { - --font-sans: 'Inter Variable', sans-serif; - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --color-foreground: var(--foreground); - --color-background: var(--background); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) * 1.4); - --radius-2xl: calc(var(--radius) * 1.8); - --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } - html { - @apply font-sans; - } -} diff --git a/apps/tlmst/frontend/static/Inter-Medium.ttf b/apps/tlmst/frontend/static/Inter-Medium.ttf deleted file mode 100644 index a01f3777..00000000 Binary files a/apps/tlmst/frontend/static/Inter-Medium.ttf and /dev/null differ diff --git a/apps/tlmst/frontend/static/pcmWorklet.js b/apps/tlmst/frontend/static/pcmWorklet.js deleted file mode 100644 index 6a62b8b0..00000000 --- a/apps/tlmst/frontend/static/pcmWorklet.js +++ /dev/null @@ -1,27 +0,0 @@ -// AudioWorklet processor that forwards captured microphone PCM to the main -// thread. Used as the WebKit/WKWebView fallback for audio capture, where -// MediaStreamTrackProcessor is unavailable. -// -// Each render quantum delivers up to 128 frames per channel. The input -// buffers are reused across quanta, so we copy them before transferring the -// backing ArrayBuffers to the main thread (zero-copy hand-off). -class PCMCaptureProcessor extends AudioWorkletProcessor { - process(inputs) { - const input = inputs[0]; - if (input && input.length > 0 && input[0].length > 0) { - const channels = input.map((channel) => { - const copy = new Float32Array(channel.length); - copy.set(channel); - return copy; - }); - this.port.postMessage( - { channels }, - channels.map((c) => c.buffer), - ); - } - // Returning true keeps the processor alive even during input gaps. - return true; - } -} - -registerProcessor("pcm-capture", PCMCaptureProcessor); diff --git a/apps/tlmst/frontend/static/style.css b/apps/tlmst/frontend/static/style.css deleted file mode 100644 index 0b9c5827..00000000 --- a/apps/tlmst/frontend/static/style.css +++ /dev/null @@ -1,157 +0,0 @@ -:root { - font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", - "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: rgba(27, 38, 54, 1); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 400; - src: local(""), - url("./Inter-Medium.ttf") format("truetype"); -} - -h3 { - font-size: 3em; - line-height: 1.1; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -button { - width: 60px; - height: 30px; - line-height: 30px; - border-radius: 3px; - border: none; - margin: 0 0 0 20px; - padding: 0 8px; - cursor: pointer; -} - -.result { - height: 20px; - line-height: 20px; -} - -body { - margin: 0; - display: flex; - place-items: center; - place-content: center; - min-width: 320px; - min-height: 100vh; -} - -.container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; -} - -.logo:hover { - filter: drop-shadow(0 0 2em #e80000aa); -} - -.logo.vanilla:hover { - filter: drop-shadow(0 0 2em #f7df1eaa); -} - -.result { - height: 20px; - line-height: 20px; - margin: 1.5rem auto; - text-align: center; -} - -.footer { - margin-top: 1rem; - align-content: center; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} - - -.input-box .btn:hover { - background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); - color: #333333; -} - -.input-box .input { - border: none; - border-radius: 3px; - outline: none; - height: 30px; - line-height: 30px; - padding: 0 10px; - color: black; - background-color: rgba(240, 240, 240, 1); - -webkit-font-smoothing: antialiased; -} - -.input-box .input:hover { - border: none; - background-color: rgba(255, 255, 255, 1); -} - -.input-box .input:focus { - border: none; - background-color: rgba(255, 255, 255, 1); -} \ No newline at end of file diff --git a/apps/tlmst/frontend/static/svelte.svg b/apps/tlmst/frontend/static/svelte.svg deleted file mode 100644 index c5e08481..00000000 --- a/apps/tlmst/frontend/static/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/tlmst/frontend/static/wails.png b/apps/tlmst/frontend/static/wails.png deleted file mode 100644 index 8bdf4248..00000000 Binary files a/apps/tlmst/frontend/static/wails.png and /dev/null differ diff --git a/apps/tlmst/frontend/svelte.config.js b/apps/tlmst/frontend/svelte.config.js deleted file mode 100644 index c42aa379..00000000 --- a/apps/tlmst/frontend/svelte.config.js +++ /dev/null @@ -1,21 +0,0 @@ -import adapter from '@sveltejs/adapter-static'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter({ - pages: 'dist', - assets: 'dist', - fallback: undefined, - precompress: false, - strict: true - }) - } -}; - -export default config; diff --git a/apps/tlmst/frontend/tsconfig.json b/apps/tlmst/frontend/tsconfig.json deleted file mode 100644 index 6c595a65..00000000 --- a/apps/tlmst/frontend/tsconfig.json +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This file tells your IDE where the root of your JavaScript project is, and sets some - * options that it can use to provide autocompletion and other features. - * - * It extends the configuration generated by SvelteKit (in .svelte-kit/tsconfig.json), - * which provides the `$lib`/`$app` path aliases and the ambient type declarations - * (including `*.css` side-effect imports) that shadcn-svelte components rely on. - */ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "moduleResolution": "bundler", - /** - * The target and module can be set to ESNext to allow writing modern JavaScript, - * and Vite will compile down to the level of "build.target" specified in the vite config file. - * Builds will error if you use a feature that cannot be compiled down to the target level. - */ - "target": "ESNext", - "module": "ESNext", - "resolveJsonModule": true, - /** - * Enable checkJs if you'd like type checking in `.svelte` and `.js` files. - */ - "checkJs": false, - "strict": true, - "skipLibCheck": true, - /** - * Path aliases are also provided by the extended SvelteKit config, but shadcn-svelte's - * CLI preflight checks read them from this file directly, so keep them declared here too. - */ - "paths": { - "$lib": ["./src/lib"], - "$lib/*": ["./src/lib/*"] - } - }, - "include": [ - ".svelte-kit/ambient.d.ts", - ".svelte-kit/non-ambient.d.ts", - ".svelte-kit/types/**/$types.d.ts", - "src/**/*.d.ts", - "src/**/*.js", - "src/**/*.ts", - "src/**/*.svelte", - "bindings/**/*.d.ts" - ] -} diff --git a/apps/tlmst/frontend/vite.config.js b/apps/tlmst/frontend/vite.config.js deleted file mode 100644 index f04f7a33..00000000 --- a/apps/tlmst/frontend/vite.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import tailwindcss from "@tailwindcss/vite"; -import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig, searchForWorkspaceRoot } from "vite"; -import wails from "@wailsio/runtime/plugins/vite"; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [tailwindcss(), sveltekit(), wails("./bindings")], - server: { - host: "127.0.0.1", - port: Number(process.env.WAILS_VITE_PORT) || 9245, - strictPort: true, - fs: { - allow: [ - // search up for workspace root - searchForWorkspaceRoot(process.cwd()), - - // your custom rules - "./bindings/*" - ] - } - } -}); diff --git a/apps/tlmst/go.mod b/apps/tlmst/go.mod deleted file mode 100644 index f0571bb7..00000000 --- a/apps/tlmst/go.mod +++ /dev/null @@ -1,30 +0,0 @@ -module github.com/floatdrop/moq-go/apps/tlmst - -go 1.26 - -// The parent module is developed in-tree; resolve it locally. The go.work -// file already does this for workspace builds, but the replace keeps plain -// `go` commands (mod tidy, docker builds) working outside the workspace too. -replace github.com/floatdrop/moq-go => ../.. - -require ( - github.com/floatdrop/moq-go v0.0.0-00010101000000-000000000000 - github.com/quic-go/quic-go v0.60.0 - github.com/wailsapp/wails/v3 v3.0.0-alpha2.105 -) - -require ( - github.com/adrg/xdg v0.5.3 // indirect - github.com/coder/websocket v1.8.14 // indirect - github.com/ebitengine/purego v0.10.1 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect - github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect - github.com/wailsapp/wails/webview2 v1.0.24 // indirect - golang.org/x/crypto v0.52.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.45.0 // indirect -) diff --git a/apps/tlmst/go.sum b/apps/tlmst/go.sum deleted file mode 100644 index a112cb86..00000000 --- a/apps/tlmst/go.sum +++ /dev/null @@ -1,50 +0,0 @@ -github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= -github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= -github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -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.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= -github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -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.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= -github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -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/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= -github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= -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.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= -github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -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/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= -github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= -github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= -github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/wailsapp/wails/v3 v3.0.0-alpha2.105 h1:KV2zqL9fOnX7o8goTwBAwBSfyT6MoZCTWofe3jSbOHM= -github.com/wailsapp/wails/v3 v3.0.0-alpha2.105/go.mod h1:GRW1qYl54Zi/w1mjCzDrMiy76g2BLfNlpqF640hgMf0= -github.com/wailsapp/wails/webview2 v1.0.24 h1:uULnjCSaRfMlU84mS3kjLgPsRosEOIusVK1nFOHZHzs= -github.com/wailsapp/wails/webview2 v1.0.24/go.mod h1:sdf+s0nAdxlzVWf9SCxC15XaxnQPJeY+uU1Ucn3jHQM= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/apps/tlmst/main.go b/apps/tlmst/main.go deleted file mode 100644 index cbfa75d2..00000000 --- a/apps/tlmst/main.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "embed" - - "log" - - "github.com/wailsapp/wails/v3/pkg/application" -) - -// Wails uses Go's `embed` package to embed the frontend files into the binary. -// Any files in the frontend/dist folder will be embedded into the binary and -// made available to the frontend. -// See https://pkg.go.dev/embed for more information. - -//go:embed all:frontend/dist -var assets embed.FS - -func init() { - // Register the custom events the backend emits. The binding generator - // picks up registered events and provides a strongly typed JS/TS API. - application.RegisterEvent[LogEntry]("moq:log") - application.RegisterEvent[RemoteParticipant]("moq:participant-joined") - application.RegisterEvent[RemoteLeft]("moq:participant-left") - application.RegisterEvent[MediaChunk]("moq:media-chunk") -} - -// main function serves as the application's entry point. It initializes the application, creates a window, -// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and -// logs any error that might occur. -func main() { - - // Create a new Wails application by providing the necessary options. - // Variables 'Name' and 'Description' are for application metadata. - // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. - // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. - // 'Mac' options tailor the application when running an macOS. - app := application.New(application.Options{ - Name: "tlmst", - Description: "A demo of using media over quic for video conferencing", - Services: []application.Service{ - application.NewService(&SessionService{}), - }, - Assets: application.AssetOptions{ - Handler: application.AssetFileServerFS(assets), - }, - Mac: application.MacOptions{ - ApplicationShouldTerminateAfterLastWindowClosed: true, - }, - }) - - // Create a new window with the necessary options. - // 'Title' is the title of the window. - // 'Mac' options tailor the window when running on macOS. - // 'BackgroundColour' is the background colour of the window. - // 'URL' is the URL that will be loaded into the webview. - app.Window.NewWithOptions(application.WebviewWindowOptions{ - Title: "tlmst", - Mac: application.MacWindow{ - InvisibleTitleBarHeight: 50, - Backdrop: application.MacBackdropTranslucent, - TitleBar: application.MacTitleBarHiddenInset, - }, - BackgroundColour: application.NewRGB(255, 255, 255), - URL: "/", - MinWidth: 800, - MinHeight: 600, - }) - - // Run the application. This blocks until the application has been exited. - err := app.Run() - - // If an error occurred while running the application, log it and exit. - if err != nil { - log.Fatal(err) - } -} diff --git a/apps/tlmst/publisher.go b/apps/tlmst/publisher.go deleted file mode 100644 index d1c4ce25..00000000 --- a/apps/tlmst/publisher.go +++ /dev/null @@ -1,411 +0,0 @@ -package main - -import ( - "context" - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "log/slog" - "sync" - "time" - - "github.com/floatdrop/moq-go/pkg/moqt" - "github.com/floatdrop/moq-go/pkg/moqt/loc" - "github.com/floatdrop/moq-go/pkg/moqt/message" - "github.com/floatdrop/moq-go/pkg/moqt/msf" - "github.com/floatdrop/moq-go/pkg/moqt/session" - "github.com/floatdrop/moq-go/pkg/moqt/wire" -) - -// Track names within a room namespace. -const ( - videoTrackName = "video" - audioTrackName = "audio" -) - -// WebCodecs timestamps are microseconds, so LOC objects use a 1µs timescale. -const mediaTimescale = 1_000_000 - -// audioGroupObjects bounds how many Opus frames share one MoQ group. Opus -// frames are independently decodable, so the only goal is to keep groups from -// growing unbounded; ~50 frames is roughly one second at 20ms/frame. -const audioGroupObjects = 50 - -// VideoConfig describes the H.264 video track. The frontend fills it once its -// WebCodecs VideoEncoder is configured. -type VideoConfig struct { - Codec string `json:"codec"` // e.g. "avc1.42E01F" - Width uint32 `json:"width"` - Height uint32 `json:"height"` - Framerate float64 `json:"framerate"` - Bitrate uint64 `json:"bitrate"` -} - -// AudioConfig describes the Opus audio track. -type AudioConfig struct { - Codec string `json:"codec"` // "opus" - Samplerate uint32 `json:"samplerate"` // 48000 - ChannelConfig string `json:"channelConfig"` // "1" or "2" - Bitrate uint64 `json:"bitrate"` -} - -// newUserID returns a short random hex identifier for a participant. -func newUserID() string { - var b [4]byte - _, _ = rand.Read(b[:]) - return hex.EncodeToString(b[:]) -} - -// publisher owns the outbound side of a session: it announces the room -// namespace and publishes the catalog, video, and audio tracks. Encoded media -// arrives from the frontend via PublishVideoChunk / PublishAudioChunk. -type publisher struct { - ctx context.Context - sess *session.Session - log *slog.Logger - userID string - ns wire.TrackNamespace - - nsStream session.Stream - - closeOnce sync.Once - - mu sync.Mutex - started bool - - // Catalog state. The catalog object is emitted exactly once at start; - // late-joining subscribers retrieve it via Joining FETCH from the - // relay's Object Cache, which is configured (via the operator's - // CacheTTLPolicy on cmd/relay) to retain catalog-named tracks for - // the lifetime of the publisher session. No periodic republishing - // is necessary. - catAlias uint64 - catalogSeq *msf.GroupSequencer - - catStream *session.Publication - videoStream *session.Publication - audioStream *session.Publication - - videoAlias uint64 - audioAlias uint64 - - videoSeq *msf.GroupSequencer - audioSeq *msf.GroupSequencer - - // Current video GOP subgroup. A new group opens on each keyframe. - videoSG *session.OutgoingSubgroupStream - haveVideo bool - videoObjCount int // object index within the current video group - - // Current audio group subgroup, rotated every audioGroupObjects frames. - audioSG *session.OutgoingSubgroupStream - audioObjCount int -} - -// newPublisher picks a user id and prepares publisher state. The namespace is -// announced later, by start, only after the media tracks have been published — -// so that any peer which discovers us can immediately subscribe to our tracks -// (announcing earlier created a race where a fast subscriber would find no -// upstream yet). -func newPublisher(ctx context.Context, sess *session.Session, log *slog.Logger) *publisher { - id := newUserID() - return &publisher{ - ctx: ctx, - sess: sess, - log: log, - userID: id, - ns: wire.Namespace("room", id), - videoSeq: msf.NewGroupSequencer(), - audioSeq: msf.NewGroupSequencer(), - catalogSeq: msf.NewGroupSequencer(), - } -} - -// start publishes the catalog, video, and audio tracks for the room. The -// catalog describes both media tracks; the video/audio publish streams stay -// open for the lifetime of the session so chunks can be pushed onto them. -func (p *publisher) start(video VideoConfig, audio AudioConfig) error { - p.mu.Lock() - defer p.mu.Unlock() - if p.started { - return nil - } - - nsString := "room/" + p.userID - live := true - cat := msf.BeginBroadcast([]msf.Track{ - { - Name: videoTrackName, - Namespace: nsString, - Packaging: msf.PackagingLOC, - IsLive: &live, - Role: msf.RoleVideo, - Codec: video.Codec, - Width: video.Width, - Height: video.Height, - Framerate: video.Framerate, - Bitrate: video.Bitrate, - Timescale: mediaTimescale, - }, - { - Name: audioTrackName, - Namespace: nsString, - Packaging: msf.PackagingLOC, - IsLive: &live, - Role: msf.RoleAudio, - Codec: audio.Codec, - Samplerate: audio.Samplerate, - ChannelConfig: audio.ChannelConfig, - Bitrate: audio.Bitrate, - Timescale: mediaTimescale, - }, - }, time.Time{}) - if err := cat.Validate(); err != nil { - return fmt.Errorf("catalog validate: %w", err) - } - catalogBytes, err := json.Marshal(cat) - if err != nil { - return fmt.Errorf("marshal catalog: %w", err) - } - - // Ordering: emit the catalog object BEFORE PublishNamespace so - // the relay's per-track cache already holds the catalog when a - // discovering peer reacts to our namespace announce. The - // alternative (announce first, then emit) loses both the live - // SUBSCRIBE and the Joining FETCH for late joiners — the live - // filter is future-only (FilterLargestObject) and the FETCH's - // joining location is snapshotted at SUBSCRIBE_OK time, so a - // hasLargest=false snapshot makes the FETCH permanently return - // INVALID_RANGE regardless of subsequent emits. Catalog-first - // closes both windows. - - // Catalog track. - catAlias := p.sess.AllocOutboundTrackAlias() - catStream, err := p.sess.Publish(p.ctx, &message.Publish{ - Namespace: p.ns, - Name: []byte(msf.CatalogTrackName), - TrackAlias: catAlias, - }) - if err != nil { - return fmt.Errorf("PUBLISH catalog: %w", err) - } - p.catStream = catStream - p.catAlias = catAlias - go drainRequestStream(p.log, "catalog", catStream) - // Emit the catalog exactly once. The relay caches it for the - // lifetime of this publisher's session (the operator's - // CacheTTLPolicy gives the "catalog" name infinite retention), - // so late-joining subscribers' Joining FETCH backfills the - // catalog without us republishing on a timer. - if err := p.emitObject(catAlias, p.catalogSeq.Next(), nil, catalogBytes); err != nil { - return fmt.Errorf("emit catalog object: %w", err) - } - - // Video track. - p.videoAlias = p.sess.AllocOutboundTrackAlias() - p.videoStream, err = p.sess.Publish(p.ctx, &message.Publish{ - Namespace: p.ns, - Name: []byte(videoTrackName), - TrackAlias: p.videoAlias, - }) - if err != nil { - return fmt.Errorf("PUBLISH video: %w", err) - } - go drainRequestStream(p.log, "video", p.videoStream) - - // Audio track. - p.audioAlias = p.sess.AllocOutboundTrackAlias() - p.audioStream, err = p.sess.Publish(p.ctx, &message.Publish{ - Namespace: p.ns, - Name: []byte(audioTrackName), - TrackAlias: p.audioAlias, - }) - if err != nil { - return fmt.Errorf("PUBLISH audio: %w", err) - } - go drainRequestStream(p.log, "audio", p.audioStream) - - // Announce the namespace now that all tracks are published and - // the catalog object is queued at the relay. A peer that reacts - // to this announce will issue SUBSCRIBE+FETCH; by the time the - // relay processes those, the catalog is already in cache (it - // travelled to the relay before the namespace announce did). - nsStream, err := p.sess.PublishNamespace(p.ctx, &message.PublishNamespace{Namespace: p.ns}) - if err != nil { - return fmt.Errorf("PUBLISH_NAMESPACE /room/%s: %w", p.userID, err) - } - p.nsStream = nsStream - go drainRequestStream(p.log, "namespace", nsStream) - - p.started = true - p.log.Info("publishing started", - "namespace", "/room/"+p.userID, - "video", video.Codec, "audio", audio.Codec) - - // The catalog is emitted exactly once above. Late-joining - // subscribers recover it via Joining FETCH against the relay's - // per-track Object Cache (retained for the publisher session's - // lifetime by the operator's CacheTTLPolicy). No periodic - // re-emit is necessary: the subscriber parks the inbound FETCH - // stream until its route is registered, so the one-shot catalog - // object is never dropped on a registration race. - return nil -} - -// publishVideo writes one encoded H.264 frame to the video track. Each -// keyframe starts a new MoQ group (a GOP); delta frames append to the current -// group as consecutive objects. -func (p *publisher) publishVideo(data []byte, timestampMicros uint64, keyframe bool) error { - p.mu.Lock() - defer p.mu.Unlock() - if !p.started { - return fmt.Errorf("publisher not started") - } - - if keyframe || !p.haveVideo { - if p.videoSG != nil { - _ = p.videoSG.Close() - } - groupID := p.videoSeq.Next() - sg, err := p.sess.OpenSubgroup(message.SubgroupHeader{ - Properties: true, - SubgroupIDMode: message.SubgroupIDImplicitZero, - TrackAlias: p.videoAlias, - GroupID: groupID, - }) - if err != nil { - return fmt.Errorf("open video subgroup: %w", err) - } - p.videoSG = sg - p.haveVideo = true - p.videoObjCount = 0 - } - - if err := writeLOCObject(p.videoSG, uint64(p.videoObjCount), data, timestampMicros); err != nil { - return err - } - p.videoObjCount++ - return nil -} - -// publishAudio writes one encoded Opus frame to the audio track, rotating the -// group every audioGroupObjects frames. -func (p *publisher) publishAudio(data []byte, timestampMicros uint64) error { - p.mu.Lock() - defer p.mu.Unlock() - if !p.started { - return fmt.Errorf("publisher not started") - } - - if p.audioSG == nil || p.audioObjCount >= audioGroupObjects { - if p.audioSG != nil { - _ = p.audioSG.Close() - } - groupID := p.audioSeq.Next() - sg, err := p.sess.OpenSubgroup(message.SubgroupHeader{ - Properties: true, - SubgroupIDMode: message.SubgroupIDImplicitZero, - TrackAlias: p.audioAlias, - GroupID: groupID, - }) - if err != nil { - return fmt.Errorf("open audio subgroup: %w", err) - } - p.audioSG = sg - p.audioObjCount = 0 - } - - if err := writeLOCObject(p.audioSG, uint64(p.audioObjCount), data, timestampMicros); err != nil { - return err - } - p.audioObjCount++ - return nil -} - -// close tears down all open publish streams and withdraws the namespace. -// Idempotent: a second call after the first returns is a no-op. -func (p *publisher) close() { - p.closeOnce.Do(func() { - p.mu.Lock() - defer p.mu.Unlock() - if p.videoSG != nil { - _ = p.videoSG.Close() - p.videoSG = nil - } - if p.audioSG != nil { - _ = p.audioSG.Close() - p.audioSG = nil - } - for _, st := range []*session.Publication{p.videoStream, p.audioStream, p.catStream} { - if st != nil { - _ = st.Done(moqt.PublishDoneTrackEnded, "") - } - } - if p.nsStream != nil { - _ = p.nsStream.Close() - p.nsStream = nil - } - p.started = false - }) -} - -// emitObject opens a single-object subgroup (used for the catalog). -func (p *publisher) emitObject(alias, groupID uint64, props, payload []byte) error { - sg, err := p.sess.OpenSubgroup(message.SubgroupHeader{ - Properties: len(props) > 0, - SubgroupIDMode: message.SubgroupIDImplicitZero, - TrackAlias: alias, - GroupID: groupID, - }) - if err != nil { - return fmt.Errorf("open subgroup: %w", err) - } - if err := sg.WriteObjectAt(0, &message.SubgroupObject{ - Properties: props, - Payload: payload, - }); err != nil { - sg.Cancel(moqt.StreamResetInternalError) - return fmt.Errorf("write object: %w", err) - } - return sg.Close() -} - -// writeLOCObject appends one LOC-packaged media chunk to an open subgroup at -// the given absolute Object ID. WriteObjectAt derives the §11.4.2 delta from -// the stream's running state, so the caller passes the object's index within -// the group (0, 1, 2, … reset each new group) rather than computing deltas. -func writeLOCObject(sg *session.OutgoingSubgroupStream, objectID uint64, data []byte, timestampMicros uint64) error { - obj := loc.Object{ - Properties: loc.Properties{ - Timestamp: timestampMicros, - HasTimestamp: true, - Timescale: mediaTimescale, - HasTimescale: true, - }, - Payload: data, - } - props, payload := obj.Encode() - if err := sg.WriteObjectAt(objectID, &message.SubgroupObject{ - Properties: props, - Payload: payload, - }); err != nil { - sg.Cancel(moqt.StreamResetInternalError) - return fmt.Errorf("write object: %w", err) - } - return nil -} - -// drainRequestStream consumes control messages on a publish/announce request -// stream until it closes, logging anything that arrives. -func drainRequestStream(log *slog.Logger, label string, stream io.Reader) { - for { - msg, err := message.Parse(stream) - if err != nil { - log.Debug("request stream closed", "stream", label, "err", err) - return - } - log.Debug("request stream message", "stream", label, "type", fmt.Sprintf("%T", msg)) - } -} diff --git a/apps/tlmst/sessionservice.go b/apps/tlmst/sessionservice.go deleted file mode 100644 index 5e5aa9b4..00000000 --- a/apps/tlmst/sessionservice.go +++ /dev/null @@ -1,308 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "fmt" - "log/slog" - "sync" - "time" - - "github.com/quic-go/quic-go" - "github.com/wailsapp/wails/v3/pkg/application" - - "github.com/floatdrop/moq-go/pkg/moqt" - "github.com/floatdrop/moq-go/pkg/moqt/session" - "github.com/floatdrop/moq-go/pkg/moqt/session/quicconn" -) - -// logEventName is the Wails event carrying proxied backend log records. -const logEventName = "moq:log" - -// LogEntry is one log record forwarded from the Go backend to the frontend. -// The binding generator turns this into a typed event payload. -type LogEntry struct { - Time string `json:"time"` - Level string `json:"level"` - Message string `json:"message"` - Attrs map[string]string `json:"attrs"` -} - -// SessionService establishes and holds a MoQ (Media over QUIC Transport) -// client session on behalf of the frontend. -type SessionService struct { - // appCtx stays valid for the lifetime of the app and is cancelled just - // before shutdown. Captured in ServiceStartup. - appCtx context.Context - - mu sync.Mutex - sess *session.Session - pub *publisher - sub *subscriber - userID string - stats *statsCollector -} - -// ServiceStartup captures the application-scoped context. -func (s *SessionService) ServiceStartup(ctx context.Context, _ application.ServiceOptions) error { - s.appCtx = ctx - return nil -} - -// ServiceShutdown closes any open session on app exit. -func (s *SessionService) ServiceShutdown() error { - s.mu.Lock() - sess, pub, sub := s.sess, s.pub, s.sub - s.sess, s.pub, s.sub, s.stats = nil, nil, nil, nil - s.mu.Unlock() - if sub != nil { - sub.close() - } - if pub != nil { - pub.close() - } - if sess != nil { - _ = sess.Close(moqt.SessionNoError, "shutting down") - } - return nil -} - -// Leave tears down the current session, returning the UI to its initial state. -// It is safe to call when no session is open. -func (s *SessionService) Leave() error { - s.mu.Lock() - sess, pub, sub := s.sess, s.pub, s.sub - s.sess, s.pub, s.sub, s.stats = nil, nil, nil, nil - s.mu.Unlock() - if sub != nil { - sub.close() - } - if pub != nil { - pub.close() - } - if sess == nil { - return nil - } - return sess.Close(moqt.SessionNoError, "user left") -} - -// Join establishes a MoQ session against the relay at addr. It blocks until -// the QUIC connection and MOQT SETUP handshake complete (or fail). Throughout -// establishment, every backend log line is proxied to the frontend as a -// "moq:log" event via the context logger, so the UI can render progress live. -func (s *SessionService) Join(addr string) error { - // Route this establishment's logs to the frontend. - logger := slog.New(newEventHandler(slog.LevelDebug)) - - // Bound the handshake so an unreachable relay doesn't hang the UI forever. - // The session does not retain this context past Client(), so cancelling it - // once Join returns is safe. - ctx, cancel := context.WithTimeout(s.appCtx, 30*time.Second) - defer cancel() - - logger.Info("joining relay", "addr", addr) - - tlsCfg := &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec — development tool - NextProtos: []string{"moq-00"}, - } - // Collect QUIC transport metrics (RTT, loss, cwnd, throughput) for the - // debug panel. quic-go v0.59 exposes these only through its qlog tracing - // hook, so the collector doubles as the trace sink. - stats := newStatsCollector() - quicCfg := &quic.Config{ - MaxIdleTimeout: 30 * time.Second, - KeepAlivePeriod: 5 * time.Second, - EnableDatagrams: true, - Tracer: stats.tracer, - } - - logger.Debug("dialing QUIC", "addr", addr) - qconn, err := quic.DialAddr(ctx, addr, tlsCfg, quicCfg) - if err != nil { - logger.Error("QUIC dial failed", "addr", addr, "err", err.Error()) - return fmt.Errorf("dial %s: %w", addr, err) - } - - logger.Debug("QUIC connected, performing MOQT handshake") - sess, err := session.Client(ctx, quicconn.New(qconn), - session.WithImplementation("tlmst/0.1"), - ) - if err != nil { - logger.Error("MOQT handshake failed", "err", err.Error()) - return fmt.Errorf("moqt handshake: %w", err) - } - - logger.Info("session established", "addr", addr) - - // Prepare the publisher. Namespace announcement and track publishing happen - // later in StartPublishing; both must outlive Join, so the publisher runs on - // the app-scoped context (not the handshake timeout) while keeping the same - // frontend-proxying logger. - pub := newPublisher(s.appCtx, sess, logger) - - // Swap in the new session, closing any previous one outside the lock. - s.mu.Lock() - old := s.sess - oldPub := s.pub - oldSub := s.sub - s.sess = sess - s.pub = pub - s.sub = nil - s.userID = pub.userID - s.stats = stats - s.mu.Unlock() - if oldSub != nil { - oldSub.close() - } - if oldPub != nil { - oldPub.close() - } - if old != nil { - _ = old.Close(moqt.SessionNoError, "replaced by new session") - } - - return nil -} - -// StartSubscribing begins discovering other participants via SUBSCRIBE_NAMESPACE -// and subscribing to their tracks. The frontend calls this from the call screen -// once its remote-media event listeners are attached. -func (s *SessionService) StartSubscribing() error { - logger := slog.New(newEventHandler(slog.LevelDebug)) - - s.mu.Lock() - if s.sub != nil || s.sess == nil { - s.mu.Unlock() - return nil - } - sub := newSubscriber(s.appCtx, s.sess, logger, s.userID) - s.sub = sub - s.mu.Unlock() - - sub.start() - return nil -} - -// StartPublishing publishes the catalog, video, and audio tracks for the -// announced room. The frontend calls this once its WebCodecs encoders are -// configured, before sending any media chunks. -func (s *SessionService) StartPublishing(video VideoConfig, audio AudioConfig) error { - s.mu.Lock() - pub := s.pub - s.mu.Unlock() - if pub == nil { - return fmt.Errorf("no active session") - } - return pub.start(video, audio) -} - -// PublishVideoChunk forwards one H.264 access unit (the bytes of a WebCodecs -// EncodedVideoChunk) to the video track. timestampMicros is the chunk's -// presentation timestamp in microseconds. The data arrives as a base64 string -// on the wire; Wails (via encoding/json) decodes it into the []byte for us. -func (s *SessionService) PublishVideoChunk(data []byte, timestampMicros uint64, keyframe bool) error { - s.mu.Lock() - pub := s.pub - s.mu.Unlock() - if pub == nil { - return fmt.Errorf("no active session") - } - return pub.publishVideo(data, timestampMicros, keyframe) -} - -// PublishAudioChunk forwards one Opus frame (the bytes of a WebCodecs -// EncodedAudioChunk) to the audio track. The data arrives as a base64 string -// on the wire; Wails (via encoding/json) decodes it into the []byte for us. -func (s *SessionService) PublishAudioChunk(data []byte, timestampMicros uint64) error { - s.mu.Lock() - pub := s.pub - s.mu.Unlock() - if pub == nil { - return fmt.Errorf("no active session") - } - return pub.publishAudio(data, timestampMicros) -} - -// Stats returns a snapshot of the current QUIC connection's transport metrics -// for the debug panel. It is safe to call with no session open (it reports -// Connected=false and zeroed counters). -func (s *SessionService) Stats() ConnStats { - s.mu.Lock() - stats := s.stats - connected := s.sess != nil - s.mu.Unlock() - if stats == nil { - return ConnStats{Connected: connected} - } - snap := stats.Snapshot() - snap.Connected = connected - return snap -} - -// eventHandler is an slog.Handler that forwards every record to the frontend -// as a Wails "moq:log" event instead of writing to an io.Writer. -type eventHandler struct { - level slog.Leveler - attrs []slog.Attr - group string -} - -func newEventHandler(level slog.Leveler) *eventHandler { - return &eventHandler{level: level} -} - -func (h *eventHandler) Enabled(_ context.Context, l slog.Level) bool { - return l >= h.level.Level() -} - -func (h *eventHandler) Handle(_ context.Context, r slog.Record) error { - attrs := make(map[string]string, len(h.attrs)+r.NumAttrs()) - put := func(a slog.Attr) { - key := a.Key - if h.group != "" { - key = h.group + "." + key - } - attrs[key] = a.Value.Resolve().String() - } - for _, a := range h.attrs { - put(a) - } - r.Attrs(func(a slog.Attr) bool { - put(a) - return true - }) - - entry := LogEntry{ - Time: r.Time.Format(time.RFC3339Nano), - Level: r.Level.String(), - Message: r.Message, - Attrs: attrs, - } - if app := application.Get(); app != nil { - app.Event.EmitEvent(&application.CustomEvent{Name: logEventName, Data: entry}) - } - return nil -} - -func (h *eventHandler) WithAttrs(attrs []slog.Attr) slog.Handler { - if len(attrs) == 0 { - return h - } - nh := *h - nh.attrs = append(append([]slog.Attr(nil), h.attrs...), attrs...) - return &nh -} - -func (h *eventHandler) WithGroup(name string) slog.Handler { - if name == "" { - return h - } - nh := *h - if nh.group != "" { - nh.group += "." + name - } else { - nh.group = name - } - return &nh -} diff --git a/apps/tlmst/stats.go b/apps/tlmst/stats.go deleted file mode 100644 index 590bcd77..00000000 --- a/apps/tlmst/stats.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "context" - "sync" - - "github.com/quic-go/quic-go" - "github.com/quic-go/quic-go/qlog" - "github.com/quic-go/quic-go/qlogwriter" -) - -// ConnStats is a snapshot of QUIC transport metrics for the active session, -// polled by the debug panel via SessionService.Stats. RTTs are milliseconds; -// byte/packet counters are cumulative since the connection opened. The panel -// diffs successive snapshots to derive rates (throughput, loss %). -type ConnStats struct { - Connected bool `json:"connected"` - SmoothedRTTMs float64 `json:"smoothedRttMs"` - LatestRTTMs float64 `json:"latestRttMs"` - MinRTTMs float64 `json:"minRttMs"` - CongestionBytes uint64 `json:"congestionBytes"` - BytesInFlight uint64 `json:"bytesInFlight"` - PacketsSent uint64 `json:"packetsSent"` - PacketsReceived uint64 `json:"packetsReceived"` - PacketsLost uint64 `json:"packetsLost"` - BytesSent uint64 `json:"bytesSent"` - BytesReceived uint64 `json:"bytesReceived"` -} - -// statsCollector implements qlogwriter.Trace + qlogwriter.Recorder. quic-go -// v0.59 surfaces transport metrics only through its qlog tracing hook -// (quic.Config.Tracer), so rather than write a qlog file we intercept the -// event stream and fold the events we care about (RTT, congestion window, -// loss, throughput) into a snapshot. -// -// quic-go calls RecordEvent from the connection's internal goroutine while the -// debug panel calls Snapshot from a Wails RPC goroutine, so all field access is -// guarded by mu. -type statsCollector struct { - mu sync.Mutex - s ConnStats -} - -func newStatsCollector() *statsCollector { return &statsCollector{} } - -// tracer is wired into quic.Config.Tracer. A client session opens exactly one -// connection, so the same collector backs the whole session and we ignore the -// per-connection arguments. -func (c *statsCollector) tracer(context.Context, bool, quic.ConnectionID) qlogwriter.Trace { - return c -} - -// AddProducer and SupportsSchemas satisfy qlogwriter.Trace: the collector is -// its own single event producer and accepts every schema (we filter by event -// type in RecordEvent instead). -func (c *statsCollector) AddProducer() qlogwriter.Recorder { return c } -func (c *statsCollector) SupportsSchemas(string) bool { return true } - -// Close satisfies qlogwriter.Recorder; there is nothing to flush. -func (c *statsCollector) Close() error { return nil } - -// RecordEvent folds one qlog event into the running snapshot. Event types we -// don't track fall through the switch and are dropped. -func (c *statsCollector) RecordEvent(ev qlogwriter.Event) { - c.mu.Lock() - defer c.mu.Unlock() - switch e := ev.(type) { - case qlog.MetricsUpdated: - // quic-go only sets a field when it is non-zero, so hold the last - // known RTT/cwnd instead of letting the gauges flap back to zero - // on a metrics update that only carries bytes_in_flight. - if e.SmoothedRTT != 0 { - c.s.SmoothedRTTMs = float64(e.SmoothedRTT.Microseconds()) / 1000 - } - if e.LatestRTT != 0 { - c.s.LatestRTTMs = float64(e.LatestRTT.Microseconds()) / 1000 - } - if e.MinRTT != 0 { - c.s.MinRTTMs = float64(e.MinRTT.Microseconds()) / 1000 - } - if e.CongestionWindow != 0 { - c.s.CongestionBytes = uint64(e.CongestionWindow) - } - c.s.BytesInFlight = uint64(e.BytesInFlight) - case qlog.PacketLost: - c.s.PacketsLost++ - case qlog.PacketSent: - c.s.PacketsSent++ - c.s.BytesSent += uint64(e.Raw.Length) - case qlog.PacketReceived: - c.s.PacketsReceived++ - c.s.BytesReceived += uint64(e.Raw.Length) - } -} - -// Snapshot returns a copy of the current metrics. Connected is set by the -// caller, which knows whether a session is actually live. -func (c *statsCollector) Snapshot() ConnStats { - c.mu.Lock() - defer c.mu.Unlock() - return c.s -} diff --git a/apps/tlmst/subscriber.go b/apps/tlmst/subscriber.go deleted file mode 100644 index 769035c9..00000000 --- a/apps/tlmst/subscriber.go +++ /dev/null @@ -1,475 +0,0 @@ -package main - -import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "log/slog" - "sync" - - "github.com/wailsapp/wails/v3/pkg/application" - - "github.com/floatdrop/moq-go/pkg/moqt/loc" - "github.com/floatdrop/moq-go/pkg/moqt/message" - "github.com/floatdrop/moq-go/pkg/moqt/msf" - "github.com/floatdrop/moq-go/pkg/moqt/session" - "github.com/floatdrop/moq-go/pkg/moqt/wire" -) - -// Wails events emitted toward the frontend for remote participants. -const ( - participantJoinedEvent = "moq:participant-joined" - participantLeftEvent = "moq:participant-left" - mediaChunkEvent = "moq:media-chunk" -) - -// RemoteParticipant announces a discovered peer and its track configuration. -type RemoteParticipant struct { - ID string `json:"id"` - Video *VideoConfig `json:"video,omitempty"` - Audio *AudioConfig `json:"audio,omitempty"` -} - -// RemoteLeft signals a peer withdrew its namespace. -type RemoteLeft struct { - ID string `json:"id"` -} - -// MediaChunk carries one encoded frame from a remote peer to the frontend, -// where WebCodecs decodes and renders it. -// -// GroupID/ObjectID are the absolute MoQ coordinates of the object (§11.4.2). -// The frontend needs them to restore decode order: each video GOP is its own -// subgroup = its own QUIC stream, and the subscriber reads streams -// concurrently (one goroutine per subgroup), so chunks from adjacent GOPs can -// reach the frontend out of order over a lossy link. The player feeds frames -// to the decoder in (GroupID, ObjectID) order and jumps to the newest keyframe -// rather than decoding stale deltas against a reset reference — which is what -// produced the severe smearing seen against a remote relay. -type MediaChunk struct { - ParticipantID string `json:"participantId"` - Kind string `json:"kind"` // "video" | "audio" - Data string `json:"data"` // base64-encoded codec bytes - TimestampMicros uint64 `json:"timestampMicros"` - Keyframe bool `json:"keyframe"` - GroupID uint64 `json:"groupId"` - ObjectID uint64 `json:"objectId"` -} - -// route identifies what an inbound data stream carries. -type route struct { - participantID string - kind string // "catalog" | "video" | "audio" -} - -type remoteParticipant struct { - id string - ns wire.TrackNamespace - gotCatalog bool -} - -// subscriber discovers peers via SUBSCRIBE_NAMESPACE, subscribes to each -// peer's catalog/video/audio tracks, and forwards decoded LOC payloads to the -// frontend as Wails events. -type subscriber struct { - ctx context.Context - cancel context.CancelFunc - sess *session.Session - log *slog.Logger - selfUserID string - - mu sync.Mutex - aliasRoutes map[uint64]route // SUBGROUP TrackAlias -> route - fetchRoutes map[uint64]route // FETCH RequestID -> route (catalog backfill) - participants map[string]*remoteParticipant - mediaSeen map[string]bool // userID+"/"+kind -> first frame logged - - // Data streams can be accepted by acceptLoop BEFORE the - // subscribe/fetch call that triggered them has registered its - // route (the relay opens the FETCH data stream right after - // FETCH_OK, which is exactly what unblocks our Fetch call). Park - // such early streams here keyed by alias/RequestID and start - // reading them as soon as the route is registered, so we never - // drop the one-shot catalog object on the floor. - pendingFetch map[uint64]*session.IncomingFetchStream // RequestID -> parked FETCH stream - pendingSubgroup map[uint64][]*session.IncomingSubgroupStream // TrackAlias -> parked subgroup streams -} - -func newSubscriber(ctx context.Context, sess *session.Session, log *slog.Logger, selfUserID string) *subscriber { - ctx, cancel := context.WithCancel(ctx) - return &subscriber{ - ctx: ctx, - cancel: cancel, - sess: sess, - log: log, - selfUserID: selfUserID, - aliasRoutes: make(map[uint64]route), - fetchRoutes: make(map[uint64]route), - participants: make(map[string]*remoteParticipant), - mediaSeen: make(map[string]bool), - pendingFetch: make(map[uint64]*session.IncomingFetchStream), - pendingSubgroup: make(map[uint64][]*session.IncomingSubgroupStream), - } -} - -// start launches namespace discovery and the inbound data-stream loop. -func (s *subscriber) start() { - go s.discoverLoop() - go s.acceptLoop() -} - -func (s *subscriber) close() { - s.cancel() -} - -// discoverLoop subscribes to the "room" namespace prefix and reacts to -// NAMESPACE / NAMESPACE_DONE announcements. -func (s *subscriber) discoverLoop() { - prefix := wire.Namespace("room") - stream, err := s.sess.SubscribeNamespace(s.ctx, &message.SubscribeNamespace{ - TrackNamespacePrefix: prefix, - }) - if err != nil { - if s.ctx.Err() == nil { - s.log.Error("SUBSCRIBE_NAMESPACE failed", "err", err.Error()) - } - return - } - s.log.Info("subscribed to namespace prefix", "prefix", "/room") - - for { - msg, err := message.Parse(stream) - if err != nil { - if s.ctx.Err() == nil && !errors.Is(err, io.EOF) { - s.log.Debug("namespace stream closed", "err", err.Error()) - } - return - } - switch m := msg.(type) { - case *message.Namespace: - s.onNamespace(prefix, m.TrackNamespaceSuffix) - case *message.NamespaceDone: - s.onNamespaceDone(m.TrackNamespaceSuffix) - } - } -} - -// onNamespace handles a newly announced peer namespace. The relay forwards our -// own namespace back to us, so we exclude it by user id. -func (s *subscriber) onNamespace(prefix, suffix wire.TrackNamespace) { - if len(suffix) == 0 { - return - } - userID := string(suffix[0]) - if userID == s.selfUserID { - return // ourselves - } - - s.mu.Lock() - if _, exists := s.participants[userID]; exists { - s.mu.Unlock() - return - } - fullNS := append(append(wire.TrackNamespace{}, prefix...), suffix...) - s.participants[userID] = &remoteParticipant{id: userID, ns: fullNS} - s.mu.Unlock() - - s.log.Info("peer discovered", "user", userID) - if err := s.subscribeCatalog(userID, fullNS); err != nil { - s.log.Error("subscribe catalog failed", "user", userID, "err", err.Error()) - } -} - -func (s *subscriber) onNamespaceDone(suffix wire.TrackNamespace) { - if len(suffix) == 0 { - return - } - userID := string(suffix[0]) - - s.mu.Lock() - delete(s.participants, userID) - for alias, r := range s.aliasRoutes { - if r.participantID == userID { - delete(s.aliasRoutes, alias) - } - } - for id, r := range s.fetchRoutes { - if r.participantID == userID { - delete(s.fetchRoutes, id) - } - } - s.mu.Unlock() - - s.log.Info("peer left", "user", userID) - emitEvent(participantLeftEvent, RemoteLeft{ID: userID}) -} - -// subscribeCatalog subscribes to a peer's catalog track at the live edge and -// issues a relative Joining FETCH so the already-published catalog object is -// backfilled (FilterLargestObject alone only delivers future objects). -// -// The publisher's ordering is: emit catalog object → PublishNamespace. -// By the time we see the peer's namespace announce on the control -// stream, the publisher's catalog object has already reached the relay -// (it travelled before the announce did), so the relay's SUBSCRIBE_OK -// snapshot reliably captures hasLargest=true. The Joining FETCH then -// returns the cached catalog on the first try. -func (s *subscriber) subscribeCatalog(userID string, ns wire.TrackNamespace) error { - subMsg := &message.Subscribe{ - Namespace: ns, - Name: []byte(msf.CatalogTrackName), - Parameters: message.Parameters{message.LargestObjectFilter()}, - } - catSub, err := s.sess.Subscribe(s.ctx, subMsg) - if err != nil { - return fmt.Errorf("SUBSCRIBE catalog: %w", err) - } - s.addAliasRoute(catSub.TrackAlias(), route{userID, "catalog"}) - - fetchMsg := &message.Fetch{ - FetchType: message.FetchTypeRelativeJoining, - Joining: &message.JoiningFetch{JoiningRequestID: subMsg.RequestID, JoiningStart: 0}, - } - if _, err := s.sess.Fetch(s.ctx, fetchMsg); err != nil { - // Non-fatal: the live SUBSCRIBE stays armed; if the - // publisher emits a fresh catalog later, we'll receive it - // that way. - s.log.Warn("catalog joining FETCH failed", "user", userID, "err", err.Error()) - return nil - } - s.addFetchRoute(fetchMsg.RequestID, route{userID, "catalog"}) - return nil -} - -// onCatalog parses a catalog object and, the first time, announces the peer to -// the frontend and subscribes to its media tracks. -func (s *subscriber) onCatalog(userID string, payload []byte) { - var cat msf.Catalog - if err := json.Unmarshal(payload, &cat); err != nil { - s.log.Warn("parse catalog failed", "user", userID, "err", err.Error()) - return - } - - s.mu.Lock() - p := s.participants[userID] - if p == nil || p.gotCatalog { - s.mu.Unlock() - return - } - p.gotCatalog = true - ns := p.ns - s.mu.Unlock() - - var video, audio *msf.Track - for i := range cat.Tracks { - t := &cat.Tracks[i] - if t.Packaging != msf.PackagingLOC { - continue - } - switch t.Role { - case msf.RoleVideo: - video = t - case msf.RoleAudio: - audio = t - } - } - - announce := RemoteParticipant{ID: userID} - if video != nil { - announce.Video = &VideoConfig{ - Codec: video.Codec, Width: video.Width, Height: video.Height, - Framerate: video.Framerate, Bitrate: video.Bitrate, - } - } - if audio != nil { - announce.Audio = &AudioConfig{ - Codec: audio.Codec, Samplerate: audio.Samplerate, - ChannelConfig: audio.ChannelConfig, Bitrate: audio.Bitrate, - } - } - s.log.Info("peer catalog", "user", userID, "hasVideo", video != nil, "hasAudio", audio != nil) - emitEvent(participantJoinedEvent, announce) - - if video != nil { - if err := s.subscribeMedia(userID, ns, video.Name, "video"); err != nil { - s.log.Error("subscribe video failed", "user", userID, "err", err.Error()) - } - } - if audio != nil { - if err := s.subscribeMedia(userID, ns, audio.Name, "audio"); err != nil { - s.log.Error("subscribe audio failed", "user", userID, "err", err.Error()) - } - } -} - -func (s *subscriber) subscribeMedia(userID string, ns wire.TrackNamespace, name, kind string) error { - subMsg := &message.Subscribe{ - Namespace: ns, - Name: []byte(name), - Parameters: message.Parameters{message.LargestObjectFilter()}, - } - sub, err := s.sess.Subscribe(s.ctx, subMsg) - if err != nil { - return fmt.Errorf("SUBSCRIBE %s: %w", kind, err) - } - s.addAliasRoute(sub.TrackAlias(), route{userID, kind}) - s.log.Info("subscribed to track", "user", userID, "kind", kind, "alias", sub.TrackAlias()) - return nil -} - -// acceptLoop receives inbound data streams and dispatches their objects by the -// route registered for the stream's TrackAlias (subgroups) or RequestID (fetch). -func (s *subscriber) acceptLoop() { - for { - ds, err := s.sess.AcceptDataStream(s.ctx) - if err != nil { - if s.ctx.Err() != nil { - return - } - if errors.Is(err, session.ErrPaddingStream) { - continue - } - s.log.Debug("accept data stream ended", "err", err.Error()) - return - } - switch st := ds.(type) { - case *session.IncomingSubgroupStream: - // Park the stream if its alias route hasn't been - // registered yet; addAliasRoute will replay it. This - // closes the race where the relay's data stream beats - // the Subscribe()-side route registration. - s.mu.Lock() - r, ok := s.aliasRoutes[st.Header.TrackAlias] - if !ok { - s.pendingSubgroup[st.Header.TrackAlias] = append( - s.pendingSubgroup[st.Header.TrackAlias], st) - s.mu.Unlock() - continue - } - s.mu.Unlock() - go s.readSubgroup(r, true, st) - case *session.IncomingFetchStream: - // Same race for the catalog joining FETCH: the relay - // opens this uni-stream immediately after FETCH_OK, so - // it can arrive before addFetchRoute runs. Park it - // until the route exists rather than dropping the - // one-shot catalog object. - s.mu.Lock() - r, ok := s.fetchRoutes[st.Header.RequestID] - if !ok { - s.pendingFetch[st.Header.RequestID] = st - s.mu.Unlock() - continue - } - s.mu.Unlock() - go s.readFetch(r, true, st) - } - } -} - -func (s *subscriber) readSubgroup(r route, known bool, st *session.IncomingSubgroupStream) { - for { - obj, err := st.ReadDecoded() - if err != nil { - return - } - if !known { - continue // drain unknown stream (e.g. alias not yet registered) - } - switch r.kind { - case "catalog": - s.onCatalog(r.participantID, obj.Payload) - case "video": - s.forwardMedia(r.participantID, "video", obj.Payload, obj.Properties, obj.GroupID, obj.ObjectID) - case "audio": - s.forwardMedia(r.participantID, "audio", obj.Payload, obj.Properties, obj.GroupID, obj.ObjectID) - } - } -} - -func (s *subscriber) readFetch(r route, known bool, st *session.IncomingFetchStream) { - for { - obj, err := st.ReadDecoded() - if err != nil { - return - } - if !known || obj.EndOfNonExistentRange || obj.EndOfUnknownRange { - continue - } - if r.kind == "catalog" { - s.onCatalog(r.participantID, obj.Payload) - } - } -} - -// forwardMedia decodes the LOC wrapper to recover the codec timestamp and -// forwards the encoded payload to the frontend, tagged with its absolute MoQ -// coordinates so the player can restore decode order (see MediaChunk). -// -// A video object is a keyframe iff it is object 0 of its group: the publisher -// opens a fresh group on every H.264 keyframe, so object 0 is always the IDR. -// Opus frames are independently decodable, so audio is always "key". -func (s *subscriber) forwardMedia(participantID, kind string, payload, props []byte, groupID, objectID uint64) { - keyframe := kind == "audio" || objectID == 0 - decoded, err := loc.Decode(props, payload) - if err != nil { - s.log.Debug("loc decode failed", "kind", kind, "err", err.Error()) - return - } - // Log the first frame of each track so the panel confirms media is flowing - // from the relay into the backend (distinct from frontend decode/render). - key := participantID + "/" + kind - s.mu.Lock() - if !s.mediaSeen[key] { - s.mediaSeen[key] = true - s.mu.Unlock() - s.log.Info("receiving media", "user", participantID, "kind", kind, "keyframe", keyframe) - } else { - s.mu.Unlock() - } - emitEvent(mediaChunkEvent, MediaChunk{ - ParticipantID: participantID, - Kind: kind, - Data: base64.StdEncoding.EncodeToString(decoded.Payload), - TimestampMicros: decoded.Properties.Timestamp, - Keyframe: keyframe, - GroupID: groupID, - ObjectID: objectID, - }) -} - -func (s *subscriber) addAliasRoute(alias uint64, r route) { - s.mu.Lock() - s.aliasRoutes[alias] = r - parked := s.pendingSubgroup[alias] - delete(s.pendingSubgroup, alias) - s.mu.Unlock() - // Replay any subgroup streams that were accepted before the route - // existed (see acceptLoop). - for _, st := range parked { - go s.readSubgroup(r, true, st) - } -} - -func (s *subscriber) addFetchRoute(reqID uint64, r route) { - s.mu.Lock() - s.fetchRoutes[reqID] = r - parked := s.pendingFetch[reqID] - delete(s.pendingFetch, reqID) - s.mu.Unlock() - // Replay a FETCH stream that arrived before the route was - // registered (the catalog-backfill race). - if parked != nil { - go s.readFetch(r, true, parked) - } -} - -// emitEvent dispatches a custom event to the frontend if the app is running. -func emitEvent(name string, data any) { - if app := application.Get(); app != nil { - app.Event.EmitEvent(&application.CustomEvent{Name: name, Data: data}) - } -} diff --git a/pkg/moqt/session/quicconn/relayqit_test.go b/pkg/moqt/session/quicconn/relayqit_test.go index ddf4b0cf..be688025 100644 --- a/pkg/moqt/session/quicconn/relayqit_test.go +++ b/pkg/moqt/session/quicconn/relayqit_test.go @@ -18,7 +18,7 @@ import ( ) // startRelayOverQUIC boots a real relay on a loopback QUIC listener and returns -// its dial address. enableDatagrams mirrors cmd/relay / tlmst when true; passing +// its dial address. enableDatagrams mirrors cmd/relay when true; passing // false exercises the case where the transport lacks DATAGRAM support, which must // NOT take down the relay's request/data handling. func startRelayOverQUIC(t *testing.T, enableDatagrams bool) (addr string, quicCfg *quic.Config) { @@ -62,15 +62,15 @@ func dialRelayClient(t *testing.T, addr string, quicCfg *quic.Config) *session.S return sess } -// publishPeer mirrors tlmst's publisher.start: PUBLISH catalog (+ emit the -// one-shot catalog object), PUBLISH video+audio, then announce the namespace -// LAST. Aliases are session-local so the fixed values are fine per peer. +// publishPeer reproduces a conferencing publisher's startup: PUBLISH catalog +// (+ emit the one-shot catalog object), PUBLISH video+audio, then announce the +// namespace LAST. Aliases are session-local so the fixed values are fine per peer. func publishPeer(t *testing.T, sess *session.Session, id string) wire.TrackNamespace { t.Helper() ns := wire.Namespace("room", id) - // Mirror tlmst exactly: pre-allocate the alias, PUBLISH with it, then emit - // the one-shot catalog object on that SAME alias. This is the pattern that + // Pre-allocate the alias, PUBLISH with it, then emit the one-shot catalog + // object on that SAME alias. This is the pattern that // broke when AllocOutboundTrackAlias handed out 0 (the catalog's first // alias) and Publish silently re-allocated it — sending the object under a // stale alias the relay never bound. @@ -126,8 +126,8 @@ func publishPeer(t *testing.T, sess *session.Session, id string) wire.TrackNames // awaitPeerAndFetchCatalog discovers selfID's view of the namespace, waits for // the announce whose suffix is wantID (skipping its own), then pairs SUBSCRIBE -// (FilterLargestObject) with a relative Joining FETCH against wantID's catalog — -// exactly tlmst's subscribeCatalog. Fails if the FETCH is rejected. +// (FilterLargestObject) with a relative Joining FETCH against wantID's catalog. +// Fails if the FETCH is rejected. func awaitPeerAndFetchCatalog(t *testing.T, sess *session.Session, selfID, wantID string) { t.Helper() nsStream, err := sess.SubscribeNamespace(t.Context(), &message.SubscribeNamespace{ @@ -199,9 +199,9 @@ func TestReproRelayCatalogOverQUIC(t *testing.T) { awaitPeerAndFetchCatalog(t, sub, "peerB", "peerA") } -// TestReproRelayMutualConference is tlmst's actual topology: two peers, each -// session BOTH publishes its own catalog/video/audio AND subscribes to the -// other's catalog via a joining FETCH. +// TestReproRelayMutualConference reproduces a mutual conferencing topology: two +// peers, each session BOTH publishes its own catalog/video/audio AND subscribes +// to the other's catalog via a joining FETCH. func TestReproRelayMutualConference(t *testing.T) { addr, quicCfg := startRelayOverQUIC(t, true)