/* 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 #include #include #include #include #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; }