Files
MeowOS-SDK/tuku/ui/library/library.c
T
AskaEth bed6a95485 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 零警告
2026-11-04 19:44:18 +08:00

651 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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);
}