reader R1: os.meow.reader 书架+TXT 垂直链路骨架
- core: 编码检测(BOM→UTF-8严格→GB18030/Big5统计, Big5与GB重叠按GB18030兜底, §3.5风险注明) + GB18030双字节码表(uint16[126][190]=47880B, tools/gen_gb18030_table.py 生成, 生成物入仓) + 流式排版器(贪心断行+Kinsoku避头尾+分页+重排锚定, rd_meas_fn 测量回调替 FreeType, 宿主可测) + TXT 流式加载器(按段读, 不整文件入内存, \n 恒独立换行可安全分段) - engine: FreeType 封装(FT_New_Face 按需读文件, NO_HINTING/LIGHT 灰阶, LRU 512 key=码点+字号, blend565, rdft_advance 只测宽不栅格化) - app: 书架(扫 Books/ 识别 5 后缀, 最近读置顶, 空态提示) + 阅读页(W_CUSTOM 画布 3 栏翻页 +中央菜单占位(字号±/行距±/返回) +顶栏进度%/章节名) + 书桥(段表+flat 页索引, 渲染, 重排回锚) + 进度持久化(MeowData/os.meow.reader/progress/, path hash→seg/ch/fs/ls/ts) - 取舍: 正文字体 FreeType 直读 TF MeowOS/fonts/ TTF/OTF; 无字体回退 SDK 位图字库(meow_ui_text, 未打包字体(许可/体积), 全 glyph 兜底 R2); 页位图缓存当前+前后各1页(3×691KB), 翻页动画默认无 - 内存实测(宿主 --wrap malloc, 73页书): 书结构 130KB + 当前段刮擦~1.6MB(行/页容量按 n 偏高,R2 收紧) + 字形缓存~650KB + 3页位图 2.07MB ≈ 4.45MB 动态, 在 ≤8MB 阅读态预算内 - 测试: 宿主 core 58/58(enc/gb18030金标准/kinsoku/page/reflow/txt); qemu-mipsel ft_smoke PASS + book_smoke(12页/渲染有墨/翻页/重排回锚) PASS; reader 交叉 -Wall -Wextra 0 warning - 产物: reader(mipsel 静态1.2MB)+build_reader.sh(vendor FreeType 可复现); vendor/二进制 gitignore
This commit is contained in:
1 parent
33d3059038
commit
553a7d5d9e
26 files changed
+6155
No files matched your search
@@ -0,0 +1,184 @@
|
||||
/* app/shelf.c — 书架:扫 TF 卡 `Books/`,识别 TXT/FB2/EPUB/MOBI/CBZ(R1 仅 TXT 可开,
|
||||
* 其它显示「R2 支持」占位);卡片列表 + 最近阅读置顶(按 progress ts 降序)。
|
||||
* 用 W_LISTVIEW(结构化列表)+ App 自绘标题。空态=清屏提示。
|
||||
*/
|
||||
#include "../reader_internal.h"
|
||||
#include "progress.h"
|
||||
#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)
|
||||
|
||||
typedef struct shelf_book {
|
||||
char path[400];
|
||||
char title[128];
|
||||
char sub[160];
|
||||
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
|
||||
long size;
|
||||
uint64_t ts; /* 最近阅读 unix;无=0 */
|
||||
} shelf_book;
|
||||
|
||||
static shelf_book g_books[256];
|
||||
static int g_nbooks;
|
||||
static int g_active;
|
||||
static meow_widget *g_list;
|
||||
static uint32_t g_bg, g_fg, g_dim;
|
||||
static char g_notice[160]; /* 空态/提示 */
|
||||
static int g_notice_ms; /* 剩余显示 ms(0=无) */
|
||||
|
||||
static const char *fmt_name(int f)
|
||||
{
|
||||
switch (f) { case 0: return "TXT"; case 1: return "FB2"; case 2: return "EPUB";
|
||||
case 3: return "MOBI"; case 4: return "CBZ"; default: return "?"; }
|
||||
}
|
||||
static int fmt_from_ext(const char *name)
|
||||
{
|
||||
const char *d = strrchr(name, '.');
|
||||
if (!d) return -1;
|
||||
if (strcasecmp(d, ".txt") == 0) return 0;
|
||||
if (strcasecmp(d, ".fb2") == 0) return 1;
|
||||
if (strcasecmp(d, ".epub") == 0) return 2;
|
||||
if (strcasecmp(d, ".mobi") == 0) return 3;
|
||||
if (strcasecmp(d, ".cbz") == 0) return 4;
|
||||
return -1;
|
||||
}
|
||||
static void fmt_size(char *out, int outsz, long s)
|
||||
{
|
||||
if (s >= 1 << 20) snprintf(out, outsz, "%.1fMB", (double)s / (1 << 20));
|
||||
else if (s >= 1 << 10) snprintf(out, outsz, "%.0fKB", (double)s / (1 << 10));
|
||||
else snprintf(out, outsz, "%ldB", s);
|
||||
}
|
||||
|
||||
static int cmp_recent_then_name(const void *a, const void *b)
|
||||
{
|
||||
const shelf_book *x = (const shelf_book *)a, *y = (const shelf_book *)b;
|
||||
if (x->ts != y->ts) return (x->ts < y->ts) ? 1 : -1; /* 最近在前 */
|
||||
return strcasecmp(x->title, y->title);
|
||||
}
|
||||
|
||||
static void scan_books(void)
|
||||
{
|
||||
const char *dir = READER_SD_ROOT "/Books";
|
||||
g_nbooks = 0;
|
||||
DIR *dp = opendir(dir);
|
||||
if (!dp) {
|
||||
snprintf(g_notice, sizeof g_notice, "未找到 %s 目录,请放入书籍(TXT)", dir);
|
||||
g_notice_ms = 4000;
|
||||
return;
|
||||
}
|
||||
struct dirent *de;
|
||||
while ((de = readdir(dp)) && g_nbooks < 256) {
|
||||
if (de->d_name[0] == '.') continue;
|
||||
int f = fmt_from_ext(de->d_name);
|
||||
if (f < 0) continue;
|
||||
shelf_book *b = &g_books[g_nbooks];
|
||||
if (snprintf(b->path, sizeof b->path, "%s/%s", dir, de->d_name) >= (int)sizeof b->path) continue;
|
||||
strncpy(b->title, de->d_name, sizeof b->title - 1); b->title[sizeof b->title - 1] = 0;
|
||||
char *dot = strrchr(b->title, '.');
|
||||
if (dot) *dot = 0; /* 标题去扩展名 */
|
||||
b->fmt = f;
|
||||
struct stat st;
|
||||
b->size = (stat(b->path, &st) == 0) ? (long)st.st_size : 0;
|
||||
rdprog p;
|
||||
b->ts = (rdprog_load(b->path, &p) == 1) ? p.ts : 0;
|
||||
char sz[24]; fmt_size(sz, sizeof sz, b->size);
|
||||
snprintf(b->sub, sizeof b->sub, "%s · %s%s", fmt_name(f), sz,
|
||||
b->ts ? " · 读" : "");
|
||||
g_nbooks++;
|
||||
}
|
||||
closedir(dp);
|
||||
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)
|
||||
{
|
||||
(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;
|
||||
}
|
||||
|
||||
static void shelf_list_tapcb(int idx, int op, void *ctx)
|
||||
{
|
||||
(void)op; (void)ctx;
|
||||
if (idx < 0 || idx >= g_nbooks) return;
|
||||
const shelf_book *b = &g_books[idx];
|
||||
if (b->fmt == 0) { reader_open(b->path); }
|
||||
else { snprintf(g_notice, sizeof g_notice, "%s 格式 R2 支持(R1 先看 TXT)", fmt_name(b->fmt)); g_notice_ms = 2500; }
|
||||
}
|
||||
|
||||
/* 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)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
g_active = 1;
|
||||
scan_books(); /* 回到书架即重扫(新增/最近读置顶刷新) */
|
||||
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);
|
||||
shelf_redraw();
|
||||
}
|
||||
static void shelf_on_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 0; }
|
||||
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; }
|
||||
}
|
||||
|
||||
meow_view *shelf_view_create(void)
|
||||
{
|
||||
static meow_view_hooks hooks = { shelf_on_create, shelf_on_enter, shelf_on_leave,
|
||||
shelf_on_destroy, NULL, NULL, NULL };
|
||||
return meow_view_create(ACT_SHELF, MEOW_VIEW_ACTIVITY, &hooks, NULL);
|
||||
}
|
||||
|
||||
int shelf_is_active(void) { return g_active; }
|
||||
|
||||
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_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(R2)", 20, g_dim);
|
||||
}
|
||||
if (g_notice_ms > 0) {
|
||||
meow_ui_fill_rect(0, 620, 480, 44, g_bg);
|
||||
meow_ui_text(20, 630, g_notice, 20, th->primary);
|
||||
}
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
int shelf_on_tap(int x, int y)
|
||||
{
|
||||
if (g_list && g_nbooks > 0) {
|
||||
int idx = meow_listview_tap_at(g_list, x, y);
|
||||
if (idx >= 0) { shelf_redraw(); return 1; } /* 选中已由 SDK 标脏;重绘 */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user