feat(tuku): 图库 M3 骨架——书库浏览/主题同步/阅读器与锁屏接缝

- 新增顶层 tuku/ 工程(package dev.tuku.app、二进制 tuku):app.c/internal 头/logic/ui 分层、app.json、build/pack 脚本
- 书库:从 TF 卡根浏览文件夹,含图目录=一本书;logic/library 纯 C 扫描+自然排序(page2<page10)
- 流式缩略图:后台线程解码+meow_img_downscale(96),首帧/滚动不阻塞;磁盘缓存 MeowData/<pkg>/cache(mtime 失效+LRU)
- 列表 meow_listview 虚拟化 + meow_scroll fling/snap;行=缩略图+目录名+页数
- 主题:全部语义色 + meow_theme_add_listener 即时重绘;明暗/风格族两态可读
- 导航:meow_view page push/pop、TopAppBar ⚙→设置;reader/settings/lock 最小桩
- 接缝:mr_reader_get/set_direction(默认 RTL)、lock_view_should_show()(桩 0)
- 宿主 gcc 测试 46 项 PASS;交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
This commit is contained in:
AskaEth committed 2026-11-04 19:44:18 +08:00
1 parent c0396e40ce
commit bed6a95485
17 files changed
+1676

No files matched your search

+650
View File
@@ -0,0 +1,650 @@
/* ui/library/library.c — 图库根页(ACT_LIBRARY):
*
* TopAppBar(标题 + 返回 + ⚙ 位图动作)+ meow_listview 虚拟化目录列表。
* 任意文件夹可浏览;无元数据但含 PNG/JPEG/BMP 的文件夹即一本书(含图目录=册)。
* 行=缩略图 + 目录名 + 页数;目录由 logic/library 扫描并自然排序。
*
* 流畅度(用户指令优先):
* - 首帧只绘文字行:bind 不读盘;缩略图请求投递给后台线程(pthread)异步解码,
* 完成后在 on_tick 补位并只 invalidate 列表视口;
* - 缩略图 meow_img_downscale 到 96px,并落磁盘缓存 MeowData/<pkg>/cache
* (BMP 原始缩略图,文件名含源文件 mtime+size,源文件变化自动失效;≥64 张按
* mtime LRU 删除);
* - 列表滚动用 meow_scroll_wrap:跟手 track + 松手 fling + 行高 snap。
*
* 绘制/颜色:SDK 注册表非保留绘制,全部取 meow_theme_get() 语义色;主题切换由 app.c 的
* listener 整帧 invalidate。缩略图缓存只在列表重建时释放(避免池内悬垂指针),上限 24 张。
*/
#include "tuku_internal.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#define MR_THUMB_MAX 24
#define MR_THUMB_DIM 96
#define MR_TOPAPPBAR_H 56
#define MR_DEFAULT_ROW_H 56
#define MR_CACHE_MAX_FILES 64
#define MR_REQ_MAX 64
#define MR_RES_MAX 16
typedef struct thumb_req {
int gen;
int idx;
char cover[MR_PATH_MAX];
} thumb_req;
typedef struct thumb_res {
int gen;
int idx;
meow_img *img;
} thumb_res;
static mr_page_ctrl g_ctrl;
static mr_library g_lib;
static char g_cur_dir[MR_PATH_MAX] = MR_ROOT_DIR;
static meow_widget *g_tb;
static meow_widget *g_lv;
static meow_scroll *g_scroll;
static int g_list_x, g_list_y, g_list_w, g_list_h;
static struct {
int idx;
meow_img *img;
} g_thumbs[MR_THUMB_MAX];
static int g_thumb_n;
/* ---- 异步缩略图请求/结果队列 ---- */
static pthread_t g_worker;
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
static int g_worker_on;
static int g_worker_stop;
static int g_gen;
static thumb_req g_req[MR_REQ_MAX];
static int g_req_head, g_req_tail, g_req_n;
static char g_inflight[MR_PATH_MAX];
static thumb_res g_res[MR_RES_MAX];
static int g_res_n;
/* ---- 磁盘缩略图缓存 ---- */
static char g_cache_dir[MR_PATH_MAX];
static int g_cache_on;
static int g_drag;
static int g_dragged;
static int g_drag_last_y;
/* ============================================================ */
/* 磁盘缓存(BMP 原始缩略图;文件名含源 mtime+size 做失效) */
/* ============================================================ */
static unsigned long long fnv1a64(const char *s)
{
unsigned long long h = 1469598103934665603ULL;
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
h ^= (unsigned long long)*p;
h *= 1099511628211ULL;
}
return h;
}
static void cache_init(void)
{
const char *data = meow_config_dir();
if (!data || !data[0]) return;
int n = snprintf(g_cache_dir, sizeof g_cache_dir, "%s", data);
if (n <= 0 || n >= (int)sizeof g_cache_dir) { g_cache_dir[0] = '\0'; return; }
char *slash = strrchr(g_cache_dir, '/');
if (slash && strcmp(slash, "/data") == 0) *slash = '\0';
size_t len = strlen(g_cache_dir);
n = snprintf(g_cache_dir + len, sizeof g_cache_dir - len, "/cache");
if (n <= 0 || n >= (int)(sizeof g_cache_dir - len)) { g_cache_dir[0] = '\0'; return; }
if (mkdir(g_cache_dir, 0755) != 0 && errno != EEXIST) {
g_cache_dir[0] = '\0';
return;
}
g_cache_on = 1;
}
static void cache_path(char *out, int outsz, const char *cover, const struct stat *st)
{
unsigned long long h = fnv1a64(cover);
snprintf(out, (size_t)outsz, "%s/%016llx_%lld_%lld.bmp", g_cache_dir, h,
(long long)st->st_mtime, (long long)st->st_size);
}
/* LRU:缓存文件数超过 MR_CACHE_MAX_FILES 时删最旧(读命中会 utimes 刷新 mtime)。
* 每次只删一个最旧文件、最多扫 8 轮,避免在 14MB 内存口径下分配大数组。 */
static void cache_prune(void)
{
if (!g_cache_on || !g_cache_dir[0]) return;
for (int pass = 0; pass < 8; pass++) {
DIR *d = opendir(g_cache_dir);
if (!d) return;
int count = 0, found = 0;
char oldest[MR_PATH_MAX + 96];
oldest[0] = '\0';
time_t oldest_t = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const char *dot = strrchr(de->d_name, '.');
if (!dot || strcmp(dot, ".bmp") != 0) continue;
char full[MR_PATH_MAX + 96];
int n = snprintf(full, sizeof full, "%s/%s", g_cache_dir, de->d_name);
if (n <= 0 || n >= (int)sizeof full) continue;
struct stat st;
if (stat(full, &st) != 0) continue;
count++;
if (!found || st.st_mtime < oldest_t) {
found = 1;
oldest_t = st.st_mtime;
snprintf(oldest, sizeof oldest, "%s", full);
}
}
closedir(d);
if (count <= MR_CACHE_MAX_FILES || !found) break;
unlink(oldest);
}
}
static void put_u16(unsigned char *p, unsigned v)
{
p[0] = (unsigned char)(v & 0xFF);
p[1] = (unsigned char)((v >> 8) & 0xFF);
}
static void put_u32(unsigned char *p, uint32_t v)
{
p[0] = (unsigned char)(v & 0xFF);
p[1] = (unsigned char)((v >> 8) & 0xFF);
p[2] = (unsigned char)((v >> 16) & 0xFF);
p[3] = (unsigned char)((v >> 24) & 0xFF);
}
/* 写 32bpp BI_RGB BMP(stb_image 可读回)。失败返回 -1(缓存只是加速,静默降级)。 */
static int write_bmp(const char *path, const meow_img *im)
{
int w = meow_img_w(im), h = meow_img_h(im);
const unsigned char *px = meow_img_pixels(im);
if (!path || w <= 0 || h <= 0 || w > MR_THUMB_DIM || h > MR_THUMB_DIM || !px) return -1;
uint32_t stride = (uint32_t)w * 4;
uint32_t imgsz = stride * (uint32_t)h;
unsigned char fh[14] = { 0 };
unsigned char ih[40] = { 0 };
fh[0] = 'B'; fh[1] = 'M';
put_u32(fh + 2, 14 + 40 + imgsz);
put_u32(fh + 10, 14 + 40);
put_u32(ih + 0, 40);
put_u32(ih + 4, (uint32_t)w);
put_u32(ih + 8, (uint32_t)h);
put_u16(ih + 12, 1);
put_u16(ih + 14, 32);
put_u32(ih + 20, imgsz);
put_u32(ih + 24, 2835);
put_u32(ih + 28, 2835);
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = (fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
fwrite(ih, 1, sizeof ih, f) == sizeof ih);
unsigned char row[MR_THUMB_DIM * 4];
for (int y = h - 1; ok && y >= 0; y--) {
const unsigned char *src = px + (size_t)y * (size_t)w * 4;
for (int x = 0; x < w; x++) {
row[x * 4 + 0] = src[x * 4 + 2];
row[x * 4 + 1] = src[x * 4 + 1];
row[x * 4 + 2] = src[x * 4 + 0];
row[x * 4 + 3] = src[x * 4 + 3];
}
if (fwrite(row, 1, stride, f) != stride) ok = 0;
}
if (fclose(f) != 0) ok = 0;
if (!ok) unlink(path);
return ok ? 0 : -1;
}
/* 后台线程:缓存命中直接读;否则解码源图 → downscale → 落缓存。 */
static meow_img *thumb_load(const char *cover)
{
struct stat st;
if (stat(cover, &st) != 0 || !S_ISREG(st.st_mode)) return NULL;
char cp[MR_PATH_MAX + 96];
if (g_cache_on && g_cache_dir[0]) {
cache_path(cp, (int)sizeof cp, cover, &st);
if (access(cp, R_OK) == 0) {
meow_img *im = meow_img_load_file(cp);
if (im) {
utimes(cp, NULL); /* LRU 读命中刷新 */
return im;
}
unlink(cp); /* 损坏缓存 */
}
}
meow_img *im = meow_img_load_file(cover);
if (!im) return NULL;
meow_img *small = meow_img_downscale(im, MR_THUMB_DIM);
if (small) { meow_img_free(im); im = small; }
if (g_cache_on && g_cache_dir[0]) {
if (write_bmp(cp, im) == 0) cache_prune();
}
return im;
}
static void *thumb_worker(void *arg)
{
(void)arg;
for (;;) {
thumb_req req;
pthread_mutex_lock(&g_lock);
while (!g_worker_stop && g_req_n == 0)
pthread_cond_wait(&g_cv, &g_lock);
if (g_worker_stop) { pthread_mutex_unlock(&g_lock); break; }
req = g_req[g_req_head];
g_req_head = (g_req_head + 1) % MR_REQ_MAX;
g_req_n--;
snprintf(g_inflight, sizeof g_inflight, "%s", req.cover);
pthread_mutex_unlock(&g_lock);
meow_img *im = thumb_load(req.cover);
pthread_mutex_lock(&g_lock);
g_inflight[0] = '\0';
if (im && !g_worker_stop && g_res_n < MR_RES_MAX) {
g_res[g_res_n].gen = req.gen;
g_res[g_res_n].idx = req.idx;
g_res[g_res_n].img = im;
g_res_n++;
} else if (im) {
meow_img_free(im);
}
pthread_mutex_unlock(&g_lock);
}
return NULL;
}
static void worker_start(void)
{
if (g_worker_on) return;
g_worker_stop = 0;
if (pthread_create(&g_worker, NULL, thumb_worker, NULL) == 0) g_worker_on = 1;
}
static void worker_stop(void)
{
if (!g_worker_on) return;
pthread_mutex_lock(&g_lock);
g_worker_stop = 1;
pthread_cond_broadcast(&g_cv);
pthread_mutex_unlock(&g_lock);
pthread_join(g_worker, NULL);
g_worker_on = 0;
pthread_mutex_lock(&g_lock);
for (int i = 0; i < g_res_n; i++) meow_img_free(g_res[i].img);
g_res_n = 0;
g_req_n = 0;
g_req_head = g_req_tail = 0;
pthread_mutex_unlock(&g_lock);
}
/* 请求缩略图:bind 只投递,不阻塞首绘/滚动。 */
static void thumb_enqueue(int idx, const char *cover)
{
if (!g_worker_on || !cover || !cover[0]) return;
pthread_mutex_lock(&g_lock);
if (g_worker_stop) { pthread_mutex_unlock(&g_lock); return; }
if (strcmp(g_inflight, cover) == 0) { pthread_mutex_unlock(&g_lock); return; }
for (int i = 0; i < g_req_n; i++) {
int p = (g_req_head + i) % MR_REQ_MAX;
if (strcmp(g_req[p].cover, cover) == 0) { pthread_mutex_unlock(&g_lock); return; }
}
if (g_req_n >= MR_REQ_MAX) { pthread_mutex_unlock(&g_lock); return; }
g_req[g_req_tail].gen = g_gen;
g_req[g_req_tail].idx = idx;
snprintf(g_req[g_req_tail].cover, sizeof g_req[g_req_tail].cover, "%s", cover);
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
g_req_n++;
pthread_cond_signal(&g_cv);
pthread_mutex_unlock(&g_lock);
}
/* ============================================================ */
/* 缩略图内存缓存 */
/* ============================================================ */
static void thumbs_free(void)
{
for (int i = 0; i < g_thumb_n; i++) meow_img_free(g_thumbs[i].img);
g_thumb_n = 0;
}
static meow_img *thumb_find(int idx)
{
for (int i = 0; i < g_thumb_n; i++)
if (g_thumbs[i].idx == idx) return g_thumbs[i].img;
return NULL;
}
static int thumb_put(int idx, meow_img *im)
{
if (g_thumb_n >= MR_THUMB_MAX) return 0;
g_thumbs[g_thumb_n].idx = idx;
g_thumbs[g_thumb_n].img = im;
g_thumb_n++;
return 1;
}
/* ============================================================ */
/* 列表虚拟化数据源 */
/* ============================================================ */
static int lib_count(void *ctx)
{
(void)ctx;
return g_lib.count;
}
static void lib_bind(int idx, meow_lv_item *item, void *ctx)
{
(void)ctx;
memset(item, 0, sizeof *item);
if (idx < 0 || idx >= g_lib.count) return;
mr_library_entry *e = &g_lib.items[idx];
item->title = e->name;
item->subtitle = e->subtitle;
item->right = MEOW_LIST_RIGHT_ARROW;
item->enabled = 1;
item->user = e;
meow_img *im = thumb_find(idx);
if (im) {
item->icon_kind = MEOW_LIST_ICON_IMG;
item->icon = im;
} else {
item->icon_kind = e->has_images ? MEOW_LIST_ICON_FILE : MEOW_LIST_ICON_FOLDER;
if (e->cover[0]) thumb_enqueue(idx, e->cover);
}
}
/* ============================================================ */
/* 布局 / 列表重建 */
/* ============================================================ */
static int lib_top_y(void)
{
return meow_ui_statusbar_height() + MR_TOPAPPBAR_H;
}
static void list_rebuild(void)
{
if (g_scroll) { meow_scroll_free(g_scroll); g_scroll = NULL; }
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
g_list_x = 0;
g_list_y = lib_top_y();
g_list_w = meow_ui_width();
g_list_h = meow_ui_height() - g_list_y;
if (g_list_h < 1) g_list_h = 1;
g_lv = meow_listview_create(g_list_x, g_list_y, g_list_w, g_list_h, 0, NULL, NULL,
MEOW_LIST_SEL_NONE);
if (!g_lv) return;
meow_listview_set_vsource(g_lv, lib_count, lib_bind, NULL);
meow_listview_set_item_renderer(g_lv, 2, 1, 1);
meow_listview_set_item_tier(g_lv, 2);
meow_listview_set_empty_text(g_lv, "No folders with PNG/JPEG/BMP");
g_scroll = meow_scroll_wrap(g_lv, 1);
if (g_scroll) meow_scroll_set_snap(g_scroll, MR_DEFAULT_ROW_H);
}
static void update_topbar(void)
{
if (!g_tb) return;
const char *title = "图库";
if (strcmp(g_cur_dir, MR_ROOT_DIR) != 0) {
const char *slash = strrchr(g_cur_dir, '/');
if (slash && slash[1]) title = slash + 1;
}
meow_topappbar_set_title(g_tb, title);
meow_topappbar_set_back(g_tb, strcmp(g_cur_dir, MR_ROOT_DIR) != 0);
}
static void list_invalidate(void)
{
meow_ui_invalidate(g_list_x, g_list_y, g_list_w, g_list_h);
}
/* 目录切换:作废旧异步结果、释放内存缩略图、重新扫描、重建列表(新池强制重新 bind)。 */
static void reset_async(void)
{
pthread_mutex_lock(&g_lock);
g_gen++;
g_req_n = 0;
g_req_head = g_req_tail = 0;
for (int i = 0; i < g_res_n; i++) meow_img_free(g_res[i].img);
g_res_n = 0;
pthread_mutex_unlock(&g_lock);
}
static void load_dir(const char *dir)
{
snprintf(g_cur_dir, sizeof g_cur_dir, "%s", dir);
reset_async();
thumbs_free();
if (mr_library_scan(&g_lib, g_cur_dir, MR_LIB_MAX_DEPTH) < 0) {
mr_library_free(&g_lib);
meow_log(MEOW_LOG_WARN, "library scan failed: %s", g_cur_dir);
}
list_rebuild();
update_topbar();
g_drag = 0;
g_dragged = 0;
if (g_ctrl.active)
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
}
int library_at_root(void)
{
return strcmp(g_cur_dir, MR_ROOT_DIR) == 0;
}
int library_go_up(void)
{
if (library_at_root()) return 0;
char parent[MR_PATH_MAX];
if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0;
/* 不越过书库根(父级仍须在根目录之下或等于根)。 */
if (strcmp(parent, MR_ROOT_DIR) != 0 &&
strncmp(parent, MR_ROOT_DIR "/", sizeof(MR_ROOT_DIR)) != 0)
snprintf(parent, sizeof parent, "%s", MR_ROOT_DIR);
load_dir(parent);
return 1;
}
/* ============================================================ */
/* 事件 */
/* ============================================================ */
void library_on_tap(int x, int y)
{
if (!g_ctrl.active) return;
g_drag = 0;
g_dragged = 0;
if (g_tb) {
int hit = meow_topappbar_hit(g_tb, x, y);
if (hit == MEOW_TOPBAR_BACK) { library_go_up(); return; }
if (hit >= 0) { meow_view_page_push(ACT_SETTINGS); return; }
}
if (!g_lv) return;
int idx = meow_listview_hit(g_lv, x, y);
if (idx < 0 || idx >= g_lib.count) return;
mr_library_entry *e = &g_lib.items[idx];
if (e->has_images) mr_reader_open_dir(e->path);
else load_dir(e->path);
}
void library_on_swipe(int dir, int dist)
{
if (!g_ctrl.active || !g_lv) return;
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_dragged) { /* 本次手势已实时跟手滚动,抬手只补 fling */
g_dragged = 0;
g_drag = 0;
if (g_scroll) meow_scroll_fling(g_scroll);
return;
}
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
if (del == 0) del = (dir == MEOW_SWIPE_UP) ? 120 : -120;
if (g_scroll) {
meow_scroll_track(g_scroll, del, meow_ui_time_ms());
meow_scroll_fling(g_scroll);
} else {
meow_listview_scroll_by(g_lv, del);
}
list_invalidate();
}
void library_on_touch_down(int x, int y)
{
if (!g_ctrl.active || !g_lv) return;
/* 屏底 28px 是系统「上滑回桌面」热区:不从这里启动列表拖拽,避免拦截系统手势。 */
if (y >= meow_ui_height() - 28) return;
if (x < g_list_x || x >= g_list_x + g_list_w ||
y < g_list_y || y >= g_list_y + g_list_h) return;
g_drag = 1;
g_dragged = 0;
g_drag_last_y = y;
}
void library_on_touch_move(int x, int y)
{
(void)x;
if (!g_ctrl.active || !g_drag || !g_lv) return;
int dy = g_drag_last_y - y;
if (dy == 0) return;
if (dy > 1 || dy < -1) g_dragged = 1;
g_drag_last_y = y;
if (g_scroll) meow_scroll_track(g_scroll, dy, meow_ui_time_ms());
else meow_listview_scroll_by(g_lv, dy);
list_invalidate();
}
/* 16ms tick:收下后台解码好的缩略图并局部补位。 */
void library_on_tick(void)
{
if (!g_ctrl.active) return;
thumb_res got[MR_RES_MAX];
int n = 0;
pthread_mutex_lock(&g_lock);
n = g_res_n;
for (int i = 0; i < n; i++) got[i] = g_res[i];
g_res_n = 0;
pthread_mutex_unlock(&g_lock);
int changed = 0;
for (int i = 0; i < n; i++) {
if (got[i].gen == g_gen && got[i].idx >= 0 && got[i].idx < g_lib.count &&
!thumb_find(got[i].idx) && thumb_put(got[i].idx, got[i].img))
changed = 1;
else
meow_img_free(got[i].img);
}
if (changed) list_invalidate();
}
/* ============================================================ */
/* view 生命周期 */
/* ============================================================ */
static void library_page_create(meow_view *v, void *ctx)
{
mr_page_ctrl *c = (mr_page_ctrl *)ctx;
c->view = v;
cache_init();
worker_start();
int w = meow_ui_width();
int sb = meow_ui_statusbar_height();
g_tb = meow_topappbar_create(0, sb, w, MR_TOPAPPBAR_H, "图库");
if (g_tb) {
int ai = meow_topappbar_add_action(g_tb, "");
if (ai >= 0) meow_topappbar_set_item_icon(g_tb, ai, MEOW_ICON_SETTINGS);
}
load_dir(MR_ROOT_DIR);
}
static void library_page_enter(meow_view *v, void *ctx)
{
((mr_page_ctrl *)ctx)->active = 1;
v->x = 0;
v->y = 0;
v->w = meow_ui_width();
v->h = meow_ui_height();
meow_ui_set_fullscreen(0); /* 图库保留状态栏(reader 退出时若未复原也在此兜底) */
meow_ui_set_page_bg(meow_theme_get()->bg);
int sb = meow_ui_statusbar_height();
if (g_tb) meow_widget_set_rect(g_tb, 0, sb, meow_ui_width(), MR_TOPAPPBAR_H);
if (g_lv) {
int y = sb + MR_TOPAPPBAR_H;
int h = meow_ui_height() - y;
if (h < 1) h = 1;
meow_widget_set_rect(g_lv, 0, y, meow_ui_width(), h);
g_list_y = y;
g_list_w = meow_ui_width();
g_list_h = h;
}
g_drag = 0;
g_dragged = 0;
update_topbar();
mr_nav_report();
meow_ui_paint_full();
}
static void library_page_leave(meow_view *v, void *ctx)
{
(void)v;
((mr_page_ctrl *)ctx)->active = 0;
g_drag = 0;
g_dragged = 0;
if (g_scroll) meow_scroll_stop(g_scroll);
}
static void library_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
worker_stop();
if (g_scroll) { meow_scroll_free(g_scroll); g_scroll = NULL; }
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
thumbs_free();
mr_library_free(&g_lib);
}
meow_view *library_view_create(void)
{
static const meow_view_hooks h = {
.on_create = library_page_create,
.on_enter = library_page_enter,
.on_leave = library_page_leave,
.on_destroy = library_page_destroy,
};
return meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
}
+55
View File
@@ -0,0 +1,55 @@
/* ui/lock/lock.c — 锁屏页(ACT_LOCK)M3 桩 + M5 接缝。
*
* M5 在此实现「APP 启动 PIN」:lock_view_should_show() 读配置决定是否锁,
* 锁屏页负责 PIN 输入/校验后 page_pop 回书库。本批桩恒返回 0(不锁),页面仅可编译占位。
*/
#include "tuku_internal.h"
static mr_page_ctrl g_ctrl;
int lock_view_should_show(void)
{
return 0; /* M5:读 APP 启动 PIN 配置后返回 1 */
}
static void lock_draw(meow_view *v, const meow_theme *th, void *ctx)
{
(void)v; (void)ctx;
int w = meow_ui_width();
int h = meow_ui_height();
int cy = h / 2;
meow_ui_text_center(w / 2, cy - 80, "LOCKED", th->font_title, th->primary);
meow_ui_text_center(w / 2, cy - 36, "Startup PIN (M5)", th->font_body, th->onSurface);
meow_ui_text_center(w / 2, cy + 8, "PIN entry will be implemented in M5",
th->font_body, th->onSurfaceVariant);
}
static void lock_page_enter(meow_view *v, void *ctx)
{
((mr_page_ctrl *)ctx)->active = 1;
v->x = 0;
v->y = 0;
v->w = meow_ui_width();
v->h = meow_ui_height();
meow_ui_set_fullscreen(0);
meow_ui_set_page_bg(meow_theme_get()->bg);
mr_nav_report();
meow_ui_paint_full();
}
static void lock_page_leave(meow_view *v, void *ctx)
{
(void)v;
((mr_page_ctrl *)ctx)->active = 0;
}
meow_view *lock_view_create(void)
{
static const meow_view_hooks h = {
.on_enter = lock_page_enter,
.on_leave = lock_page_leave,
.on_draw = lock_draw,
};
return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
}
+85
View File
@@ -0,0 +1,85 @@
/* ui/reader/reader.c — 阅读器页(ACT_READER,全屏)M3 骨架。
*
* M4 在此实现翻页/图片显示;本批只落可编译桩 + 既有接缝:
* - 书库经 mr_reader_open_dir(path) 导航进来(app.c/library.c 不改);
* - 方向状态走 logic/reader 的 mr_reader_get/set_direction(默认 RTL)。
* 绘制走 view on_draw(注册表驱动),主题切换由 app.c 的 listener invalidate 全屏重画。
*/
#include "tuku_internal.h"
#include <stdio.h>
#include <string.h>
static mr_page_ctrl g_ctrl;
static char g_dir[MR_PATH_MAX];
const char *mr_reader_dir(void)
{
return g_dir;
}
void mr_reader_open_dir(const char *path)
{
if (!path) return;
snprintf(g_dir, sizeof g_dir, "%s", path);
meow_log(MEOW_LOG_INFO, "reader open: %s", g_dir);
meow_view_page_push(ACT_READER);
}
void reader_on_tap(int x, int y)
{
(void)x; (void)y;
if (!g_ctrl.active) return;
mr_reader_set_direction(mr_reader_get_direction() == MR_DIR_RTL ? MR_DIR_LTR : MR_DIR_RTL);
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
}
static void reader_draw(meow_view *v, const meow_theme *th, void *ctx)
{
(void)v; (void)ctx;
int w = meow_ui_width();
const char *dir = g_dir[0] ? g_dir : "(no directory)";
size_t n = strlen(dir);
if (n > 64) dir += n - 64;
meow_ui_text_center(w / 2, 160, "READER", th->font_title, th->primary);
meow_ui_text_center(w / 2, 200, "(M4 will render pages here)", th->font_body,
th->onSurfaceVariant);
meow_ui_text_center(w / 2, 280, "Folder:", th->font_body, th->onSurfaceVariant);
meow_ui_text_center(w / 2, 308, dir, th->font_body, th->onSurface);
meow_ui_text_center(w / 2, 372,
mr_reader_get_direction() == MR_DIR_RTL ? "Direction: RTL" : "Direction: LTR",
th->font_body, th->onSurface);
meow_ui_text_center(w / 2, 420, "tap = toggle direction", th->font_body, th->onSurfaceVariant);
meow_ui_text_center(w / 2, 448, "left-edge swipe = back", th->font_body, th->onSurfaceVariant);
}
static void reader_page_enter(meow_view *v, void *ctx)
{
((mr_page_ctrl *)ctx)->active = 1;
v->x = 0;
v->y = 0;
v->w = meow_ui_width();
v->h = meow_ui_height();
meow_ui_set_fullscreen(1); /* app.json route.reader.fullscreen 的运行时兜底 */
meow_ui_set_page_bg(meow_theme_get()->bg);
mr_nav_report();
meow_ui_paint_full();
}
static void reader_page_leave(meow_view *v, void *ctx)
{
(void)v;
((mr_page_ctrl *)ctx)->active = 0;
meow_ui_set_fullscreen(0);
}
meow_view *reader_view_create(void)
{
static const meow_view_hooks h = {
.on_enter = reader_page_enter,
.on_leave = reader_page_leave,
.on_draw = reader_draw,
};
return meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
}
+55
View File
@@ -0,0 +1,55 @@
/* ui/settings/settings.c — 设置页(ACT_SETTINGS)M3 骨架。
*
* M5 在此实现 APP 启动 PIN 等设置项;本批只落可编译桩,入口为书库 TopAppBar ⚙。
* 返回:左缘返回(app.c SWIPE_LEFT → page_pop)。
*/
#include "tuku_internal.h"
static mr_page_ctrl g_ctrl;
static void settings_draw(meow_view *v, const meow_theme *th, void *ctx)
{
(void)v; (void)ctx;
int w = meow_ui_width();
int sb = meow_ui_statusbar_height();
meow_ui_fill_rect(0, sb, w, 56, th->surfaceContainer);
meow_ui_text(w / 2, sb + 16, "Settings", th->font_title, th->onSurface);
meow_ui_text_center(w / 2, sb + 100, "APP options", th->font_body, th->onSurfaceVariant);
meow_ui_text_center(w / 2, sb + 140, "Startup PIN — M5", th->font_body, th->onSurface);
meow_ui_text_center(w / 2, sb + 180, "Reader direction — M4", th->font_body,
th->onSurface);
meow_ui_text_center(w / 2, sb + 240, "Theme follows the system automatically",
th->font_body, th->onSurfaceVariant);
meow_ui_text_center(w / 2, sb + 300, "left-edge swipe = back", th->font_body,
th->onSurfaceVariant);
}
static void settings_page_enter(meow_view *v, void *ctx)
{
((mr_page_ctrl *)ctx)->active = 1;
v->x = 0;
v->y = 0;
v->w = meow_ui_width();
v->h = meow_ui_height();
meow_ui_set_fullscreen(0);
meow_ui_set_page_bg(meow_theme_get()->bg);
mr_nav_report();
meow_ui_paint_full();
}
static void settings_page_leave(meow_view *v, void *ctx)
{
(void)v;
((mr_page_ctrl *)ctx)->active = 0;
}
meow_view *settings_view_create(void)
{
static const meow_view_hooks h = {
.on_enter = settings_page_enter,
.on_leave = settings_page_leave,
.on_draw = settings_draw,
};
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
}