feat: M6.40批 — 阅读器UI改版(书架list·grid+空态骨架+界面设置+App设置页+header·footer+背景黑默认) + 系统级音量翻页映射 + P1 MD3(select面板28/对话框按钮胶囊) + MeowPanel写端点护栏 + SDK公开仓·文档同步

- reader: app.c/shelf.c/reader.c 全套UI; 新增 reader_conf(reader.conf)/settings(App设置页)/reader_ipc(音量翻页IPC响应); app.json加volume_page
- launcher: keys.c meow_volpage_*门 + util.c app_id→fd映射(HELLO记fd/断开清) + app_launcher.c 前台三处接线(进入sink/按键路径volpage_key/离开clear) → 前台声明volume_page的App音量键→MSG_KEY_PAGEUP/PAGEDOWN翻页(非阅读器前台音量照旧)
- sdk: meow_select.c 底部面板28 + meow_ui_dialog.c 对话框按钮胶囊(h/2) + test_meow_ui_select.c修采样
- meowpanel: /api/net/policy·/api/power/policy 加防自锁+policy默认关两条护栏(panel.policy_enabled默认0)
- 文档: MEOW_APP_SDK.md §12.6 + meowpanel README端点表

0warning;宿主测试全过;二进制留批量批次未入库
This commit is contained in:
AskaEth committed 2026-09-04 18:13:18 +08:00
1 parent b364b6371a
commit 2826c1e905
27 files changed
+1704 -129

No files matched your search

+299 -55
View File
@@ -9,6 +9,7 @@
#include "meow_ui_internal.h" /* back_buf/pitch/bpp 访问器(决策见报告) */
#include "book.h"
#include "progress.h"
#include "reader_conf.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
@@ -17,7 +18,10 @@
#include <time.h>
#define RD_MENU_H 64
#define RD_MENU_BTN 7
#define RD_MENU_BTN 4 /* 返回 | 目录 | 界面 | 设置(MVP 单条) */
#define RD_HEADER_H 30 /* 页头条高(书名+章节名) */
#define RD_FOOT_H 18 /* 页底条高(章节+页码) */
#define RD_UI_H 268 /* 「界面」排版面板高 */
static rd_book *g_book;
static int g_reader_active;
@@ -29,6 +33,7 @@ static int g_fs, g_ls;
static uint16_t *g_page_buf[RD_PAGE_CACHE]; /* [0]=prev [1]=cur [2]=next(FreeType 页位图) */
static int g_has_font; /* 1=FreeType 路径;0=SDK 字库兜底 */
static int g_menu_open;
static int g_ui_open; /* 「界面」排版面板(B) */
static int g_dir_open; /* 目录覆盖层 */
static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
static char g_font_path[400];
@@ -49,6 +54,8 @@ static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
#define RD_READER_DEFAULT_FS 20
#define RD_READER_DEFAULT_LS 130
@@ -87,6 +94,41 @@ static uint16_t rgb565(uint32_t c)
return (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
}
/* 按 rdconf.bg/theme 解析阅读页底/字色(默认纯黑,B 定案)。 */
static void reader_resolve_bg(uint32_t *bg, uint32_t *fg)
{
const rdconf *c = rdconf_get();
if (!c) { *bg = 0x000000; *fg = 0x9AA4C8; return; }
if (strcmp(c->bg, "black") == 0) { *bg = 0x000000; *fg = 0x9AA4C8; return; }
if (strcmp(c->bg, "white") == 0) { *bg = 0xFFFFFF; *fg = 0x1A1A2A; return; }
if (strcmp(c->bg, "sepia") == 0) { *bg = 0xF5EFDC; *fg = 0x3B3A32; return; }
/* auto:跟随主题(rd.theme=light/dark 时强制该明暗的 surface/onSurface) */
meow_theme_mode m = MEOW_THEME_AUTO;
if (strcmp(c->theme, "light") == 0) m = MEOW_THEME_LIGHT;
else if (strcmp(c->theme, "dark") == 0) m = MEOW_THEME_DARK;
if (m == MEOW_THEME_AUTO) {
const meow_theme *th = meow_theme_get();
*bg = th->surface; *fg = th->onSurface;
} else {
meow_theme t = meow_theme_defaults(m);
*bg = t.surface; *fg = t.onSurface;
}
}
/* 「界面」面板改背景(写 rdconf.bg + 重解析颜色 + 重排版页缓冲)。 */
static void reader_set_bg(const char *bg)
{
rdconf *c = rdconf_get();
if (!c) return;
strncpy(c->bg, bg, sizeof c->bg - 1); c->bg[sizeof c->bg - 1] = 0;
if (strcmp(c->bg, "black") && strcmp(c->bg, "white") &&
strcmp(c->bg, "sepia") && strcmp(c->bg, "auto")) strcpy(c->bg, "black");
rdconf_persist();
reader_resolve_bg(&g_page_bg, &g_page_fg);
reader_refill_page();
reader_redraw();
}
/* 递归(深度≤3)扫 MeowOS/fonts/ 找第一个 .otf/.ttf。0=找到。 */
static int find_font_rec(const char *dir, int depth)
{
@@ -220,6 +262,152 @@ static void reader_draw_scrub(const meow_theme *th)
}
}
/* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ----------------
* 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。
* 几何 draw/hit 共用(常量)。 */
#define UI_LABEL_X 20
#define UI_CTRL_X 130
#define UI_CTRL_W (480 - UI_CTRL_X - 16)
#define UI_ROW_H 34
static int ui_panel_y(void) { return g_ph - RD_UI_H; }
/* 单个「值按钮」:[-]/[+] 或文本钮。cx=控件区起点;返回该按钮中点 x。 */
static void ui_draw_step(int x, int w, int y, const char *tag, int active, const meow_theme *th)
{
if (active) meow_ui_fill_rect(x, y, w, 28, th->primaryContainer);
else meow_ui_fill_rect(x, y, w, 28, th->surfaceContainerHighest);
uint32_t tc = active ? th->onPrimaryContainer : th->fg;
meow_ui_text_center(x + w / 2, y + 4, tag, 20, tc);
}
static void ui_draw_chips(const char *const *labels, int n, int cur, int x, int y, int w, const meow_theme *th)
{
int gap = 4;
int cw = (w - (n - 1) * gap) / n;
for (int i = 0; i < n; i++) {
int cx = x + i * (cw + gap);
if (i == cur) meow_gfx_fill_round(cx, y, cw, 28, 14, th->primaryContainer);
else meow_gfx_fill_round(cx, y, cw, 28, 14, th->surfaceContainerHighest);
uint32_t tc = (i == cur) ? th->onPrimaryContainer : th->fg;
meow_ui_text_center(cx + cw / 2, y + 4, labels[i], 20, tc);
}
}
static void reader_draw_ui_panel(const meow_theme *th)
{
int y0 = ui_panel_y();
int top = y0 + 30;
int ry;
const rdconf *c = rdconf_get();
/* 面板底 + scrim */
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
meow_ui_fill_rect(0, y0, g_pw, RD_UI_H, th->surfaceContainer);
meow_ui_draw_rect(0, y0, g_pw, 1, th->outlineVariant);
meow_ui_text_center(240, y0 + 5, "界面 · 排版", 20, th->fg);
/* 0 字号 */
ry = top + 0 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "字号", 20, th->fg);
char v[16]; snprintf(v, sizeof v, "%d", g_fs);
ui_draw_step(UI_CTRL_X, 40, ry + 3, "-", 0, th);
meow_ui_text_center(UI_CTRL_X + 40 + (UI_CTRL_W - 80) / 2, ry + 6, v, 20, th->fg);
ui_draw_step(UI_CTRL_X + UI_CTRL_W - 40, 40, ry + 3, "+", 0, th);
/* 1 行距 */
ry = top + 1 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "行距", 20, th->fg);
snprintf(v, sizeof v, "%d", g_ls);
ui_draw_step(UI_CTRL_X, 40, ry + 3, "-", 0, th);
meow_ui_text_center(UI_CTRL_X + 40 + (UI_CTRL_W - 80) / 2, ry + 6, v, 20, th->fg);
ui_draw_step(UI_CTRL_X + UI_CTRL_W - 40, 40, ry + 3, "+", 0, th);
/* 2 背景(4 档:黑/白/米黄/自动;当前=标亮) */
ry = top + 2 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "背景", 20, th->fg);
static const char *bg_lbl[4] = { "黑", "白", "米黄", "自动" };
int bgcur = 0;
if (c) { if (!strcmp(c->bg, "white")) bgcur = 1; else if (!strcmp(c->bg, "sepia")) bgcur = 2;
else if (!strcmp(c->bg, "auto")) bgcur = 3; }
ui_draw_chips(bg_lbl, 4, bgcur, UI_CTRL_X, ry + 3, UI_CTRL_W, th);
/* 3 顶栏显隐(开/关) */
ry = top + 3 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "顶栏", 20, th->fg);
static const char *yn[2] = { "开", "关" };
ui_draw_chips(yn, 2, g_show_header ? 0 : 1, UI_CTRL_X, ry + 3, UI_CTRL_W, th);
/* 4 底栏显隐 */
ry = top + 4 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "底栏", 20, th->fg);
ui_draw_chips(yn, 2, g_show_footer ? 0 : 1, UI_CTRL_X, ry + 3, UI_CTRL_W, th);
/* 5 编码(TXT;显示当前,点循环) */
ry = top + 5 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "编码", 20, th->fg);
if (g_book && !rd_book_is_image(g_book)) {
ui_draw_step(UI_CTRL_X + UI_CTRL_W - 120, 120, ry + 3, enc_cycle_name(g_enc_idx), 0, th);
} else {
meow_ui_text(UI_CTRL_X, ry + 6, "—", 20, th->dim);
}
/* 6 返回阅读 */
ry = top + 6 * UI_ROW_H;
ui_draw_step(UI_CTRL_X, UI_CTRL_W, ry + 2, "返回阅读", 1, th);
}
/* 「界面」面板命中;返回动作码(见下方 enum)。0=无。 */
enum {
UI_NONE = 0, UI_FS_DEC, UI_FS_INC, UI_LS_DEC, UI_LS_INC,
UI_BG_0, UI_BG_1, UI_BG_2, UI_BG_3, UI_HDR_TOGGLE, UI_FTR_TOGGLE, UI_ENC, UI_CLOSE
};
static int reader_ui_hit(int x, int y)
{
int y0 = ui_panel_y();
if (y < y0) return UI_NONE;
int top = y0 + 30;
int ry;
int cx = UI_CTRL_X, cw = UI_CTRL_W;
/* 0 字号 */
ry = top + 0 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H) {
if (x >= cx && x < cx + 40) return UI_FS_DEC;
if (x >= cx + cw - 40 && x < cx + cw) return UI_FS_INC;
return UI_NONE;
}
/* 1 行距 */
ry = top + 1 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H) {
if (x >= cx && x < cx + 40) return UI_LS_DEC;
if (x >= cx + cw - 40 && x < cx + cw) return UI_LS_INC;
return UI_NONE;
}
/* 2 背景 4 chips */
ry = top + 2 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw) {
int gap = 4, cwp = (cw - 3 * gap) / 4;
int k = (x - cx) / (cwp + gap);
if (k < 0) k = 0;
if (k > 3) k = 3;
return UI_BG_0 + k;
}
/* 3 顶栏 */
ry = top + 3 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_HDR_TOGGLE;
/* 4 底栏 */
ry = top + 4 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_FTR_TOGGLE;
/* 5 编码 */
ry = top + 5 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_ENC;
/* 6 返回阅读 */
ry = top + 6 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_CLOSE;
return UI_NONE;
}
static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)w; (void)ctx;
@@ -233,40 +421,44 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
rd_book_render_text(g_book, page, reader_draw_line_atlas, NULL);
}
}
/* 顶栏:章节名 + 页 n/N + 进度% + (TXT)编码 */
/* 页头条(书名 + 章节名;可选,rd.header) */
int sbh = meow_ui_statusbar_height();
int pages = g_book ? rd_book_pages(g_book) : 0;
int page = g_book ? rd_book_current(g_book) : 0;
char right[40];
int p = pages > 0 ? (page * 100 / pages) : 0;
if (pages > 0) snprintf(right, sizeof right, "%d/%d %d%%", page + 1, pages, p);
else snprintf(right, sizeof right, "%d%%", p);
meow_ui_fill_rect(0, sbh, g_pw, (g_top - 4 - sbh), g_page_bg);
meow_ui_text(16, sbh + 4, g_chapter[0] ? g_chapter : "阅读", 20, th->fg);
meow_ui_text_right(g_pw - 16, sbh + 4, right, 20, th->fg);
meow_ui_draw_rect(0, g_top - 5, g_pw, 1, th->outline);
const char *bk = g_book ? rd_book_basename(g_book) : "阅读";
if (g_show_header) {
meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg);
meow_ui_text(16, sbh + 6, bk, 16, g_page_fg);
meow_ui_text_right(g_pw - 16, sbh + 6, g_chapter[0] ? g_chapter : "阅读", 16, g_page_fg);
meow_ui_draw_rect(0, sbh + RD_HEADER_H - 1, g_pw, 1, th->outlineVariant);
}
/* 页底条(章节 + 页码;可选,rd.footer) */
if (g_show_footer) {
char ft[48];
if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", page + 1, pages);
else snprintf(ft, sizeof ft, "0/0");
int fy = g_ph - RD_FOOT_H;
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg);
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
}
/* R3 刮擦指示条(非目录覆盖时显示;菜单为底部条,与右缘条并存即可) */
if (g_book && !g_dir_open) reader_draw_scrub(th);
/* R3 刮擦指示条(非目录/面板覆盖时显示;菜单为底部条,与右缘条并存即可) */
if (g_book && !g_dir_open && !g_ui_open) reader_draw_scrub(th);
if (g_menu_open) {
int y = g_ph - RD_MENU_H;
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
/* 当前编码提示(菜单上方淡条;仅文本类书有意义) */
if (g_book && !rd_book_is_image(g_book)) {
char en[64]; snprintf(en, sizeof en, "编码:%s", enc_cycle_name(g_enc_idx));
meow_ui_fill_rect_alpha(0, y - 26, g_pw, 24, 0x000000, 130);
meow_ui_text_right(g_pw - 16, y - 22, en, 16, th->dim);
}
/* 7 按钮:返回 | 目录 | 字− | 字+ | 距− | 距+ | 编码 */
/* 4 按钮(MVP 单条):返回 | 目录 | 界面 | 设置 */
int bw = g_pw / RD_MENU_BTN;
const char *labels[RD_MENU_BTN] = { "返回", "目录", "字-", "字+", "距-", "距+", "编码" };
const char *labels[RD_MENU_BTN] = { "返回", "目录", "界面", "设置" };
for (int i = 0; i < RD_MENU_BTN; i++) {
int bx = i * bw;
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
meow_ui_text_center(bx + bw / 2, y + (RD_MENU_H - 20) / 2 - 6, labels[i], 20, th->fg);
meow_ui_text_center(bx + bw / 2, y + (RD_MENU_H - 20) / 2, labels[i], 20, th->fg);
}
}
if (g_ui_open && g_book) reader_draw_ui_panel(th);
if (g_dir_open && g_book) {
/* 目录覆盖层:半透明底 + 条目列表(当前行高亮) */
int n = rd_book_toc_n(g_book);
@@ -389,28 +581,28 @@ static void reader_scrub_jump(int page)
reader_redraw(); /* 整页落定:正文 + 顶栏 + 刮擦条 */
reader_reflow_persist();
}
/* 拖动中局部重绘(不动正文页,仅顶栏 n/N + 右缘条/预览)。内容页保持(预览语义)。 */
/* 拖动中局部重绘(不动正文页,仅页底页码 + 右缘条/预览)。内容页保持(预览语义)。 */
static void reader_scrub_preview(int page)
{
g_scrub_page = page;
if (!g_book) return;
const meow_theme *th = meow_theme_get();
int sbh = meow_ui_statusbar_height();
int pages = rd_book_pages(g_book);
int y0 = rect_y0(), y1 = rect_y1();
char right[40];
int p = pages > 0 ? (g_scrub_page * 100 / pages) : 0;
if (pages > 0) snprintf(right, sizeof right, "%d/%d %d%%", g_scrub_page + 1, pages, p);
else snprintf(right, sizeof right, "%d%%", p);
/* 顶栏重画 */
meow_ui_fill_rect(0, sbh, g_pw, (g_top - 4 - sbh), g_page_bg);
meow_ui_text(16, sbh + 4, g_chapter[0] ? g_chapter : "阅读", 20, th->fg);
meow_ui_text_right(g_pw - 16, sbh + 4, right, 20, th->fg);
meow_ui_draw_rect(0, g_top - 5, g_pw, 1, th->outline);
char ft[48];
if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", g_scrub_page + 1, pages);
else snprintf(ft, sizeof ft, "0/0");
/* 页底条页码重画(页码已移到底条) */
if (g_show_footer) {
int fy = g_ph - RD_FOOT_H;
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : "阅读", 16, g_page_fg);
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
}
/* 右缘条区清残留(恢复页面内容)再画条 + 预览框;无页位图(字库兜底)则跳过恢复 */
if (g_page_buf[RD_PAGE_SLOT_CUR]) blit_sub(g_page_buf[RD_PAGE_SLOT_CUR], g_pw - 120, y0, 120, y1 - y0);
reader_draw_scrub(th);
meow_ui_flush_region(0, sbh, g_pw, g_top - sbh);
if (g_show_footer) meow_ui_flush_region(0, g_ph - RD_FOOT_H, g_pw, RD_FOOT_H);
meow_ui_flush_region(g_pw - 120, y0, 120, y1 - y0);
}
static void reader_scrub_begin(int x, int y)
@@ -435,7 +627,13 @@ void reader_open(const char *path)
const char *fp = resolve_font();
g_has_font = (fp != NULL);
g_fs = RD_READER_DEFAULT_FS; g_ls = RD_READER_DEFAULT_LS;
/* 全局默认字号/行距(reader.conf;进度里的每书偏好随后覆盖) */
const rdconf *rdc = rdconf_get();
g_fs = (rdc && rdc->fs >= 14 && rdc->fs <= 40) ? rdc->fs : RD_READER_DEFAULT_FS;
g_ls = (rdc && rdc->ls >= 100 && rdc->ls <= 200) ? rdc->ls : RD_READER_DEFAULT_LS;
g_show_header = rdc ? (rdc->header ? 1 : 0) : 1;
g_show_footer = rdc ? (rdc->footer ? 1 : 0) : 1;
/* 进度(含字号/行距偏好) */
rdprog prog;
@@ -444,10 +642,10 @@ void reader_open(const char *path)
int sbh = meow_ui_statusbar_height();
g_pw = meow_ui_width(); g_ph = meow_ui_height();
g_top = sbh + 34; g_mx = 16; g_my = 20;
g_top = sbh + RD_HEADER_H; g_mx = 16; g_my = 20;
const meow_theme *th = meow_theme_get();
g_page_bg = th->surface; g_page_fg = th->onSurface;
/* 阅读页底/字色:rd.bg(默认纯黑,B)→ 覆盖 th->surface */
reader_resolve_bg(&g_page_bg, &g_page_fg);
/* 开书(FreeType 路径带齐几何;无字体则 g_has_font=0 → atlas 兜底) */
int rc = rd_book_open(&g_book, path, fp, g_fs, g_ls, g_pw, g_ph, g_mx, g_my, g_top);
@@ -469,7 +667,7 @@ void reader_open(const char *path)
/* 复位动画/刮擦状态(跨书) */
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
g_scrub = 0; g_menu_open = 0; g_dir_open = 0;
g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0;
/* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */
if (g_book && !rd_book_is_image(g_book)) {
@@ -517,7 +715,7 @@ static void reader_on_destroy(meow_view *v, void *ctx)
if (g_book) rd_book_close(g_book);
g_book = NULL;
reader_free_page_bufs();
g_menu_open = 0; g_dir_open = 0; g_scrub = 0; g_reader_active = 0;
g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_scrub = 0; g_reader_active = 0;
}
meow_view *reader_view_create(void)
@@ -565,6 +763,44 @@ void reader_on_tap(int x, int y)
g_dir_open = 0; reader_redraw();
return;
}
if (g_ui_open) {
int a = reader_ui_hit(x, y);
switch (a) {
case UI_FS_DEC: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break;
case UI_FS_INC: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break;
case UI_LS_DEC: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break;
case UI_LS_INC: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break;
case UI_BG_0: reader_set_bg("black"); break;
case UI_BG_1: reader_set_bg("white"); break;
case UI_BG_2: reader_set_bg("sepia"); break;
case UI_BG_3: reader_set_bg("auto"); break;
case UI_HDR_TOGGLE:
g_show_header = !g_show_header;
if (rdconf_get()) { rdconf_get()->header = g_show_header; rdconf_persist(); }
reader_redraw();
break;
case UI_FTR_TOGGLE:
g_show_footer = !g_show_footer;
if (rdconf_get()) { rdconf_get()->footer = g_show_footer; rdconf_persist(); }
reader_redraw();
break;
case UI_ENC: /* 编码切换(TXT/Big5 繁体书) */
if (g_book && !rd_book_is_image(g_book)) {
int ni = (g_enc_idx + 1) % RD_ENC_CYCLE_N;
if (rd_book_set_enc(g_book, g_enc_cycle[ni]) == 0) {
g_enc_idx = ni;
chapter_name();
reader_refill_page();
reader_redraw();
reader_reflow_persist();
}
}
break;
case UI_CLOSE: g_ui_open = 0; reader_redraw(); break;
default: if (a == UI_NONE) { g_ui_open = 0; reader_redraw(); } break;
}
return;
}
if (g_menu_open) {
/* 菜单按钮命中 */
int y0 = g_ph - RD_MENU_H;
@@ -581,22 +817,13 @@ void reader_on_tap(int x, int y)
if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
else { g_menu_open = 0; reader_redraw(); }
return;
case 2: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break;
case 3: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break;
case 4: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break;
case 5: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break;
case 6: /* 编码切换(TXT/Big5 繁体书) */
if (g_book && !rd_book_is_image(g_book)) {
int ni = (g_enc_idx + 1) % RD_ENC_CYCLE_N;
if (rd_book_set_enc(g_book, g_enc_cycle[ni]) == 0) {
g_enc_idx = ni;
chapter_name();
reader_refill_page();
reader_redraw();
reader_reflow_persist();
}
}
break;
case 2: /* 界面(阅读排版设置) */
g_ui_open = 1; g_menu_open = 0; reader_redraw();
return;
case 3: /* 设置 → App 设置页 */
reader_reflow_persist();
meow_view_page_push(ACT_SETTINGS);
return;
}
if (y >= y0) return;
}
@@ -615,6 +842,7 @@ void reader_on_swipe(int dir, int dx, int dy)
(void)dx; (void)dy;
if (!g_book) return;
if (g_scrub) g_scrub = 0; /* 刮擦中被滑走 → 退出刮擦 */
if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; }
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
if (dir == MEOW_SWIPE_LEFT) reader_turn_animated(1);
else if (dir == MEOW_SWIPE_RIGHT) reader_turn_animated(-1);
@@ -628,8 +856,24 @@ void reader_on_touch_move(int x, int y)
if (g_scrub && g_book) reader_scrub_preview(scrub_page_for_y(y));
}
/* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数;
* 真正翻页在 UI 线程 reader_on_tick 消费(避免跨线程直接改页面状态)。 */
void reader_page_key(int dir)
{
int p = g_volpage_pending + dir;
if (p > 2) p = 2;
if (p < -2) p = -2;
g_volpage_pending = p;
}
void reader_on_tick(uint64_t now)
{
(void)now;
if (g_volpage_pending) {
int dir = g_volpage_pending > 0 ? 1 : -1;
g_volpage_pending = 0;
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open)
reader_turn_animated(dir);
}
if (g_notice_ms > 0) { g_notice_ms--; if (g_notice_ms == 0) reader_redraw(); }
}
+117
View File
@@ -0,0 +1,117 @@
/* app/reader_conf.c — 阅读器 App 私有配置实现(见 app/reader_conf.h)。 */
#include "reader_conf.h"
#include "meow_ui.h" /* meow_config_dir() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
/* 从 MEOW_DATA_DIR(<TF>/MeowData/<pkg>/data)剥掉尾 "/data",得到 <TF>/MeowData/<pkg>。 */
static void strip_data_suffix(const char *d, char *out, int outsz)
{
if (!d || !d[0]) { strncpy(out, "/tmp", (size_t)outsz - 1); out[outsz - 1] = 0; return; }
size_t n = strlen(d);
const char *suffix = "/data";
size_t sl = strlen(suffix);
size_t keep;
if (n >= sl && strcmp(d + n - sl, suffix) == 0) keep = n - sl;
else keep = n;
if (keep >= (size_t)outsz) keep = (size_t)outsz - 1;
memcpy(out, d, keep);
out[keep] = 0;
}
static void conf_path(char *out, int outsz)
{
const char *data = meow_config_dir();
char base[360];
strip_data_suffix(data ? data : "", base, sizeof base);
if (snprintf(out, (size_t)outsz, "%s/reader.conf", base) >= outsz) out[0] = 0;
/* mkdir -p base */
char tmp[400];
strncpy(tmp, base, sizeof tmp - 1); tmp[sizeof tmp - 1] = 0;
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') { *p = 0; mkdir(tmp, 0755); *p = '/'; }
}
mkdir(tmp, 0755);
}
static void conf_defaults(rdconf *c)
{
c->fs = 20;
c->ls = 130;
strcpy(c->bg, "black"); /* B 定案:默认纯黑 */
strcpy(c->theme, "auto");
strcpy(c->shelf, "list"); /* A 定案:默认 list */
c->header = 1;
c->footer = 1;
c->font[0] = 0;
}
static void clamp_str(char *v, size_t sz)
{
v[sz - 1] = 0;
}
rdconf *rdconf_get(void)
{
static rdconf g;
static int loaded = 0;
if (loaded) return &g;
conf_defaults(&g);
char path[440];
conf_path(path, sizeof path);
FILE *f = path[0] ? fopen(path, "r") : NULL;
if (f) {
char line[512];
while (fgets(line, sizeof line, f)) {
line[strcspn(line, "\r\n")] = 0;
char *eq = strchr(line, '=');
if (!eq) continue;
*eq = 0;
const char *k = line, *v = eq + 1;
if (!strcmp(k, "ui.shelf_layout")) { strncpy(g.shelf, v, sizeof g.shelf - 1); g.shelf[sizeof g.shelf - 1] = 0; }
else if (!strcmp(k, "rd.fs")) { int t = atoi(v); if (t >= 14 && t <= 40) g.fs = t; }
else if (!strcmp(k, "rd.ls")) { int t = atoi(v); if (t >= 100 && t <= 200) g.ls = t; }
else if (!strcmp(k, "rd.bg")) { strncpy(g.bg, v, sizeof g.bg - 1); g.bg[sizeof g.bg - 1] = 0; }
else if (!strcmp(k, "rd.theme")) { strncpy(g.theme, v, sizeof g.theme - 1); g.theme[sizeof g.theme - 1] = 0; }
else if (!strcmp(k, "rd.header")) { g.header = atoi(v) ? 1 : 0; }
else if (!strcmp(k, "rd.footer")) { g.footer = atoi(v) ? 1 : 0; }
else if (!strcmp(k, "rd.font")) { strncpy(g.font, v, sizeof g.font - 1); g.font[sizeof g.font - 1] = 0; }
}
fclose(f);
}
/* 校验合法取值(越界回默认/回退),避免脏档破坏 UI */
if (strcmp(g.shelf, "grid") && strcmp(g.shelf, "list")) strcpy(g.shelf, "list");
if (strcmp(g.bg, "black") && strcmp(g.bg, "white") &&
strcmp(g.bg, "sepia") && strcmp(g.bg, "auto")) strcpy(g.bg, "black");
if (strcmp(g.theme, "light") && strcmp(g.theme, "dark") &&
strcmp(g.theme, "auto")) strcpy(g.theme, "auto");
clamp_str(g.shelf, sizeof g.shelf);
clamp_str(g.bg, sizeof g.bg);
clamp_str(g.theme, sizeof g.theme);
clamp_str(g.font, sizeof g.font);
loaded = 1;
return &g;
}
int rdconf_persist(void)
{
rdconf *c = rdconf_get();
char path[440];
conf_path(path, sizeof path);
if (!path[0]) return -1;
FILE *f = fopen(path, "wb");
if (!f) return -5;
fprintf(f, "ui.shelf_layout=%s\n", c->shelf);
fprintf(f, "rd.fs=%d\n", c->fs);
fprintf(f, "rd.ls=%d\n", c->ls);
fprintf(f, "rd.bg=%s\n", c->bg);
fprintf(f, "rd.theme=%s\n", c->theme);
fprintf(f, "rd.header=%d\n", c->header);
fprintf(f, "rd.footer=%d\n", c->footer);
fprintf(f, "rd.font=%s\n", c->font);
fclose(f);
return 0;
}
+45
View File
@@ -0,0 +1,45 @@
/* app/reader_conf.h — 阅读器 App 私有配置(reader.conf,key=value 文本)。
*
* 存储域:<TF>/MeowData/os.meow.reader/reader.conf(复用 progress.c 的 meow_config_dir()
* 去皮 /data 派生包根;无 env/宿主回退 /tmp/meowcfg_<pid>/)。不进系统 settings.conf。
*
* 键(本模块常量):
* ui.shelf_layout = list|grid 书架默认布局(shelf.c 读)
* rd.fs = int (14..40) 全局默认字号(无进度书时 reader_open 用)
* rd.ls = int (100..200) 全局默认行距
* rd.bg = auto|black|white|sepia 阅读页底色预设(默认 black;B 定案)
* rd.theme= auto|light|dark 明暗(auto=跟随系统)
* rd.header= 0|1 阅读页顶条(书名+章节名)显隐
* rd.footer= 0|1 阅读页底条(章节+页码)显隐
* rd.font = <路径>|空 字体(进阶;空=resolve_font 自动)
*/
#ifndef READER_CONF_H
#define READER_CONF_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct rdconf {
int fs; /* 默认字号 14..40 */
int ls; /* 默认行距 100..200 */
char bg[8]; /* auto|black|white|sepia */
char theme[8]; /* auto|light|dark */
char shelf[8]; /* list|grid */
int header; /* 1=阅读页顶条 */
int footer; /* 1=阅读页底条 */
char font[400]; /* 字体路径(空=自动) */
} rdconf;
/* 惰性载入 + 缓存(首次调用读盘;无档=默认)。返回指向静态副本,勿改字段外部生命周期。 */
rdconf *rdconf_get(void);
/* 把当前缓存写回 reader.conf。0=成功;负值=IO/路径错(不崩)。 */
int rdconf_persist(void);
#ifdef __cplusplus
}
#endif
#endif /* READER_CONF_H */
+186
View File
@@ -0,0 +1,186 @@
/* app/reader_ipc.c — 系统翻页信号接收(见 app/reader_ipc.h)。 */
#include "reader_ipc.h"
#include "../reader_internal.h" /* reader_page_key() */
#include "meow_ipc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static int g_fd = -1;
static pthread_t g_thr;
static int g_thr_on = 0;
static volatile int g_quit = 0;
static const char *sock_path(void)
{
const char *e = getenv("MEOW_IPC_SOCK");
return (e && e[0]) ? e : MEOW_IPC_PATH;
}
static void app_id_of(char *out, int outsz)
{
const char *e = getenv("MEOW_LOG_APP");
snprintf(out, (size_t)outsz, "%s", (e && e[0]) ? e : "os.meow.reader");
}
static int connect_path(const char *path)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
return fd;
}
static int write_all(int fd, const uint8_t *p, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, p + off, n - off);
if (w < 0) { if (errno == EINTR) continue; return 0; }
if (w == 0) return 0;
off += (size_t)w;
}
return 1;
}
static int read_all(int fd, uint8_t *p, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t r = read(fd, p + off, n - off);
if (r < 0) { if (errno == EINTR) continue; return 0; }
if (r == 0) return 0;
off += (size_t)r;
}
return 1;
}
static int send_frame(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &len, 4);
if (!write_all(fd, hdr, sizeof hdr)) return -1;
if (len && payload && !write_all(fd, payload, len)) return -1;
return 0;
}
static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
if (!read_all(fd, hdr, sizeof hdr)) return -1;
uint32_t magic, len; uint16_t ver;
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2);
memcpy(&len, hdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
if (len > rcap) len = rcap;
if (len > 0 && !read_all(fd, rbuf, len)) return -1;
return (int)len;
}
static int conn_locked(void)
{
if (g_fd >= 0) return g_fd;
int fd = connect_path(sock_path());
if (fd < 0) return -1;
char app[MEOW_APP_ID_MAX];
app_id_of(app, (int)sizeof app);
m_hello_t h;
memset(&h, 0, sizeof h);
snprintf(h.app_id, sizeof h.app_id, "%s", app);
h.version = 1;
h.flags = 0; /* 非媒体源;仅声明存在,收系统翻页信号 */
if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) != 0) { close(fd); return -1; }
uint8_t rbuf[256];
uint16_t rt;
if (read_frame(fd, &rt, rbuf, sizeof rbuf) < 0 ||
(rt != MSG_ACK && rt != MSG_ERR)) { close(fd); return -1; }
g_fd = fd;
return fd;
}
static void mark_dead_locked(void)
{
if (g_fd >= 0) { close(g_fd); g_fd = -1; }
}
static void *ipc_thread_main(void *arg)
{
(void)arg;
while (!g_quit) {
pthread_mutex_lock(&g_mu);
int fd = g_fd;
if (fd < 0) fd = conn_locked(); /* 懒连/重连(best-effort;失败则重试) */
pthread_mutex_unlock(&g_mu);
if (fd < 0) { usleep(500000); continue; }
struct pollfd p;
p.fd = fd; p.events = POLLIN; p.revents = 0;
int pr = poll(&p, 1, 200);
if (pr < 0 && errno != EINTR) {
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
continue;
}
if (pr > 0 && (p.revents & (POLLHUP | POLLERR | POLLNVAL))) {
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
continue;
}
if (pr > 0 && (p.revents & POLLIN)) {
uint8_t rbuf[MEOW_IPC_MAX];
uint16_t type;
int n = read_frame(fd, &type, rbuf, sizeof rbuf);
if (n < 0) {
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
continue;
}
/* UI 线程经 reader_on_tick 消费(跨线程仅置 pending 计数,不直接改页) */
if (type == MSG_KEY_PAGEUP) reader_page_key(-1);
else if (type == MSG_KEY_PAGEDOWN) reader_page_key(1);
}
}
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
return NULL;
}
void reader_ipc_start(void)
{
pthread_mutex_lock(&g_mu);
int need = !g_thr_on;
if (need) { g_quit = 0; g_thr_on = 1; }
pthread_mutex_unlock(&g_mu);
if (need) {
pthread_attr_t at;
pthread_attr_init(&at);
pthread_attr_setstacksize(&at, 64 * 1024); /* 小栈:阅读态 ≤8MB 纪律 */
pthread_create(&g_thr, &at, ipc_thread_main, NULL);
pthread_attr_destroy(&at);
pthread_detach(g_thr);
}
}
void reader_ipc_stop(void)
{
pthread_mutex_lock(&g_mu);
g_quit = 1;
mark_dead_locked();
g_thr_on = 0;
pthread_mutex_unlock(&g_mu);
}
+32
View File
@@ -0,0 +1,32 @@
/* app/reader_ipc.h — 阅读器系统翻页信号接收(launcher 经 IPC 下发 MSG_KEY_PAGEUP/PAGEDOWN)。
*
* 系统级能力(READER_UI_DESIGN §E / 用户 2026-09-04 拍板):音量键翻页由 launcher 判决
* (仅当前台 App = 本阅读器且 app.json 声明 volume_page),下发消息;本模块在 App 侧
* 维持一条持久 IPC 连接接收并转成 `reader_page_key(±1)`。
*
* 设计(对齐 SDK meow_ui_media.c 模式):
* - 后台线程懒连 meow_ipc.sock,HELLO(app_id=os.meow.reader) 后 poll 读下发帧;
* - 收到 MSG_KEY_PAGEUP/PAGEDOWN → 调 reader_page_key(-1/+1)(UI 线程经 tick 消费,
* 避免跨线程直接改页面状态);
* - best-effort:launcher 不在 / 断线 → 重试,绝不崩;App 退出 reader_ipc_stop() 收尾。
*
* 内存纪律:显式小线程栈(64KB),符合阅读态 ≤8MB。
*/
#ifndef READER_IPC_H
#define READER_IPC_H
#ifdef __cplusplus
extern "C" {
#endif
/* 启动翻页信号接收线程(幂等)。读入 app_id = "os.meow.reader"。
* 转发目标:reader_page_key(dir)(reader_internal.h)。 */
void reader_ipc_start(void);
/* 停止线程并断开(App 退出时调用;幂等)。 */
void reader_ipc_stop(void);
#ifdef __cplusplus
}
#endif
#endif /* READER_IPC_H */
+250
View File
@@ -0,0 +1,250 @@
/* app/settings.c — 全局设置页(C,书架顶栏「设置」入口 → 独立页 ACT_SETTINGS)。
*
* 配置项(MVP,见 READER_UI_DESIGN §4.3):默认字号/默认行距/默认背景(黑白米黄)/主题(亮暗跟随
* 全局)/书架布局(list-grid)/页头条/页底条。全部存 App 私有 reader.conf(rdconf),不进系统
* settings.conf。UI:meow_topappbar(返回+标题) + meow_listview 行(每行 label + 当前值尾部箭头,
* 点行开 meow_dialog(SELECT) 单选;值即改即存)。
*/
#include "../reader_internal.h"
#include "progress.h"
#include "reader_conf.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SET_X 0
#define SET_W 480
#define SET_APPBAR_H 56
static meow_widget *g_appbar;
static meow_widget *g_list;
static int g_active;
/* 行项枚举(顺序 = 列表顺序) */
enum { ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR, ITEM_COUNT };
static const char *const g_labels[ITEM_COUNT] = {
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条"
};
static void settings_redraw(void);
static const char *bg_label(const rdconf *c)
{
if (!c) return "黑";
if (!strcmp(c->bg, "white")) return "白";
if (!strcmp(c->bg, "sepia")) return "米黄";
if (!strcmp(c->bg, "auto")) return "自动";
return "黑";
}
static const char *theme_label(const rdconf *c)
{
if (!c) return "跟随系统";
if (!strcmp(c->theme, "light")) return "日";
if (!strcmp(c->theme, "dark")) return "夜";
return "跟随系统";
}
static const char *shelf_label(const rdconf *c)
{
if (!c) return "列表";
return (!strcmp(c->shelf, "grid")) ? "宫格" : "列表";
}
/* 当前行值(subtitle)。 */
static void item_value(int item, char *out, int outsz)
{
const rdconf *c = rdconf_get();
if (!c) { snprintf(out, (size_t)outsz, "-"); return; }
switch (item) {
case ITEM_FS: snprintf(out, (size_t)outsz, "%d", c->fs); break;
case ITEM_LS: snprintf(out, (size_t)outsz, "%d", c->ls); break;
case ITEM_BG: snprintf(out, (size_t)outsz, "%s", bg_label(c)); break;
case ITEM_THEME: snprintf(out, (size_t)outsz, "%s", theme_label(c)); break;
case ITEM_SHELF: snprintf(out, (size_t)outsz, "%s", shelf_label(c)); break;
case ITEM_HDR: snprintf(out, (size_t)outsz, "%s", c->header ? "显示" : "隐藏"); break;
case ITEM_FTR: snprintf(out, (size_t)outsz, "%s", c->footer ? "显示" : "隐藏"); break;
default: snprintf(out, (size_t)outsz, "-"); break;
}
}
/* ---- listview 行源(src2:每次绘制读最新 rdconf 值) ---- */
static int settings_src(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= ITEM_COUNT) return 0;
static char valbuf[16];
item_value(idx, valbuf, sizeof valbuf);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = g_labels[idx];
out->subtitle = valbuf;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0;
out->enabled = 1;
return 1;
}
/* 由当前值求 SELECT 对话框的选中下标。 */
static int idx_for(int item, const char *const *opts, int n, const rdconf *c)
{
if (item == ITEM_FS) { for (int i = 0; i < n; i++) if (atoi(opts[i]) == c->fs) return i; return 0; }
if (item == ITEM_LS) { for (int i = 0; i < n; i++) if (atoi(opts[i]) == c->ls) return i; return 0; }
if (item == ITEM_BG) {
for (int i = 0; i < n; i++) if (!strcmp(opts[i], bg_label(c))) return i;
return 0;
}
if (item == ITEM_THEME) {
for (int i = 0; i < n; i++) if (!strcmp(opts[i], theme_label(c))) return i;
return 0;
}
if (item == ITEM_SHELF) {
for (int i = 0; i < n; i++) if (!strcmp(opts[i], shelf_label(c))) return i;
return 0;
}
if (item == ITEM_HDR) return c->header ? 0 : 1;
if (item == ITEM_FTR) return c->footer ? 0 : 1;
return 0;
}
/* 选完整数数组项(字号/行距);写回 rdconf。 */
static void apply_fs_ls(int item, const char *sel)
{
rdconf *c = rdconf_get();
if (!c) return;
int v = atoi(sel);
if (item == ITEM_FS) { if (v >= 14 && v <= 40) c->fs = v; }
else if (item == ITEM_LS) { if (v >= 100 && v <= 200) c->ls = v; }
rdconf_persist();
}
static void apply_choice(int item, int sel)
{
rdconf *c = rdconf_get();
if (!c) return;
switch (item) {
case ITEM_BG:
if (sel < 0) sel = 0;
if (sel > 3) sel = 3;
if (sel == 0) strcpy(c->bg, "black");
else if (sel == 1) strcpy(c->bg, "white");
else if (sel == 2) strcpy(c->bg, "sepia");
else strcpy(c->bg, "auto");
break;
case ITEM_THEME:
if (sel < 0) sel = 0;
if (sel > 2) sel = 2;
if (sel == 0) strcpy(c->theme, "auto");
else if (sel == 1) strcpy(c->theme, "light");
else strcpy(c->theme, "dark");
break;
case ITEM_SHELF:
shelf_set_view(sel ? 1 : 0); /* 写 rdconf + 重建书架 */
return;
case ITEM_HDR: c->header = sel ? 0 : 1; break; /* 0 显示 1 隐藏 */
case ITEM_FTR: c->footer = sel ? 0 : 1; break;
default: return;
}
rdconf_persist();
}
/* 打开一个 SELECT 对话框并应用。 */
static void settings_open_choice(int item)
{
const rdconf *c = rdconf_get();
if (!c) return;
const char *title = g_labels[item];
meow_dialog *d = meow_dialog_create(MEOW_DLG_SELECT, title);
if (!d) return;
const char *opts[16]; int n = 0; int cur = 0;
static char numopts[16][8];
if (item == ITEM_FS) {
for (int v = 14; v <= 40; v += 2) { snprintf(numopts[n], sizeof numopts[n], "%d", v); opts[n] = numopts[n]; n++; }
} else if (item == ITEM_LS) {
for (int v = 100; v <= 200; v += 10) { snprintf(numopts[n], sizeof numopts[n], "%d", v); opts[n] = numopts[n]; n++; }
} else if (item == ITEM_BG) {
static const char *b[4] = { "黑", "白", "米黄", "自动" };
for (int i = 0; i < 4; i++) opts[n++] = b[i];
} else if (item == ITEM_THEME) {
static const char *t[3] = { "跟随系统", "日", "夜" };
for (int i = 0; i < 3; i++) opts[n++] = t[i];
} else if (item == ITEM_SHELF) {
static const char *s[2] = { "列表", "宫格" };
for (int i = 0; i < 2; i++) opts[n++] = s[i];
} else if (item == ITEM_HDR || item == ITEM_FTR) {
static const char *h[2] = { "显示", "隐藏" };
for (int i = 0; i < 2; i++) opts[n++] = h[i];
}
cur = idx_for(item, opts, n, c);
meow_dialog_set_items(d, opts, n, cur);
meow_dialog_show(d);
int res = meow_dialog_result(d);
meow_dialog_free(d);
if (res < 0 || res >= n) return; /* 取消 */
if (item == ITEM_FS || item == ITEM_LS) apply_fs_ls(item, opts[res]);
else apply_choice(item, res);
settings_redraw();
}
static void settings_redraw(void)
{
const meow_theme *th = meow_theme_get();
meow_ui_clear(th->bg);
if (g_appbar) meow_widget_draw(g_appbar);
if (g_list) meow_widget_draw(g_list);
meow_ui_flush();
}
/* view 生命周期 */
static void settings_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void settings_on_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 1;
int sbh = meow_ui_statusbar_height();
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
g_appbar = meow_topappbar_create(0, sbh, 480, SET_APPBAR_H, "阅读设置");
if (g_appbar) meow_topappbar_set_back(g_appbar, 1);
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
int ly = sbh + SET_APPBAR_H + 8;
g_list = meow_listview_create(SET_X, ly, SET_W, 720 - ly, ITEM_COUNT,
settings_src, NULL, MEOW_LIST_SEL_SINGLE);
if (g_list) {
meow_listview_set_container(g_list, 1);
meow_listview_set_item_tier(g_list, 2);
}
settings_redraw();
}
static void settings_on_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 0; }
static void settings_on_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
}
meow_view *settings_view_create(void)
{
static meow_view_hooks hooks = { settings_on_create, settings_on_enter, settings_on_leave,
settings_on_destroy, NULL, NULL, NULL };
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &hooks, NULL);
}
int settings_is_active(void) { return g_active; }
void settings_reload(void) { if (g_active) settings_redraw(); }
int settings_on_tap(int x, int y)
{
/* 顶栏返回 */
if (g_appbar) {
int bi = meow_topappbar_hit(g_appbar, x, y);
if (bi == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; }
if (bi >= 0) return 1;
}
if (g_list) {
int idx = meow_listview_tap_at(g_list, x, y);
if (idx >= 0 && idx < ITEM_COUNT) {
settings_open_choice(idx);
return 1;
}
if (idx >= 0) { settings_redraw(); return 1; }
}
return 0;
}
+268 -35
View File
@@ -1,19 +1,26 @@
/* app/shelf.c — 书架:扫 TF 卡 `Books/`,识别 TXT/FB2/EPUB/MOBI/CBZ(R1 仅 TXT 可开,
* 其它显示「R2 支持」占位);卡片列表 + 最近阅读置顶(按 progress ts 降序)。
* 用 W_LISTVIEW(结构化列表)+ App 自绘标题。空态=清屏提示。
* 其它显示「R2 支持」占位);MD3 布局(meow_topappbar + list/grid)+ 最近阅读置顶 + 段头分组。
*
* 布局(README §3.2,用户 2026-09-04 拍板):
* - 默认 list(meow_listview_set_container(on) MD3 圆角卡),可切 grid(meow_lazygrid 4 列封面卡);
* 布局存 reader.conf `ui.shelf_layout`(rdconf_get()->shelf);
* - 顶栏 meow_topappbar:标题「书架」+ 动作(布局切换 / 刷新 / App 设置入口);
* - 无书也渲染完整骨架(顶栏 + 内容区 + 居中空态卡 + 指引 + 支持格式);
* - 封面:MVP 无封面提取(engine F1);无则圆角占位(surfaceContainerHigh + 格式徽标)。
*/
#include "../reader_internal.h"
#include "progress.h"
#include "reader_conf.h"
#include "meow_ui_internal.h" /* meow_gfx_fill_round(宫格卡圆角填充) */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define SHELF_LIST_X 0
#define SHELF_LIST_Y 64
#define SHELF_LIST_W 480
#define SHELF_LIST_H (720 - 64)
#define SHELF_APPBAR_H 56
#define SHELF_LIST_X 0
#define SHELF_LIST_W 480
typedef struct shelf_book {
char path[400];
@@ -27,11 +34,21 @@ typedef struct shelf_book {
static shelf_book g_books[256];
static int g_nbooks;
static int g_active;
static meow_widget *g_list;
static meow_widget *g_list; /* listview(布局=list) */
static meow_lazygrid *g_grid; /* lazygrid(布局=grid) */
static meow_widget *g_appbar; /* meow_topappbar */
static int g_layout; /* 0=list 1=grid(读 rdconf->shelf) */
static uint32_t g_bg, g_fg, g_dim;
static char g_notice[160]; /* 空态/提示 */
static char g_notice[160]; /* 提示(opendir 失败等) */
static int g_notice_ms; /* 剩余显示 ms(0=无) */
/* 段头行(list 模式):把扫描结果排队为 [段头(fmt), 书, 书, ..., 段头, ...]。
* 书量小用段头(README §3.2),格式分组(TXT/FB2/EPUB/MOBI/CBZ)。 */
#define SHELF_MAX_ROWS 520
typedef struct shelf_row { char section; int fmt; int book; } shelf_row;
static shelf_row g_rows[SHELF_MAX_ROWS];
static int g_nrows;
static const char *fmt_name(int f)
{
switch (f) { case 0: return "TXT"; case 1: return "FB2"; case 2: return "EPUB";
@@ -96,28 +113,174 @@ static void scan_books(void)
qsort(g_books, (size_t)g_nbooks, sizeof(shelf_book), cmp_recent_then_name);
}
/* listview 行源 */
static int shelf_list_src(int idx, meow_list_item *out, void *ctx)
/* 排队段头(按格式):为 list 模式生成 [段头(fmt) | 书] 序列。 */
static void build_rows(void)
{
(void)ctx;
if (idx < 0 || idx >= g_nbooks) return 0;
const shelf_book *b = &g_books[idx];
out->icon_kind = MEOW_LIST_ICON_FILE;
out->title = b->title;
out->subtitle = b->sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->enabled = 1;
out->on = 0;
return 1;
g_nrows = 0;
static const int fmts[] = { 0, 1, 2, 3, 4 };
for (size_t fi = 0; fi < sizeof fmts / sizeof fmts[0]; fi++) {
int fmt = fmts[fi];
int have = 0;
if (g_nrows >= SHELF_MAX_ROWS) break;
for (int i = 0; i < g_nbooks; i++) {
if (g_books[i].fmt != fmt) continue;
if (!have) {
if (g_nrows >= SHELF_MAX_ROWS) break;
g_rows[g_nrows].section = 1; g_rows[g_nrows].fmt = fmt; g_rows[g_nrows].book = -1;
g_nrows++;
have = 1;
}
if (g_nrows >= SHELF_MAX_ROWS) break;
g_rows[g_nrows].section = 0; g_rows[g_nrows].fmt = fmt; g_rows[g_nrows].book = i;
g_nrows++;
}
}
}
/* ---- list 数据源(vsource + 段头;container on) ---- */
static int shelf_list_count(void *ctx) { (void)ctx; return g_nrows; }
static void shelf_list_bind(int idx, meow_lv_item *item, void *ctx)
{
(void)ctx;
item->icon = NULL;
item->icon_kind = MEOW_LIST_ICON_NONE;
item->right = MEOW_LIST_RIGHT_NONE;
item->on = 0;
item->enabled = 1;
item->user = NULL;
item->title = NULL;
item->subtitle = NULL;
item->section = 0;
if (idx < 0 || idx >= g_nrows) return;
if (g_rows[idx].section) {
item->section = 1;
item->title = fmt_name(g_rows[idx].fmt);
item->enabled = 0;
} else {
int bi = g_rows[idx].book;
if (bi < 0 || bi >= g_nbooks) return;
item->title = g_books[bi].title;
item->subtitle = g_books[bi].sub;
item->icon_kind = MEOW_LIST_ICON_FILE;
item->right = MEOW_LIST_RIGHT_ARROW;
}
}
static void shelf_list_tapcb(int idx, int op, void *ctx)
{
(void)op; (void)ctx;
if (idx < 0 || idx >= g_nrows || g_rows[idx].section) return;
int bi = g_rows[idx].book;
if (bi < 0 || bi >= g_nbooks) return;
reader_open(g_books[bi].path);
}
/* ---- grid 数据源(meow_lazygrid,4 列封面卡) ---- */
static int shelf_grid_count(void *ctx) { (void)ctx; return g_nbooks; }
static void shelf_grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
{
(void)ctx;
item->title = NULL; item->subtitle = NULL; item->icon = NULL;
item->enabled = 1; item->user = NULL;
if (idx < 0 || idx >= g_nbooks) return;
item->title = g_books[idx].title;
item->subtitle = g_books[idx].sub;
}
/* 标题截断(格内防溢出):按字节收窄(UTF-8 截断可能断码,但仅显示用,断码由字库兜底为 tofu;
* 更稳妥在这里按格子宽度自适应——MVP 简单截 8 字节 + "…")。 */
static void grid_title_cut(const char *src, char *out, int outsz)
{
int n = 0;
while (src[n] && n < (int)sizeof g_books[0].title - 1 && n < 7 * 3) n++; /* ≤7 个 3 字节字 */
if ((size_t)n >= strlen(src)) { strncpy(out, src, (size_t)outsz - 1); out[outsz - 1] = 0; return; }
int m = n; while (m > 0 && ((unsigned char)src[m]) > 0x7F && m > n - 2) m--; /* 断码回退 */
if (m < 0) m = 0;
strncpy(out, src, (size_t)m); out[m] = 0;
size_t L = strlen(out);
if (L + 4 < (size_t)outsz) { out[L] = ' '; out[L + 1] = ' '; out[L + 2] = '.'; out[L + 3] = '.'; out[L + 4] = '.';
out[L + 5] = 0; }
}
static void shelf_grid_draw(int index, int x, int y, int w, int h, int state, void *ctx)
{
(void)ctx;
const meow_theme *th = meow_theme_get();
if (index < 0 || index >= g_nbooks) return;
const shelf_book *b = &g_books[index];
uint32_t card = th->surfaceContainerLow;
if (state & MEOW_LAZYGRID_ST_SELECTED) card = th->primaryContainer;
meow_gfx_fill_round(x, y, w, h, th->radius_card, card);
int pad = 8;
int cw = w - 2 * pad, chh = h - 2 * pad - meow_ui_text_height(16) - 6;
int cx = x + pad, cy = y + pad;
if (cw > 0 && chh > 0) {
meow_gfx_fill_round(cx, cy, cw, chh, MEOW_SHAPE_SMALL, th->surfaceContainerHigh);
/* 格式徽标(居中) */
char fc[16]; int fs = 24;
snprintf(fc, sizeof fc, "%s", fmt_name(b->fmt));
meow_ui_text_center(cx + cw / 2, cy + (chh - meow_ui_text_height(fs)) / 2, fc, fs, th->primary);
}
char t[128]; grid_title_cut(b->title, t, sizeof t);
meow_ui_text_center(x + w / 2, y + h - meow_ui_text_height(16) - 2, t, 16, th->onSurface);
if (state & MEOW_LAZYGRID_ST_SELECTED) {
meow_gfx_fill_round_alpha(x, y, w, h, th->radius_card, th->primary, 26);
}
}
static void shelf_grid_tapcb(int idx, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_nbooks) return;
reader_open(g_books[idx].path);
}
/* ---- 控件装配(按当前布局重建 list / grid) ---- */
static void shelf_build_content(void)
{
const meow_theme *th = meow_theme_get();
int sbh = meow_ui_statusbar_height();
int top = sbh + SHELF_APPBAR_H;
int list_y = top + 22; /* 副题区下 */
int list_h = 720 - list_y;
if (list_h < 0) list_h = 0;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
if (g_layout == 1) { /* grid */
int cols = 4, gap = 8;
int cw = (SHELF_LIST_W - (cols + 1) * 8) / cols; /* 16 边距 */
int chh = cw + 30; /* 封面 + 标题 */
g_grid = meow_lazygrid_create(SHELF_LIST_X, list_y, SHELF_LIST_W, list_h, cols, cw, chh, gap);
if (g_grid) {
meow_lazygrid_set_vsource(g_grid, shelf_grid_count, shelf_grid_bind, NULL);
meow_lazygrid_set_item_draw(g_grid, shelf_grid_draw, NULL);
meow_lazygrid_set_on_tap(g_grid, shelf_grid_tapcb, NULL);
}
} else { /* list */
g_list = meow_listview_create(SHELF_LIST_X, list_y, SHELF_LIST_W, list_h,
g_nrows, NULL, NULL, MEOW_LIST_SEL_SINGLE);
if (g_list) {
meow_listview_set_vsource(g_list, shelf_list_count, shelf_list_bind, NULL);
meow_listview_set_container(g_list, 1);
meow_listview_set_item_tier(g_list, 2);
meow_listview_set_on_tap(g_list, shelf_list_tapcb, NULL);
}
}
(void)th;
}
/* 顶栏动作:0=布局切换 1=刷新 2=设置。布局切换动作标签 = 将切换到的布局。 */
static void shelf_appbar_build(void)
{
int sbh = meow_ui_statusbar_height();
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
g_appbar = meow_topappbar_create(0, sbh, 480, SHELF_APPBAR_H, "书架");
if (!g_appbar) return;
meow_topappbar_add_action(g_appbar, g_layout ? "列表" : "宫格"); /* 0 布局切换 */
meow_topappbar_add_action(g_appbar, "刷新"); /* 1 刷新 */
meow_topappbar_add_action(g_appbar, "设置"); /* 2 设置 */
meow_topappbar_set_item_icon(g_appbar, 1, MEOW_ICON_REFRESH);
meow_topappbar_set_item_icon(g_appbar, 2, MEOW_ICON_SETTINGS);
}
/* view 生命周期 */
static void shelf_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void shelf_on_enter(meow_view *v, void *ctx)
@@ -125,13 +288,16 @@ static void shelf_on_enter(meow_view *v, void *ctx)
(void)v; (void)ctx;
g_active = 1;
scan_books(); /* 回到书架即重扫(新增/最近读置顶刷新) */
build_rows();
const meow_theme *th = meow_theme_get();
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
/* 无控件时重建列表(首次/回到书架) */
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
g_list = meow_listview_create(SHELF_LIST_X, SHELF_LIST_Y, SHELF_LIST_W, SHELF_LIST_H,
g_nbooks, shelf_list_src, NULL, MEOW_LIST_SEL_SINGLE);
meow_listview_set_on_tap(g_list, shelf_list_tapcb, NULL);
/* 布局:读 reader.conf ui.shelf_layout(A) */
const rdconf *c = rdconf_get();
g_layout = (c && c->shelf[0] && strcmp(c->shelf, "grid") == 0) ? 1 : 0;
shelf_appbar_build();
shelf_build_content();
shelf_redraw();
}
static void shelf_on_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 0; }
@@ -139,6 +305,8 @@ static void shelf_on_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
}
meow_view *shelf_view_create(void)
@@ -150,20 +318,53 @@ meow_view *shelf_view_create(void)
int shelf_is_active(void) { return g_active; }
/* 切换 list/grid(书架顶栏「布局切换」动作;改 reader.conf 并重建)。 */
void shelf_set_view(int grid)
{
rdconf *c = rdconf_get();
if (!c) return;
strcpy(c->shelf, grid ? "grid" : "list");
rdconf_persist();
g_layout = grid ? 1 : 0;
if (g_active) {
shelf_appbar_build();
shelf_build_content();
shelf_redraw();
}
}
void shelf_redraw(void)
{
const meow_theme *th = meow_theme_get();
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
meow_ui_clear(g_bg);
meow_ui_text(20, 22, "书架", 28, g_fg);
/* 副标题:格式化说明 */
char sub[96]; snprintf(sub, sizeof sub, "Books/ · %d 本", g_nbooks);
meow_ui_text(20, 24 + 34, sub, 20, g_dim);
if (g_appbar) meow_widget_draw(g_appbar);
if (g_list) meow_widget_draw(g_list);
if (g_nbooks == 0) {
meow_ui_text(30, 320, "书架为空:请往 TF 卡 Books/ 放书籍", 20, g_dim);
meow_ui_text(30, 348, "支持 TXT / FB2 / EPUB / MOBI / CBZ", 20, g_dim);
/* 副题:Books/ · N 本(有书时显示格式说明;无书并入空态卡) */
if (g_nbooks > 0) {
char sub[96]; snprintf(sub, sizeof sub, "Books/ · %d 本", g_nbooks);
int sbh = meow_ui_statusbar_height();
meow_ui_text(24, sbh + SHELF_APPBAR_H + 3, sub, 16, g_dim);
}
if (g_nbooks > 0) {
if (g_list) meow_widget_draw(g_list);
if (g_grid) meow_lazygrid_draw(g_grid, th);
} else {
/* 无书也渲染完整骨架(顶栏已在)+ 居中空态卡 + 指引 + 支持格式 */
int sbh = meow_ui_statusbar_height();
int top = sbh + SHELF_APPBAR_H;
int area_h = 720 - top;
int cw = 360, chh = 200;
int cx = (480 - cw) / 2;
int cy = top + (area_h - chh) / 2;
if (cy < top) cy = top;
meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->surfaceContainer);
meow_ui_text_center(480 / 2, cy + 40, "书架为空", 28, th->fg);
meow_ui_text_center(480 / 2, cy + 76, "请往 TF 卡 Books/ 放书籍", 20, th->onSurfaceVariant);
meow_ui_text_center(480 / 2, cy + 104, "支持 TXT / FB2 / EPUB / MOBI / CBZ", 20, th->onSurfaceVariant);
/* 「去放书」文本按钮(提示) */
meow_ui_text_center(480 / 2, cy + 150, "Books/ 目录", 20, th->primary);
}
if (g_notice_ms > 0) {
meow_ui_fill_rect(0, 620, 480, 44, g_bg);
@@ -174,9 +375,41 @@ void shelf_redraw(void)
int shelf_on_tap(int x, int y)
{
if (g_list && g_nbooks > 0) {
/* 顶栏动作先命中 */
if (g_appbar) {
int bi = meow_topappbar_hit(g_appbar, x, y);
if (bi == 0) { /* 布局切换 */
shelf_set_view(g_layout ? 0 : 1);
return 1;
} else if (bi == 1) { /* 刷新:重扫 */
scan_books(); build_rows();
shelf_build_content();
shelf_redraw();
return 1;
} else if (bi == 2) { /* App 设置入口 */
meow_view_page_push(ACT_SETTINGS);
return 1;
}
}
if (g_nbooks <= 0) { shelf_redraw(); return 0; }
if (g_layout == 1 && g_grid) {
int idx = meow_lazygrid_tap_at(g_grid, x, y);
if (idx >= 0) shelf_redraw();
return 0;
}
if (g_list) {
int idx = meow_listview_tap_at(g_list, x, y);
if (idx >= 0) { shelf_redraw(); return 1; } /* 选中已由 SDK 标脏;重绘 */
if (idx >= 0) shelf_redraw();
return 0;
}
return 0;
}
/* 书架的滚动(list/grid;app.c on_swipe 路由到本页时调用)。 */
void shelf_on_swipe(int dir, int dx, int dy)
{
(void)dir; (void)dx;
if (g_nbooks <= 0) return;
if (g_layout == 1 && g_grid) { meow_lazygrid_scroll_by(g_grid, dy); meow_lazygrid_draw(g_grid, meow_theme_get()); meow_ui_flush(); }
else if (g_list) { meow_listview_scroll_by(g_list, dy); meow_widget_draw(g_list); meow_ui_flush(); }
}