{"owner":"xfangfang","repo":"wiliwili","hasSkills":true,"totalSkillsCount":1,"totalTokensCount":4481,"categories":["root-instruction"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# AGENTS.md — wiliwili Codebase Guide\n\nwiliwili is a cross-platform Bilibili client (Nintendo Switch, PSVita, PS4, PC) built in C++17. The UI is rendered via **nanovg**, windowing via **GLFW** or **SDL2**, and video playback via **mpv + FFmpeg**.\n\n---\n\n## Architecture Layers (Android-inspired)\n\n| Layer | Directory | Role |\n|---|---|---|\n| Activity | `wiliwili/include/activity/`, `wiliwili/source/activity/` | Top-level full-screen pages |\n| Fragment | `wiliwili/include/fragment/`, `wiliwili/source/fragment/` | Sub-pages composed within Activities |\n| Presenter | `wiliwili/include/presenter/` | Async data-fetching logic; Fragments inherit Presenters |\n| View | `wiliwili/include/view/` | Custom reusable UI components (MPVCore, RecyclingGrid, VideoView…) |\n| API | `wiliwili/include/api/bilibili/` | Bilibili REST/gRPC wrappers via cpr + nlohmann/json |\n| Utils | `wiliwili/include/utils/` | Intent (navigation), ProgramConfig (settings), ImageHelper, EventHelper |\n\nThe UI framework is a custom fork of **borealis** (`library/borealis/`). All views inherit `brls::Box` or its subclasses.\n\n---\n\n## Key Patterns\n\n### Presenter / async safety\nEvery Fragment that fetches data inherits a Presenter class. Use the macros in `wiliwili/include/presenter/presenter.h` to guard callbacks against use-after-free:\n```cpp\nvoid requestSomething() {\n    ASYNC_RETAIN  // captures token, tokenCounter\n    bilibili::HTTP::getResultAsync<MyType>(url, params, [ASYNC_TOKEN](auto result) {\n        ASYNC_RELEASE  // returns early if the view was destroyed\n        brls::Threading::sync([this, result]() {\n            // safe to update UI here (main thread)\n        });\n    });\n}\n```\nUse `CHECK_AND_SET_REQUEST` / `UNSET_REQUEST` to prevent duplicate in-flight requests.\n\n### XML-driven UI\n- Layout files live in `resources/xml/` (`activity/`, `fragment/`, `views/`).\n- Inflate in constructor: `this->inflateFromXMLRes(\"xml/fragment/home_recommends.xml\");`\n- Bind a child by XML id: `BRLS_BIND(RecyclingGrid, recyclingGrid, \"home/recommends/recyclingGrid\");`\n- Custom views must be registered before use: `brls::Application::registerXMLView(\"RecyclingGrid\", RecyclingGrid::create);` (done in `Register::initCustomView()` called from `main.cpp`).\n- Valid custom XML element names (all registered in `Register::initCustomView()`):\n  - **UI widgets:** `AutoTabFrame`, `RecyclingGrid`, `VideoView`, `VideoProfile`, `QRImage`, `SVGImage`, `TextBox`, `VideoProgressSlider`, `GalleryView`, `CustomButton`, `HintLabel`\n  - **App views:** `UserInfoView`, `UpUserSmall`, `VideoComment`, `ButtonClose`, `CheckBox`, `SelectorCell`, `AnimationImage`, `ShareBox`, `DynamicVideoCardView`, `DynamicArticleView`\n  - **Fragments:** `HomeTab`, `DynamicTab`, `MineTab`, `HomeRecommends`, `HomeHotsAll`, `HomeHotsHistory`, `HomeHotsWeekly`, `HomeHotsRank`, `HomeHots`, `HomeLive`, `HomeBangumi`, `HomeCinema`, `MineHistory`, `MineLater`, `MineCollection`, `MineBangumi`, `SearchTab`, `SearchOrder`, `SearchVideo`, `SearchCinema`, `SearchBangumi`, `SearchHots`, `SearchHistory`\n- i18n key syntax in XML: `@i18n/wiliwili/some/key`; translation files are in `resources/i18n/{en-US,zh-Hans,zh-Hant,ja,ko,it,ja-RYU}/wiliwili.json`.\n\n### Navigation — use `Intent`, never push directly\n`wiliwili/include/utils/activity_helper.hpp` defines all navigation entry points:\n```cpp\nIntent::openBV(\"BV1Da411Y7U4\");         // video by BV id\nIntent::openSeasonByEpId(323434);       // bangumi episode\nIntent::openLive(1942240);              // live room\nIntent::openSearch(\"keyword\");\nIntent::openSetting();\nIntent::openCollection(\"2511565362\");   // favorites folder\nIntent::openPgcFilter(\"/page/home/pgc/more?type=2&...\"); // PGC filter\n```\n\n### HTTP API calls\nAll calls are async via `bilibili::HTTP` (`wiliwili/include/api/bilibili/util/http.hpp`):\n```cpp\n// Standard GET → parses {\"code\":0,\"data\":{...}}\nHTTP::getResultAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For WBI-signed endpoints (most web-interface APIs since 2023):\nHTTP::getResultWithWbiAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For app-signed endpoints (pass needSign=true):\nHTTP::getResultAsync<MyResult>(url, params, callback, error, /*needSign=*/true);\n\n// POST with typed response:\nHTTP::postResultAsync<MyResult>(url, params, payload, callback, error);\n```\n- API URL constants: `wiliwili/include/api/bilibili/api.h`; JSON result structs: `wiliwili/include/api/bilibili/result/`\n- `parseJson` looks for `data` (object/array) first, then `result` (object) as fallback when `code==0`\n- **WBI signing** (`getResultWithWbiAsync`): fetches `img_key`+`sub_key` from `Api::Nav`, computes `mixin_key`, appends `wts` + `w_rid`. Keys cached for 1 hour. Required for most `/x/web-interface/` endpoints since 2023.\n- HTTP defaults: `User-Agent: wiliwili`, `Referer: https://www.bilibili.com/client`, timeout 10000ms. Proxy and TLS verify are configurable via `SettingItem::HTTP_PROXY*` / `SettingItem::TLS_VERIFY`.\n\n### RecyclingGrid (custom recycler view)\nImplement `RecyclingGridDataSource` and call `recyclingGrid->setDataSource(...)`. Example from `player_activity.cpp`:\n```cpp\nclass DataSourceFoo : public RecyclingGridDataSource {\n    RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override {\n        auto* item = (MyCell*)recycler->dequeueReusableCell(\"Cell\");\n        item->setData(list[index]);\n        return item;\n    }\n    size_t getItemCount() override { return list.size(); }\n    void onItemSelected(RecyclingGrid* recycler, size_t index) override { /* handle tap */ }\n    void clearData() override { list.clear(); }\n};\n```\n`recyclingGrid->onNextPage([]{...})` triggers when the user scrolls to the end (infinite scroll).\n\n### Image loading\nAlways append a platform-aware suffix:\n```cpp\nImageHelper::with(imageView)->load(url + ImageHelper::h_ext);    // horizontal thumbnail (@672w_378h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::v_ext);    // vertical thumbnail (@312w_420h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::face_ext); // avatar (@96w_96h on PC)\n```\n`IMAGE_EXT` resolves to `.webp` when `USE_WEBP` is defined, else `.jpg`. PSV uses smaller sizes (e.g. `@256w_144h` for horizontal).\n\n### Global event bus\nDefined in `wiliwili/include/utils/event_helper.hpp`:\n- `MPV_E` — player state (`MPV_LOADED`, `MPV_PAUSE`, `MPV_RESUME`, `UPDATE_PROGRESS`, `END_OF_FILE`, `RESET`, `RESTART`, `CACHE_SPEED_CHANGE`, `VIDEO_SPEED_CHANGE`, …)\n- `APP_E` — app-wide custom events (string key + void*)\n- `SEARCH_E` — search page events\n\nSubscribe: `MPV_E->subscribe([](MpvEventEnum e){ ... });`  Fire: `MPV_E->fire(MPV_PAUSE);`\n\n### Settings / ProgramConfig\n`ProgramConfig::instance()` is a singleton that must be initialized with `init()` **before** `brls::Application::init()`. All settings use the `SettingItem` enum (defined in `wiliwili/include/utils/config_helper.hpp`). Notable items:\n- `PLAYER_HWDEC` / `PLAYER_HWDEC_CUSTOM` — hardware decode method\n- `APP_RESOURCES` — active custom theme ID (requires restart to apply)\n- `KEYMAP` — button icon set: `\"xbox\"` (PC default), `\"ps\"`, `\"keyboard\"`\n- `HTTP_PROXY`, `HTTP_PROXY_STATUS`, `TLS_VERIFY` — network settings\n- `SHORTCUT_*` — rebindable keyboard shortcuts (modifier-key strings, e.g. `\"ctrl-r\"`)\n\n### Custom Theme / Layout System\nThemes live in `{configDir}/theme/{id}/` and can override any file under `resources/`.\n- A theme directory must contain `resources_meta.json`: `{\"name\":\"…\",\"desc\":\"…\",\"version\":\"…\",\"author\":\"…\"}`\n- Discovered by `ProgramConfig::loadCustomThemes()` at startup; theme ID stored in `SettingItem::APP_RESOURCES`\n- Switching themes calls `DialogHelper::quitApp()` — restart is required\n- Reference theme repo: https://github.com/xfangfang/wiliwili_theme\n\n### Custom Fonts & Icons\nDrop these files into the config directory to override built-in assets:\n\n| Filename | Purpose |\n|---|---|\n| `font.ttf` | Main UI font |\n| `icon.ttf` | Button icon font |\n| `emoji.ttf` | Emoji font |\n| `danmaku.ttf` | Danmaku overlay font |\n| `gamecontrollerdb.txt` | SDL gamepad database (desktop only) |\n\nBuilt-in keymap fonts in `resources/font/`: `keymap_xbox.ttf`, `keymap_ps.ttf`, `keymap_keyboard.ttf`.\n\n### Anime4K / Shader System\n`ShaderHelper` (singleton, `wiliwili/include/utils/shader_helper.hpp`) manages profiles loaded from `{configDir}/shader.json`:\n```json\n{\n  \"profiles\": [\n    {\n      \"name\": \"Anime4K Mode A\",\n      \"shaders\": [\"/path/to/Anime4K_Clamp_Highlights.glsl\"],\n      \"settings\": [[\"set\", \"scaler\", \"ewa_lanczossharp\"]]\n    }\n  ],\n  \"animeList\": [{ \"anime\": 28223043, \"profile\": \"Anime4K Mode A\" }]\n}\n```\n- `settings` entries: `[\"set\",\"key\",\"val\"]`, `[\"change-list\",\"key\",\"op\",\"val\"]`, or `[\"run\",\"cmd\"]`.\n- Apply/clear with `ShaderHelper::instance().setShader(index)` / `clearShader()`.\n\n### MPV Rendering Modes\n\n| Mode | Trigger | Notes                                                  |\n|---|---|--------------------------------------------------------|\n| Framebuffer (default) | `MPV_USE_FB` (auto) | GL 3.2+ / GLES 2.0+; best performance                  |\n| No framebuffer | `-DMPV_NO_FB=ON` | MPV draws fullscreen, UI is overlaid; PS4, PSV-GL, GL2 |\n| Software render | `-DMPV_SW_RENDER=ON` | CPU only; for UWP/D3D12 porting                        |\n| deko3d | `BOREALIS_USE_DEKO3D` | Switch native; 4K@60 with hardware decode              |\n| GXM | `BOREALIS_USE_GXM` | PSVita native                                          |\n| D3D11 | `BOREALIS_USE_D3D11` | Windows Native/UWP                                     |\n\nDefault hardware decode: Switch/GXM → `\"auto\"`, PSV-GL → `\"vita-copy\"`, PS4 → `\"no\"`, Desktop → `\"auto-safe\"`.\n\n### Video Quality Codes\n\n| Code | Quality | Notes |\n|---|---|---|\n| 127 | 8K | |\n| 120 | 4K | |\n| 116 | 1080P60 | Default on non-PSV |\n| 80 | 1080P | |\n| 64 | 720P | Default PSV GXM |\n| 32 | 480P | Default PSV OpenGL |\n| 16 | 360P | Default when logged out |\n\n---\n\n## Config Directory Locations\n\n| Platform | Path |\n|---|---|\n| Nintendo Switch | `/config/wiliwili/` |\n| PS4 | `/data/wiliwili/` |\n| PSVita | `ux0:/data/wiliwili/` |\n| macOS (release) | `~/Library/Application Support/wiliwili/` |\n| Linux (release) | `$XDG_CONFIG_HOME/wiliwili/` or `~/.config/wiliwili/` |\n| Windows (release) | `%LOCALAPPDATA%\\xfangfang\\wiliwili\\` |\n| Any platform (debug build) | `./config/wiliwili/` (next to binary) |\n\nMain config file: `{configDir}/wiliwili_config.json` (cookie, refreshToken, all SettingItem values, GA client ID, search history).\n\n---\n\n## Build Commands\n\n### Desktop (macOS)\n```sh\nbrew install mpv webp\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(sysctl -n hw.ncpu)\n# macOS app bundle:\nmake -C build wiliwili.app\n```\n\n### Desktop (Linux/Ubuntu)\n```sh\nsudo apt install libssl-dev libmpv-dev libwebp-dev\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(nproc)\n# System install with .desktop entry:\ncmake -B build -DPLATFORM_DESKTOP=ON -DINSTALL=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr\nmake -C build wiliwili -j$(nproc) && sudo make -C build install\n```\n\n### Nintendo Switch (Docker, recommended)\n```sh\ndocker run --rm -v $(pwd):/data devkitpro/devkita64:20251117 bash -c \"/data/scripts/build_switch.sh\"\n# deko3d variant (4K@60 hardware decode):\n# see scripts/build_switch_deko3d.sh\n```\nSwitch requires custom ffmpeg/mpv (devkitpro defaults don't support network streams):\n```sh\nbase_url=\"https://github.com/xfangfang/wiliwili/releases/download/v0.1.0\"\nsudo dkp-pacman -U $base_url/switch-ffmpeg-7.1-1-any.pkg.tar.zst \\\n                   $base_url/switch-libmpv-0.36.0-3-any.pkg.tar.zst\ncmake -B cmake-build-switch -DPLATFORM_SWITCH=ON\nmake -C cmake-build-switch wiliwili.nro -j$(nproc)\n```\n\n### PSVita / PS4 (Docker)\n```sh\n# PSVita GXM (recommended):\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_psv_builder:latest-gxm \\\n    \"cmake -B cmake-build-psv -G Ninja -DPLATFORM_PSV=ON -DUSE_GXM=ON \\\n     -DUSE_SYSTEM_CURL=ON -DUSE_VITA_SHARK=OFF -DCMAKE_BUILD_TYPE=Release && \\\n     cmake --build cmake-build-psv\"\n# PS4:\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_ps4_builder:latest \\\n    \"cmake -B cmake-build-ps4 -DPLATFORM_PS4=ON -DMPV_NO_FB=ON \\\n     -DUSE_SYSTEM_CPR=ON && make -C cmake-build-ps4 -j$(nproc)\"\n```\n\n### Useful CMake flags\n| Flag | Purpose |\n|---|---|\n| `-DUSE_SDL2=ON` | Use SDL2 instead of GLFW |\n| `-DMPV_NO_FB=ON` | No framebuffer (PS4, PSV GL, GL 2.0) |\n| `-DMPV_SW_RENDER=ON` | CPU software rendering |\n| `-DMPV_BUNDLE_DLL=ON` | Bundle mpv.dll into exe (Windows + USE_LIBROMFS=ON) |\n| `-DDISABLE_OPENCC=ON` | Skip Chinese conversion library |\n| `-DDISABLE_WEBP=ON` | Skip WebP support |\n| `-DINSTALL=ON` | Linux system install with desktop entry |\n| `-DDEBUG_SANITIZER=ON` | Enable ASan/UBSan (debug only) |\n| `-DUSE_LIBROMFS=ON` | Embed resources into binary |\n| `-DBUILTIN_NSP=ON` | Embed NSP forwarder (Switch only) |\n| `-DAPP_PLATFORM_CUSTOM_LIBS=ON` | Manual deps: `APP_PLATFORM_INCLUDE` + `APP_PLATFORM_LINK_OPTION` |\n\n---\n\n## Debugging\n\n- Run with `-d` for debug log level, `-v` for borealis visual debug overlay, `-t` for mpv terminal output, `-o <file>` to write log to file.\n- To test a specific page without navigating the UI, uncomment the relevant `Intent::open*()` line in `main.cpp` (many test BV IDs are already commented there).\n- `ProgramConfig::instance().init()` loads cookies and all settings; it must run before `brls::Application::init()`.\n- In-app network diagnostics: Settings → Tools → Network Diagnostics (`fragment/setting_network`). Shows API connectivity, system/server time diff, WiFi state, IP, DNS.\n- On Switch: if the app is black-screen on startup, delete `/config/wiliwili/` from SD card and retry.\n\n---\n\n## Platform Macros\n\n| Macro | Platform |\n|---|---|\n| `__SWITCH__` | Nintendo Switch |\n| `__PSV__` | PlayStation Vita |\n| `PS4` | PlayStation 4 |\n| `BOREALIS_USE_OPENGL` / `BOREALIS_USE_DEKO3D` / `BOREALIS_USE_D3D11` / `BOREALIS_USE_GXM` | Rendering backend |\n| `MPV_USE_FB` / `MPV_NO_FB` | Framebuffer mode (auto-derived from above) |\n| `USE_WEBP` | WebP image decoding enabled |\n\n---\n\n## Key Files at a Glance\n\n| File | Description |\n|---|---|\n| `wiliwili/source/main.cpp` | Entry point; registers views, launches activity |\n| `wiliwili/include/utils/activity_helper.hpp` | `Intent` — all navigation |\n| `wiliwili/include/utils/config_helper.hpp` | `ProgramConfig` singleton, `SettingItem` enum |\n| `wiliwili/include/utils/event_helper.hpp` | Global event buses (`MPV_E`, `APP_E`, `SEARCH_E`) |\n| `wiliwili/include/presenter/presenter.h` | `ASYNC_RETAIN`/`ASYNC_RELEASE` macros |\n| `wiliwili/include/api/bilibili/util/http.hpp` | `HTTP::getResultAsync`, `getResultWithWbiAsync` |\n| `wiliwili/include/api/bilibili/util/wbi.hpp` | WBI signing implementation |\n| `wiliwili/include/api/bilibili/api.h` | All Bilibili API URL constants |\n| `wiliwili/include/view/recycling_grid.hpp` | Recycler list widget |\n| `wiliwili/include/utils/image_helper.hpp` | Async image loading + platform-sized URL suffixes |\n| `wiliwili/include/utils/shader_helper.hpp` | Anime4K / mpv shader profile management |\n| `wiliwili/include/view/mpv_core.hpp` | MPV singleton, playback control, rendering backend |\n| `wiliwili/include/view/danmaku_core.hpp` | Danmaku (bullet comment) overlay rendering |\n| `wiliwili/include/api/live/danmaku_live.hpp` | Live room WebSocket danmaku via mongoose |\n| `resources/xml/` | All UI layout files |\n| `resources/i18n/` | Translations (en-US, zh-Hans, zh-Hant, ja, ko, it, ja-RYU) |\n| `scripts/` | Build scripts for each platform; `README.md` covers custom ffmpeg/mpv for Switch |\n\n"},"files":{"AGENTS.md":"# AGENTS.md — wiliwili Codebase Guide\n\nwiliwili is a cross-platform Bilibili client (Nintendo Switch, PSVita, PS4, PC) built in C++17. The UI is rendered via **nanovg**, windowing via **GLFW** or **SDL2**, and video playback via **mpv + FFmpeg**.\n\n---\n\n## Architecture Layers (Android-inspired)\n\n| Layer | Directory | Role |\n|---|---|---|\n| Activity | `wiliwili/include/activity/`, `wiliwili/source/activity/` | Top-level full-screen pages |\n| Fragment | `wiliwili/include/fragment/`, `wiliwili/source/fragment/` | Sub-pages composed within Activities |\n| Presenter | `wiliwili/include/presenter/` | Async data-fetching logic; Fragments inherit Presenters |\n| View | `wiliwili/include/view/` | Custom reusable UI components (MPVCore, RecyclingGrid, VideoView…) |\n| API | `wiliwili/include/api/bilibili/` | Bilibili REST/gRPC wrappers via cpr + nlohmann/json |\n| Utils | `wiliwili/include/utils/` | Intent (navigation), ProgramConfig (settings), ImageHelper, EventHelper |\n\nThe UI framework is a custom fork of **borealis** (`library/borealis/`). All views inherit `brls::Box` or its subclasses.\n\n---\n\n## Key Patterns\n\n### Presenter / async safety\nEvery Fragment that fetches data inherits a Presenter class. Use the macros in `wiliwili/include/presenter/presenter.h` to guard callbacks against use-after-free:\n```cpp\nvoid requestSomething() {\n    ASYNC_RETAIN  // captures token, tokenCounter\n    bilibili::HTTP::getResultAsync<MyType>(url, params, [ASYNC_TOKEN](auto result) {\n        ASYNC_RELEASE  // returns early if the view was destroyed\n        brls::Threading::sync([this, result]() {\n            // safe to update UI here (main thread)\n        });\n    });\n}\n```\nUse `CHECK_AND_SET_REQUEST` / `UNSET_REQUEST` to prevent duplicate in-flight requests.\n\n### XML-driven UI\n- Layout files live in `resources/xml/` (`activity/`, `fragment/`, `views/`).\n- Inflate in constructor: `this->inflateFromXMLRes(\"xml/fragment/home_recommends.xml\");`\n- Bind a child by XML id: `BRLS_BIND(RecyclingGrid, recyclingGrid, \"home/recommends/recyclingGrid\");`\n- Custom views must be registered before use: `brls::Application::registerXMLView(\"RecyclingGrid\", RecyclingGrid::create);` (done in `Register::initCustomView()` called from `main.cpp`).\n- Valid custom XML element names (all registered in `Register::initCustomView()`):\n  - **UI widgets:** `AutoTabFrame`, `RecyclingGrid`, `VideoView`, `VideoProfile`, `QRImage`, `SVGImage`, `TextBox`, `VideoProgressSlider`, `GalleryView`, `CustomButton`, `HintLabel`\n  - **App views:** `UserInfoView`, `UpUserSmall`, `VideoComment`, `ButtonClose`, `CheckBox`, `SelectorCell`, `AnimationImage`, `ShareBox`, `DynamicVideoCardView`, `DynamicArticleView`\n  - **Fragments:** `HomeTab`, `DynamicTab`, `MineTab`, `HomeRecommends`, `HomeHotsAll`, `HomeHotsHistory`, `HomeHotsWeekly`, `HomeHotsRank`, `HomeHots`, `HomeLive`, `HomeBangumi`, `HomeCinema`, `MineHistory`, `MineLater`, `MineCollection`, `MineBangumi`, `SearchTab`, `SearchOrder`, `SearchVideo`, `SearchCinema`, `SearchBangumi`, `SearchHots`, `SearchHistory`\n- i18n key syntax in XML: `@i18n/wiliwili/some/key`; translation files are in `resources/i18n/{en-US,zh-Hans,zh-Hant,ja,ko,it,ja-RYU}/wiliwili.json`.\n\n### Navigation — use `Intent`, never push directly\n`wiliwili/include/utils/activity_helper.hpp` defines all navigation entry points:\n```cpp\nIntent::openBV(\"BV1Da411Y7U4\");         // video by BV id\nIntent::openSeasonByEpId(323434);       // bangumi episode\nIntent::openLive(1942240);              // live room\nIntent::openSearch(\"keyword\");\nIntent::openSetting();\nIntent::openCollection(\"2511565362\");   // favorites folder\nIntent::openPgcFilter(\"/page/home/pgc/more?type=2&...\"); // PGC filter\n```\n\n### HTTP API calls\nAll calls are async via `bilibili::HTTP` (`wiliwili/include/api/bilibili/util/http.hpp`):\n```cpp\n// Standard GET → parses {\"code\":0,\"data\":{...}}\nHTTP::getResultAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For WBI-signed endpoints (most web-interface APIs since 2023):\nHTTP::getResultWithWbiAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For app-signed endpoints (pass needSign=true):\nHTTP::getResultAsync<MyResult>(url, params, callback, error, /*needSign=*/true);\n\n// POST with typed response:\nHTTP::postResultAsync<MyResult>(url, params, payload, callback, error);\n```\n- API URL constants: `wiliwili/include/api/bilibili/api.h`; JSON result structs: `wiliwili/include/api/bilibili/result/`\n- `parseJson` looks for `data` (object/array) first, then `result` (object) as fallback when `code==0`\n- **WBI signing** (`getResultWithWbiAsync`): fetches `img_key`+`sub_key` from `Api::Nav`, computes `mixin_key`, appends `wts` + `w_rid`. Keys cached for 1 hour. Required for most `/x/web-interface/` endpoints since 2023.\n- HTTP defaults: `User-Agent: wiliwili`, `Referer: https://www.bilibili.com/client`, timeout 10000ms. Proxy and TLS verify are configurable via `SettingItem::HTTP_PROXY*` / `SettingItem::TLS_VERIFY`.\n\n### RecyclingGrid (custom recycler view)\nImplement `RecyclingGridDataSource` and call `recyclingGrid->setDataSource(...)`. Example from `player_activity.cpp`:\n```cpp\nclass DataSourceFoo : public RecyclingGridDataSource {\n    RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override {\n        auto* item = (MyCell*)recycler->dequeueReusableCell(\"Cell\");\n        item->setData(list[index]);\n        return item;\n    }\n    size_t getItemCount() override { return list.size(); }\n    void onItemSelected(RecyclingGrid* recycler, size_t index) override { /* handle tap */ }\n    void clearData() override { list.clear(); }\n};\n```\n`recyclingGrid->onNextPage([]{...})` triggers when the user scrolls to the end (infinite scroll).\n\n### Image loading\nAlways append a platform-aware suffix:\n```cpp\nImageHelper::with(imageView)->load(url + ImageHelper::h_ext);    // horizontal thumbnail (@672w_378h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::v_ext);    // vertical thumbnail (@312w_420h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::face_ext); // avatar (@96w_96h on PC)\n```\n`IMAGE_EXT` resolves to `.webp` when `USE_WEBP` is defined, else `.jpg`. PSV uses smaller sizes (e.g. `@256w_144h` for horizontal).\n\n### Global event bus\nDefined in `wiliwili/include/utils/event_helper.hpp`:\n- `MPV_E` — player state (`MPV_LOADED`, `MPV_PAUSE`, `MPV_RESUME`, `UPDATE_PROGRESS`, `END_OF_FILE`, `RESET`, `RESTART`, `CACHE_SPEED_CHANGE`, `VIDEO_SPEED_CHANGE`, …)\n- `APP_E` — app-wide custom events (string key + void*)\n- `SEARCH_E` — search page events\n\nSubscribe: `MPV_E->subscribe([](MpvEventEnum e){ ... });`  Fire: `MPV_E->fire(MPV_PAUSE);`\n\n### Settings / ProgramConfig\n`ProgramConfig::instance()` is a singleton that must be initialized with `init()` **before** `brls::Application::init()`. All settings use the `SettingItem` enum (defined in `wiliwili/include/utils/config_helper.hpp`). Notable items:\n- `PLAYER_HWDEC` / `PLAYER_HWDEC_CUSTOM` — hardware decode method\n- `APP_RESOURCES` — active custom theme ID (requires restart to apply)\n- `KEYMAP` — button icon set: `\"xbox\"` (PC default), `\"ps\"`, `\"keyboard\"`\n- `HTTP_PROXY`, `HTTP_PROXY_STATUS`, `TLS_VERIFY` — network settings\n- `SHORTCUT_*` — rebindable keyboard shortcuts (modifier-key strings, e.g. `\"ctrl-r\"`)\n\n### Custom Theme / Layout System\nThemes live in `{configDir}/theme/{id}/` and can override any file under `resources/`.\n- A theme directory must contain `resources_meta.json`: `{\"name\":\"…\",\"desc\":\"…\",\"version\":\"…\",\"author\":\"…\"}`\n- Discovered by `ProgramConfig::loadCustomThemes()` at startup; theme ID stored in `SettingItem::APP_RESOURCES`\n- Switching themes calls `DialogHelper::quitApp()` — restart is required\n- Reference theme repo: https://github.com/xfangfang/wiliwili_theme\n\n### Custom Fonts & Icons\nDrop these files into the config directory to override built-in assets:\n\n| Filename | Purpose |\n|---|---|\n| `font.ttf` | Main UI font |\n| `icon.ttf` | Button icon font |\n| `emoji.ttf` | Emoji font |\n| `danmaku.ttf` | Danmaku overlay font |\n| `gamecontrollerdb.txt` | SDL gamepad database (desktop only) |\n\nBuilt-in keymap fonts in `resources/font/`: `keymap_xbox.ttf`, `keymap_ps.ttf`, `keymap_keyboard.ttf`.\n\n### Anime4K / Shader System\n`ShaderHelper` (singleton, `wiliwili/include/utils/shader_helper.hpp`) manages profiles loaded from `{configDir}/shader.json`:\n```json\n{\n  \"profiles\": [\n    {\n      \"name\": \"Anime4K Mode A\",\n      \"shaders\": [\"/path/to/Anime4K_Clamp_Highlights.glsl\"],\n      \"settings\": [[\"set\", \"scaler\", \"ewa_lanczossharp\"]]\n    }\n  ],\n  \"animeList\": [{ \"anime\": 28223043, \"profile\": \"Anime4K Mode A\" }]\n}\n```\n- `settings` entries: `[\"set\",\"key\",\"val\"]`, `[\"change-list\",\"key\",\"op\",\"val\"]`, or `[\"run\",\"cmd\"]`.\n- Apply/clear with `ShaderHelper::instance().setShader(index)` / `clearShader()`.\n\n### MPV Rendering Modes\n\n| Mode | Trigger | Notes                                                  |\n|---|---|--------------------------------------------------------|\n| Framebuffer (default) | `MPV_USE_FB` (auto) | GL 3.2+ / GLES 2.0+; best performance                  |\n| No framebuffer | `-DMPV_NO_FB=ON` | MPV draws fullscreen, UI is overlaid; PS4, PSV-GL, GL2 |\n| Software render | `-DMPV_SW_RENDER=ON` | CPU only; for UWP/D3D12 porting                        |\n| deko3d | `BOREALIS_USE_DEKO3D` | Switch native; 4K@60 with hardware decode              |\n| GXM | `BOREALIS_USE_GXM` | PSVita native                                          |\n| D3D11 | `BOREALIS_USE_D3D11` | Windows Native/UWP                                     |\n\nDefault hardware decode: Switch/GXM → `\"auto\"`, PSV-GL → `\"vita-copy\"`, PS4 → `\"no\"`, Desktop → `\"auto-safe\"`.\n\n### Video Quality Codes\n\n| Code | Quality | Notes |\n|---|---|---|\n| 127 | 8K | |\n| 120 | 4K | |\n| 116 | 1080P60 | Default on non-PSV |\n| 80 | 1080P | |\n| 64 | 720P | Default PSV GXM |\n| 32 | 480P | Default PSV OpenGL |\n| 16 | 360P | Default when logged out |\n\n---\n\n## Config Directory Locations\n\n| Platform | Path |\n|---|---|\n| Nintendo Switch | `/config/wiliwili/` |\n| PS4 | `/data/wiliwili/` |\n| PSVita | `ux0:/data/wiliwili/` |\n| macOS (release) | `~/Library/Application Support/wiliwili/` |\n| Linux (release) | `$XDG_CONFIG_HOME/wiliwili/` or `~/.config/wiliwili/` |\n| Windows (release) | `%LOCALAPPDATA%\\xfangfang\\wiliwili\\` |\n| Any platform (debug build) | `./config/wiliwili/` (next to binary) |\n\nMain config file: `{configDir}/wiliwili_config.json` (cookie, refreshToken, all SettingItem values, GA client ID, search history).\n\n---\n\n## Build Commands\n\n### Desktop (macOS)\n```sh\nbrew install mpv webp\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(sysctl -n hw.ncpu)\n# macOS app bundle:\nmake -C build wiliwili.app\n```\n\n### Desktop (Linux/Ubuntu)\n```sh\nsudo apt install libssl-dev libmpv-dev libwebp-dev\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(nproc)\n# System install with .desktop entry:\ncmake -B build -DPLATFORM_DESKTOP=ON -DINSTALL=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr\nmake -C build wiliwili -j$(nproc) && sudo make -C build install\n```\n\n### Nintendo Switch (Docker, recommended)\n```sh\ndocker run --rm -v $(pwd):/data devkitpro/devkita64:20251117 bash -c \"/data/scripts/build_switch.sh\"\n# deko3d variant (4K@60 hardware decode):\n# see scripts/build_switch_deko3d.sh\n```\nSwitch requires custom ffmpeg/mpv (devkitpro defaults don't support network streams):\n```sh\nbase_url=\"https://github.com/xfangfang/wiliwili/releases/download/v0.1.0\"\nsudo dkp-pacman -U $base_url/switch-ffmpeg-7.1-1-any.pkg.tar.zst \\\n                   $base_url/switch-libmpv-0.36.0-3-any.pkg.tar.zst\ncmake -B cmake-build-switch -DPLATFORM_SWITCH=ON\nmake -C cmake-build-switch wiliwili.nro -j$(nproc)\n```\n\n### PSVita / PS4 (Docker)\n```sh\n# PSVita GXM (recommended):\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_psv_builder:latest-gxm \\\n    \"cmake -B cmake-build-psv -G Ninja -DPLATFORM_PSV=ON -DUSE_GXM=ON \\\n     -DUSE_SYSTEM_CURL=ON -DUSE_VITA_SHARK=OFF -DCMAKE_BUILD_TYPE=Release && \\\n     cmake --build cmake-build-psv\"\n# PS4:\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_ps4_builder:latest \\\n    \"cmake -B cmake-build-ps4 -DPLATFORM_PS4=ON -DMPV_NO_FB=ON \\\n     -DUSE_SYSTEM_CPR=ON && make -C cmake-build-ps4 -j$(nproc)\"\n```\n\n### Useful CMake flags\n| Flag | Purpose |\n|---|---|\n| `-DUSE_SDL2=ON` | Use SDL2 instead of GLFW |\n| `-DMPV_NO_FB=ON` | No framebuffer (PS4, PSV GL, GL 2.0) |\n| `-DMPV_SW_RENDER=ON` | CPU software rendering |\n| `-DMPV_BUNDLE_DLL=ON` | Bundle mpv.dll into exe (Windows + USE_LIBROMFS=ON) |\n| `-DDISABLE_OPENCC=ON` | Skip Chinese conversion library |\n| `-DDISABLE_WEBP=ON` | Skip WebP support |\n| `-DINSTALL=ON` | Linux system install with desktop entry |\n| `-DDEBUG_SANITIZER=ON` | Enable ASan/UBSan (debug only) |\n| `-DUSE_LIBROMFS=ON` | Embed resources into binary |\n| `-DBUILTIN_NSP=ON` | Embed NSP forwarder (Switch only) |\n| `-DAPP_PLATFORM_CUSTOM_LIBS=ON` | Manual deps: `APP_PLATFORM_INCLUDE` + `APP_PLATFORM_LINK_OPTION` |\n\n---\n\n## Debugging\n\n- Run with `-d` for debug log level, `-v` for borealis visual debug overlay, `-t` for mpv terminal output, `-o <file>` to write log to file.\n- To test a specific page without navigating the UI, uncomment the relevant `Intent::open*()` line in `main.cpp` (many test BV IDs are already commented there).\n- `ProgramConfig::instance().init()` loads cookies and all settings; it must run before `brls::Application::init()`.\n- In-app network diagnostics: Settings → Tools → Network Diagnostics (`fragment/setting_network`). Shows API connectivity, system/server time diff, WiFi state, IP, DNS.\n- On Switch: if the app is black-screen on startup, delete `/config/wiliwili/` from SD card and retry.\n\n---\n\n## Platform Macros\n\n| Macro | Platform |\n|---|---|\n| `__SWITCH__` | Nintendo Switch |\n| `__PSV__` | PlayStation Vita |\n| `PS4` | PlayStation 4 |\n| `BOREALIS_USE_OPENGL` / `BOREALIS_USE_DEKO3D` / `BOREALIS_USE_D3D11` / `BOREALIS_USE_GXM` | Rendering backend |\n| `MPV_USE_FB` / `MPV_NO_FB` | Framebuffer mode (auto-derived from above) |\n| `USE_WEBP` | WebP image decoding enabled |\n\n---\n\n## Key Files at a Glance\n\n| File | Description |\n|---|---|\n| `wiliwili/source/main.cpp` | Entry point; registers views, launches activity |\n| `wiliwili/include/utils/activity_helper.hpp` | `Intent` — all navigation |\n| `wiliwili/include/utils/config_helper.hpp` | `ProgramConfig` singleton, `SettingItem` enum |\n| `wiliwili/include/utils/event_helper.hpp` | Global event buses (`MPV_E`, `APP_E`, `SEARCH_E`) |\n| `wiliwili/include/presenter/presenter.h` | `ASYNC_RETAIN`/`ASYNC_RELEASE` macros |\n| `wiliwili/include/api/bilibili/util/http.hpp` | `HTTP::getResultAsync`, `getResultWithWbiAsync` |\n| `wiliwili/include/api/bilibili/util/wbi.hpp` | WBI signing implementation |\n| `wiliwili/include/api/bilibili/api.h` | All Bilibili API URL constants |\n| `wiliwili/include/view/recycling_grid.hpp` | Recycler list widget |\n| `wiliwili/include/utils/image_helper.hpp` | Async image loading + platform-sized URL suffixes |\n| `wiliwili/include/utils/shader_helper.hpp` | Anime4K / mpv shader profile management |\n| `wiliwili/include/view/mpv_core.hpp` | MPV singleton, playback control, rendering backend |\n| `wiliwili/include/view/danmaku_core.hpp` | Danmaku (bullet comment) overlay rendering |\n| `wiliwili/include/api/live/danmaku_live.hpp` | Live room WebSocket danmaku via mongoose |\n| `resources/xml/` | All UI layout files |\n| `resources/i18n/` | Translations (en-US, zh-Hans, zh-Hant, ja, ko, it, ja-RYU) |\n| `scripts/` | Build scripts for each platform; `README.md` covers custom ffmpeg/mpv for Switch |\n\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/xfangfang/wiliwili/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md — wiliwili Codebase Guide\n\nwiliwili is a cross-platform Bilibili client (Nintendo Switch, PSVita, PS4, PC) built in C++17. The UI is rendered via **nanovg**, windowing via **GLFW** or **SDL2**, and video playback via **mpv + FFmpeg**.\n\n---\n\n## Architecture Layers (Android-inspired)\n\n| Layer | Directory | Role |\n|---|---|---|\n| Activity | `wiliwili/include/activity/`, `wiliwili/source/activity/` | Top-level full-screen pages |\n| Fragment | `wiliwili/include/fragment/`, `wiliwili/source/fragment/` | Sub-pages composed within Activities |\n| Presenter | `wiliwili/include/presenter/` | Async data-fetching logic; Fragments inherit Presenters |\n| View | `wiliwili/include/view/` | Custom reusable UI components (MPVCore, RecyclingGrid, VideoView…) |\n| API | `wiliwili/include/api/bilibili/` | Bilibili REST/gRPC wrappers via cpr + nlohmann/json |\n| Utils | `wiliwili/include/utils/` | Intent (navigation), ProgramConfig (settings), ImageHelper, EventHelper |\n\nThe UI framework is a custom fork of **borealis** (`library/borealis/`). All views inherit `brls::Box` or its subclasses.\n\n---\n\n## Key Patterns\n\n### Presenter / async safety\nEvery Fragment that fetches data inherits a Presenter class. Use the macros in `wiliwili/include/presenter/presenter.h` to guard callbacks against use-after-free:\n```cpp\nvoid requestSomething() {\n    ASYNC_RETAIN  // captures token, tokenCounter\n    bilibili::HTTP::getResultAsync<MyType>(url, params, [ASYNC_TOKEN](auto result) {\n        ASYNC_RELEASE  // returns early if the view was destroyed\n        brls::Threading::sync([this, result]() {\n            // safe to update UI here (main thread)\n        });\n    });\n}\n```\nUse `CHECK_AND_SET_REQUEST` / `UNSET_REQUEST` to prevent duplicate in-flight requests.\n\n### XML-driven UI\n- Layout files live in `resources/xml/` (`activity/`, `fragment/`, `views/`).\n- Inflate in constructor: `this->inflateFromXMLRes(\"xml/fragment/home_recommends.xml\");`\n- Bind a child by XML id: `BRLS_BIND(RecyclingGrid, recyclingGrid, \"home/recommends/recyclingGrid\");`\n- Custom views must be registered before use: `brls::Application::registerXMLView(\"RecyclingGrid\", RecyclingGrid::create);` (done in `Register::initCustomView()` called from `main.cpp`).\n- Valid custom XML element names (all registered in `Register::initCustomView()`):\n  - **UI widgets:** `AutoTabFrame`, `RecyclingGrid`, `VideoView`, `VideoProfile`, `QRImage`, `SVGImage`, `TextBox`, `VideoProgressSlider`, `GalleryView`, `CustomButton`, `HintLabel`\n  - **App views:** `UserInfoView`, `UpUserSmall`, `VideoComment`, `ButtonClose`, `CheckBox`, `SelectorCell`, `AnimationImage`, `ShareBox`, `DynamicVideoCardView`, `DynamicArticleView`\n  - **Fragments:** `HomeTab`, `DynamicTab`, `MineTab`, `HomeRecommends`, `HomeHotsAll`, `HomeHotsHistory`, `HomeHotsWeekly`, `HomeHotsRank`, `HomeHots`, `HomeLive`, `HomeBangumi`, `HomeCinema`, `MineHistory`, `MineLater`, `MineCollection`, `MineBangumi`, `SearchTab`, `SearchOrder`, `SearchVideo`, `SearchCinema`, `SearchBangumi`, `SearchHots`, `SearchHistory`\n- i18n key syntax in XML: `@i18n/wiliwili/some/key`; translation files are in `resources/i18n/{en-US,zh-Hans,zh-Hant,ja,ko,it,ja-RYU}/wiliwili.json`.\n\n### Navigation — use `Intent`, never push directly\n`wiliwili/include/utils/activity_helper.hpp` defines all navigation entry points:\n```cpp\nIntent::openBV(\"BV1Da411Y7U4\");         // video by BV id\nIntent::openSeasonByEpId(323434);       // bangumi episode\nIntent::openLive(1942240);              // live room\nIntent::openSearch(\"keyword\");\nIntent::openSetting();\nIntent::openCollection(\"2511565362\");   // favorites folder\nIntent::openPgcFilter(\"/page/home/pgc/more?type=2&...\"); // PGC filter\n```\n\n### HTTP API calls\nAll calls are async via `bilibili::HTTP` (`wiliwili/include/api/bilibili/util/http.hpp`):\n```cpp\n// Standard GET → parses {\"code\":0,\"data\":{...}}\nHTTP::getResultAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For WBI-signed endpoints (most web-interface APIs since 2023):\nHTTP::getResultWithWbiAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For app-signed endpoints (pass needSign=true):\nHTTP::getResultAsync<MyResult>(url, params, callback, error, /*needSign=*/true);\n\n// POST with typed response:\nHTTP::postResultAsync<MyResult>(url, params, payload, callback, error);\n```\n- API URL constants: `wiliwili/include/api/bilibili/api.h`; JSON result structs: `wiliwili/include/api/bilibili/result/`\n- `parseJson` looks for `data` (object/array) first, then `result` (object) as fallback when `code==0`\n- **WBI signing** (`getResultWithWbiAsync`): fetches `img_key`+`sub_key` from `Api::Nav`, computes `mixin_key`, appends `wts` + `w_rid`. Keys cached for 1 hour. Required for most `/x/web-interface/` endpoints since 2023.\n- HTTP defaults: `User-Agent: wiliwili`, `Referer: https://www.bilibili.com/client`, timeout 10000ms. Proxy and TLS verify are configurable via `SettingItem::HTTP_PROXY*` / `SettingItem::TLS_VERIFY`.\n\n### RecyclingGrid (custom recycler view)\nImplement `RecyclingGridDataSource` and call `recyclingGrid->setDataSource(...)`. Example from `player_activity.cpp`:\n```cpp\nclass DataSourceFoo : public RecyclingGridDataSource {\n    RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override {\n        auto* item = (MyCell*)recycler->dequeueReusableCell(\"Cell\");\n        item->setData(list[index]);\n        return item;\n    }\n    size_t getItemCount() override { return list.size(); }\n    void onItemSelected(RecyclingGrid* recycler, size_t index) override { /* handle tap */ }\n    void clearData() override { list.clear(); }\n};\n```\n`recyclingGrid->onNextPage([]{...})` triggers when the user scrolls to the end (infinite scroll).\n\n### Image loading\nAlways append a platform-aware suffix:\n```cpp\nImageHelper::with(imageView)->load(url + ImageHelper::h_ext);    // horizontal thumbnail (@672w_378h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::v_ext);    // vertical thumbnail (@312w_420h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::face_ext); // avatar (@96w_96h on PC)\n```\n`IMAGE_EXT` resolves to `.webp` when `USE_WEBP` is defined, else `.jpg`. PSV uses smaller sizes (e.g. `@256w_144h` for horizontal).\n\n### Global event bus\nDefined in `wiliwili/include/utils/event_helper.hpp`:\n- `MPV_E` — player state (`MPV_LOADED`, `MPV_PAUSE`, `MPV_RESUME`, `UPDATE_PROGRESS`, `END_OF_FILE`, `RESET`, `RESTART`, `CACHE_SPEED_CHANGE`, `VIDEO_SPEED_CHANGE`, …)\n- `APP_E` — app-wide custom events (string key + void*)\n- `SEARCH_E` — search page events\n\nSubscribe: `MPV_E->subscribe([](MpvEventEnum e){ ... });`  Fire: `MPV_E->fire(MPV_PAUSE);`\n\n### Settings / ProgramConfig\n`ProgramConfig::instance()` is a singleton that must be initialized with `init()` **before** `brls::Application::init()`. All settings use the `SettingItem` enum (defined in `wiliwili/include/utils/config_helper.hpp`). Notable items:\n- `PLAYER_HWDEC` / `PLAYER_HWDEC_CUSTOM` — hardware decode method\n- `APP_RESOURCES` — active custom theme ID (requires restart to apply)\n- `KEYMAP` — button icon set: `\"xbox\"` (PC default), `\"ps\"`, `\"keyboard\"`\n- `HTTP_PROXY`, `HTTP_PROXY_STATUS`, `TLS_VERIFY` — network settings\n- `SHORTCUT_*` — rebindable keyboard shortcuts (modifier-key strings, e.g. `\"ctrl-r\"`)\n\n### Custom Theme / Layout System\nThemes live in `{configDir}/theme/{id}/` and can override any file under `resources/`.\n- A theme directory must contain `resources_meta.json`: `{\"name\":\"…\",\"desc\":\"…\",\"version\":\"…\",\"author\":\"…\"}`\n- Discovered by `ProgramConfig::loadCustomThemes()` at startup; theme ID stored in `SettingItem::APP_RESOURCES`\n- Switching themes calls `DialogHelper::quitApp()` — restart is required\n- Reference theme repo: https://github.com/xfangfang/wiliwili_theme\n\n### Custom Fonts & Icons\nDrop these files into the config directory to override built-in assets:\n\n| Filename | Purpose |\n|---|---|\n| `font.ttf` | Main UI font |\n| `icon.ttf` | Button icon font |\n| `emoji.ttf` | Emoji font |\n| `danmaku.ttf` | Danmaku overlay font |\n| `gamecontrollerdb.txt` | SDL gamepad database (desktop only) |\n\nBuilt-in keymap fonts in `resources/font/`: `keymap_xbox.ttf`, `keymap_ps.ttf`, `keymap_keyboard.ttf`.\n\n### Anime4K / Shader System\n`ShaderHelper` (singleton, `wiliwili/include/utils/shader_helper.hpp`) manages profiles loaded from `{configDir}/shader.json`:\n```json\n{\n  \"profiles\": [\n    {\n      \"name\": \"Anime4K Mode A\",\n      \"shaders\": [\"/path/to/Anime4K_Clamp_Highlights.glsl\"],\n      \"settings\": [[\"set\", \"scaler\", \"ewa_lanczossharp\"]]\n    }\n  ],\n  \"animeList\": [{ \"anime\": 28223043, \"profile\": \"Anime4K Mode A\" }]\n}\n```\n- `settings` entries: `[\"set\",\"key\",\"val\"]`, `[\"change-list\",\"key\",\"op\",\"val\"]`, or `[\"run\",\"cmd\"]`.\n- Apply/clear with `ShaderHelper::instance().setShader(index)` / `clearShader()`.\n\n### MPV Rendering Modes\n\n| Mode | Trigger | Notes                                                  |\n|---|---|--------------------------------------------------------|\n| Framebuffer (default) | `MPV_USE_FB` (auto) | GL 3.2+ / GLES 2.0+; best performance                  |\n| No framebuffer | `-DMPV_NO_FB=ON` | MPV draws fullscreen, UI is overlaid; PS4, PSV-GL, GL2 |\n| Software render | `-DMPV_SW_RENDER=ON` | CPU only; for UWP/D3D12 porting                        |\n| deko3d | `BOREALIS_USE_DEKO3D` | Switch native; 4K@60 with hardware decode              |\n| GXM | `BOREALIS_USE_GXM` | PSVita native                                          |\n| D3D11 | `BOREALIS_USE_D3D11` | Windows Native/UWP                                     |\n\nDefault hardware decode: Switch/GXM → `\"auto\"`, PSV-GL → `\"vita-copy\"`, PS4 → `\"no\"`, Desktop → `\"auto-safe\"`.\n\n### Video Quality Codes\n\n| Code | Quality | Notes |\n|---|---|---|\n| 127 | 8K | |\n| 120 | 4K | |\n| 116 | 1080P60 | Default on non-PSV |\n| 80 | 1080P | |\n| 64 | 720P | Default PSV GXM |\n| 32 | 480P | Default PSV OpenGL |\n| 16 | 360P | Default when logged out |\n\n---\n\n## Config Directory Locations\n\n| Platform | Path |\n|---|---|\n| Nintendo Switch | `/config/wiliwili/` |\n| PS4 | `/data/wiliwili/` |\n| PSVita | `ux0:/data/wiliwili/` |\n| macOS (release) | `~/Library/Application Support/wiliwili/` |\n| Linux (release) | `$XDG_CONFIG_HOME/wiliwili/` or `~/.config/wiliwili/` |\n| Windows (release) | `%LOCALAPPDATA%\\xfangfang\\wiliwili\\` |\n| Any platform (debug build) | `./config/wiliwili/` (next to binary) |\n\nMain config file: `{configDir}/wiliwili_config.json` (cookie, refreshToken, all SettingItem values, GA client ID, search history).\n\n---\n\n## Build Commands\n\n### Desktop (macOS)\n```sh\nbrew install mpv webp\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(sysctl -n hw.ncpu)\n# macOS app bundle:\nmake -C build wiliwili.app\n```\n\n### Desktop (Linux/Ubuntu)\n```sh\nsudo apt install libssl-dev libmpv-dev libwebp-dev\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(nproc)\n# System install with .desktop entry:\ncmake -B build -DPLATFORM_DESKTOP=ON -DINSTALL=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr\nmake -C build wiliwili -j$(nproc) && sudo make -C build install\n```\n\n### Nintendo Switch (Docker, recommended)\n```sh\ndocker run --rm -v $(pwd):/data devkitpro/devkita64:20251117 bash -c \"/data/scripts/build_switch.sh\"\n# deko3d variant (4K@60 hardware decode):\n# see scripts/build_switch_deko3d.sh\n```\nSwitch requires custom ffmpeg/mpv (devkitpro defaults don't support network streams):\n```sh\nbase_url=\"https://github.com/xfangfang/wiliwili/releases/download/v0.1.0\"\nsudo dkp-pacman -U $base_url/switch-ffmpeg-7.1-1-any.pkg.tar.zst \\\n                   $base_url/switch-libmpv-0.36.0-3-any.pkg.tar.zst\ncmake -B cmake-build-switch -DPLATFORM_SWITCH=ON\nmake -C cmake-build-switch wiliwili.nro -j$(nproc)\n```\n\n### PSVita / PS4 (Docker)\n```sh\n# PSVita GXM (recommended):\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_psv_builder:latest-gxm \\\n    \"cmake -B cmake-build-psv -G Ninja -DPLATFORM_PSV=ON -DUSE_GXM=ON \\\n     -DUSE_SYSTEM_CURL=ON -DUSE_VITA_SHARK=OFF -DCMAKE_BUILD_TYPE=Release && \\\n     cmake --build cmake-build-psv\"\n# PS4:\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_ps4_builder:latest \\\n    \"cmake -B cmake-build-ps4 -DPLATFORM_PS4=ON -DMPV_NO_FB=ON \\\n     -DUSE_SYSTEM_CPR=ON && make -C cmake-build-ps4 -j$(nproc)\"\n```\n\n### Useful CMake flags\n| Flag | Purpose |\n|---|---|\n| `-DUSE_SDL2=ON` | Use SDL2 instead of GLFW |\n| `-DMPV_NO_FB=ON` | No framebuffer (PS4, PSV GL, GL 2.0) |\n| `-DMPV_SW_RENDER=ON` | CPU software rendering |\n| `-DMPV_BUNDLE_DLL=ON` | Bundle mpv.dll into exe (Windows + USE_LIBROMFS=ON) |\n| `-DDISABLE_OPENCC=ON` | Skip Chinese conversion library |\n| `-DDISABLE_WEBP=ON` | Skip WebP support |\n| `-DINSTALL=ON` | Linux system install with desktop entry |\n| `-DDEBUG_SANITIZER=ON` | Enable ASan/UBSan (debug only) |\n| `-DUSE_LIBROMFS=ON` | Embed resources into binary |\n| `-DBUILTIN_NSP=ON` | Embed NSP forwarder (Switch only) |\n| `-DAPP_PLATFORM_CUSTOM_LIBS=ON` | Manual deps: `APP_PLATFORM_INCLUDE` + `APP_PLATFORM_LINK_OPTION` |\n\n---\n\n## Debugging\n\n- Run with `-d` for debug log level, `-v` for borealis visual debug overlay, `-t` for mpv terminal output, `-o <file>` to write log to file.\n- To test a specific page without navigating the UI, uncomment the relevant `Intent::open*()` line in `main.cpp` (many test BV IDs are already commented there).\n- `ProgramConfig::instance().init()` loads cookies and all settings; it must run before `brls::Application::init()`.\n- In-app network diagnostics: Settings → Tools → Network Diagnostics (`fragment/setting_network`). Shows API connectivity, system/server time diff, WiFi state, IP, DNS.\n- On Switch: if the app is black-screen on startup, delete `/config/wiliwili/` from SD card and retry.\n\n---\n\n## Platform Macros\n\n| Macro | Platform |\n|---|---|\n| `__SWITCH__` | Nintendo Switch |\n| `__PSV__` | PlayStation Vita |\n| `PS4` | PlayStation 4 |\n| `BOREALIS_USE_OPENGL` / `BOREALIS_USE_DEKO3D` / `BOREALIS_USE_D3D11` / `BOREALIS_USE_GXM` | Rendering backend |\n| `MPV_USE_FB` / `MPV_NO_FB` | Framebuffer mode (auto-derived from above) |\n| `USE_WEBP` | WebP image decoding enabled |\n\n---\n\n## Key Files at a Glance\n\n| File | Description |\n|---|---|\n| `wiliwili/source/main.cpp` | Entry point; registers views, launches activity |\n| `wiliwili/include/utils/activity_helper.hpp` | `Intent` — all navigation |\n| `wiliwili/include/utils/config_helper.hpp` | `ProgramConfig` singleton, `SettingItem` enum |\n| `wiliwili/include/utils/event_helper.hpp` | Global event buses (`MPV_E`, `APP_E`, `SEARCH_E`) |\n| `wiliwili/include/presenter/presenter.h` | `ASYNC_RETAIN`/`ASYNC_RELEASE` macros |\n| `wiliwili/include/api/bilibili/util/http.hpp` | `HTTP::getResultAsync`, `getResultWithWbiAsync` |\n| `wiliwili/include/api/bilibili/util/wbi.hpp` | WBI signing implementation |\n| `wiliwili/include/api/bilibili/api.h` | All Bilibili API URL constants |\n| `wiliwili/include/view/recycling_grid.hpp` | Recycler list widget |\n| `wiliwili/include/utils/image_helper.hpp` | Async image loading + platform-sized URL suffixes |\n| `wiliwili/include/utils/shader_helper.hpp` | Anime4K / mpv shader profile management |\n| `wiliwili/include/view/mpv_core.hpp` | MPV singleton, playback control, rendering backend |\n| `wiliwili/include/view/danmaku_core.hpp` | Danmaku (bullet comment) overlay rendering |\n| `wiliwili/include/api/live/danmaku_live.hpp` | Live room WebSocket danmaku via mongoose |\n| `resources/xml/` | All UI layout files |\n| `resources/i18n/` | Translations (en-US, zh-Hans, zh-Hant, ja, ko, it, ja-RYU) |\n| `scripts/` | Build scripts for each platform; `README.md` covers custom ffmpeg/mpv for Switch |\n\n","isInternal":false,"tokens":4481,"sizeBytes":15623}],"systemPromptSnippet":"<yakaai_skills repo=\"xfangfang/wiliwili\">\n<!-- File: AGENTS.md (Tokens: ~4481 | Category: root-instruction) -->\n# AGENTS.md — wiliwili Codebase Guide\n\nwiliwili is a cross-platform Bilibili client (Nintendo Switch, PSVita, PS4, PC) built in C++17. The UI is rendered via **nanovg**, windowing via **GLFW** or **SDL2**, and video playback via **mpv + FFmpeg**.\n\n---\n\n## Architecture Layers (Android-inspired)\n\n| Layer | Directory | Role |\n|---|---|---|\n| Activity | `wiliwili/include/activity/`, `wiliwili/source/activity/` | Top-level full-screen pages |\n| Fragment | `wiliwili/include/fragment/`, `wiliwili/source/fragment/` | Sub-pages composed within Activities |\n| Presenter | `wiliwili/include/presenter/` | Async data-fetching logic; Fragments inherit Presenters |\n| View | `wiliwili/include/view/` | Custom reusable UI components (MPVCore, RecyclingGrid, VideoView…) |\n| API | `wiliwili/include/api/bilibili/` | Bilibili REST/gRPC wrappers via cpr + nlohmann/json |\n| Utils | `wiliwili/include/utils/` | Intent (navigation), ProgramConfig (settings), ImageHelper, EventHelper |\n\nThe UI framework is a custom fork of **borealis** (`library/borealis/`). All views inherit `brls::Box` or its subclasses.\n\n---\n\n## Key Patterns\n\n### Presenter / async safety\nEvery Fragment that fetches data inherits a Presenter class. Use the macros in `wiliwili/include/presenter/presenter.h` to guard callbacks against use-after-free:\n```cpp\nvoid requestSomething() {\n    ASYNC_RETAIN  // captures token, tokenCounter\n    bilibili::HTTP::getResultAsync<MyType>(url, params, [ASYNC_TOKEN](auto result) {\n        ASYNC_RELEASE  // returns early if the view was destroyed\n        brls::Threading::sync([this, result]() {\n            // safe to update UI here (main thread)\n        });\n    });\n}\n```\nUse `CHECK_AND_SET_REQUEST` / `UNSET_REQUEST` to prevent duplicate in-flight requests.\n\n### XML-driven UI\n- Layout files live in `resources/xml/` (`activity/`, `fragment/`, `views/`).\n- Inflate in constructor: `this->inflateFromXMLRes(\"xml/fragment/home_recommends.xml\");`\n- Bind a child by XML id: `BRLS_BIND(RecyclingGrid, recyclingGrid, \"home/recommends/recyclingGrid\");`\n- Custom views must be registered before use: `brls::Application::registerXMLView(\"RecyclingGrid\", RecyclingGrid::create);` (done in `Register::initCustomView()` called from `main.cpp`).\n- Valid custom XML element names (all registered in `Register::initCustomView()`):\n  - **UI widgets:** `AutoTabFrame`, `RecyclingGrid`, `VideoView`, `VideoProfile`, `QRImage`, `SVGImage`, `TextBox`, `VideoProgressSlider`, `GalleryView`, `CustomButton`, `HintLabel`\n  - **App views:** `UserInfoView`, `UpUserSmall`, `VideoComment`, `ButtonClose`, `CheckBox`, `SelectorCell`, `AnimationImage`, `ShareBox`, `DynamicVideoCardView`, `DynamicArticleView`\n  - **Fragments:** `HomeTab`, `DynamicTab`, `MineTab`, `HomeRecommends`, `HomeHotsAll`, `HomeHotsHistory`, `HomeHotsWeekly`, `HomeHotsRank`, `HomeHots`, `HomeLive`, `HomeBangumi`, `HomeCinema`, `MineHistory`, `MineLater`, `MineCollection`, `MineBangumi`, `SearchTab`, `SearchOrder`, `SearchVideo`, `SearchCinema`, `SearchBangumi`, `SearchHots`, `SearchHistory`\n- i18n key syntax in XML: `@i18n/wiliwili/some/key`; translation files are in `resources/i18n/{en-US,zh-Hans,zh-Hant,ja,ko,it,ja-RYU}/wiliwili.json`.\n\n### Navigation — use `Intent`, never push directly\n`wiliwili/include/utils/activity_helper.hpp` defines all navigation entry points:\n```cpp\nIntent::openBV(\"BV1Da411Y7U4\");         // video by BV id\nIntent::openSeasonByEpId(323434);       // bangumi episode\nIntent::openLive(1942240);              // live room\nIntent::openSearch(\"keyword\");\nIntent::openSetting();\nIntent::openCollection(\"2511565362\");   // favorites folder\nIntent::openPgcFilter(\"/page/home/pgc/more?type=2&...\"); // PGC filter\n```\n\n### HTTP API calls\nAll calls are async via `bilibili::HTTP` (`wiliwili/include/api/bilibili/util/http.hpp`):\n```cpp\n// Standard GET → parses {\"code\":0,\"data\":{...}}\nHTTP::getResultAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For WBI-signed endpoints (most web-interface APIs since 2023):\nHTTP::getResultWithWbiAsync<MyResult>(Api::SomeEndpoint, params, callback, error);\n\n// For app-signed endpoints (pass needSign=true):\nHTTP::getResultAsync<MyResult>(url, params, callback, error, /*needSign=*/true);\n\n// POST with typed response:\nHTTP::postResultAsync<MyResult>(url, params, payload, callback, error);\n```\n- API URL constants: `wiliwili/include/api/bilibili/api.h`; JSON result structs: `wiliwili/include/api/bilibili/result/`\n- `parseJson` looks for `data` (object/array) first, then `result` (object) as fallback when `code==0`\n- **WBI signing** (`getResultWithWbiAsync`): fetches `img_key`+`sub_key` from `Api::Nav`, computes `mixin_key`, appends `wts` + `w_rid`. Keys cached for 1 hour. Required for most `/x/web-interface/` endpoints since 2023.\n- HTTP defaults: `User-Agent: wiliwili`, `Referer: https://www.bilibili.com/client`, timeout 10000ms. Proxy and TLS verify are configurable via `SettingItem::HTTP_PROXY*` / `SettingItem::TLS_VERIFY`.\n\n### RecyclingGrid (custom recycler view)\nImplement `RecyclingGridDataSource` and call `recyclingGrid->setDataSource(...)`. Example from `player_activity.cpp`:\n```cpp\nclass DataSourceFoo : public RecyclingGridDataSource {\n    RecyclingGridItem* cellForRow(RecyclingGrid* recycler, size_t index) override {\n        auto* item = (MyCell*)recycler->dequeueReusableCell(\"Cell\");\n        item->setData(list[index]);\n        return item;\n    }\n    size_t getItemCount() override { return list.size(); }\n    void onItemSelected(RecyclingGrid* recycler, size_t index) override { /* handle tap */ }\n    void clearData() override { list.clear(); }\n};\n```\n`recyclingGrid->onNextPage([]{...})` triggers when the user scrolls to the end (infinite scroll).\n\n### Image loading\nAlways append a platform-aware suffix:\n```cpp\nImageHelper::with(imageView)->load(url + ImageHelper::h_ext);    // horizontal thumbnail (@672w_378h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::v_ext);    // vertical thumbnail (@312w_420h on PC)\nImageHelper::with(imageView)->load(url + ImageHelper::face_ext); // avatar (@96w_96h on PC)\n```\n`IMAGE_EXT` resolves to `.webp` when `USE_WEBP` is defined, else `.jpg`. PSV uses smaller sizes (e.g. `@256w_144h` for horizontal).\n\n### Global event bus\nDefined in `wiliwili/include/utils/event_helper.hpp`:\n- `MPV_E` — player state (`MPV_LOADED`, `MPV_PAUSE`, `MPV_RESUME`, `UPDATE_PROGRESS`, `END_OF_FILE`, `RESET`, `RESTART`, `CACHE_SPEED_CHANGE`, `VIDEO_SPEED_CHANGE`, …)\n- `APP_E` — app-wide custom events (string key + void*)\n- `SEARCH_E` — search page events\n\nSubscribe: `MPV_E->subscribe([](MpvEventEnum e){ ... });`  Fire: `MPV_E->fire(MPV_PAUSE);`\n\n### Settings / ProgramConfig\n`ProgramConfig::instance()` is a singleton that must be initialized with `init()` **before** `brls::Application::init()`. All settings use the `SettingItem` enum (defined in `wiliwili/include/utils/config_helper.hpp`). Notable items:\n- `PLAYER_HWDEC` / `PLAYER_HWDEC_CUSTOM` — hardware decode method\n- `APP_RESOURCES` — active custom theme ID (requires restart to apply)\n- `KEYMAP` — button icon set: `\"xbox\"` (PC default), `\"ps\"`, `\"keyboard\"`\n- `HTTP_PROXY`, `HTTP_PROXY_STATUS`, `TLS_VERIFY` — network settings\n- `SHORTCUT_*` — rebindable keyboard shortcuts (modifier-key strings, e.g. `\"ctrl-r\"`)\n\n### Custom Theme / Layout System\nThemes live in `{configDir}/theme/{id}/` and can override any file under `resources/`.\n- A theme directory must contain `resources_meta.json`: `{\"name\":\"…\",\"desc\":\"…\",\"version\":\"…\",\"author\":\"…\"}`\n- Discovered by `ProgramConfig::loadCustomThemes()` at startup; theme ID stored in `SettingItem::APP_RESOURCES`\n- Switching themes calls `DialogHelper::quitApp()` — restart is required\n- Reference theme repo: https://github.com/xfangfang/wiliwili_theme\n\n### Custom Fonts & Icons\nDrop these files into the config directory to override built-in assets:\n\n| Filename | Purpose |\n|---|---|\n| `font.ttf` | Main UI font |\n| `icon.ttf` | Button icon font |\n| `emoji.ttf` | Emoji font |\n| `danmaku.ttf` | Danmaku overlay font |\n| `gamecontrollerdb.txt` | SDL gamepad database (desktop only) |\n\nBuilt-in keymap fonts in `resources/font/`: `keymap_xbox.ttf`, `keymap_ps.ttf`, `keymap_keyboard.ttf`.\n\n### Anime4K / Shader System\n`ShaderHelper` (singleton, `wiliwili/include/utils/shader_helper.hpp`) manages profiles loaded from `{configDir}/shader.json`:\n```json\n{\n  \"profiles\": [\n    {\n      \"name\": \"Anime4K Mode A\",\n      \"shaders\": [\"/path/to/Anime4K_Clamp_Highlights.glsl\"],\n      \"settings\": [[\"set\", \"scaler\", \"ewa_lanczossharp\"]]\n    }\n  ],\n  \"animeList\": [{ \"anime\": 28223043, \"profile\": \"Anime4K Mode A\" }]\n}\n```\n- `settings` entries: `[\"set\",\"key\",\"val\"]`, `[\"change-list\",\"key\",\"op\",\"val\"]`, or `[\"run\",\"cmd\"]`.\n- Apply/clear with `ShaderHelper::instance().setShader(index)` / `clearShader()`.\n\n### MPV Rendering Modes\n\n| Mode | Trigger | Notes                                                  |\n|---|---|--------------------------------------------------------|\n| Framebuffer (default) | `MPV_USE_FB` (auto) | GL 3.2+ / GLES 2.0+; best performance                  |\n| No framebuffer | `-DMPV_NO_FB=ON` | MPV draws fullscreen, UI is overlaid; PS4, PSV-GL, GL2 |\n| Software render | `-DMPV_SW_RENDER=ON` | CPU only; for UWP/D3D12 porting                        |\n| deko3d | `BOREALIS_USE_DEKO3D` | Switch native; 4K@60 with hardware decode              |\n| GXM | `BOREALIS_USE_GXM` | PSVita native                                          |\n| D3D11 | `BOREALIS_USE_D3D11` | Windows Native/UWP                                     |\n\nDefault hardware decode: Switch/GXM → `\"auto\"`, PSV-GL → `\"vita-copy\"`, PS4 → `\"no\"`, Desktop → `\"auto-safe\"`.\n\n### Video Quality Codes\n\n| Code | Quality | Notes |\n|---|---|---|\n| 127 | 8K | |\n| 120 | 4K | |\n| 116 | 1080P60 | Default on non-PSV |\n| 80 | 1080P | |\n| 64 | 720P | Default PSV GXM |\n| 32 | 480P | Default PSV OpenGL |\n| 16 | 360P | Default when logged out |\n\n---\n\n## Config Directory Locations\n\n| Platform | Path |\n|---|---|\n| Nintendo Switch | `/config/wiliwili/` |\n| PS4 | `/data/wiliwili/` |\n| PSVita | `ux0:/data/wiliwili/` |\n| macOS (release) | `~/Library/Application Support/wiliwili/` |\n| Linux (release) | `$XDG_CONFIG_HOME/wiliwili/` or `~/.config/wiliwili/` |\n| Windows (release) | `%LOCALAPPDATA%\\xfangfang\\wiliwili\\` |\n| Any platform (debug build) | `./config/wiliwili/` (next to binary) |\n\nMain config file: `{configDir}/wiliwili_config.json` (cookie, refreshToken, all SettingItem values, GA client ID, search history).\n\n---\n\n## Build Commands\n\n### Desktop (macOS)\n```sh\nbrew install mpv webp\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(sysctl -n hw.ncpu)\n# macOS app bundle:\nmake -C build wiliwili.app\n```\n\n### Desktop (Linux/Ubuntu)\n```sh\nsudo apt install libssl-dev libmpv-dev libwebp-dev\ncmake -B build -DPLATFORM_DESKTOP=ON\nmake -C build wiliwili -j$(nproc)\n# System install with .desktop entry:\ncmake -B build -DPLATFORM_DESKTOP=ON -DINSTALL=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr\nmake -C build wiliwili -j$(nproc) && sudo make -C build install\n```\n\n### Nintendo Switch (Docker, recommended)\n```sh\ndocker run --rm -v $(pwd):/data devkitpro/devkita64:20251117 bash -c \"/data/scripts/build_switch.sh\"\n# deko3d variant (4K@60 hardware decode):\n# see scripts/build_switch_deko3d.sh\n```\nSwitch requires custom ffmpeg/mpv (devkitpro defaults don't support network streams):\n```sh\nbase_url=\"https://github.com/xfangfang/wiliwili/releases/download/v0.1.0\"\nsudo dkp-pacman -U $base_url/switch-ffmpeg-7.1-1-any.pkg.tar.zst \\\n                   $base_url/switch-libmpv-0.36.0-3-any.pkg.tar.zst\ncmake -B cmake-build-switch -DPLATFORM_SWITCH=ON\nmake -C cmake-build-switch wiliwili.nro -j$(nproc)\n```\n\n### PSVita / PS4 (Docker)\n```sh\n# PSVita GXM (recommended):\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_psv_builder:latest-gxm \\\n    \"cmake -B cmake-build-psv -G Ninja -DPLATFORM_PSV=ON -DUSE_GXM=ON \\\n     -DUSE_SYSTEM_CURL=ON -DUSE_VITA_SHARK=OFF -DCMAKE_BUILD_TYPE=Release && \\\n     cmake --build cmake-build-psv\"\n# PS4:\ndocker run --rm -v $(pwd):/src/ xfangfang/wiliwili_ps4_builder:latest \\\n    \"cmake -B cmake-build-ps4 -DPLATFORM_PS4=ON -DMPV_NO_FB=ON \\\n     -DUSE_SYSTEM_CPR=ON && make -C cmake-build-ps4 -j$(nproc)\"\n```\n\n### Useful CMake flags\n| Flag | Purpose |\n|---|---|\n| `-DUSE_SDL2=ON` | Use SDL2 instead of GLFW |\n| `-DMPV_NO_FB=ON` | No framebuffer (PS4, PSV GL, GL 2.0) |\n| `-DMPV_SW_RENDER=ON` | CPU software rendering |\n| `-DMPV_BUNDLE_DLL=ON` | Bundle mpv.dll into exe (Windows + USE_LIBROMFS=ON) |\n| `-DDISABLE_OPENCC=ON` | Skip Chinese conversion library |\n| `-DDISABLE_WEBP=ON` | Skip WebP support |\n| `-DINSTALL=ON` | Linux system install with desktop entry |\n| `-DDEBUG_SANITIZER=ON` | Enable ASan/UBSan (debug only) |\n| `-DUSE_LIBROMFS=ON` | Embed resources into binary |\n| `-DBUILTIN_NSP=ON` | Embed NSP forwarder (Switch only) |\n| `-DAPP_PLATFORM_CUSTOM_LIBS=ON` | Manual deps: `APP_PLATFORM_INCLUDE` + `APP_PLATFORM_LINK_OPTION` |\n\n---\n\n## Debugging\n\n- Run with `-d` for debug log level, `-v` for borealis visual debug overlay, `-t` for mpv terminal output, `-o <file>` to write log to file.\n- To test a specific page without navigating the UI, uncomment the relevant `Intent::open*()` line in `main.cpp` (many test BV IDs are already commented there).\n- `ProgramConfig::instance().init()` loads cookies and all settings; it must run before `brls::Application::init()`.\n- In-app network diagnostics: Settings → Tools → Network Diagnostics (`fragment/setting_network`). Shows API connectivity, system/server time diff, WiFi state, IP, DNS.\n- On Switch: if the app is black-screen on startup, delete `/config/wiliwili/` from SD card and retry.\n\n---\n\n## Platform Macros\n\n| Macro | Platform |\n|---|---|\n| `__SWITCH__` | Nintendo Switch |\n| `__PSV__` | PlayStation Vita |\n| `PS4` | PlayStation 4 |\n| `BOREALIS_USE_OPENGL` / `BOREALIS_USE_DEKO3D` / `BOREALIS_USE_D3D11` / `BOREALIS_USE_GXM` | Rendering backend |\n| `MPV_USE_FB` / `MPV_NO_FB` | Framebuffer mode (auto-derived from above) |\n| `USE_WEBP` | WebP image decoding enabled |\n\n---\n\n## Key Files at a Glance\n\n| File | Description |\n|---|---|\n| `wiliwili/source/main.cpp` | Entry point; registers views, launches activity |\n| `wiliwili/include/utils/activity_helper.hpp` | `Intent` — all navigation |\n| `wiliwili/include/utils/config_helper.hpp` | `ProgramConfig` singleton, `SettingItem` enum |\n| `wiliwili/include/utils/event_helper.hpp` | Global event buses (`MPV_E`, `APP_E`, `SEARCH_E`) |\n| `wiliwili/include/presenter/presenter.h` | `ASYNC_RETAIN`/`ASYNC_RELEASE` macros |\n| `wiliwili/include/api/bilibili/util/http.hpp` | `HTTP::getResultAsync`, `getResultWithWbiAsync` |\n| `wiliwili/include/api/bilibili/util/wbi.hpp` | WBI signing implementation |\n| `wiliwili/include/api/bilibili/api.h` | All Bilibili API URL constants |\n| `wiliwili/include/view/recycling_grid.hpp` | Recycler list widget |\n| `wiliwili/include/utils/image_helper.hpp` | Async image loading + platform-sized URL suffixes |\n| `wiliwili/include/utils/shader_helper.hpp` | Anime4K / mpv shader profile management |\n| `wiliwili/include/view/mpv_core.hpp` | MPV singleton, playback control, rendering backend |\n| `wiliwili/include/view/danmaku_core.hpp` | Danmaku (bullet comment) overlay rendering |\n| `wiliwili/include/api/live/danmaku_live.hpp` | Live room WebSocket danmaku via mongoose |\n| `resources/xml/` | All UI layout files |\n| `resources/i18n/` | Translations (en-US, zh-Hans, zh-Hant, ja, ko, it, ja-RYU) |\n| `scripts/` | Build scripts for each platform; `README.md` covers custom ffmpeg/mpv for Switch |\n</yakaai_skills>"}