mirror of
https://github.com/fabiodalez-dev/Monochrome-Android-APK.git
synced 2026-10-18 23:07:33 -05:00
Add tracks.albums.coverArt to artist endpoint include in HiFi.ts. Album items were landing in includedMap without coverArt relationship, so resolveArtworkId returned null and every track row showed Picsum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
527 lines
23 KiB
Bash
Executable File
527 lines
23 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# Monochrome / Fabiodalez Music — Android Build Script
|
|
# Pulls latest from GitHub, applies Android patches, builds APK.
|
|
#
|
|
# Patches applied temporarily during build (reverted after):
|
|
# index.html — script tag, viewport-fit, brand, CDN preconnect
|
|
# package.json — Capacitor dependencies
|
|
# js/app.js — search debounce 3000→500 ms
|
|
# js/api.js — search result limit = 100 (was backend default ≈25)
|
|
# js/cache.js — per-type TTL + query normalization (trim/lowercase/diacritics)
|
|
# js/app.js — search debounce 3000ms → 800ms
|
|
# js/HiFi.ts — add artists.profileArt to unified search include (fix Picsum covers)
|
|
#
|
|
# All reverted automatically on exit. Upstream repo stays clean.
|
|
# ─────────────────────────────────────────────────────────
|
|
|
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
APK_OUTPUT="$PROJECT_DIR/android/app/build/outputs/apk/debug/app-debug.apk"
|
|
APK_COPY="$PROJECT_DIR/Monochrome-debug.apk"
|
|
|
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
|
|
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
|
|
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
|
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
|
|
|
cd "$PROJECT_DIR"
|
|
|
|
cleanup() {
|
|
echo ""
|
|
echo "▶ Cleaning up patched files..."
|
|
# Revert tracked files that may have been overwritten by git archive.
|
|
for f in \
|
|
.gitignore \
|
|
android/app/src/main/java/tf/monochrome/music/BackgroundAudioPlugin.java
|
|
do
|
|
if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
|
|
git checkout -- "$f" 2>/dev/null || true
|
|
fi
|
|
done
|
|
# Remove all upstream web app files we extracted (they are not tracked in this repo).
|
|
rm -rf \
|
|
index.html package.json package-lock.json bun.lock bun.lockb \
|
|
vite.config.ts vite-plugin-auth-gate.js vite-plugin-blob.ts \
|
|
vite-plugin-svg-use.ts vite-plugin-upload.js \
|
|
styles.css stream-stub.js tsconfig.json tsconfig-eslint.json \
|
|
eslint.config.js lhci.yml nginx.conf \
|
|
js/ src/ public/ assets/ functions/ images/ dist/ node_modules/ \
|
|
.npmrc .prettierrc .stylelintrc.json .htmlhintrc .gitmodules \
|
|
.dockerignore .vscode/ .wrangler/ \
|
|
android/build.gradle android/capacitor.settings.gradle \
|
|
android/gradle.properties android/settings.gradle android/variables.gradle \
|
|
android/gradlew android/gradlew.bat android/gradle/ android/.gitignore
|
|
rm -rf node_modules/@capgo/capacitor-media-session 2>/dev/null || true
|
|
echo " ✓ Upstream web app files removed."
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "══════════════════════════════════════════"
|
|
echo " Fabiodalez Music — Android Build"
|
|
echo "══════════════════════════════════════════"
|
|
|
|
# ── 1. Pull latest upstream web app source ──
|
|
# This repo tracks only the Android wrapper. Web app files come from upstream.
|
|
# We use git archive (not git pull) because the histories are unrelated.
|
|
echo ""
|
|
echo "▶ Pulling latest from upstream/main..."
|
|
cleanup 2>/dev/null || true
|
|
git fetch upstream
|
|
|
|
UPSTREAM_SHA=$(git rev-parse upstream/main)
|
|
SYNC_FILE="$PROJECT_DIR/.upstream-sync-sha"
|
|
LAST_SHA=$(cat "$SYNC_FILE" 2>/dev/null || echo "")
|
|
|
|
if [ "$UPSTREAM_SHA" = "$LAST_SHA" ] && [ -f index.html ]; then
|
|
echo " Already up to date ($(git rev-parse --short upstream/main))."
|
|
read -p " Build anyway? (y/N) " -n 1 -r
|
|
echo
|
|
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 0
|
|
else
|
|
N=$(git rev-list --count "${LAST_SHA:-upstream/main^}..upstream/main" 2>/dev/null || echo "?")
|
|
echo " $N new commits. Extracting web app from upstream/main..."
|
|
# Extract upstream web app files, excluding our Android additions and docs.
|
|
git archive upstream/main | tar -x \
|
|
--exclude='capacitor.config.ts' \
|
|
--exclude='README.md' \
|
|
--exclude='android/app' \
|
|
--exclude='android/android-service.js' \
|
|
--exclude='android/fm-logger.js' \
|
|
--exclude='android/capacitor-cordova-android-plugins' \
|
|
--exclude='ios' \
|
|
--exclude='extension' \
|
|
--exclude='docker' \
|
|
--exclude='.devcontainer' \
|
|
--exclude='.github' \
|
|
--exclude='CONTRIBUTING.md' \
|
|
--exclude='DOCKER.md' \
|
|
--exclude='INSTANCES.md' \
|
|
--exclude='THEME_GUIDE.md' \
|
|
--exclude='license'
|
|
echo "$UPSTREAM_SHA" > "$SYNC_FILE"
|
|
echo " ✓ Updated to $(git rev-parse --short upstream/main)."
|
|
fi
|
|
|
|
# ── 2a. Pre-npm patch: fix upstream broken override ──
|
|
# (Upstream package.json "overrides" forces sourcemap-codec@^1.4.14, but
|
|
# npm registry only has up to 1.4.8 — registry package is deprecated in favor
|
|
# of @jridgewell/sourcemap-codec. Downgrade the override so npm install works.)
|
|
if grep -q '"sourcemap-codec": "\^1.4.14"' package.json; then
|
|
sed -i '' 's|"sourcemap-codec": "\^1.4.14"|"sourcemap-codec": "^1.4.8"|' package.json
|
|
echo " ✓ package.json: sourcemap-codec override 1.4.14 -> 1.4.8 (broken upstream)"
|
|
fi
|
|
|
|
# ── 2b. Install deps + add wrapper-only Capacitor plugins ──
|
|
echo ""
|
|
echo "▶ Installing dependencies..."
|
|
npm install 2>&1 | tail -3
|
|
npm install --save @capacitor/status-bar @capacitor/splash-screen 2>&1 | tail -3
|
|
echo " ✓ Done."
|
|
|
|
# ── 2c. Shim @capgo/capacitor-media-session with no-op ──
|
|
# Upstream player.js now imports @capgo/capacitor-media-session for MediaSession.
|
|
# We use our own AudioForegroundService instead (full foreground service, notification,
|
|
# Bluetooth auto-pause, hardware keys). This shim makes the import resolve at
|
|
# Vite build time but all calls become silent no-ops.
|
|
SHIM_DIR="node_modules/@capgo/capacitor-media-session"
|
|
if [ -d "$SHIM_DIR" ]; then
|
|
rm -rf "$SHIM_DIR"
|
|
fi
|
|
mkdir -p "$SHIM_DIR"
|
|
cat > "$SHIM_DIR/index.js" <<'SHIMEOF'
|
|
export const MediaSession = {
|
|
setActionHandler: async () => {},
|
|
setMetadata: async () => {},
|
|
setPlaybackState: async () => {},
|
|
setPositionState: async () => {},
|
|
};
|
|
SHIMEOF
|
|
cat > "$SHIM_DIR/package.json" <<'SHIMEOF'
|
|
{"name":"@capgo/capacitor-media-session","version":"0.0.0-shim","main":"index.js","module":"index.js","type":"module"}
|
|
SHIMEOF
|
|
echo " ✓ @capgo/capacitor-media-session shimmed (no-op → our AudioForegroundService handles it)."
|
|
|
|
# ── 3. HTML patches (sed) ──
|
|
echo ""
|
|
echo "▶ Patching for Android build..."
|
|
|
|
# 3a. Add script tags
|
|
# Logger FIRST (synchronous, non-module) in <head> — captures ALL console output from start
|
|
sed -i '' 's|</head>|<script src="./js/fm-logger.js"></script></head>|' index.html
|
|
# Service JS LAST (module) in <body> — loads after DOM
|
|
sed -i '' 's|</body>|<script type="module" src="./js/android-service.js"></script></body>|' index.html
|
|
|
|
# 3b. (workbox audio/video CacheFirst → NetworkOnly is patched via Python below)
|
|
|
|
# 3c. Brand: "Monochrome" → "Fabiodalez" in sidebar logo
|
|
sed -i '' 's|<span>Monochrome</span>|<span>Fabiodalez</span>|' index.html
|
|
|
|
# 3d. (#11/#20) Extra CDN preconnect + DNS prefetch for Tidal / api.tidal.com /
|
|
# streams.tidal.com — reduces first-byte latency on stream URL fetch.
|
|
# Idempotent: only apply if api.tidal.com preconnect is not already present.
|
|
if ! grep -q 'preconnect.*api\.tidal\.com' index.html; then
|
|
sed -i '' 's|<link rel="preconnect" href="https://resources.tidal.com" crossorigin />|<link rel="preconnect" href="https://resources.tidal.com" crossorigin />\
|
|
<link rel="preconnect" href="https://api.tidal.com" crossorigin />\
|
|
<link rel="dns-prefetch" href="https://streams.tidal.com" />\
|
|
<link rel="dns-prefetch" href="https://cdn.tidal.com" />\
|
|
<link rel="dns-prefetch" href="https://manifests.tidal.com" />|' index.html
|
|
fi
|
|
|
|
echo " ✓ index.html patched (script tag + viewport-fit + brand + CDN preconnect)."
|
|
|
|
# ── 3e. JS upstream optimizations via Python (multi-line, safer than sed) ──
|
|
# These patches apply *only at build time*; git checkout in the cleanup trap
|
|
# above restores the original files afterwards.
|
|
python3 <<'PYEOF'
|
|
import sys
|
|
import os
|
|
|
|
PROJECT_DIR = os.environ.get("PROJECT_DIR", os.getcwd())
|
|
|
|
def patch(path, before, after, label):
|
|
full = os.path.join(PROJECT_DIR, path)
|
|
with open(full, "r", encoding="utf-8") as f:
|
|
src = f.read()
|
|
if before not in src:
|
|
print(" ! " + label + ": pattern not found, skipping (upstream may have changed)")
|
|
return False
|
|
src = src.replace(before, after, 1)
|
|
with open(full, "w", encoding="utf-8") as f:
|
|
f.write(src)
|
|
print(" + " + label)
|
|
return True
|
|
|
|
# ── #53: Add live streaming instances to hardcoded fallback list ──
|
|
# The uptime worker returns streaming:[] so the default list is the only fallback
|
|
# when the network fetch fails on a fresh install. We add the same 3 live instances
|
|
# used in the web app (index.html) and android-service.js localStorage bootstrap.
|
|
# frankfurt-2 was previously here but is now DOWN (504).
|
|
patch(
|
|
"js/storage.js",
|
|
""" streaming: [
|
|
{ url: 'https://hifi.geeked.wtf', version: '2.7' },""",
|
|
""" streaming: [
|
|
{ url: 'https://eu-central.monochrome.tf', version: '2.10' },
|
|
{ url: 'https://us-west.monochrome.tf', version: '2.10' },
|
|
{ url: 'https://hifi-api.kennyy.com.br', version: '2.10' },
|
|
{ url: 'https://hifi.geeked.wtf', version: '2.7' },""",
|
|
"storage.js: add live streaming instances to fallback",
|
|
)
|
|
|
|
# ── #54: REMOVED — was forcing native HiFiClient for streaming, which gives
|
|
# only preview (1:40) because the browser client credentials aren't premium.
|
|
# Streaming MUST go through proxy instances (they have premium credentials).
|
|
|
|
# ── #1: debounce 3000ms → 800ms ──
|
|
# 3 seconds after the last keystroke is too slow for mobile. 800ms is the
|
|
# standard for search bars and still avoids firing on every keystroke.
|
|
# "Renders partial queries" is normal search behavior (like Spotify/Google).
|
|
patch(
|
|
"js/app.js",
|
|
"}, 3000);",
|
|
"}, 800);",
|
|
"app.js: search debounce 3000ms -> 800ms",
|
|
)
|
|
|
|
# The limit=100 and cache normalization patches below still apply.
|
|
|
|
# ── #52: Enrich albums missing artist/cover from track data ──
|
|
# The v2 API often returns albums without artist or cover in search results,
|
|
# but the tracks in the same response DO have this data. Copy it over.
|
|
patch(
|
|
"js/ui.js",
|
|
""" if (finalAlbums.length === 0 && finalTracks.length > 0) {
|
|
const albumMap = new Map();
|
|
finalTracks.forEach((track) => {
|
|
if (track.album && !albumMap.has(track.album.id)) {
|
|
albumMap.set(track.album.id, track.album);
|
|
}
|
|
});
|
|
finalAlbums = Array.from(albumMap.values());
|
|
}""",
|
|
""" if (finalAlbums.length === 0 && finalTracks.length > 0) {
|
|
const albumMap = new Map();
|
|
finalTracks.forEach((track) => {
|
|
if (track.album && !albumMap.has(track.album.id)) {
|
|
albumMap.set(track.album.id, track.album);
|
|
}
|
|
});
|
|
finalAlbums = Array.from(albumMap.values());
|
|
}
|
|
|
|
// Enrich albums that have no artist or cover from track data
|
|
// (v2 API search often omits these for album entries, but tracks have them)
|
|
if (finalAlbums.length > 0 && finalTracks.length > 0) {
|
|
const trackInfoMap = new Map();
|
|
finalTracks.forEach((track) => {
|
|
if (track.album && track.album.id && !trackInfoMap.has(track.album.id)) {
|
|
trackInfoMap.set(track.album.id, {
|
|
artist: track.artist || (track.artists && track.artists[0]) || null,
|
|
cover: (track.album && track.album.cover) || null,
|
|
});
|
|
}
|
|
});
|
|
finalAlbums.forEach((album) => {
|
|
const info = trackInfoMap.get(album.id);
|
|
if (!info) return;
|
|
if (!album.artist && !album.artists?.length && info.artist) {
|
|
album.artist = info.artist;
|
|
album.artists = [info.artist];
|
|
}
|
|
if (!album.cover && info.cover) {
|
|
album.cover = info.cover;
|
|
}
|
|
});
|
|
}""",
|
|
"ui.js: enrich albums missing artist/cover from tracks",
|
|
)
|
|
|
|
# ── #3 + #9 + #10: cache.js — TTL per type, query normalization ──
|
|
patch(
|
|
"js/cache.js",
|
|
" this.ttl = options.ttl || 1000 * 60 * 30;",
|
|
""" this.ttl = options.ttl || 1000 * 60 * 30;
|
|
// Per-type TTL overrides (search results decay faster than detail pages).
|
|
this.ttlByType = {
|
|
search_all: 1000 * 60 * 10,
|
|
search_tracks: 1000 * 60 * 10,
|
|
search_artists: 1000 * 60 * 60,
|
|
search_albums: 1000 * 60 * 30,
|
|
search_playlists: 1000 * 60 * 60,
|
|
search_videos: 1000 * 60 * 15,
|
|
};""",
|
|
"cache.js: per-type TTL map",
|
|
)
|
|
|
|
patch(
|
|
"js/cache.js",
|
|
""" generateKey(type, params) {
|
|
const paramString = typeof params === 'object' ? JSON.stringify(params) : String(params);
|
|
return `${type}:${paramString}`;
|
|
}""",
|
|
""" generateKey(type, params) {
|
|
let normalized = params;
|
|
if (typeof params === 'string' && typeof type === 'string' && type.startsWith('search')) {
|
|
// Normalize: trim + lowercase + strip diacritics so "Björk"/"bjork"/" BJORK " hit the same key.
|
|
normalized = params
|
|
.trim()
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\\u0300-\\u036f]/g, '');
|
|
}
|
|
const paramString = typeof normalized === 'object' ? JSON.stringify(normalized) : String(normalized);
|
|
return `${type}:${paramString}`;
|
|
}""",
|
|
"cache.js: query normalization (trim/lowercase/NFD)",
|
|
)
|
|
|
|
patch(
|
|
"js/cache.js",
|
|
""" async get(type, params) {
|
|
const key = this.generateKey(type, params);
|
|
|
|
if (this.memoryCache.has(key)) {
|
|
const cached = this.memoryCache.get(key);
|
|
if (Date.now() - cached.timestamp < this.ttl) {
|
|
return cached.data;
|
|
}
|
|
this.memoryCache.delete(key);
|
|
}
|
|
|
|
if (this.db) {
|
|
try {
|
|
const cached = await this.getFromIndexedDB(key);
|
|
if (cached && Date.now() - cached.timestamp < this.ttl) {""",
|
|
""" async get(type, params) {
|
|
const key = this.generateKey(type, params);
|
|
const effectiveTtl = (this.ttlByType && this.ttlByType[type]) || this.ttl;
|
|
|
|
if (this.memoryCache.has(key)) {
|
|
const cached = this.memoryCache.get(key);
|
|
if (Date.now() - cached.timestamp < effectiveTtl) {
|
|
return cached.data;
|
|
}
|
|
this.memoryCache.delete(key);
|
|
}
|
|
|
|
if (this.db) {
|
|
try {
|
|
const cached = await this.getFromIndexedDB(key);
|
|
if (cached && Date.now() - cached.timestamp < effectiveTtl) {""",
|
|
"cache.js: per-type TTL lookup in get()",
|
|
)
|
|
|
|
# ── #51 (bonus): api.js — explicit &limit=100 on all search endpoints so a
|
|
# prolific artist returns all their albums/tracks, not just the backend default (~25).
|
|
patch(
|
|
"js/api.js",
|
|
"const response = await this.fetchWithRetry(`/search/?q=${encodeURIComponent(query)}`, options);",
|
|
"const response = await this.fetchWithRetry(`/search/?q=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
|
|
"api.js: search() unified — limit=100",
|
|
)
|
|
|
|
patch(
|
|
"js/api.js",
|
|
"const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options);",
|
|
"const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
|
|
"api.js: searchTracks — limit=100",
|
|
)
|
|
|
|
patch(
|
|
"js/api.js",
|
|
"const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}`, options);",
|
|
"const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
|
|
"api.js: searchAlbums — limit=100",
|
|
)
|
|
|
|
patch(
|
|
"js/api.js",
|
|
"const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`, options);",
|
|
"const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
|
|
"api.js: searchPlaylists — limit=100",
|
|
)
|
|
|
|
patch(
|
|
"js/api.js",
|
|
"""const response = await this.fetchWithRetry(`/search/?v=${encodeURIComponent(query)}`, {
|
|
...options,
|
|
});""",
|
|
"""const response = await this.fetchWithRetry(`/search/?v=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, {
|
|
...options,
|
|
});""",
|
|
"api.js: searchVideos — limit=100",
|
|
)
|
|
|
|
# ── searchArtists uses a different query shape, handle separately ──
|
|
# (Pattern may vary — best-effort search by the /search/?a= shape.)
|
|
try:
|
|
with open(os.path.join(PROJECT_DIR, "js/api.js"), "r", encoding="utf-8") as f:
|
|
api_src = f.read()
|
|
import re
|
|
new_src, n = re.subn(
|
|
r"(`/search/\?a=\$\{encodeURIComponent\(query\)\})(`)",
|
|
r"\1&limit=${(options && options.limit) || 100}\2",
|
|
api_src,
|
|
)
|
|
if n > 0:
|
|
with open(os.path.join(PROJECT_DIR, "js/api.js"), "w", encoding="utf-8") as f:
|
|
f.write(new_src)
|
|
print(" + api.js: searchArtists — limit=100")
|
|
else:
|
|
print(" ! api.js: searchArtists pattern not found, skipping")
|
|
except Exception as e:
|
|
print(" ! api.js: searchArtists patch failed: " + str(e))
|
|
|
|
# ── Workbox: CacheFirst → NetworkOnly for audio/video ──
|
|
# Tidal CDN streams don't serve CORS headers → CacheFirst fails with
|
|
# "no-response" in the service worker → audio won't play.
|
|
patch(
|
|
"vite.config.ts",
|
|
"handler: 'CacheFirst',\n options: {\n cacheName: 'media',",
|
|
"handler: 'NetworkOnly',\n options: {\n cacheName: 'media',",
|
|
"vite.config.ts: workbox audio/video CacheFirst -> NetworkOnly",
|
|
)
|
|
|
|
# ── #55: HiFi.ts unified search — add artists.profileArt to include ──
|
|
# The unified search (q=) omits 'artists.profileArt' from the include list,
|
|
# so artist items in the included array have no profileArt relationships and
|
|
# no artwork entries are returned. resolveArtworkId(item,'profileArt') always
|
|
# returns null → artist.picture = null → getCoverUrl(null) → random Picsum.
|
|
# The per-artist search (a=) already has artists.profileArt — this aligns q=.
|
|
patch(
|
|
"js/HiFi.ts",
|
|
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,playlists,videos'",
|
|
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,artists.profileArt,playlists,videos'",
|
|
"HiFi.ts: add artists.profileArt to unified search include (fixes Picsum artist covers)",
|
|
)
|
|
|
|
# ── #56: HiFi.ts artist page — add tracks.albums.coverArt to include ──
|
|
# The artist endpoint includes tracks,tracks.albums (top tracks with album ref)
|
|
# but NOT tracks.albums.coverArt. Album items land in includedMap without the
|
|
# coverArt relationship, so resolveArtworkId(albumItem,'coverArt') returns null
|
|
# → track.album.cover = null → Picsum on every track row in the artist page.
|
|
patch(
|
|
"js/HiFi.ts",
|
|
"include: 'albums,albums.coverArt,tracks,tracks.albums,biography,profileArt',",
|
|
"include: 'albums,albums.coverArt,tracks,tracks.albums,tracks.albums.coverArt,biography,profileArt',",
|
|
"HiFi.ts: add tracks.albums.coverArt to artist page include (fixes Picsum track covers)",
|
|
)
|
|
|
|
print(" ✓ Upstream JS optimizations applied.")
|
|
PYEOF
|
|
|
|
echo " ✓ JS upstream optimizations applied (debounce, cache, limits)."
|
|
|
|
# ── 4. Copy wrapper JS files from android/ storage ──
|
|
cp "$PROJECT_DIR/android/android-service.js" js/android-service.js
|
|
# fm-logger must go into public/js/ so Vite copies it to dist/js/ as a static asset.
|
|
# Putting it in js/ (source dir) would leave it out of the dist/ bundle entirely.
|
|
mkdir -p public/js
|
|
cp "$PROJECT_DIR/android/fm-logger.js" public/js/fm-logger.js
|
|
echo " ✓ android-service.js + fm-logger.js copied."
|
|
|
|
# ── 5. Init Capacitor Android if needed ──
|
|
if [ ! -d "$PROJECT_DIR/android" ]; then
|
|
npx cap add android 2>/dev/null
|
|
echo " ✓ Android platform added."
|
|
fi
|
|
|
|
# ── 6. Build web ──
|
|
echo ""
|
|
echo "▶ Building web app..."
|
|
npx vite build 2>&1 | tail -3
|
|
echo " ✓ Web build complete."
|
|
|
|
# ── 7. Sync to Android ──
|
|
echo ""
|
|
echo "▶ Syncing to Android..."
|
|
npx cap sync android 2>&1 | tail -2
|
|
echo " ✓ Synced."
|
|
|
|
# ── 7b. Fix duplicate splash resources ──
|
|
# Upstream ships splash.png, Capacitor sync generates splash.xml — Gradle fails on duplicates.
|
|
if [ -f "$PROJECT_DIR/android/app/src/main/res/drawable/splash.png" ] && \
|
|
[ -f "$PROJECT_DIR/android/app/src/main/res/drawable/splash.xml" ]; then
|
|
rm "$PROJECT_DIR/android/app/src/main/res/drawable/splash.png"
|
|
echo " ✓ Removed duplicate splash.png (keeping splash.xml)."
|
|
fi
|
|
|
|
# ── 7c. Fix upstream Java typo (Kotlin backticks in BackgroundAudioPlugin.java) ──
|
|
# Commit "mobile contribs" (493ac9f) accidentally used Kotlin-style backticks on
|
|
# @PluginMethod in a .java file. javac rejects them. Strip them before gradle.
|
|
BGPLUGIN="$PROJECT_DIR/android/app/src/main/java/tf/monochrome/music/BackgroundAudioPlugin.java"
|
|
if [ -f "$BGPLUGIN" ] && grep -q '`@PluginMethod`' "$BGPLUGIN"; then
|
|
sed -i '' 's|`@PluginMethod`|@PluginMethod|g' "$BGPLUGIN"
|
|
echo " ✓ Stripped Kotlin backticks from BackgroundAudioPlugin.java (upstream typo fix)."
|
|
fi
|
|
|
|
# ── 8. Build APK ──
|
|
echo ""
|
|
echo "▶ Building APK..."
|
|
cd "$PROJECT_DIR/android"
|
|
./gradlew assembleDebug -q
|
|
cd "$PROJECT_DIR"
|
|
|
|
# ── 9. #48: safer APK copy + size check ──
|
|
if [ -f "$APK_OUTPUT" ]; then
|
|
cp "$APK_OUTPUT" "$APK_COPY"
|
|
if [ -f "$APK_COPY" ]; then
|
|
SIZE=$(du -h "$APK_COPY" | awk '{print $1}')
|
|
echo " ✓ APK built (${SIZE})"
|
|
echo ""
|
|
echo "══════════════════════════════════════════"
|
|
echo " APK: $APK_COPY"
|
|
echo "══════════════════════════════════════════"
|
|
else
|
|
echo " ✗ Failed to copy APK to $APK_COPY"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo " ✗ Build failed — no APK at $APK_OUTPUT"
|
|
exit 1
|
|
fi
|
|
|
|
# cleanup() runs automatically via trap EXIT
|