ci: fix the failing job, split deploy out, and drop the Android build
Three things, one subject: making CI say the truth about this repo.
## The red on every run was ours, not the runners'
Every docker-build run came back success, success, failure — the same
shape for weeks. The failing job was `deploy`, and it was failing to
*not run*:
if: ${{ github.event.inputs.deploy == 'true' }}
On a push there is no github.event.inputs at all. This Forgejo does not
treat that as false and skip; it dispatches the job, the runner cannot
resolve it, and the task ends in "Early termination". The runners were
never at fault, and nothing about them needed changing.
The `'runs-on' key not defined` line is a red herring: the `build` job
prints it too and succeeds. It names the job's *needs* target, not the
job, and the old android-apk workflow used `needs:` happily for months.
Deploy is now its own workflow with only workflow_dispatch — no
condition to evaluate, so nothing can be dispatched by mistake. No job
in either file now carries a job-level `if`. The one conditional left is
a *step* (push to registry), and step conditions are evaluated by the
runner once the job is already running, which is why that one has always
worked.
## dev and main
docker-build now runs on `dev` as well. Both branches prove the same two
things — tests pass, image builds — and only `main` publishes the image,
so nothing on `dev` can be mistaken for something deployable. Deploying
stays a person pressing a button after looking at the change.
CONTRIBUTING.md documents the flow.
## Android
Removed: the mobile/ Capacitor project, docs/mobile-build.md, and the
Android bits of scripts/release.sh. All of it is in git history — 4613a278
is the last commit that had it — for when it is rebuilt.
src/utils/platform.js stays. isMobileClient only decides token lifetime,
it is twelve lines, and it is the contract a future app would come back
to; deleting it would be a change to auth for no gain.
.github/workflows/ went too — all five. There is no GitHub remote on
this repository, so none of them has ever run, and two of them wrote
into mobile/ paths that no longer exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
55
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
name: Deploy
|
||||
|
||||
# Its own workflow, and workflow_dispatch only — there is no push trigger, so
|
||||
# there is nothing to skip. Deploying used to be a job inside the build
|
||||
# workflow behind `if: github.event.inputs.deploy == 'true'`. On a push there
|
||||
# is no github.event.inputs at all; this Forgejo dispatched the job regardless,
|
||||
# the runner could not resolve it, and it reported "Early termination". Every
|
||||
# run of that workflow showed a failure for a job that was never meant to run.
|
||||
#
|
||||
# A separate file also matches what deploying is: a deliberate act, taken after
|
||||
# someone has looked at the change, not a consequence of pushing.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
revision:
|
||||
description: Full commit SHA to deploy. Leave blank for the branch tip.
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to the host
|
||||
runs-on: forgejo-local
|
||||
env:
|
||||
DEPLOY_DIR: ${{ vars.DEPLOY_DIR || '/home/danvics/docker/ped-ai' }}
|
||||
steps:
|
||||
# The deploy directory is also a working tree. This refuses rather than
|
||||
# resetting over someone's uncommitted work.
|
||||
- name: Refuse to deploy over uncommitted work
|
||||
run: |
|
||||
if [ -n "$(git -C "$DEPLOY_DIR" status --porcelain)" ]; then
|
||||
echo "$DEPLOY_DIR has uncommitted changes; commit or stash them first." >&2
|
||||
git -C "$DEPLOY_DIR" status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detaches HEAD at the deployed revision, which is what a deployed tree
|
||||
# should be. If DEPLOY_DIR is also where you write code, point this at a
|
||||
# checkout of its own instead — vars.DEPLOY_DIR.
|
||||
- name: Move the deploy checkout to this revision
|
||||
run: |
|
||||
REVISION="${{ github.event.inputs.revision }}"
|
||||
[ -n "$REVISION" ] || REVISION="${{ github.sha }}"
|
||||
echo "REVISION=$REVISION" >> "$GITHUB_ENV"
|
||||
git -C "$DEPLOY_DIR" fetch --quiet --all
|
||||
git -C "$DEPLOY_DIR" checkout --quiet --detach "$REVISION"
|
||||
|
||||
# deploy.sh pins the image, waits for health, asks /api/build what is
|
||||
# actually running, and rolls back if it disagrees. Schema migrations are
|
||||
# applied by the container's own entrypoint before the app starts.
|
||||
- name: Deploy and verify
|
||||
run: |
|
||||
IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3"
|
||||
"$DEPLOY_DIR/scripts/deploy.sh" "$IMAGE:$REVISION" "$REVISION"
|
||||
|
|
@ -1,18 +1,21 @@
|
|||
name: Forgejo Docker Build
|
||||
|
||||
# dev proves the tests pass and the image builds. main additionally publishes
|
||||
# the image, because main is what production runs.
|
||||
#
|
||||
# Deploying is a separate workflow, not a job here behind an "if". A job whose
|
||||
# "if" is false is still dispatched to a runner by this Forgejo, which then
|
||||
# cannot resolve it and reports "Early termination" — that was the failure on
|
||||
# every run of this workflow. Nothing here now depends on a job being skipped.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, dev]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_image:
|
||||
description: Push image to Forgejo container registry
|
||||
description: Push image to Forgejo container registry (main only)
|
||||
required: false
|
||||
default: 'true'
|
||||
deploy:
|
||||
description: Deploy the built image to the host after pushing
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
root-test:
|
||||
|
|
@ -57,8 +60,11 @@ jobs:
|
|||
docker tag ped-ai-local:latest "$IMAGE:$REVISION"
|
||||
docker tag ped-ai-local:latest "$IMAGE:latest"
|
||||
|
||||
# A step, not a job. Step conditions are evaluated by the runner once the
|
||||
# job is already running, so a false one simply skips — it cannot produce
|
||||
# the dispatch failure a job-level condition does here.
|
||||
- name: Push image to Forgejo registry
|
||||
if: ${{ github.event.inputs.push_image != 'false' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' && github.event.inputs.push_image != 'false' }}
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
run: |
|
||||
|
|
@ -67,43 +73,3 @@ jobs:
|
|||
echo "$FORGEJO_TOKEN" | docker login git.danvics.com -u danvics --password-stdin
|
||||
docker push "$IMAGE:$REVISION"
|
||||
docker push "$IMAGE:latest"
|
||||
|
||||
# ── Deploy ──────────────────────────────────────────────────────────────
|
||||
# Opt-in, because the deploy directory is also a working tree: this refuses to
|
||||
# run if it has uncommitted changes rather than resetting over someone's work.
|
||||
#
|
||||
# It moves that checkout to the built revision first, so the Compose file and
|
||||
# the entrypoint that ship with the image are the ones used to run it, then
|
||||
# hands over to scripts/deploy.sh, which pins the image, waits for health,
|
||||
# asks /api/build what is actually running and rolls back if it disagrees.
|
||||
# Schema migrations are applied by the container's own entrypoint.
|
||||
deploy:
|
||||
needs: build
|
||||
name: Deploy to the host
|
||||
runs-on: forgejo-local
|
||||
if: ${{ github.event.inputs.deploy == 'true' }}
|
||||
env:
|
||||
DEPLOY_DIR: ${{ vars.DEPLOY_DIR || '/home/danvics/docker/ped-ai' }}
|
||||
steps:
|
||||
- name: Refuse to deploy over uncommitted work
|
||||
run: |
|
||||
if [ -n "$(git -C "$DEPLOY_DIR" status --porcelain)" ]; then
|
||||
echo "$DEPLOY_DIR has uncommitted changes; commit or stash them first." >&2
|
||||
git -C "$DEPLOY_DIR" status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detaches HEAD at the deployed revision, which is what you want a
|
||||
# deployed tree to be. If DEPLOY_DIR is also where you write code, point
|
||||
# it at a checkout of its own instead — vars.DEPLOY_DIR.
|
||||
- name: Move the deploy checkout to this revision
|
||||
run: |
|
||||
REVISION="${{ github.sha }}"
|
||||
git -C "$DEPLOY_DIR" fetch --quiet --all
|
||||
git -C "$DEPLOY_DIR" checkout --quiet --detach "$REVISION"
|
||||
|
||||
- name: Deploy and verify
|
||||
run: |
|
||||
IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3"
|
||||
REVISION="${{ github.sha }}"
|
||||
"$DEPLOY_DIR/scripts/deploy.sh" "$IMAGE:$REVISION" "$REVISION"
|
||||
|
|
|
|||
148
.github/workflows/auto-version.yml
vendored
|
|
@ -1,148 +0,0 @@
|
|||
name: Auto version & release
|
||||
|
||||
# Fires on every push to main. Parses commit messages since the
|
||||
# last semver tag, decides patch/minor/major bump, creates the
|
||||
# tag, pushes. The tag push then triggers android-release.yml and
|
||||
# docker-publish.yml. Fully hands-off — you never pick a version
|
||||
# number; your commit messages do.
|
||||
#
|
||||
# Commit message grammar (Conventional Commits):
|
||||
# feat: → minor bump (new feature, backward-compatible)
|
||||
# fix: → patch bump (bug fix)
|
||||
# feat!: / BREAKING CHANGE in body → major bump
|
||||
# everything else (docs, refactor, chore, style, ci, test) → no bump
|
||||
#
|
||||
# Skip conditions (no new release created):
|
||||
# - No commits match the above patterns
|
||||
# - The most recent commit is itself a release commit ("Release v…")
|
||||
# - [skip ci] appears in any commit message since the last tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
# Opt in to Node 24 runtime early (deprecation of Node 20 begins 2026-06-02)
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
if: "github.server_url == 'https://github.com' && !contains(github.event.head_commit.message, 'Release v') && !contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Use RELEASE_PAT (a Personal Access Token you add as a repo
|
||||
# secret) so the tag push this workflow performs actually
|
||||
# triggers the downstream tag-based workflows (android-release,
|
||||
# docker-publish). GITHUB_TOKEN pushes are deliberately
|
||||
# blocked from triggering other workflows by GitHub.
|
||||
# Fine-grained PAT with "Contents: Read and write" on this
|
||||
# repo is enough.
|
||||
token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Find last semver tag
|
||||
id: last
|
||||
run: |
|
||||
LAST=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1)
|
||||
if [[ -z "$LAST" ]]; then
|
||||
LAST="v0.0.0"
|
||||
echo "no previous tag, starting from v0.0.0"
|
||||
fi
|
||||
echo "tag=$LAST"
|
||||
echo "tag=$LAST" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${LAST#v}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Decide bump type from commit messages
|
||||
id: decide
|
||||
env:
|
||||
LAST: ${{ steps.last.outputs.tag }}
|
||||
run: |
|
||||
# All commits from the last tag → HEAD (exclusive of tag commit)
|
||||
if [[ "$LAST" == "v0.0.0" ]]; then
|
||||
MSGS=$(git log --format='%s%n%b%n---')
|
||||
else
|
||||
MSGS=$(git log "${LAST}..HEAD" --format='%s%n%b%n---')
|
||||
fi
|
||||
|
||||
BUMP=none
|
||||
if echo "$MSGS" | grep -qE '(^|\n)(BREAKING CHANGE:|[a-z]+(\([^)]+\))?!:)'; then
|
||||
BUMP=major
|
||||
elif echo "$MSGS" | grep -qE '(^|\n)feat(\([^)]+\))?: '; then
|
||||
BUMP=minor
|
||||
elif echo "$MSGS" | grep -qE '(^|\n)fix(\([^)]+\))?: '; then
|
||||
BUMP=patch
|
||||
fi
|
||||
|
||||
echo "Bump type decided: $BUMP"
|
||||
echo "bump=$BUMP" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "### Commits since $LAST"
|
||||
echo '```'
|
||||
if [[ "$LAST" == "v0.0.0" ]]; then
|
||||
git log --oneline | head -20
|
||||
else
|
||||
git log "${LAST}..HEAD" --oneline
|
||||
fi
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "**Bump decision**: \`$BUMP\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Stop if no release-worthy commits
|
||||
if: steps.decide.outputs.bump == 'none'
|
||||
run: |
|
||||
echo "No feat / fix / BREAKING commits since last tag — not cutting a release."
|
||||
echo "::notice::No release cut. Commit with 'feat:', 'fix:', or BREAKING CHANGE to trigger one."
|
||||
|
||||
- name: Compute next version
|
||||
id: next
|
||||
if: steps.decide.outputs.bump != 'none'
|
||||
env:
|
||||
CUR: ${{ steps.last.outputs.version }}
|
||||
BUMP: ${{ steps.decide.outputs.bump }}
|
||||
run: |
|
||||
IFS='.' read -r MAJ MIN PAT <<< "$CUR"
|
||||
case "$BUMP" in
|
||||
major) NEXT="$((MAJ+1)).0.0" ;;
|
||||
minor) NEXT="${MAJ}.$((MIN+1)).0" ;;
|
||||
patch) NEXT="${MAJ}.${MIN}.$((PAT+1))" ;;
|
||||
esac
|
||||
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
|
||||
echo "### Next version: v$NEXT" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Configure git
|
||||
if: steps.decide.outputs.bump != 'none'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump version strings + tag + push
|
||||
if: steps.decide.outputs.bump != 'none'
|
||||
env:
|
||||
V: ${{ steps.next.outputs.next }}
|
||||
run: |
|
||||
IFS='.' read -r MAJ MIN PAT <<< "$V"
|
||||
ANDROID_CODE=$(( MAJ * 100000 + MIN * 1000 + PAT ))
|
||||
|
||||
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" package.json
|
||||
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" mobile/package.json
|
||||
sed -i -E \
|
||||
-e "s/versionCode +[0-9]+/versionCode ${ANDROID_CODE}/" \
|
||||
-e "s/versionName +\"[^\"]+\"/versionName \"${V}\"/" \
|
||||
mobile/android/app/build.gradle
|
||||
|
||||
git add package.json mobile/package.json mobile/android/app/build.gradle
|
||||
git commit -m "Release v${V}"
|
||||
git tag -a "v${V}" -m "Release v${V}"
|
||||
|
||||
git push origin HEAD
|
||||
git push origin "v${V}"
|
||||
|
||||
echo "### Released v$V" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "android-release + docker-publish workflows will now run." >> "$GITHUB_STEP_SUMMARY"
|
||||
34
.github/workflows/ci.yml
vendored
|
|
@ -1,34 +0,0 @@
|
|||
name: CI
|
||||
|
||||
# Runs root app tests on every PR and push to main.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
# Cancel superseded runs on the same ref to save minutes.
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Root app tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Unit tests
|
||||
run: npm test
|
||||
156
.github/workflows/docker-publish.yml
vendored
|
|
@ -1,156 +0,0 @@
|
|||
name: Build & Push Docker Image
|
||||
|
||||
# Multi-arch build using NATIVE runners for each platform, then a
|
||||
# manifest-list push. No QEMU emulation — amd64 builds on x86 runner,
|
||||
# arm64 builds on ubuntu-24.04-arm runner. argon2 and every other
|
||||
# native dep compile natively on their target arch.
|
||||
#
|
||||
# Result: `danielonyejesi/pediatric-ai-scribe-v3:X.Y.Z` (and :latest)
|
||||
# is one tag serving the correct variant to amd64 or arm64 hosts.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to publish (e.g. v6.2.0)'
|
||||
required: false
|
||||
default: 'latest'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
||||
IMAGE: danielonyejesi/pediatric-ai-scribe-v3
|
||||
|
||||
jobs:
|
||||
root-test:
|
||||
if: ${{ github.server_url == 'https://github.com' }}
|
||||
name: Root app tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
build:
|
||||
needs: root-test
|
||||
if: ${{ github.server_url == 'https://github.com' }}
|
||||
# Build one variant per matrix entry, push by digest only.
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker metadata (for labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE }}
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build & push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
GIT_REVISION=${{ github.sha }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
|
||||
- name: Export digest for the merge job
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
DIG="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${DIG#sha256:}"
|
||||
|
||||
- name: Upload digest artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
if: ${{ github.server_url == 'https://github.com' }}
|
||||
# Combine the two single-platform digests into one multi-arch manifest
|
||||
# published under the real tags (vX.Y.Z and latest).
|
||||
name: Merge manifests
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Resolve tag
|
||||
id: tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "tag=${{ github.event.inputs.tag || 'latest' }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE }}
|
||||
tags: |
|
||||
type=raw,value=${{ steps.tag.outputs.tag }}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Create manifest list & push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${{ env.IMAGE }}@sha256:%s " *)
|
||||
|
||||
- name: Inspect final image
|
||||
run: docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.tag.outputs.tag }}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### Multi-arch image published" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`${{ env.IMAGE }}:${{ steps.tag.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`${{ env.IMAGE }}:latest\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Platforms: linux/amd64, linux/arm64 (built on native runners)" >> $GITHUB_STEP_SUMMARY
|
||||
30
.github/workflows/security.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
name: Security audit
|
||||
|
||||
# Weekly npm audit at high+ severity for the root app. Reports to the job summary; does NOT fail the build
|
||||
# (advisories appear constantly and a red checkmark train would just get
|
||||
# muted). Re-run on demand via workflow_dispatch.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Mondays 06:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
name: npm audit (high+)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Audit root app
|
||||
run: |
|
||||
echo '## Root app advisories' >> "$GITHUB_STEP_SUMMARY"
|
||||
npm audit --audit-level=high --json > legacy-audit.json || true
|
||||
node -e "const a=require('./legacy-audit.json');const m=a.metadata?.vulnerabilities||{};console.log('high:'+(m.high||0)+' critical:'+(m.critical||0));" >> "$GITHUB_STEP_SUMMARY"
|
||||
continue-on-error: true
|
||||
103
.github/workflows/version-bump.yml
vendored
|
|
@ -1,103 +0,0 @@
|
|||
name: Version bump & release
|
||||
|
||||
# Manual trigger — click "Run workflow" in the Actions tab, choose
|
||||
# patch / minor / major. The workflow computes the next semver,
|
||||
# updates package.json, mobile/package.json, and the Android
|
||||
# build.gradle, commits the change, tags it, and pushes — which
|
||||
# triggers the android-release and docker-publish workflows.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: 'Semver bump type'
|
||||
required: true
|
||||
type: choice
|
||||
default: patch
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
custom:
|
||||
description: 'Or exact version (e.g. 7.0.0) — overrides bump'
|
||||
required: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
if: ${{ github.server_url == 'https://github.com' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Compute next version
|
||||
id: v
|
||||
run: |
|
||||
CUR=$(grep -m1 '"version"' package.json | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
||||
echo "current=$CUR"
|
||||
IFS='.' read -r MAJ MIN PAT <<< "$CUR"
|
||||
|
||||
if [[ -n "${{ github.event.inputs.custom }}" ]]; then
|
||||
NEXT="${{ github.event.inputs.custom }}"
|
||||
else
|
||||
case "${{ github.event.inputs.bump }}" in
|
||||
major) NEXT="$((MAJ+1)).0.0" ;;
|
||||
minor) NEXT="${MAJ}.$((MIN+1)).0" ;;
|
||||
patch) NEXT="${MAJ}.${MIN}.$((PAT+1))" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! [[ "$NEXT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::invalid version: $NEXT"; exit 1
|
||||
fi
|
||||
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
|
||||
echo "current=$CUR" >> "$GITHUB_OUTPUT"
|
||||
echo "### Version bump" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Current: $CUR" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Next: $NEXT" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump version strings
|
||||
env:
|
||||
V: ${{ steps.v.outputs.next }}
|
||||
run: |
|
||||
IFS='.' read -r MAJ MIN PAT <<< "$V"
|
||||
ANDROID_CODE=$(( MAJ * 100000 + MIN * 1000 + PAT ))
|
||||
|
||||
# package.json (top-level "version": "...")
|
||||
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" package.json
|
||||
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" mobile/package.json
|
||||
|
||||
# Android
|
||||
sed -i -E \
|
||||
-e "s/versionCode +[0-9]+/versionCode ${ANDROID_CODE}/" \
|
||||
-e "s/versionName +\"[^\"]+\"/versionName \"${V}\"/" \
|
||||
mobile/android/app/build.gradle
|
||||
|
||||
git diff --stat
|
||||
|
||||
- name: Commit, tag, push
|
||||
env:
|
||||
V: ${{ steps.v.outputs.next }}
|
||||
run: |
|
||||
git add package.json mobile/package.json mobile/android/app/build.gradle
|
||||
git commit -m "Release v${V}"
|
||||
git tag -a "v${V}" -m "Release v${V}"
|
||||
git push origin HEAD
|
||||
git push origin "v${V}"
|
||||
echo "### Pushed" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- tag: v${V}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- android-release + docker-publish workflows will now run" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
# Contributing
|
||||
|
||||
<!-- Pipeline verified 2026-04-15: auto-version + PAT + multi-arch docker -->
|
||||
|
||||
## Commit format
|
||||
|
||||
[Conventional Commits](https://www.conventionalcommits.org). `.github/workflows/auto-version.yml`
|
||||
parses messages since the last semver tag and decides whether to bump.
|
||||
[Conventional Commits](https://www.conventionalcommits.org). Nothing parses
|
||||
these automatically any more — the auto-version workflow was a GitHub one and
|
||||
this repository has no GitHub remote — but the prefixes still say what a change
|
||||
is, and `scripts/release.sh` still wants a version chosen the same way.
|
||||
|
||||
| Prefix | Bump | |
|
||||
|---|---|---|
|
||||
|
|
@ -14,21 +14,47 @@ parses messages since the last semver tag and decides whether to bump.
|
|||
| `feat!:` / `fix!:` / `BREAKING CHANGE:` in body | major | breaking change |
|
||||
| `docs:` `refactor:` `chore:` `test:` `style:` `ci:` `build:` | none | no release |
|
||||
|
||||
Append `[skip ci]` to suppress the run for that commit.
|
||||
|
||||
## Manual release
|
||||
|
||||
```bash
|
||||
scripts/release.sh 6.2.0 --push # local
|
||||
scripts/release.sh 6.2.0 --push # bump, commit, tag, push
|
||||
```
|
||||
|
||||
or Actions tab → **Version bump & release** → Run workflow → pick bump type.
|
||||
## Branches
|
||||
|
||||
## What a tag push triggers
|
||||
`main` is production. It is what `scripts/deploy.sh` deploys and what the
|
||||
container registry publishes from. `dev` is where work lands first.
|
||||
|
||||
| Workflow | Output |
|
||||
|---|---|
|
||||
| `docker-publish.yml` | `danielonyejesi/pediatric-ai-scribe-v3:{version,latest}` on Docker Hub (amd64) |
|
||||
```
|
||||
feature work ──▶ dev ──▶ (tests pass, you try it) ──▶ main ──▶ deploy
|
||||
```
|
||||
|
||||
| Branch | On push, CI does | Publishes an image |
|
||||
|---|---|---|
|
||||
| `dev` | runs the test suite, then builds the image | no |
|
||||
| `main` | runs the test suite, builds the image, pushes it to the registry | yes |
|
||||
|
||||
`dev` builds the image but does not publish it, so nothing on `dev` can be
|
||||
mistaken for something deployable. Both branches prove the same two things —
|
||||
the tests pass and the image builds — which is the point: by the time a change
|
||||
reaches `main` the only new question is whether it is *right*, not whether it
|
||||
works mechanically.
|
||||
|
||||
Deploying is never automatic. It is the **Deploy** workflow, run by hand from
|
||||
the Actions tab, after you have looked at the change. That is deliberate: the
|
||||
step between "tests pass" and "this is live" is a person deciding, and a push
|
||||
is not a decision. `scripts/deploy.sh` then pins the image, waits for health,
|
||||
asks `/api/build` which revision is actually serving, and rolls back if the
|
||||
answer disagrees.
|
||||
|
||||
To merge up:
|
||||
|
||||
```bash
|
||||
git checkout main && git merge --no-ff dev && git push forgejo main
|
||||
```
|
||||
|
||||
`--no-ff` keeps the merge visible, so a release is one commit to point at and
|
||||
one commit to revert.
|
||||
|
||||
## Local dev
|
||||
|
||||
|
|
@ -43,10 +69,6 @@ automatically on fresh page loads).
|
|||
|
||||
Server code changes require `./scripts/build-image.sh && docker compose up -d --no-build`.
|
||||
|
||||
## Mobile
|
||||
|
||||
See `docs/mobile-build.md`.
|
||||
|
||||
## DB migrations
|
||||
|
||||
`src/db/database.js` is the baseline (idempotent CREATE-IF-NOT-EXISTS). New
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ Primary references:
|
|||
- `docs/deployment.md` — production deployment.
|
||||
- `docs/scaling.md` — scaling priorities and readiness work.
|
||||
- `docs/openid-setup.md` — OIDC provider setup.
|
||||
- `docs/mobile-build.md` — the Capacitor wrapper and app-store build notes.
|
||||
- `docs/ops-docs-ped-ai-and-milvus.md` — operational notes for the retrieval stack.
|
||||
- `docs/improvements.md` — the running list of what to improve next.
|
||||
- `docs/logic/README.md` — the deeper code walkthrough.
|
||||
|
|
|
|||
16
TODO.md
|
|
@ -59,16 +59,16 @@ and green (674 tests, three consecutive clean runs).
|
|||
|
||||
## Deployment readiness (CI/CD and Kubernetes)
|
||||
|
||||
What already exists: `ci.yml` (tests on PR and main), `security.yml` (weekly
|
||||
audit), `docker-publish.yml` and Android/versioning workflows, a Dockerfile
|
||||
`HEALTHCHECK`, and `/api/health`.
|
||||
What already exists: `.forgejo/workflows/docker-build.yml` (tests then image,
|
||||
on `dev` and `main`; publishes from `main` only), `.forgejo/workflows/deploy.yml`
|
||||
(manual dispatch), a Dockerfile `HEALTHCHECK`, and `/api/health`. The GitHub
|
||||
workflows were removed — this repository has no GitHub remote, so none of them
|
||||
ever ran.
|
||||
|
||||
Worth doing before Kubernetes, roughly in order:
|
||||
- [ ] **Fail CI on vulnerabilities.** `security.yml` runs weekly and does not
|
||||
gate merges. `npm audit --audit-level=high` in `ci.yml` would have caught
|
||||
the nodemailer advisories at the PR.
|
||||
- [ ] **Build the image on PRs too.** `docker-publish.yml` only runs on tags, so
|
||||
a Dockerfile break is found at release time.
|
||||
- [ ] **Fail CI on vulnerabilities.** Nothing runs `npm audit` any more; the
|
||||
weekly GitHub job went with the rest. `npm audit --audit-level=high` as a
|
||||
step in `docker-build.yml` would have caught the nodemailer advisories.
|
||||
- [ ] **Run the e2e suite in CI.** `scripts/e2e.sh` and
|
||||
`docker-compose.e2e.yml` exist but nothing calls them.
|
||||
- [ ] **Separate liveness from readiness.** `/api/health` is one endpoint;
|
||||
|
|
|
|||
|
|
@ -61,21 +61,9 @@ public/ # SPA
|
|||
components/ # per-tab HTML fragments
|
||||
css/styles.css
|
||||
|
||||
mobile/ # Capacitor wrapper
|
||||
capacitor.config.json # appId com.pedshub.scribe
|
||||
src/ # launcher (server-URL picker)
|
||||
android/ # generated AS project + native Java
|
||||
|
||||
.forgejo/workflows/
|
||||
android-apk.yml # signed APK on tag push; optional Play upload
|
||||
docker-build.yml # Forgejo registry Docker image build
|
||||
|
||||
.github/workflows/
|
||||
auto-version.yml # conventional-commits → semver bump → tag
|
||||
android-release.yml # legacy GitHub tag APK release path
|
||||
docker-publish.yml # multi-arch image on tag push
|
||||
version-bump.yml # manual dispatch override
|
||||
build-apk.yml # legacy TWA APK
|
||||
docker-build.yml # dev + main: tests, image; main also publishes
|
||||
deploy.yml # manual dispatch: deploy.sh against the host
|
||||
```
|
||||
|
||||
## Request pipeline
|
||||
|
|
|
|||
|
|
@ -222,15 +222,15 @@ Container marked unhealthy after 5 failures.
|
|||
|
||||
## CI / CD
|
||||
|
||||
On push (and tag push), these workflows run (depending on runner/site):
|
||||
Forgejo Actions only; there is no GitHub remote on this repository.
|
||||
|
||||
| Workflow | Output | Runtime |
|
||||
| Workflow | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| `docker-publish.yml` | Multi-arch image (amd64 + arm64 via native runners) on Docker Hub | ~4 min |
|
||||
| `.forgejo/workflows/docker-build.yml` | push to `dev` or `main` | test suite, then build the image. On `main` only, push it to `git.danvics.com/danvics/pediatric-ai-scribe-v3:{revision,latest}` |
|
||||
| `.forgejo/workflows/deploy.yml` | manual dispatch | `scripts/deploy.sh` against the host: pin the image, wait for health, verify `/api/build`, roll back on disagreement |
|
||||
|
||||
Triggered by `auto-version.yml` (reads commit messages, bumps + tags via
|
||||
`RELEASE_PAT`) or manually via `Actions → Version bump & release` or
|
||||
`scripts/release.sh X.Y.Z --push`.
|
||||
Deploying is never automatic — see "Branches" in `CONTRIBUTING.md`. Versioning
|
||||
is manual: `scripts/release.sh X.Y.Z --push`.
|
||||
|
||||
## Ports
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,7 @@ public/
|
|||
css/styles.css
|
||||
template-guide.md downloadable user template guide
|
||||
|
||||
mobile/ Capacitor 6 wrapper (Android + iOS)
|
||||
.github/workflows/ CI (auto-version, APK, docker)
|
||||
.forgejo/workflows/ CI (tests + image on dev/main; manual deploy)
|
||||
```
|
||||
|
||||
## Backend
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
# Mobile Build And Release
|
||||
|
||||
> **The CI build has been removed.** `.forgejo/workflows/android-apk.yml` and
|
||||
> the two GitHub APK workflows are gone: CI now covers the Docker image and the
|
||||
> web app only. The Capacitor project under `mobile/` is still here and still
|
||||
> builds by hand with the Gradle commands below — nothing builds it for you.
|
||||
|
||||
Capacitor 6 wrapper around the hosted Ped-AI web app. The launcher defaults to `https://app.pedshub.com`, lets the user change the server URL, and stores that URL locally. Android is buildable on Linux. The iOS project exists but requires macOS and Xcode to produce an `.ipa`.
|
||||
|
||||
This is not a separate native clinical app. The native shell provides WebView hosting, microphone permission plumbing, secure storage, and mobile packaging for the same authenticated web app.
|
||||
|
||||
## One-time setup
|
||||
|
||||
### Keystore
|
||||
|
||||
```bash
|
||||
keytool -genkeypair -v -keystore ~/pedscribe-release.jks \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 -alias pedscribe
|
||||
```
|
||||
|
||||
Store the password in a password manager. Back up the `.jks` file off the
|
||||
machine. Losing it = can't sign updates; Play Store requires signature
|
||||
continuity (unless you're on Play App Signing).
|
||||
|
||||
### Android Studio (optional, IDE workflow only)
|
||||
|
||||
```bash
|
||||
export CAPACITOR_ANDROID_STUDIO_PATH="/snap/android-studio/current/bin/studio.sh"
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
|
||||
Required repo secrets (set once, via Settings → Secrets and variables → Actions
|
||||
or `gh secret set`):
|
||||
|
||||
- `ANDROID_KEYSTORE_BASE64` — `base64 -w0 ~/pedscribe-release.jks`
|
||||
- `ANDROID_KEYSTORE_PASSWORD`
|
||||
- `ANDROID_KEY_ALIAS` — `pedscribe`
|
||||
- `ANDROID_KEY_PASSWORD`
|
||||
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_B64` — base64 of your Google Play service
|
||||
account JSON (optional). If present, the same tag build also runs `bundleRelease`
|
||||
and uploads the AAB to Play's `internal` track.
|
||||
|
||||
Optional Play Store flow:
|
||||
- Service account must have permissions to edit releases on the app in Play.
|
||||
- Build task is `bundleRelease`, tracked as `com.pedshub.scribe`.
|
||||
- Upload lane is `fastlane/android publish_internal` (under `mobile/android/fastlane`).
|
||||
|
||||
Tag a release:
|
||||
|
||||
```bash
|
||||
# conventional-commits prefix auto-tags (see CONTRIBUTING.md)
|
||||
git commit -m "feat: ..." && git push # auto-version workflow bumps minor
|
||||
git commit -m "fix: ..." && git push # auto-version workflow bumps patch
|
||||
|
||||
# or force an exact version
|
||||
scripts/release.sh X.Y.Z --push
|
||||
```
|
||||
|
||||
APK lands on the Forgejo release. Obtainium can still track
|
||||
`git.danvics.com/danvics/pediatric-ai-scribe-v3` releases automatically.
|
||||
Play Store upload is handled automatically for tagged builds only when
|
||||
`GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_B64` is configured.
|
||||
|
||||
## Local build (fallback / debugging)
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
npm install
|
||||
npx cap sync android
|
||||
cd android
|
||||
./gradlew assembleRelease \
|
||||
-Pandroid.injected.signing.store.file=$HOME/pedscribe-release.jks \
|
||||
-Pandroid.injected.signing.store.password='<pass>' \
|
||||
-Pandroid.injected.signing.key.alias=pedscribe \
|
||||
-Pandroid.injected.signing.key.password='<pass>'
|
||||
```
|
||||
|
||||
Output: `android/app/build/outputs/apk/release/app-release.apk`
|
||||
For Play Store, swap `assembleRelease` → `bundleRelease`; output: `.aab` under
|
||||
`bundle/release/`.
|
||||
|
||||
If web assets or Capacitor config changed, run `npx cap sync android` from `mobile/` before building.
|
||||
|
||||
### Single-quote the password
|
||||
|
||||
Keystore passwords with shell metacharacters (`)`, `$`, `!`, space, etc.) must
|
||||
be single-quoted. Backslash line continuations get eaten by some terminal
|
||||
paste handlers — prefer one-line commands.
|
||||
|
||||
## Reinstall on device
|
||||
|
||||
```bash
|
||||
adb install -r android/app/build/outputs/apk/release/app-release.apk
|
||||
```
|
||||
|
||||
`-r` keeps app data (saved server URL, auth token in Keystore, IndexedDB).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **JDK 17 only.** Newer JDK (21/25) breaks Android Gradle Plugin. Set
|
||||
`org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64` in `~/.gradle/gradle.properties`
|
||||
if the system default is different.
|
||||
- **QEMU multi-arch Docker builds fail** with SIGILL on native modules (argon2).
|
||||
Docker Hub workflow is x86-only. Use a native ARM runner if you need ARM64.
|
||||
- **`npx cap` must run inside `mobile/`**, not repo root.
|
||||
- **Foreground recording on Android 14+** requires `foregroundServiceType="microphone"`
|
||||
in `AndroidManifest.xml` plus the 3-arg `startForeground(id, notif, TYPE_MICROPHONE)`.
|
||||
Already applied.
|
||||
- **Mic "denied" after permission grant** — WebView intercepts the prompt.
|
||||
Fix: long-press app icon → App info → Permissions → Microphone → Allow.
|
||||
|
||||
## Files
|
||||
|
||||
Note on `com.pedshub.scribe`: that's the Android applicationId — the OS-level
|
||||
unique identifier for this app. Chosen by reverse-DNS of `pedshub.com`. It is
|
||||
**not** a reference to the separate PedsHub Quiz app; they share a prefix by
|
||||
coincidence. Don't rename it — Android treats applicationId as the primary
|
||||
key; renaming breaks Play Store update continuity and forces every installed
|
||||
user to uninstall + reinstall.
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `mobile/capacitor.config.json` | appId, name, WebView config, plugin opts |
|
||||
| `mobile/src/` | launcher HTML and server URL entry, defaulting to `https://app.pedshub.com` |
|
||||
| `mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java` | JS bridge + WebView mic permission |
|
||||
| `mobile/android/app/src/main/java/com/pedshub/scribe/AudioRecordingService.java` | foreground service for background recording |
|
||||
| `mobile/android/app/src/main/AndroidManifest.xml` | permissions, intents, backup rules |
|
||||
| `mobile/android/fastlane/Fastfile` | internal Play track upload lane |
|
||||
36
mobile/.gitignore
vendored
|
|
@ -1,36 +0,0 @@
|
|||
# Node / npm — keep package-lock.json for reproducible CI builds,
|
||||
# ignore only the installed tree.
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Capacitor generated files (rewritten by `npx cap sync`)
|
||||
# Keep the *project* (mobile/android/, mobile/ios/) but not the
|
||||
# per-sync mirrors.
|
||||
android/app/src/main/assets/public/
|
||||
android/app/src/main/assets/capacitor.config.json
|
||||
android/app/src/main/assets/capacitor.plugins.json
|
||||
android/app/capacitor.build.gradle
|
||||
android/capacitor.settings.gradle
|
||||
android/capacitor-cordova-android-plugins/
|
||||
|
||||
ios/App/App/public/
|
||||
ios/App/capacitor-cordova-ios-plugins/
|
||||
ios/App/Pods/
|
||||
ios/App/Podfile.lock
|
||||
|
||||
# Android build outputs & local state
|
||||
android/.gradle/
|
||||
android/build/
|
||||
android/app/build/
|
||||
android/app/release/
|
||||
android/local.properties
|
||||
android/app/release/output-metadata.json
|
||||
android/.idea/
|
||||
*.apk
|
||||
*.aab
|
||||
*.jks
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
156
mobile/README.md
|
|
@ -1,156 +0,0 @@
|
|||
# PedScribe Mobile App
|
||||
|
||||
Capacitor mobile wrapper for the hosted Ped-AI web app. The app defaults to `https://app.pedshub.com`, lets users choose a self-hosted server URL, and keeps clinical workflows API-backed through the same Express service as the browser app.
|
||||
|
||||
## Features
|
||||
|
||||
- Hosted web workflow inside a native WebView; server updates reach mobile clients without app-store releases
|
||||
- Configurable server URL (supports self-hosted instances)
|
||||
- Haptic feedback on recording start/stop
|
||||
- Keep screen awake during recording
|
||||
- Deep linking (pedscribe:// and https://app.pedshub.com)
|
||||
- Share intent (receive text/PDFs from other apps)
|
||||
- Push notification support
|
||||
- **Biometric sign-in** (Face ID / Touch ID / fingerprint) — credentials
|
||||
stored in iOS Keychain / Android Keystore, gated by OS biometric.
|
||||
Enrolled on first password sign-in (opt-in prompt). 2FA still applies
|
||||
on top — biometric replaces the password step only.
|
||||
- Android and iOS project scaffolds for store builds
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- Android Studio (for Android builds): `sudo snap install android-studio --classic`
|
||||
- Xcode 15+ (for iOS builds, macOS only)
|
||||
- Apple Developer account ($99/yr for App Store)
|
||||
- Google Play Developer account ($25 one-time)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
npm install
|
||||
npx cap sync
|
||||
```
|
||||
|
||||
## Build Android
|
||||
|
||||
```bash
|
||||
# Open in Android Studio
|
||||
npx cap open android
|
||||
|
||||
# Build menu: Build > Generate Signed Bundle / APK > APK
|
||||
# Sign with your keystore (create one on first build)
|
||||
# APK output: android/app/build/outputs/apk/release/
|
||||
|
||||
# Or build from command line:
|
||||
cd android && ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
## Build iOS (macOS only)
|
||||
|
||||
```bash
|
||||
# Open in Xcode
|
||||
npx cap open ios
|
||||
|
||||
# In Xcode:
|
||||
# 1. Select your team/signing certificate
|
||||
# 2. Product > Archive
|
||||
# 3. Distribute App > App Store Connect
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. App launches with a local launcher page
|
||||
2. First launch: user enters their PedScribe server URL (default: app.pedshub.com)
|
||||
3. URL is saved locally for future launches
|
||||
4. App navigates to the remote web app inside a native WebView
|
||||
5. Native plugins provide background recording, haptics, and push notifications
|
||||
|
||||
### Background Recording
|
||||
|
||||
**Android:** `AudioRecordingService` is a foreground service that:
|
||||
- Acquires a partial wake lock (CPU stays active, screen can sleep)
|
||||
- Shows a persistent notification ("Recording in progress...")
|
||||
- Includes a "Stop Recording" quick action in the notification
|
||||
- Maximum 1-hour wake lock duration
|
||||
|
||||
**iOS:** Uses `UIBackgroundModes: audio` in Info.plist, which tells iOS to keep the app alive for audio capture when backgrounded or screen-locked.
|
||||
|
||||
### Deep Linking
|
||||
|
||||
- `pedscribe://` custom URL scheme opens the app directly
|
||||
- `https://app.pedshub.com` links open in the app instead of the browser (Android App Links)
|
||||
|
||||
### Share Intent (Android)
|
||||
|
||||
Other apps can share text or PDFs directly into PedScribe:
|
||||
- Share a lab result from your email into the Chart Review tab
|
||||
- Share a referral note into the Hospital Course tab
|
||||
|
||||
## Capacitor Plugins Included
|
||||
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| @capacitor/app | App lifecycle management |
|
||||
| @capacitor/haptics | Vibration feedback on recording start/stop |
|
||||
| @capacitor/keyboard | Keyboard management for WebView |
|
||||
| @capacitor/push-notifications | Push notification support |
|
||||
| @capacitor/screen-orientation | Screen orientation control |
|
||||
| @capacitor/share | Native share dialog |
|
||||
| @capacitor/splash-screen | Launch splash screen |
|
||||
| @capacitor/status-bar | Status bar styling |
|
||||
|
||||
## App Structure
|
||||
|
||||
```
|
||||
mobile/
|
||||
capacitor.config.json # Capacitor configuration
|
||||
package.json # Dependencies
|
||||
src/
|
||||
index.html # Launcher page (server URL config)
|
||||
launcher.js # Auto-redirect + native feature init
|
||||
launcher.css # Launcher styles
|
||||
android/ # Android native project
|
||||
app/src/main/
|
||||
java/com/pedshub/scribe/
|
||||
MainActivity.java
|
||||
AudioRecordingService.java
|
||||
AndroidManifest.xml # Permissions, deep links, share intent
|
||||
ios/ # iOS native project
|
||||
App/App/
|
||||
Info.plist # Background audio, microphone, deep links
|
||||
```
|
||||
|
||||
## Updating the Web App
|
||||
|
||||
The mobile app wraps the remote web app — updating the server automatically updates all mobile clients. No app store update needed for web changes.
|
||||
|
||||
To update native features (plugins, permissions, splash screen):
|
||||
```bash
|
||||
cd mobile
|
||||
npm install
|
||||
npx cap sync
|
||||
# Then rebuild in Android Studio / Xcode
|
||||
```
|
||||
|
||||
## Generating App Icons
|
||||
|
||||
Replace the default Capacitor icons with PedScribe branding:
|
||||
|
||||
1. Create a 1024x1024 PNG icon
|
||||
2. Install the assets tool: `npm install -D @capacitor/assets`
|
||||
3. Place your icon as `assets/icon-only.png` and `assets/splash.png`
|
||||
4. Run: `npx capacitor-assets generate`
|
||||
|
||||
This generates all required sizes for both platforms.
|
||||
|
||||
## App Store Listing Suggestions
|
||||
|
||||
**Title:** PedScribe - Pediatric AI Scribe
|
||||
**Subtitle:** Voice-to-Note Clinical Documentation
|
||||
**Category:** Medical
|
||||
**Keywords:** pediatric, scribe, medical, documentation, HPI, SOAP, clinical, AI, voice
|
||||
|
||||
**Description:**
|
||||
PedScribe is an AI-powered clinical documentation tool for pediatric physicians. Record patient encounters, and the AI generates structured medical notes — HPIs, SOAP notes, hospital courses, chart reviews, and more. Includes pediatric calculators, developmental milestone tracking, and a learning hub with quizzes. Self-hosted for maximum privacy with HIPAA-compliant AI providers.
|
||||
101
mobile/android/.gitignore
vendored
|
|
@ -1,101 +0,0 @@
|
|||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
2
mobile/android/app/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
/build/*
|
||||
!/build/.npmkeep
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace "com.pedshub.scribe"
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.pedshub.scribe"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
// Version values below are overwritten by scripts/release.sh from
|
||||
// the root package.json. versionCode auto-increments per release.
|
||||
versionCode 714016
|
||||
versionName "7.14.16"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
try {
|
||||
def servicesJSON = file('google-services.json')
|
||||
if (servicesJSON.text) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
}
|
||||
21
mobile/android/app/proguard-rules.pro
vendored
|
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Biometric login (capacitor-native-biometric). USE_BIOMETRIC is the
|
||||
API 28+ permission; older devices ignore it. No legacy FINGERPRINT
|
||||
entry needed because capacitor-native-biometric targets API 23+. -->
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep linking: pedscribe:// and https://app.pedshub.com -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="pedscribe" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="app.pedshub.com" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share intent: receive text/files from other apps -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".AudioRecordingService"
|
||||
android:foregroundServiceType="microphone"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
</manifest>
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||||
<title>PedScribe</title>
|
||||
<link rel="stylesheet" href="launcher.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="launcher">
|
||||
<!-- Auto-redirect screen (shown when server URL is saved) -->
|
||||
<div id="connecting-screen" style="display:none;">
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
|
||||
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
|
||||
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>PedScribe</h1>
|
||||
<p class="subtitle">Connecting...</p>
|
||||
<div class="spinner"></div>
|
||||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Server URL setup screen -->
|
||||
<div id="setup-screen">
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
|
||||
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
|
||||
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>PedScribe</h1>
|
||||
<p class="subtitle">AI-Powered Pediatric Clinical Documentation</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Server URL</label>
|
||||
<input type="url" id="server-url" placeholder="https://app.pedshub.com" autocapitalize="none" autocorrect="off" spellcheck="false">
|
||||
</div>
|
||||
|
||||
<button id="btn-connect" class="btn-primary">
|
||||
Connect
|
||||
</button>
|
||||
|
||||
<p class="hint">Enter the URL of your Pediatric AI Scribe server. If you don't have one, use the default.</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>Pediatric AI Scribe by PedsHub</p>
|
||||
<p>Committed to healthcare equity</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="launcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #1d4ed8 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.launcher {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.logo-icon svg { width: 100%; height: 100%; }
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input::placeholder { color: rgba(255,255,255,0.4); }
|
||||
.form-group input:focus { border-color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.2); }
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
color: #1d4ed8;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:active { transform: scale(0.98); }
|
||||
.btn-primary:disabled { opacity: 0.5; }
|
||||
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.6);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
margin-top: 16px;
|
||||
font-family: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
font-size: 11px;
|
||||
opacity: 0.3;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid rgba(255,255,255,0.2);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Error state */
|
||||
.error-msg {
|
||||
background: rgba(239,68,68,0.2);
|
||||
border: 1px solid rgba(239,68,68,0.4);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-top: 12px;
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
// PedScribe Mobile Launcher
|
||||
// Handles configurable server URL and auto-redirect
|
||||
|
||||
(function() {
|
||||
var STORAGE_KEY = 'pedscribe_server_url';
|
||||
var DEFAULT_URL = 'https://app.pedshub.com';
|
||||
|
||||
var setupScreen = document.getElementById('setup-screen');
|
||||
var connectingScreen = document.getElementById('connecting-screen');
|
||||
var urlInput = document.getElementById('server-url');
|
||||
var connectBtn = document.getElementById('btn-connect');
|
||||
var changeBtn = document.getElementById('btn-change-server');
|
||||
|
||||
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (savedUrl) {
|
||||
showConnecting(savedUrl);
|
||||
} else {
|
||||
urlInput.value = DEFAULT_URL;
|
||||
showScreen('setup');
|
||||
}
|
||||
|
||||
// Connect button
|
||||
connectBtn.addEventListener('click', function() {
|
||||
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
|
||||
if (!url.startsWith('http')) url = 'https://' + url;
|
||||
|
||||
connectBtn.disabled = true;
|
||||
connectBtn.textContent = 'Connecting...';
|
||||
haptic();
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, url);
|
||||
navigateToServer(url);
|
||||
});
|
||||
|
||||
urlInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') connectBtn.click();
|
||||
});
|
||||
|
||||
// Change server
|
||||
changeBtn.addEventListener('click', function() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
urlInput.value = savedUrl || DEFAULT_URL;
|
||||
showScreen('setup');
|
||||
urlInput.focus();
|
||||
});
|
||||
|
||||
// Screen management
|
||||
function showScreen(which) {
|
||||
setupScreen.style.display = which === 'setup' ? '' : 'none';
|
||||
connectingScreen.style.display = which === 'connecting' ? '' : 'none';
|
||||
}
|
||||
|
||||
function showConnecting(url) {
|
||||
showScreen('connecting');
|
||||
setTimeout(function() { navigateToServer(url); }, 800);
|
||||
}
|
||||
|
||||
function navigateToServer(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function haptic() {
|
||||
try {
|
||||
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
|
||||
window.Capacitor.Plugins.Haptics.impact({ style: 'medium' });
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
})();
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
package com.pedshub.scribe;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
/**
|
||||
* Foreground service that keeps the app alive during audio recording.
|
||||
* Acquires a partial wake lock to prevent CPU sleep during recording.
|
||||
* The Capacitor web app sends a message to start/stop this service when recording.
|
||||
*/
|
||||
public class AudioRecordingService extends Service {
|
||||
|
||||
private static final String CHANNEL_ID = "recording_channel";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
private static final String WAKE_LOCK_TAG = "PedScribe:AudioRecording";
|
||||
|
||||
public static final String ACTION_STOP = "com.pedshub.scribe.STOP_RECORDING";
|
||||
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
createNotificationChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Acquire wake lock to keep CPU active during recording.
|
||||
// 8h cap is a safety net — onDestroy() releases early when recording
|
||||
// stops. The cap prevents a runaway lock if the service leaks.
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
if (pm != null) {
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
|
||||
wakeLock.acquire(8 * 60 * 60 * 1000L);
|
||||
}
|
||||
|
||||
// Stop action in notification
|
||||
Intent stopIntent = new Intent(this, AudioRecordingService.class);
|
||||
stopIntent.setAction(ACTION_STOP);
|
||||
PendingIntent stopPending = PendingIntent.getService(
|
||||
this, 0, stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Pediatric AI Scribe")
|
||||
.setContentText("Recording in progress...")
|
||||
.setSmallIcon(android.R.drawable.ic_btn_speak_now)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.addAction(android.R.drawable.ic_media_pause, "Stop Recording", stopPending)
|
||||
.build();
|
||||
|
||||
// Android 14 (SDK 34) requires the 3-arg form with an explicit
|
||||
// foregroundServiceType matching the manifest declaration, else
|
||||
// the service is killed with MissingForegroundServiceTypeException.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(NOTIFICATION_ID, notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
wakeLock = null;
|
||||
}
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Recording",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("Shows when audio recording is active");
|
||||
channel.setShowBadge(false);
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
package com.pedshub.scribe;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.print.PrintAttributes;
|
||||
import android.print.PrintDocumentAdapter;
|
||||
import android.print.PrintManager;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Base64;
|
||||
import android.view.WindowManager;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.PermissionRequest;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
|
||||
private static final int MIC_PERMISSION_CODE = 1001;
|
||||
private PermissionRequest pendingPermissionRequest;
|
||||
private WebView printWebView;
|
||||
|
||||
// True between startForegroundService() and stopForegroundService(), i.e.
|
||||
// while the web app has an active MediaRecorder. Drives the keep-screen-on
|
||||
// flag and the timer-throttling workaround below.
|
||||
private volatile boolean recordingActive = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Request mic permission upfront
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{ Manifest.permission.RECORD_AUDIO }, MIC_PERMISSION_CODE);
|
||||
}
|
||||
|
||||
// Allow the Cloudflare Turnstile iframe to use storage.
|
||||
setupThirdPartyCookies();
|
||||
|
||||
// Setup WebView mic permission granting
|
||||
setupWebViewPermissions();
|
||||
|
||||
// Register JS interface for foreground service control
|
||||
setupRecordingBridge();
|
||||
|
||||
// Register JS interface for Android's print / Save as PDF flow.
|
||||
setupPrintBridge();
|
||||
|
||||
// Register JS interface for saving generated visuals to Photos.
|
||||
setupFileBridge();
|
||||
}
|
||||
|
||||
// ── Recording Lifecycle ────────────────────────────────────
|
||||
//
|
||||
// Recording happens in the WebView (MediaRecorder), not in native code,
|
||||
// so keeping the foreground service alive is necessary but not sufficient
|
||||
// — the WebView also has to keep executing JS. Two things protect that:
|
||||
//
|
||||
// 1. FLAG_KEEP_SCREEN_ON while recording, so the device does not
|
||||
// auto-lock mid-encounter. This is the case that actually bites
|
||||
// clinicians: a long pause in conversation and the screen times out.
|
||||
//
|
||||
// 2. resumeTimers() if the activity is paused anyway (user presses the
|
||||
// power button, or a call comes in). Chromium throttles timers hard
|
||||
// for hidden WebViews, which starves MediaRecorder's chunk delivery.
|
||||
// Capacitor never calls webView.onPause(), so the WebView itself is
|
||||
// still live — it is only the timers that need rescuing.
|
||||
//
|
||||
// Note resumeTimers()/pauseTimers() are process-global in WebView, not
|
||||
// per-instance; calling resume here is safe because this app has no other
|
||||
// WebView that wants throttling (printWebView is transient).
|
||||
|
||||
void setKeepScreenOn(final boolean on) {
|
||||
runOnUiThread(() -> {
|
||||
if (on) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setRecordingActive(boolean active) {
|
||||
recordingActive = active;
|
||||
setKeepScreenOn(active);
|
||||
}
|
||||
|
||||
// NB: BridgeActivity declares these public — narrowing to protected would
|
||||
// not compile.
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (recordingActive && this.bridge != null && this.bridge.getWebView() != null) {
|
||||
this.bridge.getWebView().resumeTimers();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (this.bridge != null && this.bridge.getWebView() != null) {
|
||||
this.bridge.getWebView().resumeTimers();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Third-Party Cookies ────────────────────────────────────
|
||||
//
|
||||
// Android WebView blocks third-party cookies by default (unlike Chrome,
|
||||
// which still allows them for now). Cloudflare Turnstile runs inside a
|
||||
// cross-origin iframe from challenges.cloudflare.com and needs its own
|
||||
// storage to run and persist a challenge — without this the widget
|
||||
// silently stalls or errors and never emits a token, so registration and
|
||||
// password reset are impossible from inside the app.
|
||||
//
|
||||
// This is scoped to our own WebView, which only ever loads the PedScribe
|
||||
// origin (see allowNavigation in capacitor.config.json), so it is not a
|
||||
// general relaxation of the app's cookie policy.
|
||||
|
||||
private void setupThirdPartyCookies() {
|
||||
WebView webView = this.bridge.getWebView();
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
cookieManager.setAcceptCookie(true);
|
||||
cookieManager.setAcceptThirdPartyCookies(webView, true);
|
||||
}
|
||||
|
||||
// ── WebView Microphone Permission ──────────────────────────
|
||||
|
||||
private void setupWebViewPermissions() {
|
||||
WebView webView = this.bridge.getWebView();
|
||||
final MainActivity activity = this;
|
||||
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public void onPermissionRequest(final PermissionRequest request) {
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
activity.runOnUiThread(() -> request.grant(request.getResources()));
|
||||
} else {
|
||||
pendingPermissionRequest = request;
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{ Manifest.permission.RECORD_AUDIO }, MIC_PERMISSION_CODE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == MIC_PERMISSION_CODE && pendingPermissionRequest != null) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
final PermissionRequest req = pendingPermissionRequest;
|
||||
runOnUiThread(() -> req.grant(req.getResources()));
|
||||
} else {
|
||||
pendingPermissionRequest.deny();
|
||||
}
|
||||
pendingPermissionRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Background Recording Service Bridge ───────────────────
|
||||
|
||||
private void setupRecordingBridge() {
|
||||
WebView webView = this.bridge.getWebView();
|
||||
webView.addJavascriptInterface(new RecordingBridge(this), "NativeRecording");
|
||||
}
|
||||
|
||||
private void setupPrintBridge() {
|
||||
WebView webView = this.bridge.getWebView();
|
||||
webView.addJavascriptInterface(new PrintBridge(this), "NativePrint");
|
||||
}
|
||||
|
||||
private void setupFileBridge() {
|
||||
WebView webView = this.bridge.getWebView();
|
||||
webView.addJavascriptInterface(new FileBridge(this), "NativeFiles");
|
||||
}
|
||||
|
||||
public static class RecordingBridge {
|
||||
private final MainActivity activity;
|
||||
|
||||
RecordingBridge(MainActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
public void startForegroundService() {
|
||||
Intent intent = new Intent(activity, AudioRecordingService.class);
|
||||
ContextCompat.startForegroundService(activity, intent);
|
||||
activity.setRecordingActive(true);
|
||||
}
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
public void stopForegroundService() {
|
||||
Intent intent = new Intent(activity, AudioRecordingService.class);
|
||||
intent.setAction(AudioRecordingService.ACTION_STOP);
|
||||
activity.startService(intent);
|
||||
activity.setRecordingActive(false);
|
||||
}
|
||||
|
||||
// Standalone keep-awake, exposed so the web app can hold the screen on
|
||||
// for non-recording work too. window.nativeKeepAwake() previously
|
||||
// called Capacitor's KeepAwake plugin, which is not installed in this
|
||||
// project — so it silently did nothing and the screen slept during
|
||||
// recordings.
|
||||
@android.webkit.JavascriptInterface
|
||||
public void keepAwake(boolean on) {
|
||||
activity.setKeepScreenOn(on);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PrintBridge {
|
||||
private final MainActivity activity;
|
||||
|
||||
PrintBridge(MainActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
public void printHtml(String title, String base64Html) {
|
||||
activity.runOnUiThread(() -> activity.printHtmlFromBase64(title, base64Html));
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileBridge {
|
||||
private final MainActivity activity;
|
||||
|
||||
FileBridge(MainActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
public String saveImage(String filename, String base64Png) {
|
||||
return activity.saveImageToPictures(filename, base64Png);
|
||||
}
|
||||
}
|
||||
|
||||
private void printHtmlFromBase64(String title, String base64Html) {
|
||||
try {
|
||||
byte[] decoded = Base64.decode(base64Html, Base64.DEFAULT);
|
||||
String html = new String(decoded, java.nio.charset.StandardCharsets.UTF_8);
|
||||
printWebView = new WebView(this);
|
||||
printWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
|
||||
PrintDocumentAdapter adapter = view.createPrintDocumentAdapter(title != null && !title.isEmpty() ? title : "Clinical Assistant Export");
|
||||
printManager.print(title != null && !title.isEmpty() ? title : "Clinical Assistant Export", adapter, new PrintAttributes.Builder().build());
|
||||
}
|
||||
});
|
||||
printWebView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("PedScribe", "Native print failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String saveImageToPictures(String filename, String base64Png) {
|
||||
String safeName = sanitizeFilename(filename, "clinical-visual.png");
|
||||
try {
|
||||
byte[] imageBytes = Base64.decode(base64Png, Base64.DEFAULT);
|
||||
Uri uri;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ContentResolver resolver = getContentResolver();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.Images.Media.DISPLAY_NAME, safeName);
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
|
||||
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/PedScribe");
|
||||
values.put(MediaStore.Images.Media.IS_PENDING, 1);
|
||||
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
|
||||
if (uri == null) return "error:Could not create image file";
|
||||
try (OutputStream out = resolver.openOutputStream(uri)) {
|
||||
if (out == null) return "error:Could not open image file";
|
||||
out.write(imageBytes);
|
||||
}
|
||||
values.clear();
|
||||
values.put(MediaStore.Images.Media.IS_PENDING, 0);
|
||||
resolver.update(uri, values, null, null);
|
||||
} else {
|
||||
File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "PedScribe");
|
||||
if (!dir.exists() && !dir.mkdirs()) return "error:Could not create Pictures/PedScribe";
|
||||
File file = new File(dir, safeName);
|
||||
try (OutputStream out = new FileOutputStream(file)) {
|
||||
out.write(imageBytes);
|
||||
}
|
||||
uri = Uri.fromFile(file);
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
|
||||
}
|
||||
return "saved:" + uri.toString();
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("PedScribe", "Native image save failed", e);
|
||||
return "error:" + (e.getMessage() != null ? e.getMessage() : "Image save failed");
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeFilename(String filename, String fallback) {
|
||||
String value = filename != null ? filename : fallback;
|
||||
value = value.replaceAll("[^A-Za-z0-9._-]", "-");
|
||||
if (value.length() == 0) value = fallback;
|
||||
if (!value.toLowerCase(java.util.Locale.US).endsWith(".png")) value = value + ".png";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
|
@ -1,34 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<WebView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#2563eb</color>
|
||||
<color name="colorPrimaryDark">#1d4ed8</color>
|
||||
<color name="colorAccent">#2563eb</color>
|
||||
</resources>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">PedScribe</string>
|
||||
<string name="title_activity_main">PedScribe</string>
|
||||
<string name="package_name">com.pedshub.scribe</string>
|
||||
<string name="custom_url_scheme">com.pedshub.scribe</string>
|
||||
</resources>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<access origin="*" />
|
||||
|
||||
|
||||
</widget>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Android 12+ backup rules. Exclude everything from both cloud backup
|
||||
and device-to-device transfer. The app handles PHI-adjacent data
|
||||
(Keychain/Keystore-backed auth token, cached web content) that must
|
||||
not leave the device via Google's automatic backup system.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="root" />
|
||||
<exclude domain="file" />
|
||||
<exclude domain="database" />
|
||||
<exclude domain="sharedpref" />
|
||||
<exclude domain="external" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" />
|
||||
<exclude domain="file" />
|
||||
<exclude domain="database" />
|
||||
<exclude domain="sharedpref" />
|
||||
<exclude domain="external" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
<files-path name="my_files" path="." />
|
||||
<external-files-path name="my_external_files" path="." />
|
||||
</paths>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "variables.gradle"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
json_key_file('fastlane/google-play-service-account.json')
|
||||
package_name('com.pedshub.scribe')
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
desc "Upload a signed release AAB to Google Play internal track"
|
||||
lane :publish_internal do
|
||||
upload_to_play_store(
|
||||
package_name: 'com.pedshub.scribe',
|
||||
json_key: 'fastlane/google-play-service-account.json',
|
||||
aab: ENV['AAB_PATH'] || 'app/build/outputs/bundle/release/app-release.aab',
|
||||
track: ENV['PLAY_TRACK'] || 'internal',
|
||||
skip_upload_changelogs: true,
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
release_status: 'completed',
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'fastlane'
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
BIN
mobile/android/gradle/wrapper/gradle-wrapper.jar
vendored
|
|
@ -1,7 +0,0 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
248
mobile/android/gradlew
vendored
|
|
@ -1,248 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
mobile/android/gradlew.bat
vendored
|
|
@ -1,92 +0,0 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
include ':app'
|
||||
include ':capacitor-cordova-android-plugins'
|
||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||
|
||||
apply from: 'capacitor.settings.gradle'
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
ext {
|
||||
minSdkVersion = 22
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
androidxActivityVersion = '1.8.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.12.0'
|
||||
androidxFragmentVersion = '1.6.2'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.9.0'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"appId": "com.pedshub.scribe",
|
||||
"appName": "PedScribe",
|
||||
"webDir": "src",
|
||||
"server": {
|
||||
"allowNavigation": [
|
||||
"app.pedshub.com",
|
||||
"*.pedshub.com",
|
||||
"pedshub.com",
|
||||
"peds.danvics.com"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"SplashScreen": {
|
||||
"launchShowDuration": 1500,
|
||||
"backgroundColor": "#2563eb",
|
||||
"showSpinner": true,
|
||||
"spinnerColor": "#ffffff"
|
||||
},
|
||||
"PushNotifications": {
|
||||
"presentationOptions": ["badge", "sound", "alert"]
|
||||
},
|
||||
"Keyboard": {
|
||||
"resize": "body",
|
||||
"resizeOnFullScreen": true
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"allowMixedContent": false,
|
||||
"backgroundColor": "#2563eb",
|
||||
"webContentsDebuggingEnabled": false,
|
||||
"appendUserAgent": "PedScribe-Android",
|
||||
"androidScheme": "https"
|
||||
},
|
||||
"ios": {
|
||||
"backgroundColor": "#2563eb",
|
||||
"contentInset": "automatic"
|
||||
}
|
||||
}
|
||||
13
mobile/ios/.gitignore
vendored
|
|
@ -1,13 +0,0 @@
|
|||
App/build
|
||||
App/Pods
|
||||
App/output
|
||||
App/App/public
|
||||
DerivedData
|
||||
xcuserdata
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
|
|
@ -1,408 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 48;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 0920;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
packageReferences = (
|
||||
);
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.pedshub.scribe;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.pedshub.scribe;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 108 KiB |
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-512@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>PedScribe</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>PedScribe uses Face ID to securely sign you in without re-entering your password.</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>PedScribe needs microphone access to record clinical encounters for AI-powered documentation.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>pedscribe</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
{
|
||||
"appId": "com.pedshub.scribe",
|
||||
"appName": "PedScribe",
|
||||
"webDir": "src",
|
||||
"server": {
|
||||
"allowNavigation": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"SplashScreen": {
|
||||
"launchShowDuration": 1500,
|
||||
"backgroundColor": "#2563eb",
|
||||
"showSpinner": true,
|
||||
"spinnerColor": "#ffffff"
|
||||
},
|
||||
"PushNotifications": {
|
||||
"presentationOptions": [
|
||||
"badge",
|
||||
"sound",
|
||||
"alert"
|
||||
]
|
||||
},
|
||||
"Keyboard": {
|
||||
"resize": "body",
|
||||
"resizeOnFullScreen": true
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"allowMixedContent": true,
|
||||
"backgroundColor": "#2563eb",
|
||||
"webContentsDebuggingEnabled": true,
|
||||
"appendUserAgent": "PedScribe-Android",
|
||||
"androidScheme": "https"
|
||||
},
|
||||
"ios": {
|
||||
"backgroundColor": "#2563eb",
|
||||
"contentInset": "automatic"
|
||||
},
|
||||
"packageClassList": [
|
||||
"AppPlugin",
|
||||
"BiometricAuthNative",
|
||||
"HapticsPlugin",
|
||||
"KeyboardPlugin",
|
||||
"PushNotificationsPlugin",
|
||||
"ScreenOrientationPlugin",
|
||||
"SharePlugin",
|
||||
"SplashScreenPlugin",
|
||||
"StatusBarPlugin"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<access origin="*" />
|
||||
|
||||
|
||||
</widget>
|
||||
0
mobile/ios/App/App/public/cordova.js
vendored
0
mobile/ios/App/App/public/cordova_plugins.js
vendored
|
|
@ -1,59 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||||
<title>PedScribe</title>
|
||||
<link rel="stylesheet" href="launcher.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="launcher">
|
||||
<!-- Auto-redirect screen (shown when server URL is saved) -->
|
||||
<div id="connecting-screen" style="display:none;">
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
|
||||
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
|
||||
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>PedScribe</h1>
|
||||
<p class="subtitle">Connecting...</p>
|
||||
<div class="spinner"></div>
|
||||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Server URL setup screen -->
|
||||
<div id="setup-screen">
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
|
||||
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
|
||||
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>PedScribe</h1>
|
||||
<p class="subtitle">AI-Powered Pediatric Clinical Documentation</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Server URL</label>
|
||||
<input type="url" id="server-url" placeholder="https://app.pedshub.com" autocapitalize="none" autocorrect="off" spellcheck="false">
|
||||
</div>
|
||||
|
||||
<button id="btn-connect" class="btn-primary">
|
||||
Connect
|
||||
</button>
|
||||
|
||||
<p class="hint">Enter the URL of your Pediatric AI Scribe server. If you don't have one, use the default.</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>Pediatric AI Scribe by PedsHub</p>
|
||||
<p>Committed to healthcare equity</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="launcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||