Initial import: Music_Server, MusicFree, catalog-sync
This commit is contained in:
+89
@@ -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`
|
||||
+130
@@ -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音乐` / `酷我` 同理。
|
||||
- 返回 `全部` 后恢复完整列表。
|
||||
- 其它插件榜单页不受影响。
|
||||
Reference in New Issue
Block a user