Add AirPlay button, update build script to 1.7.0
- AirPlay button in player (WebKit AirPlay API + Remote Playback fallback) - x-webkit-airplay=allow on audio element - build-and-push.sh synced from shared master (1.2.0 -> 1.7.0) - .gitignore for .files/, apple-touch-icon.png Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0d9f20690f
commit
a8452f90df
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.files/
|
||||||
@ -6,12 +6,24 @@ set -euo pipefail
|
|||||||
#
|
#
|
||||||
# Purpose:
|
# Purpose:
|
||||||
# - Build & push Docker images for each service under ./containers/*
|
# - Build & push Docker images for each service under ./containers/*
|
||||||
|
# - Branch model: `main` is permanent and tracked by the prod stack
|
||||||
|
# (image `:latest`). `dev` is a short-lived branch tracked by the dev
|
||||||
|
# stack (image `:dev`); it is recreated from `main` at the start of each
|
||||||
|
# cycle and deleted after each release. Tags `vX.Y.Z` cover rollback —
|
||||||
|
# no `release/<version>` snapshot branches.
|
||||||
# - Two modes:
|
# - Two modes:
|
||||||
# t (test) = only push :dev
|
# t (test) = build & push :dev. Switches to (or creates) the `dev`
|
||||||
# r (release) = push :<version>, :dev, :latest
|
# branch from `main` if not already on it, commits any
|
||||||
# version is read from the top of changelog.md
|
# pending changes, and pushes `dev` to origin so the dev
|
||||||
#
|
# stack picks up the matching compose/config alongside
|
||||||
# No git operations: committing and tagging is done manually.
|
# the :dev image.
|
||||||
|
# r (release) = MUST be run from `main`. Build & push :<version>,
|
||||||
|
# :dev, :latest. Fast-forwards `main` against
|
||||||
|
# `origin/main`, merges `dev` into `main` if it exists
|
||||||
|
# (local or remote), creates a `Release vX.Y.Z` commit,
|
||||||
|
# tags it, pushes `main` and the tag, and deletes the
|
||||||
|
# `dev` branch locally and on origin. Version is read
|
||||||
|
# from the top of changelog.md.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./build-and-push.sh [mode]
|
# ./build-and-push.sh [mode]
|
||||||
@ -25,14 +37,112 @@ set -euo pipefail
|
|||||||
# ## vX.Y.Z — YYYY-MM-DD
|
# ## vX.Y.Z — YYYY-MM-DD
|
||||||
# (the version is parsed from the first such line)
|
# (the version is parsed from the first such line)
|
||||||
# - One Dockerfile per service under ./containers/<service>/Dockerfile
|
# - One Dockerfile per service under ./containers/<service>/Dockerfile
|
||||||
|
#
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Self-update notice (for humans AND for Claude):
|
||||||
|
#
|
||||||
|
# The canonical/master copy of this script lives at:
|
||||||
|
# /docker/develop/shared-integrations/tooling/docker-build-and-push/build-and-push.sh
|
||||||
|
#
|
||||||
|
# When this script is copied into a project repo, it may drift from master.
|
||||||
|
# On startup the script compares its own SCRIPT_VERSION to the master copy and
|
||||||
|
# warns if a newer version exists. It does NOT auto-overwrite (too risky during
|
||||||
|
# a release run).
|
||||||
|
#
|
||||||
|
# CLAUDE INSTRUCTION:
|
||||||
|
# If you (Claude) are about to invoke this script in a project repo, first
|
||||||
|
# check whether the master copy at the path above is newer (different
|
||||||
|
# SCRIPT_VERSION or differing file contents). If so, copy the master over
|
||||||
|
# the local copy BEFORE running, then proceed. Mention the update to the user.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
SCRIPT_VERSION="1.7.0"
|
||||||
|
SCRIPT_MASTER_PATH="/docker/develop/shared-integrations/tooling/docker-build-and-push/build-and-push.sh"
|
||||||
|
|
||||||
DOCKER_REGISTRY="gitea.oskamp.info"
|
DOCKER_REGISTRY="gitea.oskamp.info"
|
||||||
DOCKER_NAMESPACE="ivooskamp"
|
DOCKER_NAMESPACE="ivooskamp"
|
||||||
|
|
||||||
CHANGELOG_FILE="docs/changelog.md"
|
CHANGELOG_FILE="docs/changelog.md"
|
||||||
CONTAINERS_DIR="containers"
|
CONTAINERS_DIR="containers"
|
||||||
|
|
||||||
|
# --- Self-update check -------------------------------------------------------
|
||||||
|
# Compare this script to the canonical master copy. If it differs, offer to
|
||||||
|
# copy master over the local copy and re-exec with the same arguments so the
|
||||||
|
# build runs against the up-to-date script.
|
||||||
|
#
|
||||||
|
# Skip with: SKIP_SELF_UPDATE=1 ./build-and-push.sh ...
|
||||||
|
self_update_check() {
|
||||||
|
if [[ "${SKIP_SELF_UPDATE:-0}" == "1" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local self_path="${BASH_SOURCE[0]}"
|
||||||
|
# Resolve to absolute path so a comparison against itself is detected.
|
||||||
|
local self_abs
|
||||||
|
self_abs="$(cd "$(dirname "$self_path")" 2>/dev/null && pwd)/$(basename "$self_path")" || self_abs="$self_path"
|
||||||
|
|
||||||
|
if [[ "$self_abs" == "$SCRIPT_MASTER_PATH" ]]; then
|
||||||
|
return 0 # We ARE the master copy.
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$SCRIPT_MASTER_PATH" ]]; then
|
||||||
|
return 0 # Master not reachable from this host; silently skip.
|
||||||
|
fi
|
||||||
|
|
||||||
|
local master_version reason=""
|
||||||
|
master_version="$(grep -m1 -E '^SCRIPT_VERSION=' "$SCRIPT_MASTER_PATH" | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||||
|
|
||||||
|
if [[ -n "$master_version" && "$master_version" != "$SCRIPT_VERSION" ]]; then
|
||||||
|
reason="version"
|
||||||
|
elif ! cmp -s "$self_abs" "$SCRIPT_MASTER_PATH"; then
|
||||||
|
reason="contents"
|
||||||
|
else
|
||||||
|
return 0 # Identical to master.
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[WARN] Local build-and-push.sh differs from master."
|
||||||
|
if [[ "$reason" == "version" ]]; then
|
||||||
|
echo " local : $SCRIPT_VERSION"
|
||||||
|
echo " master : $master_version ($SCRIPT_MASTER_PATH)"
|
||||||
|
else
|
||||||
|
echo " Same SCRIPT_VERSION ($SCRIPT_VERSION) but file contents differ."
|
||||||
|
echo " master : $SCRIPT_MASTER_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt only when stdin is a TTY; in non-interactive runs, abort safely so
|
||||||
|
# an unattended release never silently runs against a stale script.
|
||||||
|
if [[ ! -t 0 ]]; then
|
||||||
|
echo "[ERROR] Non-interactive shell — refusing to auto-update."
|
||||||
|
echo " Re-run interactively, or set SKIP_SELF_UPDATE=1 to bypass,"
|
||||||
|
echo " or update manually: cp \"$SCRIPT_MASTER_PATH\" \"$self_abs\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local reply
|
||||||
|
read -r -p "Update local script from master and re-run? [Y/n] " reply
|
||||||
|
reply="${reply:-Y}"
|
||||||
|
if [[ ! "$reply" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "[INFO] Continuing with local version $SCRIPT_VERSION (not updated)."
|
||||||
|
echo ""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! cp "$SCRIPT_MASTER_PATH" "$self_abs"; then
|
||||||
|
echo "[ERROR] Failed to copy master to $self_abs (read-only filesystem?)."
|
||||||
|
echo " Continuing with local version $SCRIPT_VERSION."
|
||||||
|
echo ""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
chmod +x "$self_abs" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "[INFO] Updated $self_abs from master. Re-executing..."
|
||||||
|
echo ""
|
||||||
|
# Re-exec with original arguments. SKIP_SELF_UPDATE=1 prevents an
|
||||||
|
# update loop if cp somehow didn't take.
|
||||||
|
export SKIP_SELF_UPDATE=1
|
||||||
|
exec "$self_abs" "$@"
|
||||||
|
}
|
||||||
|
self_update_check "$@"
|
||||||
|
|
||||||
# --- Input: prompt if missing ------------------------------------------------
|
# --- Input: prompt if missing ------------------------------------------------
|
||||||
MODE="${1:-}"
|
MODE="${1:-}"
|
||||||
if [[ -z "${MODE}" ]]; then
|
if [[ -z "${MODE}" ]]; then
|
||||||
@ -141,19 +251,86 @@ else
|
|||||||
echo "[INFO] Repo: $(pwd) (not a git checkout)"
|
echo "[INFO] Repo: $(pwd) (not a git checkout)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Determine version (release only) ----------------------------------------
|
# --- Release preflight (BEFORE any docker work) ------------------------------
|
||||||
|
# All git-side validation for a release happens here so a wrong-branch / dirty
|
||||||
|
# tree / stale main / conflicting dev / pre-existing tag aborts the run before
|
||||||
|
# anything is built or pushed to the registry. dev is merged into main now so
|
||||||
|
# the version we read from changelog.md reflects the merged state, not main's
|
||||||
|
# pre-merge state.
|
||||||
VERSION=""
|
VERSION=""
|
||||||
|
DEV_MERGED=0
|
||||||
if [[ "$MODE" == "r" ]]; then
|
if [[ "$MODE" == "r" ]]; then
|
||||||
|
if [[ ! -d ".git" ]]; then
|
||||||
|
echo "[ERROR] Release mode requires a git checkout."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_BRANCH="$(git symbolic-ref --short -q HEAD || echo)"
|
||||||
|
if [[ "$CURRENT_BRANCH" != "main" ]]; then
|
||||||
|
echo "[ERROR] Release build must run from 'main' branch. Current: ${CURRENT_BRANCH:-<detached>}."
|
||||||
|
echo " Switch with: git checkout main"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git diff --quiet HEAD -- || ! git diff --cached --quiet; then
|
||||||
|
echo "[ERROR] Working tree has uncommitted changes. Commit or stash them on the appropriate branch before releasing."
|
||||||
|
git status --short
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Fetching origin..."
|
||||||
|
git fetch origin main
|
||||||
|
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
|
||||||
|
git fetch origin dev
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git merge --ff-only origin/main 2>/dev/null; then
|
||||||
|
echo "[ERROR] Local main has diverged from origin/main. Resolve manually before releasing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Merge dev into main BEFORE reading the version, so changelog.md reflects
|
||||||
|
# the bumped state that dev brings in.
|
||||||
|
if git show-ref --verify --quiet refs/heads/dev; then
|
||||||
|
echo "[INFO] Merging local dev into main..."
|
||||||
|
if ! git merge --no-ff dev -m "Release (merge dev)"; then
|
||||||
|
echo "[ERROR] Merge of dev into main failed (conflict). Resolve manually and re-run."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
DEV_MERGED=1
|
||||||
|
elif git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
|
||||||
|
echo "[INFO] Fetching and merging origin/dev into main..."
|
||||||
|
git fetch origin dev:dev
|
||||||
|
if ! git merge --no-ff dev -m "Release (merge dev)"; then
|
||||||
|
echo "[ERROR] Merge of dev into main failed (conflict). Resolve manually and re-run."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
DEV_MERGED=1
|
||||||
|
else
|
||||||
|
echo "[INFO] No dev branch found — releasing main as-is."
|
||||||
|
fi
|
||||||
|
|
||||||
VERSION="$(read_version_from_changelog)"
|
VERSION="$(read_version_from_changelog)"
|
||||||
echo "[INFO] Release version (from $CHANGELOG_FILE): $VERSION"
|
echo "[INFO] Release version (from $CHANGELOG_FILE, post-merge): $VERSION"
|
||||||
validate_tag "$VERSION"
|
validate_tag "$VERSION"
|
||||||
validate_tag "latest"
|
validate_tag "latest"
|
||||||
|
|
||||||
|
# Tag collision = abort. A re-release of an existing version with different
|
||||||
|
# content would silently move what consumers think v0.X.Y points to.
|
||||||
|
if git rev-parse -q --verify "refs/tags/${VERSION}" >/dev/null; then
|
||||||
|
echo "[ERROR] Tag ${VERSION} already exists locally. Bump $CHANGELOG_FILE to a new version before releasing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if git ls-remote --exit-code --tags origin "refs/tags/${VERSION}" >/dev/null 2>&1; then
|
||||||
|
echo "[ERROR] Tag ${VERSION} already exists on origin. Bump $CHANGELOG_FILE to a new version before releasing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Ask for confirmation so you never accidentally re-push an old version or a wrong one.
|
# Ask for confirmation so you never accidentally re-push an old version or a wrong one.
|
||||||
read -r -p "Proceed building & pushing as ${VERSION}? [y/N] " CONFIRM
|
read -r -p "Proceed building & pushing as ${VERSION}? [y/N] " CONFIRM
|
||||||
CONFIRM="${CONFIRM:-N}"
|
CONFIRM="${CONFIRM:-N}"
|
||||||
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
|
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
|
||||||
echo "[INFO] Aborted by user."
|
echo "[INFO] Aborted by user. Note: dev has been merged into local main; reset with 'git reset --hard origin/main' if you want to undo."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@ -227,10 +404,79 @@ for img in "${BUILT_IMAGES[@]}"; do
|
|||||||
done
|
done
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo "[REMINDER] No git operations were performed. If this was a release,"
|
|
||||||
echo " commit and tag manually, e.g.:"
|
# --- Git: release commit + tag + push (release mode only) -------------------
|
||||||
|
# Preflight (branch, clean tree, ff origin/main, dev merge, tag collision,
|
||||||
|
# version parse) already ran BEFORE the build. dev is already merged into
|
||||||
|
# local main. We only need to land the Release commit, tag, and push.
|
||||||
if [[ "$MODE" == "r" ]]; then
|
if [[ "$MODE" == "r" ]]; then
|
||||||
echo " git add -A && git commit -m \"Release ${VERSION}\""
|
echo "[INFO] Finalising release: version=${VERSION}"
|
||||||
echo " git tag -a ${VERSION} -m \"Release ${VERSION}\""
|
|
||||||
echo " git push && git push --tags"
|
# Produce a clean Release commit at the tip. Preflight guarantees the working
|
||||||
|
# tree was clean at start; any post-build artefacts would be unexpected, so
|
||||||
|
# commit with --allow-empty to keep the release marker isolated.
|
||||||
|
if git diff --quiet HEAD -- && git diff --cached --quiet; then
|
||||||
|
git commit --allow-empty -m "Release ${VERSION}"
|
||||||
|
else
|
||||||
|
echo "[WARN] Working tree changed during the build — staging and including in release commit."
|
||||||
|
git add -A
|
||||||
|
git commit -m "Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag -a "${VERSION}" -m "Release ${VERSION}"
|
||||||
|
|
||||||
|
# Push main first (triggers prod webhook), then the tag.
|
||||||
|
git push origin main
|
||||||
|
git push origin "refs/tags/${VERSION}"
|
||||||
|
echo "[INFO] Pushed main and tag ${VERSION} to origin."
|
||||||
|
|
||||||
|
# Clean up dev branch — local and remote.
|
||||||
|
if [[ "$DEV_MERGED" == "1" ]]; then
|
||||||
|
if git show-ref --verify --quiet refs/heads/dev; then
|
||||||
|
git branch -D dev
|
||||||
|
echo "[INFO] Deleted local dev branch."
|
||||||
|
fi
|
||||||
|
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
|
||||||
|
git push origin --delete dev
|
||||||
|
echo "[INFO] Deleted remote dev branch."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Git: dev branch commit + push (test mode only) -------------------------
|
||||||
|
if [[ "$MODE" == "t" ]]; then
|
||||||
|
if [[ ! -d ".git" ]]; then
|
||||||
|
echo "[WARN] Not a git checkout — skipping dev branch commit/push."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_BRANCH="$(git symbolic-ref --short -q HEAD || echo)"
|
||||||
|
|
||||||
|
# Ensure we are on the dev branch. Create it if needed.
|
||||||
|
if [[ "$CURRENT_BRANCH" != "dev" ]]; then
|
||||||
|
if git show-ref --verify --quiet refs/heads/dev; then
|
||||||
|
echo "[INFO] Switching to existing local dev branch."
|
||||||
|
git checkout dev
|
||||||
|
elif git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
|
||||||
|
echo "[INFO] Checking out remote dev branch."
|
||||||
|
git fetch origin dev
|
||||||
|
git checkout -b dev origin/dev
|
||||||
|
else
|
||||||
|
echo "[INFO] Creating new dev branch from main."
|
||||||
|
git fetch origin main
|
||||||
|
git checkout -b dev origin/main
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage and commit if there are changes.
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "[INFO] Working tree clean — pushing current HEAD to dev."
|
||||||
|
else
|
||||||
|
git commit -m "Dev build $(date '+%Y-%m-%d %H:%M')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Non-force push. Diverged origin/dev fails hard — resolve manually.
|
||||||
|
git push -u origin dev
|
||||||
|
echo "[INFO] Pushed dev to origin."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -127,6 +127,30 @@ if ("mediaSession" in navigator) {
|
|||||||
navigator.mediaSession.setActionHandler("nexttrack", () => setTrack(currentIndex + 1, true));
|
navigator.mediaSession.setActionHandler("nexttrack", () => setTrack(currentIndex + 1, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── AirPlay / Remote Playback ─────────────────────────────────────────────────
|
||||||
|
const airplayBtn = document.querySelector("#airplay");
|
||||||
|
|
||||||
|
function initAirplay() {
|
||||||
|
if (!airplayBtn) return;
|
||||||
|
if (window.WebKitPlaybackTargetAvailabilityEvent) {
|
||||||
|
// Safari (iOS/macOS): native AirPlay picker
|
||||||
|
player.addEventListener("webkitplaybacktargetavailabilitychanged", (e) => {
|
||||||
|
airplayBtn.hidden = e.availability !== "available";
|
||||||
|
});
|
||||||
|
player.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", () => {
|
||||||
|
airplayBtn.classList.toggle("airplay-active", player.webkitCurrentPlaybackTargetIsWireless);
|
||||||
|
});
|
||||||
|
airplayBtn.addEventListener("click", () => player.webkitShowPlaybackTargetPicker());
|
||||||
|
} else if (player.remote && player.remote.watchAvailability) {
|
||||||
|
// Remote Playback API (Chrome/Edge)
|
||||||
|
player.remote.watchAvailability(available => { airplayBtn.hidden = !available; }).catch(() => {});
|
||||||
|
player.remote.addEventListener("connect", () => airplayBtn.classList.add("airplay-active"));
|
||||||
|
player.remote.addEventListener("disconnect", () => airplayBtn.classList.remove("airplay-active"));
|
||||||
|
airplayBtn.addEventListener("click", () => player.remote.prompt().catch(() => {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initAirplay();
|
||||||
|
|
||||||
// ── Download list (main page) ─────────────────────────────────────────────────
|
// ── Download list (main page) ─────────────────────────────────────────────────
|
||||||
async function loadDownloadList() {
|
async function loadDownloadList() {
|
||||||
const tracks = await fetchDownloads();
|
const tracks = await fetchDownloads();
|
||||||
|
|||||||
BIN
containers/sleep-meditation/site/apple-touch-icon.png
Normal file
BIN
containers/sleep-meditation/site/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@ -7,6 +7,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>Sleep Meditation</title>
|
<title>Sleep Meditation</title>
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<link rel="stylesheet" href="/styles.css">
|
<link rel="stylesheet" href="/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -25,7 +26,7 @@
|
|||||||
<label for="playlist-select">Playlist</label>
|
<label for="playlist-select">Playlist</label>
|
||||||
<select id="playlist-select" aria-label="Choose a track"></select>
|
<select id="playlist-select" aria-label="Choose a track"></select>
|
||||||
|
|
||||||
<audio id="player" controls preload="metadata" playsinline>
|
<audio id="player" controls preload="metadata" playsinline x-webkit-airplay="allow">
|
||||||
Your browser does not support the audio element.
|
Your browser does not support the audio element.
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
@ -34,6 +35,13 @@
|
|||||||
<button id="play" type="button">Play/Pause</button>
|
<button id="play" type="button">Play/Pause</button>
|
||||||
<button id="next" type="button">Next</button>
|
<button id="next" type="button">Next</button>
|
||||||
<button id="restart" type="button">Back to start</button>
|
<button id="restart" type="button">Back to start</button>
|
||||||
|
<button id="airplay" type="button" hidden>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"/>
|
||||||
|
<polygon points="12 15 17 21 7 21 12 15"/>
|
||||||
|
</svg>AirPlay
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
|
|||||||
@ -162,6 +162,18 @@ button:disabled {
|
|||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#airplay svg {
|
||||||
|
width: 1.1em;
|
||||||
|
height: 1.1em;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#airplay.airplay-active {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(180deg, #4ea8de, #2a6f97);
|
||||||
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
# Changelog (develop)
|
# Changelog (develop)
|
||||||
|
|
||||||
|
## 2026-06-06 — AirPlay button
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- AirPlay button in the player button row (`index.html`, `app.js`, `styles.css`). Hidden by default; shown only when wireless playback targets are available. Safari/iOS uses the WebKit AirPlay API (`webkitplaybacktargetavailabilitychanged` + `webkitShowPlaybackTargetPicker()`); other browsers fall back to the Remote Playback API (`remote.watchAvailability()` + `remote.prompt()`). Button highlights blue while AirPlay output is active (`webkitcurrentplaybacktargetiswirelesschanged` / `connect`/`disconnect` events).
|
||||||
|
- `x-webkit-airplay="allow"` attribute on the `<audio>` element to explicitly permit AirPlay streaming.
|
||||||
|
|
||||||
## 2026-05-10 — Released as v0.1.6
|
## 2026-05-10 — Released as v0.1.6
|
||||||
|
|
||||||
## 2026-05-10
|
## 2026-05-10
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user