Initial import: Music_Server, MusicFree, catalog-sync

This commit is contained in:
2026-05-23 16:51:14 +08:00
commit 069af30dba
847 changed files with 179878 additions and 0 deletions
@@ -0,0 +1,89 @@
# Ignore Battery Optimization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 补齐 Android 忽略电池优化权限链路,减少锁屏后一段时间被系统回收导致的停播问题。
**Architecture:** 复用现有 `NativeUtils` 原生模块承接 Android 电池优化查询与申请能力,在权限页增加对应入口和状态展示。JS 层先补回归测试,再用最小改动补 Manifest、原生模块、类型声明与文案。
**Tech Stack:** React Native, Kotlin Android module, Jest, react-test-renderer
---
### Task 1: Permissions Page Regression Test
**Files:**
- Create: `src/pages/permissions/index.test.tsx`
- Modify: `src/pages/permissions/index.tsx`
- [ ] **Step 1: Write the failing test**
```tsx
it("renders ignore battery optimization entry and calls native request handler", async () => {
// mock NativeUtils.isIgnoringBatteryOptimizations -> true
// render Permissions
// assert translated entry exists
// trigger onPress
// expect NativeUtils.requestIgnoreBatteryOptimizations toHaveBeenCalled()
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `npx jest src/pages/permissions/index.test.tsx --runInBand`
Expected: FAIL because the current page does not render or call battery optimization APIs.
- [ ] **Step 3: Write minimal implementation**
```tsx
type IPermissionTypes = "floatingWindow" | "fileStorage" | "batteryOptimization";
updates.batteryOptimization = await NativeUtils.isIgnoringBatteryOptimizations();
onPress={() => NativeUtils.requestIgnoreBatteryOptimizations()}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `npx jest src/pages/permissions/index.test.tsx --runInBand`
Expected: PASS
### Task 2: Android Native Bridge
**Files:**
- Modify: `android/app/src/main/AndroidManifest.xml`
- Modify: `android/app/src/main/java/fun/upup/musicfree/utils/UtilsModule.kt`
- Modify: `src/native/utils/index.ts`
- Modify: `src/types/core/i18n/index.d.ts`
- Modify: `src/core/i18n/languages/zh-cn.json`
- Modify: `src/core/i18n/languages/en-us.json`
- Modify: `src/core/i18n/languages/zh-tw.json`
- [ ] **Step 1: Add Android permission declaration**
```xml
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
```
- [ ] **Step 2: Add native query and request methods**
```kotlin
@ReactMethod
fun isIgnoringBatteryOptimizations(promise: Promise) { ... }
@ReactMethod
fun requestIgnoreBatteryOptimizations(promise: Promise) { ... }
```
- [ ] **Step 3: Expose methods to JS and add i18n keys**
```ts
isIgnoringBatteryOptimizations: () => Promise<boolean>;
requestIgnoreBatteryOptimizations: () => Promise<boolean>;
```
- [ ] **Step 4: Run targeted verification**
Run: `npx jest src/pages/permissions/index.test.tsx --runInBand`
Expected: PASS
Run: `npx eslint src/pages/permissions/index.tsx src/pages/permissions/index.test.tsx src/native/utils/index.ts`
Expected: exit code `0`
@@ -0,0 +1,130 @@
# TopList Platform Filter Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add an in-page platform filter to the MusicFree toplist screen so `Music_Server` toplists can be narrowed to `全部 / QQ音乐 / 网易云 / 酷我` without changing plugin or server APIs.
**Architecture:** Keep the existing plugin-level `TabView` unchanged and implement the new behavior entirely inside the current toplist scene. A small pure helper will derive filter tags and filtered sections from the existing `IMusicSheetGroupItem[]` response, and the board panel UI will render a horizontal tag row only when more than one platform group exists.
**Tech Stack:** React Native, TypeScript, Jotai state already present in toplist page, Jest for pure logic tests.
---
### Task 1: Add Pure Toplist Filter Logic
**Files:**
- Create: `D:\source\MusicFree\src\pages\topList\hooks\topListPlatformFilter.ts`
- Create: `D:\source\MusicFree\src\pages\topList\hooks\topListPlatformFilter.test.ts`
- [ ] **Step 1: Write the failing test**
```ts
import {
buildTopListPlatformTags,
filterTopListSections,
} from "./topListPlatformFilter";
describe("topListPlatformFilter", () => {
const sections = [
{ title: "QQ音乐", data: [{ id: "qq-1" }] },
{ title: "网易云", data: [{ id: "wy-1" }] },
{ title: "酷我", data: [{ id: "kw-1" }] },
];
it("prepends 全部 when there are multiple platform groups", () => {
expect(buildTopListPlatformTags(sections)).toEqual([
{ id: "", title: "全部" },
{ id: "QQ音乐", title: "QQ音乐" },
{ id: "网易云", title: "网易云" },
{ id: "酷我", title: "酷我" },
]);
});
it("returns all sections for 全部", () => {
expect(filterTopListSections(sections, "")).toEqual(sections);
});
it("returns only the matching platform section", () => {
expect(filterTopListSections(sections, "网易云")).toEqual([
{ title: "网易云", data: [{ id: "wy-1" }] },
]);
});
it("returns an empty list when the platform is missing", () => {
expect(filterTopListSections(sections, "咪咕")).toEqual([]);
});
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `npm test -- --runInBand src/pages/topList/hooks/topListPlatformFilter.test.ts`
Expected: FAIL because `topListPlatformFilter.ts` does not exist yet.
- [ ] **Step 3: Write minimal implementation**
```ts
const ALL_ID = "";
export function buildTopListPlatformTags(sections: IMusic.IMusicSheetGroupItem[]) {
if (!Array.isArray(sections) || sections.length <= 1) {
return [];
}
return [
{ id: ALL_ID, title: "全部" },
...sections.map(section => ({
id: section.title ?? "",
title: section.title ?? "",
})),
].filter(tag => tag.id || tag.title === "全部");
}
export function filterTopListSections(
sections: IMusic.IMusicSheetGroupItem[],
platformId: string,
) {
if (!platformId) {
return sections ?? [];
}
return (sections ?? []).filter(section => section.title === platformId);
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `npm test -- --runInBand src/pages/topList/hooks/topListPlatformFilter.test.ts`
Expected: PASS.
### Task 2: Wire the Filter into TopList UI
**Files:**
- Modify: `D:\source\MusicFree\src\pages\topList\components\boardPanel.tsx`
- Reuse: `D:\source\MusicFree\src\components\base\typeTag.tsx`
- [ ] **Step 1: Add local selected-platform state and render tags**
Add a horizontal `ScrollView` above the `SectionList` that renders `TypeTag` entries built from `buildTopListPlatformTags(topListData?.data || [])`. Default selected tag id is `""`.
- [ ] **Step 2: Filter sections before passing them to SectionList**
Call `filterTopListSections(topListData?.data || [], selectedPlatformId)` and use the result as the `sections` prop.
- [ ] **Step 3: Keep existing loading and empty-state behavior**
Do not change:
- loading gate based on `RequestStateCode.FINISHED`
- `TopListItem` rendering
- section header rendering
- toplist detail navigation
- [ ] **Step 4: Run the new logic test again**
Run: `npm test -- --runInBand src/pages/topList/hooks/topListPlatformFilter.test.ts`
Expected: PASS.
- [ ] **Step 5: Smoke-check the touched file compiles cleanly**
Run: `npx tsc --noEmit --pretty false`
Expected: exit code `0` if the repository typechecks cleanly. If unrelated repo-wide issues block this, note the blocker and at least ensure the edited file imports resolve correctly.
@@ -0,0 +1,121 @@
# MusicFree 榜单页平台筛选设计
日期:2026-04-21
## 背景
当前榜单页已经有一层顶部分页,用来在不同插件之间切换。但对 `Music_Server` 这类会在单个插件页内返回多组榜单的平台,页面会一次性展开 `QQ音乐 / 网易云 / 酷我` 等全部分组,导致列表过长,用户需要反复滚动才能定位目标平台。
推荐歌单页的体验更接近用户预期:先进入一个插件页,再按当前页面语义查看更小范围的数据。本次要把类似的“平台筛选”体验补到榜单页,但保持现有服务端和插件协议不变。
## 备选方案
### 方案 A:页内二级筛选
- 保留现有顶部插件分页。
- 在榜单列表上方增加一排本地筛选标签:`全部 / QQ音乐 / 网易云 / 酷我`
- 点击后只显示对应分组,未选中的分组不渲染。
优点:
- 改动最小,完全复用现有 `getTopLists()` 返回的分组结构。
- 不改插件、不改服务端、不改榜单详情链路。
- 用户理解成本最低,仍然是“先选插件,再选平台”。
缺点:
- 只解决页内分组过长,不会减少服务端返回的数据量。
### 方案 B:把页内平台分组再抬升成新的横向分页
- 保留顶部插件分页。
- 进入插件页后,再做第二层横向 `TabBar` 切换 `全部 / QQ音乐 / 网易云 / 酷我`
优点:
- 视觉上和推荐歌单页更像。
缺点:
- UI 层级更重,榜单页顶部会出现两层横向切换。
- 对移动端窄屏更拥挤,维护成本高于方案 A。
### 方案 C:改插件,按当前选中平台只返回一组榜单
- 在插件或服务端增加平台过滤参数。
- 榜单页每次切换平台都重新请求。
优点:
- 数据语义更强,可减少单次渲染数据量。
缺点:
- 需要改插件协议,可能联动 `Music_Server` 接口。
- 明显超出“仅优化榜单页交互”的范围。
## 选型
采用方案 A。
原因:
- 能直接解决“列表太长”的问题。
- 风险最小,最符合这次“只做榜单页筛选,不碰插件协议”的目标。
- 后续如果还想升级成方案 B,也可以在同一份本地筛选数据上演进。
## 交互设计
`MusicFree -> 榜单 -> 当前插件页` 内新增一排页内筛选标签。
规则如下:
- 默认选中 `全部`
- `全部` 时沿用现有表现,显示所有分组。
- 选中某个平台时,只展示该平台对应 section。
- 如果只有一个平台分组,则不显示筛选条。
- 进入榜单详情、返回榜单页时,保留当前插件页的筛选状态即可;切换到别的插件页时,各自维护自己的筛选状态。
## 代码范围
主要修改:
- `src/pages/topList/components/boardPanelWrapper.tsx`
- 负责把当前插件的榜单分组数据取出并交给展示层。
- `src/pages/topList/components/boardPanel.tsx`
- 新增筛选条 UI,并根据当前筛选结果渲染 section 列表。
可选新增:
- `src/pages/topList/hooks/`
- 若筛选逻辑稍有复杂,拆一个轻量纯函数或 hook,避免把过滤逻辑塞进组件 JSX。
明确不改:
- `keep-alive-master/Music_Free/music_server.js`
- `Music_Server` 服务端接口
- `catalog-sync`
## 数据流
1. 插件 `getTopLists()` 继续返回 `IMusicSheetGroupItem[]`,例如:
- `[{ title: "QQ音乐", data: [...] }, { title: "网易云", data: [...] }]`
2. 榜单页拿到完整分组数据后,在客户端本地构造筛选标签。
3. 当前选中的平台标签决定 `SectionList``sections` 输入。
4. 榜单详情页仍使用原有 `TopListItem -> getTopListDetail()` 流程,不做改动。
## 测试与验收
至少补一条纯逻辑测试,覆盖:
- `全部` 返回全部 section。
- 指定平台仅返回匹配 section。
- 未命中平台时返回空列表。
人工验收标准:
- `Music_Server` 插件榜单页默认显示全部榜单。
- 点击 `网易云` 后只剩网易云榜单。
- 点击 `QQ音乐` / `酷我` 同理。
- 返回 `全部` 后恢复完整列表。
- 其它插件榜单页不受影响。