kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Merge branch 'main' into global_app_bar
# Conflicts: # app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.ktpull/3297/head
commit
a643579bcb
|
@ -0,0 +1,139 @@
|
|||
name: Create Internal Release Tag
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_type:
|
||||
description: "Type of release (auto|patch|minor|major|hotfix)"
|
||||
required: true
|
||||
default: auto
|
||||
type: choice
|
||||
options: [auto, patch, minor, major, hotfix]
|
||||
hotfix_base_version:
|
||||
description: "Base version for hotfix (e.g. 2.5.0) required if release_type=hotfix"
|
||||
required: false
|
||||
dry_run:
|
||||
description: "If true, calculate but do not push tag"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-internal-tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine Latest Base Version
|
||||
id: latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# List base tags (exclude track/hotfix suffixes)
|
||||
BASE_TAGS=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n1 || true)
|
||||
echo "Found latest base tag: $BASE_TAGS"
|
||||
echo "latest_base_tag=$BASE_TAGS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compute Next Version (auto/patch/minor/major)
|
||||
id: compute
|
||||
if: ${{ inputs.release_type != 'hotfix' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RTYPE='${{ inputs.release_type }}'
|
||||
LAST='${{ steps.latest.outputs.latest_base_tag }}'
|
||||
if [ -z "$LAST" ]; then
|
||||
BASE_MAJOR=0; BASE_MINOR=0; BASE_PATCH=0
|
||||
else
|
||||
V=${LAST#v}
|
||||
IFS='.' read -r BASE_MAJOR BASE_MINOR BASE_PATCH <<< "$V"
|
||||
fi
|
||||
if [ "$RTYPE" = 'auto' ]; then
|
||||
echo "Determining bump type from commits since $LAST..."
|
||||
RANGE="$LAST..HEAD"
|
||||
[ -z "$LAST" ] && RANGE="HEAD" # first release
|
||||
LOG=$(git log --format=%s $RANGE || true)
|
||||
BUMP="patch"
|
||||
if echo "$LOG" | grep -Eiq 'BREAKING CHANGE'; then BUMP=major; fi
|
||||
if echo "$LOG" | grep -Eiq '^[a-zA-Z]+!:'; then BUMP=major; fi
|
||||
if [ "$BUMP" != major ] && echo "$LOG" | grep -Eiq '^feat(\(|:)' ; then BUMP=minor; fi
|
||||
RTYPE=$BUMP
|
||||
echo "Auto-detected bump: $RTYPE"
|
||||
fi
|
||||
case "$RTYPE" in
|
||||
major)
|
||||
NEW_MAJOR=$((BASE_MAJOR+1)); NEW_MINOR=0; NEW_PATCH=0;;
|
||||
minor)
|
||||
NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$((BASE_MINOR+1)); NEW_PATCH=0;;
|
||||
patch)
|
||||
NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$BASE_MINOR; NEW_PATCH=$((BASE_PATCH+1));;
|
||||
*) echo "Unsupported release_type for this step: $RTYPE"; exit 1;;
|
||||
esac
|
||||
NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
echo "base_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compute Hotfix Version
|
||||
id: hotfix
|
||||
if: ${{ inputs.release_type == 'hotfix' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE='${{ inputs.hotfix_base_version }}'
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "hotfix_base_version required for hotfix release_type" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! git tag --list | grep -q "^v$BASE$"; then
|
||||
echo "Base version tag v$BASE not found (production tag required)." >&2
|
||||
exit 1
|
||||
fi
|
||||
EXISTING=$(git tag --list "v${BASE}-hotfix*" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+-hotfix([0-9]+).*$/\1/' | sort -n | tail -1 || true)
|
||||
if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi
|
||||
HOTFIX_VERSION="${BASE}-hotfix${NEXT}"
|
||||
echo "hotfix_version=$HOTFIX_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Decide Internal Tag
|
||||
id: tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ '${{ inputs.release_type }}' = 'hotfix' ]; then
|
||||
BASE='${{ steps.hotfix.outputs.hotfix_version }}'
|
||||
else
|
||||
BASE='${{ steps.compute.outputs.base_version }}'
|
||||
fi
|
||||
INTERNAL_TAG="v${BASE}-internal.1"
|
||||
if git tag --list | grep -q "^${INTERNAL_TAG}$"; then
|
||||
echo "Tag ${INTERNAL_TAG} already exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "internal_tag=$INTERNAL_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Dry Run Preview
|
||||
if: ${{ inputs.dry_run == 'true' }}
|
||||
run: |
|
||||
echo "DRY RUN: Would create tag ${{ steps.tag.outputs.internal_tag }} pointing to $(git rev-parse HEAD)"
|
||||
git log -5 --oneline
|
||||
|
||||
- name: Create and Push Tag
|
||||
if: ${{ inputs.dry_run != 'true' }}
|
||||
run: |
|
||||
TAG='${{ steps.tag.outputs.internal_tag }}'
|
||||
MSG="Initial internal build for ${TAG}"
|
||||
git tag -a "$TAG" -m "$MSG"
|
||||
git push origin "$TAG"
|
||||
echo "Created and pushed $TAG"
|
||||
|
||||
- name: Output Summary
|
||||
run: |
|
||||
echo "### Internal Tag Created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tag: ${{ steps.tag.outputs.internal_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Release Type: ${{ inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||||
if [ '${{ inputs.release_type }}' = 'hotfix' ]; then
|
||||
echo "Base Hotfix Series: ${{ steps.hotfix.outputs.hotfix_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Base Version: ${{ steps.compute.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
name: Promote Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_stage:
|
||||
description: "Stage to promote to (auto|closed|open|production)"
|
||||
required: true
|
||||
default: auto
|
||||
type: choice
|
||||
options: [auto, closed, open, production]
|
||||
base_version:
|
||||
description: "Explicit base version (e.g. 2.5.0 or 2.5.0-hotfix1). If omitted, latest internal tag base is used."
|
||||
required: false
|
||||
allow_skip:
|
||||
description: "Allow skipping intermediate stages (e.g. internal->production)"
|
||||
required: false
|
||||
default: "false"
|
||||
dry_run:
|
||||
description: "If true, only compute next tag; don't push"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine Base Version
|
||||
id: base
|
||||
run: |
|
||||
set -euo pipefail
|
||||
INPUT_BASE='${{ inputs.base_version }}'
|
||||
if [ -n "$INPUT_BASE" ]; then
|
||||
# Validate an internal tag exists for provided base
|
||||
if ! git tag --list | grep -q "^v${INPUT_BASE}-internal\."; then
|
||||
echo "No internal tag found for base version v${INPUT_BASE}." >&2
|
||||
exit 1
|
||||
fi
|
||||
BASE_VERSION="$INPUT_BASE"
|
||||
else
|
||||
LATEST_INTERNAL_TAG=$(git tag --list 'v*-internal.*' --sort=-taggerdate | head -n1 || true)
|
||||
if [ -z "$LATEST_INTERNAL_TAG" ]; then
|
||||
echo "No internal tags found; nothing to promote." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Strip leading v and suffix -internal.N
|
||||
BASE_VERSION=$(echo "$LATEST_INTERNAL_TAG" | sed -E 's/^v(.*)-internal\.[0-9]+$/\1/')
|
||||
fi
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Gather Existing Stage Tags
|
||||
id: scan
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE='${{ steps.base.outputs.base_version }}'
|
||||
INTERNAL_TAGS=$(git tag --list "v${BASE}-internal.*" | sort -V || true)
|
||||
CLOSED_TAGS=$(git tag --list "v${BASE}-closed.*" | sort -V || true)
|
||||
OPEN_TAGS=$(git tag --list "v${BASE}-open.*" | sort -V || true)
|
||||
PROD_TAG=$(git tag --list "v${BASE}" || true)
|
||||
echo "internal_tags<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$INTERNAL_TAGS" >> $GITHUB_OUTPUT
|
||||
echo EOF >> $GITHUB_OUTPUT
|
||||
echo "closed_tags<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CLOSED_TAGS" >> $GITHUB_OUTPUT
|
||||
echo EOF >> $GITHUB_OUTPUT
|
||||
echo "open_tags<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$OPEN_TAGS" >> $GITHUB_OUTPUT
|
||||
echo EOF >> $GITHUB_OUTPUT
|
||||
if [ -n "$PROD_TAG" ]; then echo "production_present=true" >> $GITHUB_OUTPUT; else echo "production_present=false" >> $GITHUB_OUTPUT; fi
|
||||
if [ -z "$INTERNAL_TAGS" ]; then
|
||||
echo "No internal tags found for base version $BASE." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Determine Current Stage
|
||||
id: current
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PROD='${{ steps.scan.outputs.production_present }}'
|
||||
CLOSED='${{ steps.scan.outputs.closed_tags }}'
|
||||
OPEN='${{ steps.scan.outputs.open_tags }}'
|
||||
if [ "$PROD" = 'true' ]; then CUR=production
|
||||
elif [ -n "$OPEN" ]; then CUR=open
|
||||
elif [ -n "$CLOSED" ]; then CUR=closed
|
||||
else CUR=internal; fi
|
||||
echo "Current highest stage: $CUR"
|
||||
echo "current_stage=$CUR" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Decide Target Stage
|
||||
id: decide
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REQ='${{ inputs.target_stage }}'
|
||||
CUR='${{ steps.current.outputs.current_stage }}'
|
||||
ALLOW_SKIP='${{ inputs.allow_skip }}'
|
||||
order=(internal closed open production)
|
||||
# helper to get index
|
||||
idx() { local i=0; for s in "${order[@]}"; do [ "$s" = "$1" ] && echo $i && return; i=$((i+1)); done; echo -1; }
|
||||
if [ "$REQ" = auto ]; then
|
||||
CUR_IDX=$(idx "$CUR")
|
||||
TARGET_IDX=$((CUR_IDX+1))
|
||||
TARGET_STAGE=${order[$TARGET_IDX]:-}
|
||||
if [ -z "$TARGET_STAGE" ]; then
|
||||
echo "Already at production; nothing to promote." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
TARGET_STAGE=$REQ
|
||||
CUR_IDX=$(idx "$CUR")
|
||||
REQ_IDX=$(idx "$TARGET_STAGE")
|
||||
if [ $REQ_IDX -le $CUR_IDX ]; then
|
||||
echo "Requested stage $TARGET_STAGE is not ahead of current stage $CUR." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ALLOW_SKIP" != 'true' ] && [ $((CUR_IDX+1)) -ne $REQ_IDX ]; then
|
||||
echo "Skipping stages not allowed (current=$CUR, requested=$TARGET_STAGE). Enable allow_skip to override." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Target stage: $TARGET_STAGE"
|
||||
echo "target_stage=$TARGET_STAGE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compute New Tag
|
||||
id: tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE='${{ steps.base.outputs.base_version }}'
|
||||
TARGET='${{ steps.decide.outputs.target_stage }}'
|
||||
if [ "$TARGET" = production ]; then
|
||||
NEW_TAG="v${BASE}"
|
||||
if git tag --list | grep -q "^${NEW_TAG}$"; then
|
||||
echo "Production tag ${NEW_TAG} already exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
EXISTING=$(git tag --list "v${BASE}-${TARGET}.*" | sed -E "s/^v.*-${TARGET}\.([0-9]+)$/\1/" | sort -n | tail -1 || true)
|
||||
if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi
|
||||
NEW_TAG="v${BASE}-${TARGET}.${NEXT}"
|
||||
fi
|
||||
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Will create tag: $NEW_TAG"
|
||||
|
||||
- name: Resolve Commit to Tag (latest internal for base)
|
||||
id: commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE='${{ steps.base.outputs.base_version }}'
|
||||
LATEST_INTERNAL=$(git tag --list "v${BASE}-internal.*" --sort=-version:refname | head -n1)
|
||||
if [ -z "$LATEST_INTERNAL" ]; then
|
||||
echo "No internal tag found for base $BASE (unexpected)." >&2
|
||||
exit 1
|
||||
fi
|
||||
COMMIT=$(git rev-list -n1 "$LATEST_INTERNAL")
|
||||
echo "commit_sha=$COMMIT" >> $GITHUB_OUTPUT
|
||||
echo "Using commit $COMMIT from $LATEST_INTERNAL"
|
||||
|
||||
- name: Dry Run Summary
|
||||
if: ${{ inputs.dry_run == 'true' }}
|
||||
run: |
|
||||
echo "DRY RUN: Would tag commit ${{ steps.commit.outputs.commit_sha }} with ${{ steps.tag.outputs.new_tag }}"
|
||||
echo "Current stage: ${{ steps.current.outputs.current_stage }} -> Target: ${{ steps.decide.outputs.target_stage }}"
|
||||
git log -1 --oneline ${{ steps.commit.outputs.commit_sha }}
|
||||
|
||||
- name: Create & Push Tag
|
||||
if: ${{ inputs.dry_run != 'true' }}
|
||||
run: |
|
||||
TAG='${{ steps.tag.outputs.new_tag }}'
|
||||
COMMIT='${{ steps.commit.outputs.commit_sha }}'
|
||||
MSG="Promote ${TAG} from ${{ steps.current.outputs.current_stage }} to ${{ steps.decide.outputs.target_stage }}"
|
||||
git tag -a "$TAG" "$COMMIT" -m "$MSG"
|
||||
git push origin "$TAG"
|
||||
echo "Created and pushed $TAG"
|
||||
|
||||
- name: Promotion Summary
|
||||
run: |
|
||||
echo "### Promotion Tag Created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Base Version: ${{ steps.base.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Current Stage: ${{ steps.current.outputs.current_stage }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Target Stage: ${{ steps.decide.outputs.target_stage }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "New Tag: ${{ steps.tag.outputs.new_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
|
@ -2,6 +2,16 @@ name: Make Release
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: "If true, simulate the release without building, uploading, promoting, or creating a GitHub release"
|
||||
required: false
|
||||
default: "false"
|
||||
type: choice
|
||||
options: ["false", "true"]
|
||||
pr_number:
|
||||
description: "Optional PR number to comment on with dry-run readiness summary"
|
||||
required: false
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
@ -21,7 +31,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
FINAL_VERSION_CODE: ${{ steps.final_version_code.outputs.FINAL_VERSION_CODE }}
|
||||
HOTFIX_PATCH: ${{ steps.is_hotfix_patch.outputs.hotfix_patch }}
|
||||
BASE_TAG: ${{ steps.get_base_tag.outputs.BASE_TAG }}
|
||||
FULL_TAG: ${{ steps.get_full_tag.outputs.FULL_TAG }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
@ -45,6 +58,17 @@ jobs:
|
|||
id: get_version_name
|
||||
run: echo "APP_VERSION_NAME=$(echo ${GITHUB_REF_NAME#v} | sed 's/-.*//')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Full Tag
|
||||
id: get_full_tag
|
||||
run: echo "FULL_TAG=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Base Tag (for release/artifact naming)
|
||||
id: get_base_tag
|
||||
run: |
|
||||
# Remove track/iteration suffix (e.g., -internal.1, -closed.1, -open.1)
|
||||
BASE_TAG=$(echo ${GITHUB_REF_NAME} | sed 's/\(-internal\.[0-9]\+\|-closed\.[0-9]\+\|-open\.[0-9]\+\)$//')
|
||||
echo "BASE_TAG=$BASE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract VERSION_CODE_OFFSET from config.properties
|
||||
id: get_version_code_offset
|
||||
run: |
|
||||
|
@ -61,9 +85,89 @@ jobs:
|
|||
shell: bash
|
||||
# This matches the reproducible versionCode strategy: versionCode = git commit count + offset
|
||||
|
||||
release-google:
|
||||
- name: Check if Hotfix or Patch
|
||||
id: is_hotfix_patch
|
||||
run: |
|
||||
TAG_LOWER=$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$TAG_LOWER" == *"-hotfix"* || "$TAG_LOWER" == *"-patch"* ]]; then
|
||||
echo "hotfix_patch=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "hotfix_patch=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download Version Code Artifact (if exists)
|
||||
id: try_download_version_code
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: version-code
|
||||
path: .
|
||||
|
||||
- name: Generate and Store Version Code (first build for regular release)
|
||||
if: steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.try_download_version_code.outcome != 'success'
|
||||
id: generate_and_store_version_code
|
||||
run: |
|
||||
VERSION_CODE=${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
echo "$VERSION_CODE" > version_code.txt
|
||||
|
||||
- name: Upload Version Code Artifact (if generated)
|
||||
if: |
|
||||
(steps.is_hotfix_patch.outputs.hotfix_patch == 'true') ||
|
||||
(steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.generate_and_store_version_code.conclusion == 'success')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: version-code
|
||||
path: version_code.txt
|
||||
|
||||
- name: Set Version Code from Artifact (if exists)
|
||||
if: steps.try_download_version_code.outcome == 'success'
|
||||
id: set_version_code_from_artifact
|
||||
run: |
|
||||
VERSION_CODE=$(cat version_code.txt)
|
||||
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set Final Version Code Output
|
||||
id: final_version_code
|
||||
run: |
|
||||
if [ -f version_code.txt ]; then
|
||||
FV=$(cat version_code.txt)
|
||||
else
|
||||
FV=${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
fi
|
||||
echo "FINAL_VERSION_CODE=$FV" >> $GITHUB_OUTPUT
|
||||
|
||||
check-internal-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare-build-info
|
||||
outputs:
|
||||
exists: ${{ steps.check_release.outputs.exists }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for existing GitHub release
|
||||
id: check_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BASE_TAG=${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
EXISTING_RELEASE=$(gh release list --limit 100 --json tagName,targetCommitish | jq -r --arg BASE_TAG "$BASE_TAG" --arg COMMIT_SHA "$COMMIT_SHA" '.[] | select(.tagName == $BASE_TAG and .targetCommitish.oid == $COMMIT_SHA)')
|
||||
if [ -n "$EXISTING_RELEASE" ]; then
|
||||
echo "An existing release with tag '${BASE_TAG}' was found for this commit."
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No existing release found for this commit."
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
release-google:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, check-internal-release]
|
||||
outputs:
|
||||
INTERNAL_VERSION_CODE: ${{ steps.resolve_internal_version_code.outputs.INTERNAL_VERSION_CODE }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
@ -76,7 +180,6 @@ jobs:
|
|||
java-version: '21'
|
||||
distribution: 'jetbrains'
|
||||
- name: Setup Gradle
|
||||
if: contains(github.ref_name, '-internal')
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
@ -110,121 +213,258 @@ jobs:
|
|||
ruby-version: '3.2'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Determine Fastlane Lane
|
||||
id: fastlane_lane
|
||||
- name: Dry Run Sanity Version Code Check
|
||||
if: github.event.inputs.dry_run == 'true'
|
||||
id: dry_run_sanity
|
||||
run: |
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
if [[ "$TAG_NAME" == *"-internal"* ]]; then
|
||||
echo "lane=internal" >> $GITHUB_OUTPUT
|
||||
elif [[ "$TAG_NAME" == *"-closed"* ]]; then
|
||||
echo "lane=closed" >> $GITHUB_OUTPUT
|
||||
elif [[ "$TAG_NAME" == *"-open"* ]]; then
|
||||
echo "lane=open" >> $GITHUB_OUTPUT
|
||||
set -euo pipefail
|
||||
echo "Performing version code sanity check (dry run)..."
|
||||
# Query highest existing version code across tracks
|
||||
bundle exec fastlane get_highest_version_code || true
|
||||
HIGHEST=$(cat highest_version_code.txt 2>/dev/null || echo 0)
|
||||
HOTFIX='${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}'
|
||||
if [ "$HOTFIX" = "true" ]; then
|
||||
PLANNED=$((HIGHEST + 1))
|
||||
echo "Hotfix planned versionCode: $PLANNED (highest existing: $HIGHEST)";
|
||||
STATUS=ok
|
||||
else
|
||||
echo "lane=production" >> $GITHUB_OUTPUT
|
||||
# Regular base release planned version code (commit count + offset or reused artifact)
|
||||
PLANNED='${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }}'
|
||||
if [ -z "$PLANNED" ]; then PLANNED=0; fi
|
||||
if [ "$PLANNED" -le "$HIGHEST" ]; then
|
||||
echo "ERROR: Planned versionCode $PLANNED is not greater than highest existing $HIGHEST. Adjust VERSION_CODE_OFFSET or convert to hotfix." >&2
|
||||
STATUS=fail
|
||||
else
|
||||
echo "Planned versionCode $PLANNED is greater than existing $HIGHEST: OK";
|
||||
STATUS=ok
|
||||
fi
|
||||
fi
|
||||
echo "sanity_status=$STATUS" >> $GITHUB_OUTPUT
|
||||
echo "sanity_highest=$HIGHEST" >> $GITHUB_OUTPUT
|
||||
echo "sanity_planned=$PLANNED" >> $GITHUB_OUTPUT
|
||||
# Promotion policy validation (if this is a promotion tag in dry run)
|
||||
TAG='${{ github.ref_name }}'
|
||||
if echo "$TAG" | grep -Eq '-(closed|open)$' || [[ "$TAG" != *"-internal"* && "$TAG" != *"-closed"* && "$TAG" != *"-open"* && "$TAG" == v* ]]; then
|
||||
echo "Checking promotion policy (dry run)..."
|
||||
if ! bundle exec fastlane get_internal_track_version_code; then
|
||||
echo "ERROR: Promotion attempted but no internal artifact present." >&2
|
||||
echo "promotion_status=fail" >> $GITHUB_OUTPUT
|
||||
[ "$STATUS" = ok ] || true
|
||||
STATUS=fail
|
||||
else
|
||||
echo "Internal artifact present for promotion.";
|
||||
echo "promotion_status=ok" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "Not a promotion tag (internal build).";
|
||||
echo "promotion_status=na" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ "$STATUS" = fail ]; then
|
||||
echo "Dry run sanity check failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and Deploy Google Play Tracks with Fastlane
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }}
|
||||
|
||||
- name: Upload Google AAB artifact
|
||||
if: contains(github.ref_name, '-internal')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: google-aab
|
||||
path: app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Google APK artifact
|
||||
if: contains(github.ref_name, '-internal')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: google-apk
|
||||
path: app/build/outputs/apk/google/release/app-google-release.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Attest Google artifacts provenance
|
||||
if: contains(github.ref_name, '-internal')
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-path: |
|
||||
app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
app/build/outputs/apk/google/release/app-google-release.apk
|
||||
|
||||
release-fdroid:
|
||||
if: contains(github.ref_name, '-internal')
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare-build-info
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'jetbrains'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
|
||||
- name: Load secrets
|
||||
env:
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
- name: Resolve Version Code For Internal Build
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
id: resolve_internal_version_code
|
||||
run: |
|
||||
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
if [ "${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" = "true" ]; then
|
||||
echo "Hotfix/Patch detected; querying Google Play for highest version code..."
|
||||
bundle exec fastlane get_highest_version_code
|
||||
CODE=$(cat highest_version_code.txt || echo 0)
|
||||
NEXT_CODE=$((CODE + 1))
|
||||
# Race mitigation: re-query to ensure no concurrent allocation
|
||||
bundle exec fastlane get_highest_version_code
|
||||
NEW_HIGHEST=$(cat highest_version_code.txt || echo 0)
|
||||
if [ "$NEW_HIGHEST" -ge "$NEXT_CODE" ]; then
|
||||
echo "Detected race: highest changed from $CODE to $NEW_HIGHEST; bumping again.";
|
||||
NEXT_CODE=$((NEW_HIGHEST + 1))
|
||||
fi
|
||||
echo "Using hotfix version code: $NEXT_CODE (previous highest final: $NEW_HIGHEST)"
|
||||
echo "INTERNAL_VERSION_CODE=$NEXT_CODE" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_CODE=$NEXT_CODE" >> $GITHUB_ENV
|
||||
else
|
||||
BASE_CODE=${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }}
|
||||
echo "Using base/internal version code: $BASE_CODE"
|
||||
echo "INTERNAL_VERSION_CODE=$BASE_CODE" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_CODE=$BASE_CODE" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build F-Droid with Fastlane
|
||||
- name: Build and Deploy to Internal Track
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
VERSION_CODE: ${{ env.VERSION_CODE }}
|
||||
run: bundle exec fastlane internal
|
||||
|
||||
- name: Build F-Droid (same version code)
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ env.VERSION_CODE }}
|
||||
run: bundle exec fastlane fdroid_build
|
||||
|
||||
- name: Upload F-Droid APK artifact
|
||||
- name: Generate Build Metadata & Checksums
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
id: gen_metadata
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AAB=app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
APK_GOOGLE=app/build/outputs/apk/google/release/app-google-release.apk
|
||||
APK_FDROID=app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
SHA_AAB=$(sha256sum "$AAB" | cut -d' ' -f1)
|
||||
SHA_APK_GOOGLE=$(sha256sum "$APK_GOOGLE" | cut -d' ' -f1)
|
||||
SHA_APK_FDROID=$(sha256sum "$APK_FDROID" | cut -d' ' -f1)
|
||||
GIT_SHA=$(git rev-parse HEAD)
|
||||
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
cat > build-metadata.json <<EOF
|
||||
{
|
||||
"baseTag": "${{ needs.prepare-build-info.outputs.BASE_TAG }}",
|
||||
"fullTag": "${{ needs.prepare-build-info.outputs.FULL_TAG }}",
|
||||
"versionName": "${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}",
|
||||
"versionCode": "${{ env.VERSION_CODE }}",
|
||||
"hotfixOrPatch": "${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}",
|
||||
"gitSha": "$GIT_SHA",
|
||||
"buildTimeUtc": "$BUILD_TIME",
|
||||
"artifacts": {
|
||||
"googleAab": { "path": "$AAB", "sha256": "$SHA_AAB" },
|
||||
"googleApk": { "path": "$APK_GOOGLE", "sha256": "$SHA_APK_GOOGLE" },
|
||||
"fdroidApk": { "path": "$APK_FDROID", "sha256": "$SHA_APK_FDROID" }
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Generated build-metadata.json:"; cat build-metadata.json
|
||||
echo "AAB_SHA256=$SHA_AAB" >> $GITHUB_ENV
|
||||
echo "APK_GOOGLE_SHA256=$SHA_APK_GOOGLE" >> $GITHUB_ENV
|
||||
echo "APK_FDROID_SHA256=$SHA_APK_FDROID" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Build Metadata Artifact
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fdroid-apk
|
||||
path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
retention-days: 1
|
||||
name: build-metadata
|
||||
path: build-metadata.json
|
||||
retention-days: 7
|
||||
|
||||
- name: Attest F-Droid APK provenance
|
||||
uses: actions/attest-build-provenance@v3
|
||||
- name: Upload F-Droid APK artifact
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
subject-path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
name: fdroid-apk
|
||||
path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
retention-days: 1
|
||||
|
||||
create-internal-release:
|
||||
- name: Promotion Guard - Internal Must Exist
|
||||
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
set -e
|
||||
echo "Validating internal track has an artifact before promotion..."
|
||||
if ! bundle exec fastlane get_internal_track_version_code; then
|
||||
echo "ERROR: No internal artifact found to promote. Ensure an internal tag was built first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Fetch Internal Track Version Code (for promotion)
|
||||
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
bundle exec fastlane get_internal_track_version_code
|
||||
CODE=$(cat internal_version_code.txt)
|
||||
echo "INTERNAL_VERSION_CODE=$CODE" >> $GITHUB_ENV
|
||||
|
||||
- name: Promote on Google Play
|
||||
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ env.INTERNAL_VERSION_CODE }}
|
||||
run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }}
|
||||
|
||||
- name: Build Summary (Internal Build)
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
{
|
||||
echo "### Internal Build Summary"
|
||||
echo "Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}"
|
||||
echo "Version Code: ${{ env.VERSION_CODE }}"
|
||||
echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}"
|
||||
echo "Full Tag: ${{ needs.prepare-build-info.outputs.FULL_TAG }}"
|
||||
echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}"
|
||||
echo "Google AAB SHA256: $AAB_SHA256"
|
||||
echo "Google APK SHA256: $APK_GOOGLE_SHA256"
|
||||
echo "F-Droid APK SHA256: $APK_FDROID_SHA256"
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Dry Run Summary
|
||||
if: github.event.inputs.dry_run == 'true'
|
||||
run: |
|
||||
echo "### Release Dry Run" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tag: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Computed Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Planned Version Code Strategy: $([[ '${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}' == 'true' ]] && echo 'highest+1 from Play' || echo 'commit-count+offset (first internal) or reuse')" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Sanity Highest Existing VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_highest }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Sanity Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Promotion Policy Check: ${{ steps.dry_run_sanity.outputs.promotion_status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Would build internal artifacts: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo 'no (already exists)')" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Would promote lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a')" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Would create or update draft GitHub release: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo no)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Post Dry Run PR Comment
|
||||
if: github.event.inputs.dry_run == 'true' && github.event.inputs.pr_number != ''
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BODY=$(cat <<'EOT'
|
||||
Release Dry Run Summary
|
||||
----------------------
|
||||
Tag: ${{ github.ref_name }}
|
||||
Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}
|
||||
Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }} (highest existing: ${{ steps.dry_run_sanity.outputs.sanity_highest }})
|
||||
Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }}
|
||||
Promotion Policy: ${{ steps.dry_run_sanity.outputs.promotion_status }}
|
||||
Would Promote Lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a')
|
||||
Draft Release Action: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo 'would create/update' || echo 'none')
|
||||
EOT
|
||||
)
|
||||
gh pr comment ${{ github.event.inputs.pr_number }} --body "$BODY" || echo "Failed to post PR comment (verify pr_number)"
|
||||
|
||||
manage-github-release:
|
||||
if: github.event.inputs.dry_run != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, release-google, release-fdroid]
|
||||
if: contains(github.ref_name, '-internal')
|
||||
needs: [prepare-build-info, check-internal-release, release-google]
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Compute Release Name (Channel Aware)
|
||||
id: release_name
|
||||
run: |
|
||||
BASE='${{ needs.prepare-build-info.outputs.BASE_TAG }}'
|
||||
REF='${{ github.ref_name }}'
|
||||
if [[ "$REF" == *"-internal"* ]]; then
|
||||
NAME="$BASE (internal)"
|
||||
elif [[ "$REF" == *"-closed"* ]]; then
|
||||
NAME="$BASE (closed testing)"
|
||||
elif [[ "$REF" == *"-open"* ]]; then
|
||||
NAME="$BASE (open beta)"
|
||||
else
|
||||
NAME="$BASE"
|
||||
fi
|
||||
echo "Computed release name: $NAME"
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
name: ${{ github.ref_name }}
|
||||
tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
name: ${{ steps.release_name.outputs.name }}
|
||||
generate_release_notes: true
|
||||
files: ./artifacts/*/*
|
||||
draft: true
|
||||
|
@ -232,12 +472,8 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
promote-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, release-google]
|
||||
if: "!contains(github.ref_name, '-internal')"
|
||||
steps:
|
||||
- name: Determine Release Properties
|
||||
- name: Determine Release Properties for Promotion
|
||||
if: "!contains(github.ref_name, '-internal')"
|
||||
id: release_properties
|
||||
run: |
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
|
@ -252,12 +488,24 @@ jobs:
|
|||
echo "prerelease=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Update GitHub Release
|
||||
- name: Promote GitHub Release
|
||||
if: "!contains(github.ref_name, '-internal')"
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
name: ${{ github.ref_name }}
|
||||
tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
name: ${{ steps.release_name.outputs.name }}
|
||||
draft: ${{ steps.release_properties.outputs.draft }}
|
||||
prerelease: ${{ steps.release_properties.outputs.prerelease }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Append Metadata to Release Notes
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
run: |
|
||||
if [ -f artifacts/build-metadata/build-metadata.json ]; then
|
||||
echo "\n---\nBuild Metadata JSON:\n" > appended_notes.txt
|
||||
cat artifacts/build-metadata/build-metadata.json >> appended_notes.txt
|
||||
gh release edit ${{ needs.prepare-build-info.outputs.BASE_TAG }} --notes-file appended_notes.txt
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -39,6 +39,31 @@ git tag v2.3.5-closed.1
|
|||
git push origin v2.3.5-closed.1
|
||||
```
|
||||
|
||||
## Hotfixes & Patch Releases
|
||||
|
||||
If you need to release a hotfix or patch for a previous version (not the latest mainline), follow this process:
|
||||
|
||||
- **Tagging:** Use a tag with a suffix, such as `vX.X.X-hotfix1` or `vX.X.X-patch1` (e.g., `v2.3.5-hotfix1`).
|
||||
- **Uniqueness:** The release workflow uses the full tag (including suffix) for all artifact and release naming, so each hotfix/patch is uniquely identified.
|
||||
- **Version Code:** The workflow automatically ensures the version code for a hotfix/patch is strictly greater than any previous release, even if the hotfix is created from an older commit. This prevents Play Store upload errors due to version code regressions.
|
||||
- **Multiple Releases:** You can have multiple releases for the same base version (e.g., `v2.3.5`, `v2.3.5-hotfix1`, `v2.3.5-patch2`). Each will be published and promoted independently.
|
||||
|
||||
### Hotfix Tagging Example
|
||||
```bash
|
||||
# On a release branch or after checking out the commit to hotfix
|
||||
# Tag and push a hotfix release
|
||||
git tag v2.3.5-hotfix1
|
||||
git push origin v2.3.5-hotfix1
|
||||
# For additional hotfixes/patches:
|
||||
git tag v2.3.5-hotfix2
|
||||
git push origin v2.3.5-hotfix2
|
||||
```
|
||||
|
||||
### Policy
|
||||
- Always use a unique tag for each hotfix/patch.
|
||||
- The version code will always increase, regardless of commit history.
|
||||
- The full tag is used for all release and artifact naming.
|
||||
|
||||
## Manual Checklist
|
||||
- [ ] Verify build in Google Play Console
|
||||
- [ ] Review and publish GitHub draft release (for production)
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
|
||||
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
|
||||
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
|
||||
<ID>LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested</ID>
|
||||
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
|
||||
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
|
||||
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
|
||||
|
@ -183,6 +184,7 @@
|
|||
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen</ID>
|
||||
<ID>ModifierMissing:SettingsScreen.kt$SettingsScreen</ID>
|
||||
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
|
||||
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
|
||||
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
|
||||
<ID>ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon</ID>
|
||||
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
|
||||
|
|
|
@ -42,7 +42,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.MainScreen
|
||||
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
|
||||
import com.geeksville.mesh.ui.sharing.toSharedContact
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
|
@ -118,9 +117,8 @@ class MainActivity : AppCompatActivity() {
|
|||
Timber.d("App link data is a channel set")
|
||||
model.requestChannelUrl(it)
|
||||
} else if (it.path?.startsWith("/v/") == true || it.path?.startsWith("/V/") == true) {
|
||||
val sharedContact = it.toSharedContact()
|
||||
Timber.d("App link data is a shared contact: ${sharedContact.user.longName}")
|
||||
model.setSharedContactRequested(sharedContact)
|
||||
Timber.d("App link data is a shared contact")
|
||||
model.setSharedContactRequested(it)
|
||||
} else {
|
||||
Timber.d("App link data is not a channel set")
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import com.geeksville.mesh.copy
|
|||
import com.geeksville.mesh.repository.radio.MeshActivity
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.service.MeshServiceNotifications
|
||||
import com.geeksville.mesh.ui.sharing.toSharedContact
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -297,8 +298,17 @@ constructor(
|
|||
val sharedContactRequested: StateFlow<AdminProtos.SharedContact?>
|
||||
get() = _sharedContactRequested.asStateFlow()
|
||||
|
||||
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
|
||||
_sharedContactRequested.value = sharedContact
|
||||
fun setSharedContactRequested(url: Uri) {
|
||||
runCatching { _sharedContactRequested.value = url.toSharedContact() }
|
||||
.onFailure { ex ->
|
||||
Timber.e(ex, "Shared contact error")
|
||||
showSnackBar(R.string.contact_invalid)
|
||||
}
|
||||
}
|
||||
|
||||
/** Called immediately after activity observes requestChannelUrl */
|
||||
fun clearSharedContactRequested() {
|
||||
_sharedContactRequested.value = null
|
||||
}
|
||||
|
||||
// Connection state to our radio device
|
||||
|
@ -347,23 +357,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setChannel(channel: ChannelProtos.Channel) {
|
||||
try {
|
||||
meshService?.setChannel(channel.toByteArray())
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Set channel error")
|
||||
}
|
||||
}
|
||||
|
||||
/** Set the radio config (also updates our saved copy in preferences). */
|
||||
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
|
||||
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)
|
||||
radioConfigRepository.replaceAllSettings(channelSet.settingsList)
|
||||
|
||||
val newConfig = config { lora = channelSet.loraConfig }
|
||||
if (config.lora != newConfig.lora) setConfig(newConfig)
|
||||
}
|
||||
|
||||
fun addQuickChatAction(action: QuickChatAction) =
|
||||
viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) }
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
|
|||
import com.geeksville.mesh.ui.connections.DeviceType
|
||||
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
|
||||
import com.geeksville.mesh.ui.metrics.annotateTraceroute
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
@ -136,6 +137,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
val navController = rememberNavController()
|
||||
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val sharedContactRequested by uIViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
@ -147,7 +149,13 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
}
|
||||
|
||||
if (connectionState == ConnectionState.CONNECTED) {
|
||||
requestChannelSet?.let { newChannelSet -> ScannedQrCodeDialog(uIViewModel, newChannelSet) }
|
||||
sharedContactRequested?.let {
|
||||
SharedContactDialog(sharedContact = it, onDismiss = { uIViewModel.clearSharedContactRequested() })
|
||||
}
|
||||
|
||||
requestChannelSet?.let { newChannelSet ->
|
||||
ScannedQrCodeDialog(newChannelSet, onDismiss = { uIViewModel.clearRequestChannelUrl() })
|
||||
}
|
||||
}
|
||||
|
||||
analytics.addNavigationTrackingEffect(navController = navController)
|
||||
|
|
|
@ -49,24 +49,28 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset
|
||||
import com.geeksville.mesh.channelSet
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
fun ScannedQrCodeDialog(viewModel: UIViewModel, incoming: ChannelSet) {
|
||||
fun ScannedQrCodeDialog(
|
||||
incoming: ChannelSet,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: ScannedQrCodeViewModel = hiltViewModel(),
|
||||
) {
|
||||
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
||||
|
||||
ScannedQrCodeDialog(
|
||||
channels = channels,
|
||||
incoming = incoming,
|
||||
onDismiss = viewModel::clearRequestChannelUrl,
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = viewModel::setChannels,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.common.components
|
||||
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ConfigProtos.Config
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.channelSet
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.model.getChannelList
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ScannedQrCodeViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val channels =
|
||||
radioConfigRepository.channelSetFlow.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5_000L),
|
||||
channelSet {},
|
||||
)
|
||||
|
||||
private val localConfig =
|
||||
radioConfigRepository.localConfigFlow.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5_000L),
|
||||
LocalConfig.getDefaultInstance(),
|
||||
)
|
||||
|
||||
/** Set the radio config (also updates our saved copy in preferences). */
|
||||
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
|
||||
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)
|
||||
radioConfigRepository.replaceAllSettings(channelSet.settingsList)
|
||||
|
||||
val newConfig = config { lora = channelSet.loraConfig }
|
||||
if (localConfig.value.lora != newConfig.lora) setConfig(newConfig)
|
||||
}
|
||||
|
||||
private fun setChannel(channel: ChannelProtos.Channel) {
|
||||
try {
|
||||
serviceRepository.meshService?.setChannel(channel.toByteArray())
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Set channel error")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the radio config (also updates our saved copy in preferences)
|
||||
private fun setConfig(config: Config) {
|
||||
try {
|
||||
serviceRepository.meshService?.setConfig(config.toByteArray())
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Set config error")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,23 +25,18 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -49,14 +44,12 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
|
@ -118,7 +111,8 @@ fun ConnectionsScreen(
|
|||
val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
|
||||
val bluetoothRssi by connectionsViewModel.bluetoothRssi.collectAsStateWithLifecycle()
|
||||
|
||||
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
|
||||
val bondedBleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
|
||||
val scannedBleDevices by scanModel.scanResult.observeAsState(emptyMap())
|
||||
val discoveredTcpDevices by scanModel.discoveredTcpDevicesForUi.collectAsStateWithLifecycle()
|
||||
val recentTcpDevices by scanModel.recentTcpDevicesForUi.collectAsStateWithLifecycle()
|
||||
val usbDevices by scanModel.usbDevicesForUi.collectAsStateWithLifecycle()
|
||||
|
@ -152,15 +146,6 @@ fun ConnectionsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// State for the device scan dialog
|
||||
var showScanDialog by remember { mutableStateOf(false) }
|
||||
val scanResults by scanModel.scanResult.observeAsState(emptyMap())
|
||||
|
||||
// Observe scan results to show the dialog
|
||||
if (scanResults.isNotEmpty()) {
|
||||
showScanDialog = true
|
||||
}
|
||||
|
||||
LaunchedEffect(connectionState, regionUnset) {
|
||||
when (connectionState) {
|
||||
ConnectionState.CONNECTED -> {
|
||||
|
@ -245,7 +230,11 @@ fun ConnectionsScreen(
|
|||
DeviceType.BLE -> {
|
||||
BLEDevices(
|
||||
connectionState = connectionState,
|
||||
btDevices = bleDevices,
|
||||
bondedDevices = bondedBleDevices,
|
||||
availableDevices =
|
||||
scannedBleDevices.values.toList().filterNot { available ->
|
||||
bondedBleDevices.any { it.address == available.address }
|
||||
},
|
||||
selectedDevice = selectedDevice,
|
||||
scanModel = scanModel,
|
||||
bluetoothEnabled = bluetoothState.enabled,
|
||||
|
@ -280,7 +269,7 @@ fun ConnectionsScreen(
|
|||
val showWarningNotPaired =
|
||||
!connectionState.isConnected() &&
|
||||
!hasShownNotPairedWarning &&
|
||||
bleDevices.none { it is DeviceListEntry.Ble && it.bonded }
|
||||
bondedBleDevices.none { it is DeviceListEntry.Ble && it.bonded }
|
||||
if (showWarningNotPaired) {
|
||||
Text(
|
||||
text = stringResource(R.string.warning_not_paired),
|
||||
|
@ -294,55 +283,6 @@ fun ConnectionsScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Device Scan Dialog
|
||||
if (showScanDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
showScanDialog = false
|
||||
scanModel.clearScanResults()
|
||||
},
|
||||
) {
|
||||
Surface(shape = MaterialTheme.shapes.medium) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Select a Bluetooth device",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
)
|
||||
Column(modifier = Modifier.selectableGroup()) {
|
||||
scanResults.values.forEach { device ->
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = false, // No pre-selection in this dialog
|
||||
onClick = {
|
||||
scanModel.onSelected(device)
|
||||
scanModel.clearScanResults()
|
||||
showScanDialog = false
|
||||
},
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = device.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
scanModel.clearScanResults()
|
||||
showScanDialog = false
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
|
||||
|
|
|
@ -69,7 +69,8 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
@Composable
|
||||
fun BLEDevices(
|
||||
connectionState: ConnectionState,
|
||||
btDevices: List<DeviceListEntry>,
|
||||
bondedDevices: List<DeviceListEntry>,
|
||||
availableDevices: List<DeviceListEntry>,
|
||||
selectedDevice: String,
|
||||
scanModel: BTScanModel,
|
||||
bluetoothEnabled: Boolean,
|
||||
|
@ -153,7 +154,7 @@ fun BLEDevices(
|
|||
}
|
||||
}
|
||||
|
||||
if (btDevices.isEmpty()) {
|
||||
if (bondedDevices.isEmpty() && availableDevices.isEmpty()) {
|
||||
EmptyStateContent(
|
||||
imageVector = Icons.Rounded.BluetoothDisabled,
|
||||
text =
|
||||
|
@ -165,18 +166,19 @@ fun BLEDevices(
|
|||
actionButton = scanButton,
|
||||
)
|
||||
} else {
|
||||
TitledCard(title = stringResource(R.string.bluetooth_paired_devices)) {
|
||||
btDevices.forEach { device ->
|
||||
val connected =
|
||||
connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice
|
||||
DeviceListItem(
|
||||
connected = connected,
|
||||
device = device,
|
||||
onSelect = { scanModel.onSelected(device) },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
bondedDevices.Section(
|
||||
title = stringResource(R.string.bluetooth_paired_devices),
|
||||
connectionState = connectionState,
|
||||
selectedDevice = selectedDevice,
|
||||
onSelect = scanModel::onSelected,
|
||||
)
|
||||
|
||||
availableDevices.Section(
|
||||
title = stringResource(R.string.bluetooth_available_devices),
|
||||
connectionState = connectionState,
|
||||
selectedDevice = selectedDevice,
|
||||
onSelect = scanModel::onSelected,
|
||||
)
|
||||
|
||||
scanButton()
|
||||
}
|
||||
|
@ -213,3 +215,25 @@ private fun checkPermissionsAndScan(
|
|||
permissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun List<DeviceListEntry>.Section(
|
||||
title: String,
|
||||
connectionState: ConnectionState,
|
||||
selectedDevice: String,
|
||||
onSelect: (DeviceListEntry) -> Unit,
|
||||
) {
|
||||
if (isNotEmpty()) {
|
||||
TitledCard(title = title) {
|
||||
forEach { device ->
|
||||
val connected = connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice
|
||||
DeviceListItem(
|
||||
connected = connected,
|
||||
device = device,
|
||||
onSelect = { onSelect(device) },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,17 +109,15 @@ fun NodeListScreen(viewModel: NodeListViewModel = hiltViewModel(), navigateToNod
|
|||
floatingActionButton = {
|
||||
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
|
||||
val shareCapable = firmwareVersion.supportsQrCodeSharing()
|
||||
val scannedContact: AdminProtos.SharedContact? by
|
||||
val sharedContact: AdminProtos.SharedContact? by
|
||||
viewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
|
||||
AddContactFAB(
|
||||
unfilteredNodes = unfilteredNodes,
|
||||
scannedContact = scannedContact,
|
||||
sharedContact = sharedContact,
|
||||
modifier =
|
||||
Modifier.animateFloatingActionButton(
|
||||
visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onSharedContactImport = { contact -> viewModel.addSharedContact(contact) },
|
||||
onSharedContactRequested = { contact -> viewModel.setSharedContactRequested(contact) },
|
||||
)
|
||||
},
|
||||
|
|
|
@ -97,6 +97,8 @@ import com.geeksville.mesh.channelSet
|
|||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.navigation.ConfigRoute
|
||||
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
|
||||
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
|
||||
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
|
||||
|
@ -147,6 +149,8 @@ fun ChannelScreen(
|
|||
|
||||
var shouldAddChannelsState by remember { mutableStateOf(true) }
|
||||
|
||||
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
|
||||
/* Animate waiting for the configurations */
|
||||
var isWaiting by remember { mutableStateOf(false) }
|
||||
if (isWaiting) {
|
||||
|
@ -272,6 +276,8 @@ fun ChannelScreen(
|
|||
)
|
||||
}
|
||||
|
||||
requestChannelSet?.let { ScannedQrCodeDialog(it, onDismiss = { viewModel.clearRequestChannelUrl() }) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
|
|
|
@ -91,6 +91,10 @@ constructor(
|
|||
onError()
|
||||
}
|
||||
|
||||
fun clearRequestChannelUrl() {
|
||||
_requestChannelSet.value = null
|
||||
}
|
||||
|
||||
/** Set the radio config (also updates our saved copy in preferences). */
|
||||
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
|
||||
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)
|
||||
|
|
|
@ -30,9 +30,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.QrCodeScanner
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
@ -72,17 +70,14 @@ import java.net.MalformedURLException
|
|||
* requests using Accompanist Permissions.
|
||||
*
|
||||
* @param modifier Modifier for this composable.
|
||||
* @param onSharedContactImport Callback invoked when a shared contact is successfully imported.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun AddContactFAB(
|
||||
unfilteredNodes: List<Node>,
|
||||
scannedContact: AdminProtos.SharedContact?,
|
||||
sharedContact: AdminProtos.SharedContact?,
|
||||
modifier: Modifier = Modifier,
|
||||
onSharedContactImport: (AdminProtos.SharedContact) -> Unit = {},
|
||||
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit = {},
|
||||
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit,
|
||||
) {
|
||||
val barcodeLauncher =
|
||||
rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||
|
@ -101,37 +96,7 @@ fun AddContactFAB(
|
|||
}
|
||||
}
|
||||
|
||||
scannedContact?.let { contactToImport ->
|
||||
val nodeNum = contactToImport.nodeNum
|
||||
val node = unfilteredNodes.find { it.num == nodeNum }
|
||||
SimpleAlertDialog(
|
||||
title = R.string.import_shared_contact,
|
||||
text = {
|
||||
Column {
|
||||
if (node != null) {
|
||||
Text(text = stringResource(R.string.import_known_shared_contact_text))
|
||||
if (node.user.publicKey.size() > 0 && node.user.publicKey != contactToImport.user?.publicKey) {
|
||||
Text(
|
||||
text = stringResource(R.string.public_key_changed),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Text(text = compareUsers(node.user, contactToImport.user))
|
||||
} else {
|
||||
Text(text = userFieldsToString(contactToImport.user))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
onDismiss = { onSharedContactRequested(null) },
|
||||
confirmText = stringResource(R.string.import_label),
|
||||
onConfirm = {
|
||||
onSharedContactImport(contactToImport)
|
||||
onSharedContactRequested(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
sharedContact?.let { SharedContactDialog(sharedContact = it, onDismiss = { onSharedContactRequested(null) }) }
|
||||
|
||||
fun zxingScan() {
|
||||
Timber.d("Starting zxing QR code scanner")
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.sharing
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SimpleAlertDialog
|
||||
|
||||
/** A dialog for importing a shared contact that was scanned from a QR code. */
|
||||
@Composable
|
||||
fun SharedContactDialog(
|
||||
sharedContact: AdminProtos.SharedContact,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: SharedContactViewModel = hiltViewModel(),
|
||||
) {
|
||||
val unfilteredNodes by viewModel.unfilteredNodes.collectAsStateWithLifecycle()
|
||||
|
||||
val nodeNum = sharedContact.nodeNum
|
||||
val node = unfilteredNodes.find { it.num == nodeNum }
|
||||
|
||||
SimpleAlertDialog(
|
||||
title = R.string.import_shared_contact,
|
||||
text = {
|
||||
Column {
|
||||
if (node != null) {
|
||||
Text(text = stringResource(R.string.import_known_shared_contact_text))
|
||||
if (node.user.publicKey.size() > 0 && node.user.publicKey != sharedContact.user?.publicKey) {
|
||||
Text(
|
||||
text = stringResource(R.string.public_key_changed),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Text(text = compareUsers(node.user, sharedContact.user))
|
||||
} else {
|
||||
Text(text = userFieldsToString(sharedContact.user))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
onDismiss = onDismiss,
|
||||
confirmText = stringResource(R.string.import_label),
|
||||
onConfirm = {
|
||||
viewModel.addSharedContact(sharedContact)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.sharing
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SharedContactViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
nodeRepository: NodeRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val unfilteredNodes: StateFlow<List<Node>> =
|
||||
nodeRepository
|
||||
.getNodes()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
|
||||
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit 60c3e6600a2f4e6f49e45aeb47aafd8291a0015c
|
||||
Subproject commit c1e31a9655e9920a8b5b8eccdf7c69ef1ae42a49
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">خدمة الإشعارات</string>
|
||||
<string name="about">حول</string>
|
||||
<string name="channel_invalid">This Channel URL is invalid and can not be used</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Debug Panel</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -794,7 +795,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Servisna obaveštenja</string>
|
||||
<string name="about">O nama</string>
|
||||
<string name="channel_invalid">Ovaj URL kanala je nevažeći i ne može se koristiti.</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panel za otklanjanje grešaka</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -788,7 +789,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Сервизни известия</string>
|
||||
<string name="about">Относно</string>
|
||||
<string name="channel_invalid">URL адресът на този канал е невалиден и не може да се използва</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Панел за отстраняване на грешки</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Експортиране на журнали</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi устройства</string>
|
||||
<string name="ble_devices">BLE устройства</string>
|
||||
<string name="bluetooth_paired_devices">Сдвоени устройства</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Свързано устройство</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">Преглед на изданието</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notificacions de servei</string>
|
||||
<string name="about">Sobre</string>
|
||||
<string name="channel_invalid">La URL d\'aquest canal és invàlida i no es pot fer servir</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panell de depuració</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Servisní upozornění</string>
|
||||
<string name="about">O aplikaci</string>
|
||||
<string name="channel_invalid">Tato adresa URL kanálu je neplatná a nelze ji použít</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panel pro ladění</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Spárovaná zařízení</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Připojená zařízení</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Dienstbenachrichtigungen</string>
|
||||
<string name="about">Über</string>
|
||||
<string name="channel_invalid">Diese Kanal-URL ist ungültig und kann nicht verwendet werden</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Debug-Ausgaben</string>
|
||||
<string name="debug_decoded_payload">Dekodiertes Payload:</string>
|
||||
<string name="debug_logs_export">Protokolle exportieren</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">Keine Daten für den Besucherzähler verfügbar.</string>
|
||||
<string name="wifi_devices">WLAN Geräte</string>
|
||||
<string name="ble_devices">BLE Geräte</string>
|
||||
<string name="bluetooth_paired_devices">Gekoppelte Geräte</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Verbundene Geräte</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Sendebegrenzung überschritten. Bitte versuchen Sie es später erneut.</string>
|
||||
<string name="view_release">Version ansehen</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Ειδοποιήσεις Υπηρεσίας</string>
|
||||
<string name="about">Σχετικά</string>
|
||||
<string name="channel_invalid">Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Πίνακας αποσφαλμάτωσης</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notificaciones de servicio</string>
|
||||
<string name="about">Acerca de</string>
|
||||
<string name="channel_invalid">La URL de este canal no es válida y no puede utilizarse</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panel de depuración</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Exportar registros</string>
|
||||
|
@ -788,7 +789,8 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Teenuse teavitused</string>
|
||||
<string name="about">Teave</string>
|
||||
<string name="channel_invalid">Kanali URL on kehtetu ja seda ei saa kasutada</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Arendaja paneel</string>
|
||||
<string name="debug_decoded_payload">Dekodeeritud andmed:</string>
|
||||
<string name="debug_logs_export">Salvesta logi</string>
|
||||
|
@ -787,6 +788,7 @@
|
|||
<string name="wifi_devices">WiFi seadmed</string>
|
||||
<string name="ble_devices">Sinihamba seadmed</string>
|
||||
<string name="bluetooth_paired_devices">Seotud seadmed</string>
|
||||
<string name="bluetooth_available_devices">Saadaolevad seadmed</string>
|
||||
<string name="connected_device">Ühendatud seadmed</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Limiit ületatud. Proovi hiljem uuesti.</string>
|
||||
<string name="view_release">Näita versioon</string>
|
||||
|
@ -887,5 +889,5 @@
|
|||
<string name="last_heard_filter_label">Filtreeri viimase kuulmise aja järgi: %s</string>
|
||||
<string name="dbm_value">%1$d dBm</string>
|
||||
<string name="error_no_app_to_handle_link">Lingi haldamiseks pole rakendust saadaval.</string>
|
||||
<string name="system_settings">System Settings</string>
|
||||
<string name="system_settings">Süsteemi sätted</string>
|
||||
</resources>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Palveluilmoitukset</string>
|
||||
<string name="about">Tietoja</string>
|
||||
<string name="channel_invalid">Kanavan URL-osoite on virheellinen, eikä sitä voida käyttää</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Vianetsintäpaneeli</string>
|
||||
<string name="debug_decoded_payload">Dekoodattu data:</string>
|
||||
<string name="debug_logs_export">Vie lokitiedot</string>
|
||||
|
@ -787,6 +788,7 @@
|
|||
<string name="wifi_devices">WiFi-laitteet</string>
|
||||
<string name="ble_devices">Bluetooth-laitteet</string>
|
||||
<string name="bluetooth_paired_devices">Paritetut laitteet</string>
|
||||
<string name="bluetooth_available_devices">Käytettävissä olevat laitteet</string>
|
||||
<string name="connected_device">Yhdistetty laite</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Käyttöraja ylitetty. Yritä myöhemmin uudelleen.</string>
|
||||
<string name="view_release">Näytä versio</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notifications de service</string>
|
||||
<string name="about">A propros</string>
|
||||
<string name="channel_invalid">Cette URL de canal est invalide et ne peut pas être utilisée</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panneau de débogage</string>
|
||||
<string name="debug_decoded_payload">Contenu décodé :</string>
|
||||
<string name="debug_logs_export">Exporter les logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">Aucun journal de métriques PAX disponible.</string>
|
||||
<string name="wifi_devices">Périphériques WiFi</string>
|
||||
<string name="ble_devices">Appareils BLE</string>
|
||||
<string name="bluetooth_paired_devices">Périphériques appairés</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Périphérique connecté</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Limite de débit dépassée. Veuillez réessayer plus tard.</string>
|
||||
<string name="view_release">Voir la version</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Fógraí seirbhíse</string>
|
||||
<string name="about">Maidir le</string>
|
||||
<string name="channel_invalid">Tá an URL Cainéil seo neamhdhleathach agus ní féidir é a úsáid</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Painéal Laige</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -792,7 +793,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notificacións de servizo</string>
|
||||
<string name="about">Acerca de</string>
|
||||
<string name="channel_invalid">A ligazón desta canle non é válida e non pode usarse</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panel de depuración</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Servisne obavijesti</string>
|
||||
<string name="about">O programu</string>
|
||||
<string name="channel_invalid">Ovaj URL kanala je nevažeći i ne može se koristiti</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Otklanjanje pogrešaka</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -788,7 +789,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notifikasyon sèvis</string>
|
||||
<string name="about">Sou</string>
|
||||
<string name="channel_invalid">Kanal URL sa a pa valab e yo pa kapab itilize li</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panno Debug</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Szolgáltatás értesítések</string>
|
||||
<string name="about">A programról</string>
|
||||
<string name="channel_invalid">Ez a csatorna URL érvénytelen, ezért nem használható.</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Hibakereső panel</string>
|
||||
<string name="debug_decoded_payload">Dekódolt adat:</string>
|
||||
<string name="debug_logs_export">Naplók exportálása</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">Nem érhetők el PAX metrika naplók.</string>
|
||||
<string name="wifi_devices">WiFi eszközök</string>
|
||||
<string name="ble_devices">BLE eszközök</string>
|
||||
<string name="bluetooth_paired_devices">Párosított eszközök</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Csatlakoztatott eszköz</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Túllépted a sebességkorlátot. Próbáld újra később.</string>
|
||||
<string name="view_release">Kiadás megtekintése</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Tilkynningar um þjónustu</string>
|
||||
<string name="about">Um smáforrit</string>
|
||||
<string name="channel_invalid">Þetta rásar URL er ógilt og ónothæft</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Villuleitarborð</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notifiche di servizio</string>
|
||||
<string name="about">Informazioni</string>
|
||||
<string name="channel_invalid">L\'URL di questo Canale non è valida e non può essere usata</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Pannello Di Debug</string>
|
||||
<string name="debug_decoded_payload">Payload decodificato:</string>
|
||||
<string name="debug_logs_export">Esporta i logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">Nessun log delle metriche PAX disponibile.</string>
|
||||
<string name="wifi_devices">Dispositivi WiFi</string>
|
||||
<string name="ble_devices">Dispositivi BLE</string>
|
||||
<string name="bluetooth_paired_devices">Dispositivi associati</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Dispositivo connesso</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Limite di trasmissione superato. Riprova più tardi</string>
|
||||
<string name="view_release">Visualizza Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">התראות שירות</string>
|
||||
<string name="about">אודות</string>
|
||||
<string name="channel_invalid">כתובת ערוץ זה אינו תקין ולא ניתן לעשות בו שימוש</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">פאנל דיבאג</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -188,6 +188,7 @@
|
|||
<string name="meshtastic_service_notifications">通知サービス</string>
|
||||
<string name="about">概要</string>
|
||||
<string name="channel_invalid">このチャンネルURLは無効なため使用できません。</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">デバッグ</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -785,7 +786,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">서비스 알림</string>
|
||||
<string name="about">앱에 대하여</string>
|
||||
<string name="channel_invalid">이 채널 URL은 유효하지 않으며 사용할 수 없습니다.</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">디버그 패널</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">로그 내보내기</string>
|
||||
|
@ -784,7 +785,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Paslaugos pranešimai</string>
|
||||
<string name="about">Apie</string>
|
||||
<string name="channel_invalid">Šio kanalo URL yra neteisingas ir negali būti naudojamas</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Derinimo skydelis</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Servicemeldingen</string>
|
||||
<string name="about">Over</string>
|
||||
<string name="channel_invalid">Deze Kanaal URL is ongeldig en kan niet worden gebruikt</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Debug-paneel</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Tjeneste meldinger</string>
|
||||
<string name="about">Om</string>
|
||||
<string name="channel_invalid">Denne kanall URL er ugyldig og kan ikke benyttes</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Feilsøkningspanel</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Powiadomienia o usługach</string>
|
||||
<string name="about">O aplikacji</string>
|
||||
<string name="channel_invalid">Ten adres URL kanału jest nieprawidłowy i nie można go użyć</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panel debugowania</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notificações de serviço</string>
|
||||
<string name="about">Sobre</string>
|
||||
<string name="channel_invalid">Este link de canal é inválido e não pode ser usado</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Painel de depuração</string>
|
||||
<string name="debug_decoded_payload">Pacote Decodificado:</string>
|
||||
<string name="debug_logs_export">Exportar Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">Não há logs de métricas de PAX disponíveis.</string>
|
||||
<string name="wifi_devices">Dispositivos WiFi</string>
|
||||
<string name="ble_devices">Dispositivos BLE</string>
|
||||
<string name="bluetooth_paired_devices">Dispositivos Pareados</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Dispositivo Conectado</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Limite excedido. Por favor, tente novamente mais tarde.</string>
|
||||
<string name="view_release">Ver Lançamento</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notificações de serviço</string>
|
||||
<string name="about">Sobre</string>
|
||||
<string name="channel_invalid">O Link Deste Canal é inválido e não pode ser usado</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Painel de depuração</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notificările serviciului</string>
|
||||
<string name="about">Despre</string>
|
||||
<string name="channel_invalid">Acest URL de canal este invalid și nu poate fi folosit</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Panou debug</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -788,7 +789,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Служебные уведомления</string>
|
||||
<string name="about">О приложении</string>
|
||||
<string name="channel_invalid">Этот URL-адрес канала недействителен и не может быть использован</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Панель отладки</string>
|
||||
<string name="debug_decoded_payload">Декодированная нагрузка:</string>
|
||||
<string name="debug_logs_export">Экспортировать логи</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">Нет доступных журналов метрики пассажиров.</string>
|
||||
<string name="wifi_devices">WiFi устройства</string>
|
||||
<string name="ble_devices">Устройства BLE</string>
|
||||
<string name="bluetooth_paired_devices">Сопряженные устройства</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Подключённые устройства</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Превышен лимит запросов. Пожалуйста, повторите попытку позже.</string>
|
||||
<string name="view_release">Просмотреть релиз</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Notifikácie zo služby</string>
|
||||
<string name="about">O aplikácii</string>
|
||||
<string name="channel_invalid">URL adresa tohoto kanála nie je platná a nedá sa použiť</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Debug okno</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Obvestila storitve</string>
|
||||
<string name="about">O programu</string>
|
||||
<string name="channel_invalid">Neveljaven kanal</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Plošča za odpravljanje napak</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Njoftime shërbimi</string>
|
||||
<string name="about">Rreth</string>
|
||||
<string name="channel_invalid">Ky URL kanal është i pavlefshëm dhe nuk mund të përdoret</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Paneli i debug-ut</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Обавештења о услугама</string>
|
||||
<string name="about">О</string>
|
||||
<string name="channel_invalid">Ова URL адреса канала је неважећа и не може се користити</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Панел за отклањање грешака</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -788,7 +789,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Tjänsteaviseringar</string>
|
||||
<string name="about">Om</string>
|
||||
<string name="channel_invalid">Denna kanal-URL är ogiltig och kan inte användas</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Felsökningspanel</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Servis bildirimleri</string>
|
||||
<string name="about">Hakkında</string>
|
||||
<string name="channel_invalid">Bu Kanal URL\' si geçersiz ve kullanılamaz</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Hata Ayıklama Paneli</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">Сервісні сповіщення</string>
|
||||
<string name="about">Про</string>
|
||||
<string name="channel_invalid">URL-адреса цього каналу недійсна та не може бути використана</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Панель налагодження</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Експортувати журнали</string>
|
||||
|
@ -790,7 +791,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
<string name="view_release">View Release</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">服务通知</string>
|
||||
<string name="about">关于</string>
|
||||
<string name="channel_invalid">此频道 URL 无效,无法使用</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">调试面板</string>
|
||||
<string name="debug_decoded_payload">解码Payload:</string>
|
||||
<string name="debug_logs_export">导出程序日志</string>
|
||||
|
@ -786,7 +787,8 @@
|
|||
<string name="no_pax_metrics_logs">无可用的 PAX 计量日志。</string>
|
||||
<string name="wifi_devices">WiFi 设备</string>
|
||||
<string name="ble_devices">BLE 设备</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
<string name="routing_error_rate_limit_exceeded">超过速率限制。请稍后再试。</string>
|
||||
<string name="view_release">查看发行版</string>
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
<string name="meshtastic_service_notifications">服務通知</string>
|
||||
<string name="about">關於</string>
|
||||
<string name="channel_invalid">此頻道 URL 無效,無法使用</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">除錯面板</string>
|
||||
<string name="debug_decoded_payload">解析封包:</string>
|
||||
<string name="debug_logs_export">匯出日誌</string>
|
||||
|
@ -784,7 +785,8 @@
|
|||
<string name="no_pax_metrics_logs">沒有可用的 PAX 指標日誌。</string>
|
||||
<string name="wifi_devices">WiFi 裝置</string>
|
||||
<string name="ble_devices">藍牙裝置</string>
|
||||
<string name="bluetooth_paired_devices">配對裝置</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">連接裝置</string>
|
||||
<string name="routing_error_rate_limit_exceeded">超過速率限制,請稍後再嘗試。</string>
|
||||
<string name="view_release">查看版本資訊</string>
|
||||
|
|
|
@ -214,6 +214,7 @@
|
|||
<string name="meshtastic_service_notifications">Service notifications</string>
|
||||
<string name="about">About</string>
|
||||
<string name="channel_invalid">This Channel URL is invalid and can not be used</string>
|
||||
<string name="contact_invalid">This contact is invalid and can not be added</string>
|
||||
<string name="debug_panel">Debug Panel</string>
|
||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||
<string name="debug_logs_export">Export Logs</string>
|
||||
|
@ -354,9 +355,9 @@
|
|||
<string name="air_util_definition">Percent of airtime for transmission used within the last hour.</string>
|
||||
<string name="iaq">IAQ</string>
|
||||
<string name="encryption_psk">Shared Key</string>
|
||||
<string name="encryption_psk_text">Direct messages are using the shared key for the channel.</string>
|
||||
<string name="encryption_psk_text">Only channel messages can be sent/received. Direct Messages require the Public Key Infrastructure feature in 2.5+ firmware.</string>
|
||||
<string name="encryption_pkc">Public Key Encryption</string>
|
||||
<string name="encryption_pkc_text">Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater.</string>
|
||||
<string name="encryption_pkc_text">Direct messages are using the new public key infrastructure for encryption.</string>
|
||||
<string name="encryption_error">Public key mismatch</string>
|
||||
<string name="encryption_error_text">The public key does not match the recorded key. You may remove the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.</string>
|
||||
<string name="exchange_userinfo">Exchange user info</string>
|
||||
|
@ -821,7 +822,8 @@
|
|||
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
|
||||
<string name="wifi_devices">WiFi Devices</string>
|
||||
<string name="ble_devices">BLE Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired Devices</string>
|
||||
<string name="bluetooth_paired_devices">Paired devices</string>
|
||||
<string name="bluetooth_available_devices">Available devices</string>
|
||||
<string name="connected_device">Connected Device</string>
|
||||
|
||||
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
|
||||
|
|
|
@ -23,17 +23,25 @@ platform :android do
|
|||
|
||||
desc "Deploy a new version to the internal track on Google Play"
|
||||
lane :internal do
|
||||
aab_path = build_google_release
|
||||
upload_to_play_store(
|
||||
track: 'internal',
|
||||
aab: aab_path,
|
||||
release_status: 'completed',
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_changelogs: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
)
|
||||
begin
|
||||
aab_path = build_google_release
|
||||
upload_to_play_store(
|
||||
track: 'internal',
|
||||
aab: aab_path,
|
||||
release_status: 'completed',
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_changelogs: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
)
|
||||
rescue => exception
|
||||
if exception.message.include?("Google Api Error: forbidden: A release with version code") && exception.message.include?("already exists for this track")
|
||||
UI.message("This version code is already on the internal track. No action needed.")
|
||||
else
|
||||
raise exception
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Promote from internal track to the closed track on Google Play"
|
||||
|
@ -41,6 +49,7 @@ platform :android do
|
|||
upload_to_play_store(
|
||||
track: 'internal',
|
||||
track_promote_to: 'NewAlpha',
|
||||
version_codes: [ENV["VERSION_CODE"].to_i],
|
||||
release_status: 'completed',
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: true,
|
||||
|
@ -50,11 +59,12 @@ platform :android do
|
|||
)
|
||||
end
|
||||
|
||||
desc "Promote from closed track to the open track on Google Play"
|
||||
desc "Promote from internal track to the open track on Google Play"
|
||||
lane :open do
|
||||
upload_to_play_store(
|
||||
track: 'NewAlpha',
|
||||
track: 'internal',
|
||||
track_promote_to: 'beta',
|
||||
version_codes: [ENV["VERSION_CODE"].to_i],
|
||||
release_status: 'draft',
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: true,
|
||||
|
@ -64,11 +74,12 @@ platform :android do
|
|||
)
|
||||
end
|
||||
|
||||
desc "Promote from open track to the production track on Google Play"
|
||||
desc "Promote from internal track to the production track on Google Play"
|
||||
lane :production do
|
||||
upload_to_play_store(
|
||||
track: 'open',
|
||||
track: 'internal',
|
||||
track_promote_to: 'production',
|
||||
version_codes: [ENV["VERSION_CODE"].to_i],
|
||||
release_status: 'draft',
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: true,
|
||||
|
@ -89,6 +100,35 @@ platform :android do
|
|||
)
|
||||
end
|
||||
|
||||
desc "Get the highest version code from all Google Play tracks"
|
||||
lane :get_highest_version_code do
|
||||
require 'set'
|
||||
all_codes = Set.new
|
||||
tracks = ['internal', 'closed', 'open', 'production']
|
||||
tracks.each do |track|
|
||||
begin
|
||||
codes = google_play_track_version_codes(track: track, package_name: 'com.geeksville.mesh')
|
||||
all_codes.merge(codes.map(&:to_i))
|
||||
rescue => e
|
||||
UI.message("Could not fetch version codes for track #{track}: #{e.message}")
|
||||
end
|
||||
end
|
||||
highest = all_codes.max || 0
|
||||
UI.message("Highest version code on Google Play: #{highest}")
|
||||
File.write('highest_version_code.txt', highest.to_s)
|
||||
end
|
||||
|
||||
desc "Get the version code currently on the internal track (max if multiple)"
|
||||
lane :get_internal_track_version_code do
|
||||
codes = google_play_track_version_codes(track: 'internal', package_name: 'com.geeksville.mesh')
|
||||
if codes.nil? || codes.empty?
|
||||
UI.user_error!("No version codes found on internal track. Ensure an internal build has been published before promoting.")
|
||||
end
|
||||
max_code = codes.map(&:to_i).max
|
||||
UI.message("Internal track version code: #{max_code}")
|
||||
File.write('internal_version_code.txt', max_code.to_s)
|
||||
end
|
||||
|
||||
private_lane :build_google_release do
|
||||
gradle(
|
||||
task: "clean bundleGoogleRelease assembleGoogleRelease",
|
||||
|
|
|
@ -14,6 +14,5 @@
|
|||
<ID>ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored</ID>
|
||||
<ID>PreviewPublic:NodeItem.kt$NodeInfoPreview</ID>
|
||||
<ID>PreviewPublic:NodeItem.kt$NodeInfoSimplePreview</ID>
|
||||
<ID>TooManyFunctions:NodeListViewModel.kt$NodeListViewModel : ViewModel</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
@ -161,9 +161,6 @@ constructor(
|
|||
uiPreferencesDataSource.setNodeSort(sort.ordinal)
|
||||
}
|
||||
|
||||
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
|
||||
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
|
||||
|
||||
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
|
||||
_sharedContactRequested.value = sharedContact
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue