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:
1 parent
b364b6371a
commit
2826c1e905
27 files changed
+1704
-129
No files matched your search
@@ -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(); }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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(); }
|
||||
}
|
||||
Reference in new issue
Block a user