/* app/shelf_model.c — 书架数据模型实现(见 app/shelf_model.h)。 */ #include "shelf_model.h" #include #include #include #include #include #define SHELF_SCAN_MAX_DEPTH 2 /* 子目录深度上限(Books/a/b 有书,Books/a/b/c 不扫) */ int shelf_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; } const char *shelf_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 "?"; } } void shelf_fmt_size(char *out, int outsz, long size) { if (!out || outsz <= 0) return; if (size >= 1 << 20) snprintf(out, (size_t)outsz, "%.1fMB", (double)size / (1 << 20)); else if (size >= 1 << 10) snprintf(out, (size_t)outsz, "%.0fKB", (double)size / (1 << 10)); else snprintf(out, (size_t)outsz, "%ldB", size); } int shelf_copy_utf8(char *dst, int cap, const char *src) { if (!dst || cap <= 0) return 0; if (!src) { dst[0] = 0; return 0; } int limit = cap - 1; int n = 0; while (src[n] && n < limit) n++; if (src[n]) { /* 截断:若 src[n] 是续字节(10xxxxxx),回退到该字符首字节之前,整段丢弃。 */ while (n > 0 && ((unsigned char)src[n] & 0xC0) == 0x80) n--; } memcpy(dst, src, (size_t)n); dst[n] = 0; return n; } 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); } typedef struct scan_ctx { shelf_book *books; int cap; int n; int truncated; shelf_ts_fn ts_fn; } scan_ctx; /* 递归收集:rel = 相对 root 的目录路径(root 自身为 "")。 */ static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel) { if (depth > SHELF_SCAN_MAX_DEPTH) return; /* 防深嵌套/循环 */ if (c->n >= c->cap) return; /* 已满:不再进新目录 */ DIR *dp = opendir(dir); if (!dp) return; struct dirent *de; while ((de = readdir(dp))) { if (de->d_name[0] == '.') continue; char path[400]; if (snprintf(path, sizeof path, "%s/%s", dir, de->d_name) >= (int)sizeof path) continue; struct stat st; if (stat(path, &st) != 0) continue; if (S_ISDIR(st.st_mode)) { char subrel[SHELF_GROUP_MAX * 4]; if (snprintf(subrel, sizeof subrel, "%s%s%s", rel, rel[0] ? "/" : "", de->d_name) >= (int)sizeof subrel) continue; scan_dir(c, path, depth + 1, subrel); continue; } int f = shelf_fmt_from_ext(de->d_name); if (f < 0) continue; if (c->n >= c->cap) { c->truncated = 1; break; } /* 超限:可见提示,不静默 */ shelf_book *b = &c->books[c->n]; shelf_copy_utf8(b->path, (int)sizeof b->path, path); shelf_copy_utf8(b->title, (int)sizeof b->title, de->d_name); char *dot = strrchr(b->title, '.'); if (dot) *dot = 0; /* 标题去扩展名 */ shelf_copy_utf8(b->group, (int)sizeof b->group, rel); b->fmt = f; b->size = (long)st.st_size; b->ts = c->ts_fn ? c->ts_fn(b->path) : 0; char sz[24]; shelf_fmt_size(sz, sizeof sz, b->size); snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(f), sz, b->ts ? " · 读" : ""); c->n++; } closedir(dp); } int shelf_scan_books(const char *root, shelf_book *books, int cap, shelf_ts_fn ts_fn, shelf_scan_report *rep) { if (rep) { rep->nbooks = 0; rep->truncated = 0; rep->root_missing = 0; } if (!root || !books || cap <= 0) { if (rep) rep->root_missing = 1; return 0; } DIR *dp = opendir(root); if (!dp) { if (rep) rep->root_missing = 1; return 0; } closedir(dp); scan_ctx c; c.books = books; c.cap = cap; c.n = 0; c.truncated = 0; c.ts_fn = ts_fn; scan_dir(&c, root, 0, ""); qsort(books, (size_t)c.n, sizeof(shelf_book), cmp_recent_then_name); if (rep) { rep->nbooks = c.n; rep->truncated = c.truncated; } return c.n; } /* ---- 行序列 ---- */ /* 段头排序:根组(group=="")恒置顶,其余按目录名不区分大小写排序。 */ static int cmp_group_key(const void *pa, const void *pb) { const char *a = *(const char *const *)pa; const char *b = *(const char *const *)pb; if (!a[0] != !b[0]) return a[0] ? 1 : -1; /* "" 在前 */ return strcasecmp(a, b); } static int build_rows_format(const shelf_book *books, int nbooks, shelf_row *rows, int rowcap) { static const int fmts[] = { 0, 1, 2, 3, 4 }; int nrows = 0; for (size_t fi = 0; fi < sizeof fmts / sizeof fmts[0]; fi++) { int fmt = fmts[fi]; int have = 0; if (nrows >= rowcap) break; for (int i = 0; i < nbooks; i++) { if (books[i].fmt != fmt) continue; if (!have) { if (nrows >= rowcap) break; rows[nrows].section = 1; rows[nrows].fmt = fmt; rows[nrows].book = -1; rows[nrows].label = NULL; nrows++; have = 1; } if (nrows >= rowcap) break; rows[nrows].section = 0; rows[nrows].fmt = fmt; rows[nrows].book = i; rows[nrows].label = NULL; nrows++; } } return nrows; } static int build_rows_folder(const shelf_book *books, int nbooks, shelf_row *rows, int rowcap) { /* 去重目录键(""=根组)。键是指向 books[].group 的字符串,books 生存期覆盖 rows。 */ static const char *keys[SHELF_MAX_BOOKS]; int nkeys = 0; for (int i = 0; i < nbooks; i++) { const char *k = books[i].group; int seen = 0; for (int j = 0; j < nkeys; j++) { if (strcmp(keys[j], k) == 0) { seen = 1; break; } } if (!seen) keys[nkeys++] = k; } qsort(keys, (size_t)nkeys, sizeof keys[0], cmp_group_key); int nrows = 0; for (int ki = 0; ki < nkeys; ki++) { if (nrows >= rowcap) break; rows[nrows].section = 1; rows[nrows].fmt = -1; rows[nrows].book = -1; rows[nrows].label = keys[ki][0] ? keys[ki] : SHELF_GROUP_ROOT; nrows++; for (int i = 0; i < nbooks; i++) { if (strcmp(books[i].group, keys[ki]) != 0) continue; if (nrows >= rowcap) break; rows[nrows].section = 0; rows[nrows].fmt = books[i].fmt; rows[nrows].book = i; rows[nrows].label = NULL; nrows++; } } return nrows; } int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode, shelf_row *rows, int rowcap) { if (!books || !rows || nbooks <= 0 || rowcap <= 0) return 0; if (nbooks > SHELF_MAX_BOOKS) nbooks = SHELF_MAX_BOOKS; return folder_mode ? build_rows_folder(books, nbooks, rows, rowcap) : build_rows_format(books, nbooks, rows, rowcap); }