Releases: akang943578/global-radio
Releases · akang943578/global-radio
Release list
各平台客户端 v2.0.25
[2.0.25] - 2026-06-15
Fixed
- 修复点击播放没有声音的严重回归。v2.0.22 起在 Android 上开启了"智能音频焦点"(让收音机自动暂停以避开 Spotify/导航/来电),但在小米 HyperOS 上观察到:调
AudioManager.requestAudioFocus(GAIN)后,系统在尚未返回AUDIOFOCUS_REQUEST_GRANTED之前,会先朝我们自己的监听器发一个 phantomAUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK(属于路由握手抖动,不是真正的焦点丢失)。v2.0.22 的代码看到 LOSS 立刻动作了两件事:(1)抛 JS 事件让 player store 暂停;(2)从 native 直接 evaldocument.querySelectorAll('audio').forEach(a => a.pause())。结果:用户点播放,HTTP 音频元素play()刚 resolve,1~50 ms 之内被两条独立路径同时按下了 pause。整段播放从开始就被掐成静音。 - 修复(
android/app/src/main/java/com/globalradio/app/BackgroundAudioPlugin.java):- 加入
FOCUS_REQUEST_GUARD_MS = 1500ms握手窗口:在requestAudioFocus()调用 OS 之前打时间戳lastFocusRequestAt。listener 收到 LOSS/LOSS_TRANSIENT/LOSS_TRANSIENT_CAN_DUCK 时若距上次 request 不到 1500 ms,记日志后直接 return,不动focusHeld、不抛 JS 事件、不暂停。真正的焦点丢失(其他 app 起播、来电)都远晚于 1500 ms,影响为零。 - 撤掉 native 直接 eval
audio.pause()的暴力路径。focus loss 现在只走notifyListeners('audioFocusLost')+window.dispatchEvent('audioFocusLost')两路 JS 通知,由 player store 的handleFocusLost统一调pauseStation('focus-loss')→audio.pause()。单一暂停来源,没有 native/JS 互掐的窗口。
- 加入
- 新增 kill-switch:「设置 → 播放与网络 → 智能音频焦点」开关。默认 ON(保留 v2.0.22 的让位行为)。一旦设备上仍然出现"播放没声音 / 行为诡异",用户关掉此开关即时回滚到 v2.0.20 之前的行为:不再请求音频焦点、不再监听焦点事件,与其他 app 同时发声,但保证稳。Settings 切换会立刻通过
BackgroundAudio.setEnabled({ enabled })桥到 native(setEnabled同时 abandon 已持有的焦点),下一次playStation()立即生效,无需重启 App。 - 修复收藏拖动在小米上长按不响应;同时把列表视图左侧那条
≡拖动柄整体撤掉。原 v2.0.23 实现给列表视图加了独立 handle,但用户反馈 handle 不直观、长按整条卡片才符合预期;同时 SortableJS 默认走 HTML5 原生 Drag-and-Drop API,在 Android WebView / MIUI Chromium 上事件流不稳定,长按经常被 WebView 当成文本选择 / 上下文菜单吃掉。- 列表/网格两个
<VueDraggable>现在配置完全一致::delay="500":delay-on-touch-only="false":touch-start-threshold="5":force-fallback="true":fallback-tolerance="3":prevent-on-filter="false"filter=".no-drag-trigger,button"。 force-fallback=true是 WebView 兼容性的关键:强制 SortableJS 走自己的 pointer-event 实现,不走 HTML5 native DnD(在 Capacitor WebView 上常常根本不触发dragstart)。filter="button"把卡片内部的播放/收藏/分享按钮排除在拖动触发之外——长按整张卡片可拖,按上面的按钮仍是各自的 click handler 正常工作,不会误触发拖动。delay-on-touch-only=false让鼠标也走 500 ms 长按延迟——这样桌面 / emulator QA 的"按住卡片晃一下"和触屏体验一致。- 新加 scoped CSS:
.sortable-drag/.sortable-ghost/.sortable-fallback一律touch-action: none; user-select: none,杜绝 WebView 在 500 ms 长按期间把它当文本选择 / 滚动 / 长按菜单吃掉手势。 - 撤掉 handle 的同时也撤掉了模板里那个
≡按钮 + flex 容器,列表视图回到「单层 StationCard」结构,视觉上和 v2.0.22 一致。 - 一次性引导横幅的 localStorage key 从
favorites-reorder-hint-seen-v222改为-v225,保证之前看过旧 handle 提示的用户能再看到一次"长按整条卡片即可拖动"的提示。文案favorites.reorderHint = "长按拖动即可调整顺序"保持不变(v2.0.23 时已经是"长按"语义,现在终于和实际行为对齐)。
- 列表/网格两个
Tested
- 代码层走查:
- Native 修复对照旧版做了 commit diff 全文 review。v2.0.22 的 phantom LOSS 路径定位有据:
notifyAudioFocusLost同时调notifyListeners+ 直接 evalquerySelectorAll('audio').pause(),listener 没有任何"自己的 request 才刚发出"的守护,Xiaomi MIUI 的路由握手 LOSS 必然命中并双路杀死播放。 - 守护逻辑用
SystemClock.uptimeMillis()(不受系统时钟跳动影响),lastFocusRequestAt在调am.requestAudioFocus之前打时间戳,覆盖同步回调和异步回调两种情况。 - kill-switch 路径:
setEnabled(false)会立即abandonAudioFocusRequest+focusHeld = false,listener 即便事后被系统唤醒回调也走"feature disabled 早退"分支,不影响播放。 setAudioFocusEnabled在usePlaybackSettingsStore()第一次实例化时立即下推一次(启动期 sync 一次 native + JS 的开关状态),之后 watcher 跟着用户在 Settings 里的开关动。
- Native 修复对照旧版做了 commit diff 全文 review。v2.0.22 的 phantom LOSS 路径定位有据:
- Web 模式 Vite production build:通过,bundle 大小、TS 类型与 v2.0.24 baseline 一致;无新增依赖。
- APK build:本地 Gradle 跑通(详见 emit log)。
@CapacitorPlugin注解暴露的方法名setEnabled/requestAudioFocus/abandonAudioFocus与 JS 端BackgroundAudio.setEnabled({ enabled })一致;JS 侧setAudioFocusEnabled(false)失败也仅console.warn,不会阻塞播放。 - Emulator 实跑:本轮再次尝试在 host 上 boot AVD(gr_test API 34 + gr_light API 30),双双在 boot 过程中被 macOS jetsam 静默 SIGKILL(
/tmp/emulator-light.log无错误,进程消失),即便vm_stat报 13 GB free。本仓库的 emulator 路径目前对 host 太敏感,不再做盲目重试。修复路径完全可被用户在 Settings 关闭智能音频焦点开关回滚,万一仍有边缘 case 没有覆盖,用户可零成本切回 v2.0.20 行为。
Known limitation
- 本次未能在 emulator 上做 visual smoke。修复信赖于代码层 review + kill-switch 兜底。若真机仍然「点播放没声音」,请打开「设置 → 智能音频焦点」开关关掉,回到 v2.0.20 之前的简单行为,再回报 logcat(
adb logcat -s BackgroundAudioPlugin,重点抓requestAudioFocus result=和onAudioFocusChange行,看sinceRequest=是否进入 < 1500 ms 的守护窗口)。 - 长按 500 ms 是 mobile UX 的常见值,但若实际试用感觉太短(频繁误触发)或太长(要等很久),后续 PR 会做成可配置 / 自适应。
提供 Windows 64 位 / iPhone / Android 客户端,如有异常请清除应用缓存重试。
下载
GlobalRadio-v2.0.25.apk— Android release APK (shell mode)GlobalRadio-2.0.25.ipa— iOS unsigned IPA (sideload only; 未签名 App Store)GlobalRadio_2.0.25_x64-setup.exe— Windows 64-bit installer (Electron NSIS)
完整变更历史详见 CHANGELOG.md。
各平台客户端 v2.0.24
[2.0.24] - 2026-06-15
Fixed
- 修复偶发的已取消收藏的电台再次出现。复现:在某设备上取消收藏 B → 1 秒之内关掉浏览器(v2.0.23 的 push 防抖窗口) → push 没发出去 → 服务端仍是
[A, B, C]→ 下次任何浏览器/设备打开 → pull 服务器[A, B, C],本地[A, C],旧的 set-union 合并把 B 当成"另一台设备加的新电台",结果[A, C, B]—— B 复活。 - 修复方式(多管齐下):
- 删除操作绕过防抖直接 push:
src/stores/player.ts的removeFavorite/clearFavorites不再走scheduleUserDataPush()(200 ms debounce),改走新增的saveFavoritesImmediate()→pushUserDataNow()→ 立即调pushToServer。其他操作(添加收藏、设置改动)仍走 200 ms debounce 路径,节流压力小。 - 防抖窗口 1000 ms → 200 ms(
src/stores/userSync.tsschedulePushToServer)。意外关页面最多丢 200 ms 的非删除变更。 - 关页面前最后一搏 flush:
useUserSyncStore内部维护pendingPushPayloadref,每次collectLocalUserData()同步刷新;window.addEventListener('beforeunload' / 'pagehide')用fetch('/api/user/data', { method: 'PUT', credentials: 'include', keepalive: true, ... })把还没出门的 payload 送出去。走fetch keepalive而不是navigator.sendBeacon—— 后者只支持 POST,我们的服务端/api/user/data只接受 PUT;keepalive: true是现代标准做法,支持任意 method,64 KB body cap 完全够用。Chrome 80+ / Safari 13+ 支持,正好是我们已经要求的版本。 - 本地 vs 服务端 updatedAt 比对兜底:新增 localStorage key
radio-data-updated-at,每次collectLocalUserData()在写出 payload 前先把它的updatedAt持久化下来。pullFromServer拉到serverData.updatedAt后比较:本地新于服务端 → 说明上一次 push 没成(被关页面/网络抖动吞了),这一轮跳过 merge,反过来把本地推上服务端,由本地为准。这样即使 beforeunload flush 也没成(极端情况),下次开浏览器拉一次就能修复。
- 删除操作绕过防抖直接 push:
- 收藏排序以服务端为准,多端同步。v2.0.23 把方向搞反了:合并代码是"以本地为准",意味着设备 A 拖动重排后推到服务端,设备 B 下次 pull 时仍保留自己的旧顺序——多端没法同步。改成 server-canonical:
mergeFavorites从"先 walk local、加 server-only 在末尾"改成"先 copy server 数组(顺序、metadata 全部以服务端为准)、把仅本地有的(未推送的新增)追加在末尾"。- 配合 200 ms 防抖 + delete-immediate-push + beforeunload flush,设备 A 拖完几乎瞬间到达服务端;设备 B pull 时直接取 server 顺序。
- 单设备的拖完→刷新流程也工作得正确:拖 → 200 ms 后 push 到服务端 → 刷新 → pull 拿回服务端的(自己刚推的)顺序。
Tested
mergeFavorites单元测试脚本/tmp/v224-merge-tests.mjs,7 个 case 覆盖 deleted-on-local / 跨设备 reorder / 跨设备 add / 本地未推送 add / 空服务端 / 等同 / metadata 时间戳更新。完整输出见/tmp/v224-merge-tests.txt。v2.0.24 vs v2.0.23 side-by-side 对照证明 case 2-3 之前是 BUG(被 v2.0.23 的本地优先压住了多端同步),case 1 (resurrect) 由 mergeFavorites 单独无法解决,必须配合pullFromServer内的localUpdatedAt > serverUpdatedAt守护跳过 merge 直接推本地。- Vite production build 通过:
✓ built in 3.x s,bundle 大小相比 v2.0.23 没有变化(无新依赖,仅逻辑改动)。 - TypeScript:与 v2.0.18+ baseline 一致,无新增错误。
- 真机验证(拖完 / 删完关页面 / 多端同步)需要 POCO F5 + 第二台设备同账号配合,emulator 仍受 host RAM 限制无法正常 boot(同 v2.0.22-v2.0.23 状况)。
Note
- 服务端
stream-proxy/server.mjshandleUserData未改动,仍然只接受 PUT,鉴权走 cookie。fetch keepalive路径完全兼容现有路由。
提供 Windows 64 位 / iPhone / Android 客户端,如有异常请清除应用缓存重试。
下载
GlobalRadio-v2.0.24.apk— Android release APK (shell mode)GlobalRadio-2.0.24.ipa— iOS unsigned IPA (sideload only; 未签名 App Store)GlobalRadio_2.0.24_x64-setup.exe— Windows 64-bit installer (Electron NSIS)
完整变更历史详见 CHANGELOG.md。