meow_ui.a→9a8eb7ef; hello→fc299577; launcher→b33da20a; 宿主5套件+新test_notif_open+qemu分发 全过 出入: SDK符号用meow_ui_前缀防与launcher内同名冲突; 叠加会话真机待验; §10.5文档旧号段已过时
1588 lines
86 KiB
C
1588 lines
86 KiB
C
/* notif_center.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||
#include "meow_launcher_internal.h"
|
||
|
||
|
||
static ncp_row_t g_rows[NCP_MAX_ROWS];
|
||
|
||
static int g_nrows = 0;
|
||
|
||
static struct app *g_ncp_apps = NULL; /* run_notif_center 注入(通知点击找 app 用) */
|
||
|
||
static int g_ncp_napps = 0;
|
||
|
||
/* §10.5:App 前台叠加会话期间,run_notif_center 周期探测该 App 是否存活(kill(pid,0))。
|
||
* >0 = 正在探测(app_launcher.c run_app_notif_overlay 置位);发现 ESRCH → g_notif_ctrl_died=1
|
||
* 立即结束会话(不 SIGCONT 死进程、不恢复死帧),让前台 waitpid 循环走统一退出路径。 */
|
||
int g_notif_ctrl_pid = 0;
|
||
int g_notif_ctrl_died = 0;
|
||
|
||
|
||
/* ⑥ 勿扰面板内开关:当前 dnd 态(开面板/切换时从 meow_settings notify.dnd 读/写,零旁路)。
|
||
* ncp_collect_cb / ncp_active_count 按它过滤行(only urgency>=NCP_DND_MIN_URGENCY when on)。 */
|
||
static int g_ncp_dnd = 0;
|
||
|
||
|
||
/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。
|
||
* g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。
|
||
* run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint)
|
||
* 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */
|
||
static int g_ncp_swipe_row = -1;
|
||
|
||
static int g_ncp_swipe_off = 0;
|
||
|
||
|
||
/* ---- 阶段D附录B:通知中心顶部媒体卡片(固定于通知列表上方,活动媒体会话时显示)---- */
|
||
#define NCP_MEDIA_H 112 /* 卡片高(封面/信息/控制/进度) */
|
||
#define NCP_MEDIA_CARD_X 12
|
||
#define NCP_MEDIA_CARD_W (fb_xres - 24)
|
||
static meow_img *g_ncp_media_cover = NULL; /* 封面缓存(按 artwork 路径变化重载,不每帧解码) */
|
||
static char g_ncp_media_cover_path[200] = "";
|
||
|
||
/* 卡片顶缘(恒定 = 通知列表区上缘)。 */
|
||
static int ncp_media_y(void) { return NCP_LIST_TOP; }
|
||
|
||
/* 卡片是否显示(活动媒体会话存在且非停止)。 */
|
||
static int ncp_media_on(void) { return meow_media_has_source() ? NCP_MEDIA_H : 0; }
|
||
|
||
/* 通知列表可视区实际顶缘(含媒体卡片时下移一个卡片高)——绘制/命中统一用,避免硬编码 NCP_LIST_TOP。 */
|
||
static int ncp_list_top(void) { return NCP_LIST_TOP + ncp_media_on(); }
|
||
|
||
/* 列表可视区高(含媒体卡片时缩短)。 */
|
||
static int ncp_list_view(void) { int v = NCP_LIST_BOT - ncp_list_top(); return v > 0 ? v : 0; }
|
||
|
||
/* 卡片内按钮几何(供绘制与命中共用)。 */
|
||
#define NCP_MEDIA_BTN_Y 34 /* 相对卡片顶的按钮中心 y */
|
||
#define NCP_MEDIA_PREV_CX 300
|
||
#define NCP_MEDIA_PLAY_CX 372
|
||
#define NCP_MEDIA_NEXT_CX 444
|
||
#define NCP_MEDIA_BTN_R 22 /* play 半径;prev/next 略小 */
|
||
#define NCP_MEDIA_PROG_Y 74 /* 相对卡片顶的进度条 top */
|
||
|
||
|
||
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
|
||
static void ncp_strncpy(char *dst, const char *src, size_t len)
|
||
{
|
||
if (!dst || len == 0) return;
|
||
if (!src) { dst[0] = 0; return; }
|
||
strncpy(dst, src, len - 1);
|
||
dst[len - 1] = 0;
|
||
}
|
||
|
||
|
||
/* app_id → 显示名(apps[] 匹配则用 name,否则原样 app_id;无则空串) */
|
||
static const char *ncp_app_name(const char *app_id)
|
||
{
|
||
if (app_id && g_ncp_apps)
|
||
for (int i = 0; i < g_ncp_napps; i++)
|
||
if (!strcmp(g_ncp_apps[i].id, app_id)) return g_ncp_apps[i].name;
|
||
return app_id ? app_id : "";
|
||
}
|
||
|
||
|
||
/* 通知图标加载回调:把 app_icon(相对卡根/绝对)解成绝对路径,加载 meow_img。 */
|
||
static void ncp_load_icon(ncp_row_t *r)
|
||
{
|
||
if (!r) return;
|
||
r->img = NULL;
|
||
if (!r->icon[0]) return;
|
||
char path[MEOW_NOTIF_ICON_MAX + 80];
|
||
if (r->icon[0] == '/') snprintf(path, sizeof path, "%s", r->icon);
|
||
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", r->icon);
|
||
r->img = meow_img_load_file(path);
|
||
}
|
||
|
||
|
||
static int ncp_collect_cb(const meow_notif_t *n, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (g_nrows >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */
|
||
/* 勿扰(⑥):dnd 开时只收集 urgency>=NCP_DND_MIN_URGENCY(HIGH/CRITICAL)。 */
|
||
if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0;
|
||
ncp_row_t *r = &g_rows[g_nrows++];
|
||
memset(r, 0, sizeof *r);
|
||
r->id = n->id;
|
||
ncp_strncpy(r->app_id, n->app_id, sizeof r->app_id);
|
||
ncp_strncpy(r->category, n->category, sizeof r->category);
|
||
ncp_strncpy(r->summary, n->summary, sizeof r->summary);
|
||
ncp_strncpy(r->body, n->body, sizeof r->body);
|
||
ncp_strncpy(r->icon, n->app_icon, sizeof r->icon);
|
||
ncp_strncpy(r->target, n->target_activity, sizeof r->target);
|
||
r->created_ms = n->created_ms;
|
||
r->progress = n->progress;
|
||
r->urgency = n->urgency;
|
||
ncp_load_icon(r);
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* 释放全部缓存图标 + 清行数(面板关闭/列表刷新前用)。 */
|
||
static void ncp_rows_free(void)
|
||
{
|
||
for (int i = 0; i < g_nrows; i++)
|
||
if (g_rows[i].img) { meow_img_free(g_rows[i].img); g_rows[i].img = NULL; }
|
||
g_nrows = 0;
|
||
}
|
||
|
||
|
||
/* 重新采集通知列表(比 meow_notif_count 变化时调用)。 */
|
||
static int ncp_refresh_rows(void)
|
||
{
|
||
ncp_rows_free();
|
||
meow_notif_list(NULL, ncp_collect_cb, NULL);
|
||
return g_nrows;
|
||
}
|
||
|
||
|
||
/* 当前应显示的活跃通知数(按 g_ncp_dnd 过滤,与 g_rows 同口径)——供变化检测/重采集用。 */
|
||
static int ncp_count_cb(const meow_notif_t *n, void *ctx)
|
||
{
|
||
if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0;
|
||
(*(int *)ctx)++;
|
||
return 0;
|
||
}
|
||
|
||
static int ncp_active_count(void)
|
||
{
|
||
int c = 0;
|
||
meow_notif_list(NULL, ncp_count_cb, &c);
|
||
if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS;
|
||
return c;
|
||
}
|
||
|
||
|
||
/* 通知列表是否变化(按过滤后活跃数比;新条/撤下/TTL 释放都反映在 count,含 dnd 过滤)。 */
|
||
static int ncp_rows_changed(void)
|
||
{
|
||
return ncp_active_count() != g_nrows;
|
||
}
|
||
|
||
|
||
/* ---- 开关平移动画辅助(§9:快照 blit + 平移;只在 run_notif_center 内分配/释放)----
|
||
* g_ncp_ph = 面板高 PH = fb_yres - NCP_TOP;两块快照各 PH × fb_line 字节:
|
||
* bg_snap = 进入面板时(绘制面板前)从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拍的背景(应用列表);
|
||
* panel_snap= 最终位置整面板渲染(draw_notif_center 后拍),稳态重绘后刷新。
|
||
* 快照不可用(分配失败/无 fb)时动画降级为瞬时显示/消失(fallback)。 */
|
||
static unsigned char *g_ncp_bg_snap = NULL;
|
||
|
||
static unsigned char *g_ncp_panel_snap = NULL;
|
||
|
||
static int g_ncp_ph = 0;
|
||
|
||
|
||
static void ncp_snap_init(void)
|
||
{
|
||
int ph = fb_yres - NCP_TOP;
|
||
if (ph <= 0 || fb_line <= 0) { g_ncp_ph = 0; return; }
|
||
size_t bsz = (size_t)ph * (size_t)fb_line;
|
||
g_ncp_bg_snap = malloc(bsz);
|
||
g_ncp_panel_snap = malloc(bsz);
|
||
if (!g_ncp_bg_snap || !g_ncp_panel_snap) {
|
||
free(g_ncp_bg_snap); g_ncp_bg_snap = NULL;
|
||
free(g_ncp_panel_snap); g_ncp_panel_snap = NULL;
|
||
g_ncp_ph = 0;
|
||
return;
|
||
}
|
||
g_ncp_ph = ph;
|
||
}
|
||
|
||
static void ncp_snap_free(void)
|
||
{
|
||
free(g_ncp_bg_snap); g_ncp_bg_snap = NULL;
|
||
free(g_ncp_panel_snap); g_ncp_panel_snap = NULL;
|
||
g_ncp_ph = 0;
|
||
}
|
||
|
||
|
||
/* 从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拷贝到 dst(行宽 fb_line)。 */
|
||
static void ncp_snap_copy_rows(unsigned char *dst, const unsigned char *base)
|
||
{
|
||
if (!dst || g_ncp_ph <= 0) return;
|
||
size_t row = (size_t)fb_line;
|
||
for (int i = 0; i < g_ncp_ph; i++)
|
||
memcpy(dst + (size_t)i * row, base + (size_t)i * row, row);
|
||
}
|
||
|
||
/* 拍背景(应用列表):进入面板时、面板绘制前调用。 */
|
||
static void ncp_snap_bg(void)
|
||
{
|
||
ncp_snap_copy_rows(g_ncp_bg_snap, fb + (size_t)NCP_TOP * fb_line);
|
||
}
|
||
|
||
/* 拍最终整面板:draw_notif_center 画完后 / 每次稳态重绘后调用。 */
|
||
static void ncp_snap_panel(void)
|
||
{
|
||
ncp_snap_copy_rows(g_ncp_panel_snap, fb + (size_t)NCP_TOP * fb_line);
|
||
}
|
||
|
||
|
||
/* 重建动画帧:把 back_buf 面板区 [NCP_TOP..) 还原为「背景 + 平移后的面板」。
|
||
* off∈[-PH,0];prev_bot=上一帧覆盖底缘(exclusive),用于关(收窄)时恢复新露出背景条。
|
||
* 返回本帧覆盖底缘(exclusive,供调用方算脏区与传给下次)。 */
|
||
static int ncp_anim_rebuild(int off, int prev_bot)
|
||
{
|
||
if (!g_ncp_bg_snap || !g_ncp_panel_snap || g_ncp_ph <= 0) return NCP_TOP;
|
||
int top = NCP_TOP;
|
||
int ph = g_ncp_ph;
|
||
int cur_bot = mnp_anim_covered_bot(top, ph, off); /* ∈[top, top+ph](§9.3 裁切) */
|
||
if (cur_bot < top) cur_bot = top;
|
||
if (cur_bot > top + ph) cur_bot = top + ph;
|
||
size_t row = (size_t)fb_line;
|
||
/* 1) 面板收窄(收回中):[cur_bot, prev_bot) 新露出 → 恢复背景(应用列表)。 */
|
||
if (prev_bot > cur_bot) {
|
||
int b0 = prev_bot < top + ph ? prev_bot : top + ph;
|
||
for (int sy = cur_bot; sy < b0; sy++)
|
||
memcpy(fb + (size_t)sy * row,
|
||
g_ncp_bg_snap + (size_t)(sy - top) * row, row);
|
||
}
|
||
/* 2) 贴平移面板:[top, cur_bot) 从 panel_snap 行 yy = sy-(top+off) 拷回。 */
|
||
for (int sy = top; sy < cur_bot; sy++) {
|
||
int yy = sy - (top + off); /* ∈[0, ph)(见 §9.3 裁切论证) */
|
||
if (yy < 0) yy = 0;
|
||
if (yy >= ph) yy = ph - 1;
|
||
memcpy(fb + (size_t)sy * row,
|
||
g_ncp_panel_snap + (size_t)yy * row, row);
|
||
}
|
||
return cur_bot;
|
||
}
|
||
|
||
|
||
/* ⑨ 应用一帧面板动画(开关时间轴 §9 与跟手拖动 ⑨ 共用):把 back_buf 面板区重建为
|
||
* 「背景 + 平移后的面板」并标脏/呈现。p=当前进度;prev_bot=上一帧覆盖底缘(exclusive,
|
||
* 回填本帧);ok=快照可用(否则 no-op,动画降级为瞬时)。只刷面板矩形 + 新露出条,
|
||
* 顶= NCP_TOP(不盖状态栏)、不含 [0,NCP_TOP) 带;不重排面板控件(panel_snap 固定)。 */
|
||
static void ncp_anim_frame(int p, int *prev_bot, int ok)
|
||
{
|
||
if (!ok || g_ncp_ph <= 0 || !prev_bot) return;
|
||
int off = mnp_anim_off(p, NCP_ANIM_FIX, g_ncp_ph);
|
||
int cur_bot = ncp_anim_rebuild(off, *prev_bot);
|
||
int maxb = (*prev_bot > cur_bot) ? *prev_bot : cur_bot;
|
||
*prev_bot = cur_bot;
|
||
meow_dirty_add(0, NCP_TOP, fb_xres, maxb - NCP_TOP);
|
||
meow_ui_flush();
|
||
}
|
||
|
||
|
||
/* 把文本按宽度截断(UTF-8 安全:不切开多字节码),超出加 "..."。 */
|
||
static void ncp_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
|
||
{
|
||
if (!s) s = "";
|
||
if (ui_width(s, size) <= maxw) { ui_draw(x, y, s, color, size); return; }
|
||
int ell = ui_width("...", size);
|
||
int limit = maxw - ell;
|
||
int len = 0;
|
||
char buf[128];
|
||
while (s[len] && len < (int)sizeof buf - 8) {
|
||
/* 取当前字节对应的 UTF-8 码长(1-4),避免切断多字节字符 */
|
||
unsigned char c0 = (unsigned char)s[len];
|
||
int cp = 1;
|
||
if ((c0 & 0xE0) == 0xC0) cp = 2;
|
||
else if ((c0 & 0xF0) == 0xE0) cp = 3;
|
||
else if ((c0 & 0xF8) == 0xF0) cp = 4;
|
||
if (len + cp >= (int)sizeof buf - 1) break;
|
||
for (int k = 0; k < cp; k++) buf[len + k] = s[len + k];
|
||
buf[len + cp] = 0;
|
||
if (ui_width(buf, size) > limit) { buf[len] = 0; break; }
|
||
len += cp;
|
||
}
|
||
if (len > 0) {
|
||
if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; }
|
||
ui_draw(x, y, buf, color, size);
|
||
} else {
|
||
ui_draw(x, y, "...", color, size);
|
||
}
|
||
}
|
||
|
||
|
||
/* 画一个小圆点(近似圆 = 圆角方 r=radius;主题 chip 用)。 */
|
||
static void ncp_dot(int cx, int cy, int r, uint32_t col)
|
||
{
|
||
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, col);
|
||
}
|
||
|
||
|
||
/* 画 32×32 图标(复用 sb_blit_icon;ink 垂直中心对齐 cy)。 */
|
||
static void ncp_sb_icon(int icon, int slot_x, int cy, uint32_t col, int ink_h)
|
||
{
|
||
int saved = g_sb_icon_cy;
|
||
g_sb_icon_cy = cy;
|
||
sb_blit_icon(icon, slot_x, col, ink_h);
|
||
g_sb_icon_cy = saved;
|
||
}
|
||
|
||
|
||
/* chip 位置(4 个,居中排布)。返回下标对应 chip 的 x 左缘。 */
|
||
static int ncp_chip_x(int chip)
|
||
{
|
||
int total = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
|
||
int x0 = (fb_xres - total) / 2;
|
||
return x0 + chip * (NCP_CHIP_W + NCP_CHIP_GAP);
|
||
}
|
||
|
||
static int ncp_chip_hit(int chip, int tx, int ty)
|
||
{
|
||
int x = ncp_chip_x(chip);
|
||
return (tx >= x && tx < x + NCP_CHIP_W && ty >= NCP_CHIP_Y && ty < NCP_CHIP_Y + NCP_CHIP_H);
|
||
}
|
||
|
||
|
||
/* 面板当前 4 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */
|
||
static void ncp_chip_state(int chip, int *on, char *val, int vlen)
|
||
{
|
||
*on = 0;
|
||
if (val && vlen > 0) val[0] = 0;
|
||
if (chip == NCP_CHIP_WIFI) {
|
||
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
|
||
*on = v;
|
||
} else if (chip == NCP_CHIP_BT) {
|
||
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
|
||
*on = v;
|
||
} else if (chip == NCP_CHIP_THEME) {
|
||
int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */
|
||
*on = (v == 1);
|
||
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色");
|
||
} else if (chip == NCP_CHIP_BRIGHT) {
|
||
int v = 0; meow_settings_get_int("display.brightness", &v);
|
||
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
|
||
int vc = mnp_brightness_clamp(v, vmin);
|
||
if (val && vlen > 0) snprintf(val, vlen, "%d%%", vc);
|
||
*on = (vc >= 50); /* 过半视为"亮"态 */
|
||
} else if (chip == NCP_CHIP_POWER) {
|
||
/* 电源菜单 chip:非开关(无 on/off 态),恒示中性;点击由 ncp_chip_apply 唤起电源菜单。 */
|
||
*on = 0;
|
||
}
|
||
}
|
||
|
||
|
||
/* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。
|
||
* 返回 1=主题变化(需重刷状态栏);2=电源菜单已弹出(调用方置 after_modal=1 整帧重绘还原);
|
||
* 0=其它。电源菜单 chip 复用既有系统级 show_power_menu(含 dim/圆钮/音量导航/电源确认/8s强关/
|
||
* 空闲自动息屏),真弹对话框,动作在函数内 system();取消则返回续 panel —— 与电源键长按同款。 */
|
||
static int ncp_chip_apply(int chip)
|
||
{
|
||
if (chip == NCP_CHIP_WIFI) {
|
||
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
|
||
meow_settings_set_switch("network.wifi_enabled", !v);
|
||
s_net_toggle_wifi(!v);
|
||
s_net_refresh();
|
||
} else if (chip == NCP_CHIP_BT) {
|
||
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
|
||
meow_settings_set_switch("network.bluetooth_enabled", !v);
|
||
s_net_toggle_bt(!v);
|
||
s_net_refresh();
|
||
} else if (chip == NCP_CHIP_THEME) {
|
||
/* 快捷主题:切到 manual(确保生效),再翻转 dark/light(display.theme)。 */
|
||
int mode = THEME_MODE_MANUAL;
|
||
meow_settings_get_enum("display.theme_mode", &mode);
|
||
if (mode != THEME_MODE_MANUAL) meow_settings_set_enum("display.theme_mode", THEME_MODE_MANUAL);
|
||
int v = 0; meow_settings_get_enum("display.theme", &v);
|
||
meow_settings_set_enum("display.theme", v ? 0 : 1);
|
||
paint_sb_right(); /* 状态栏文字/底色随主题刷新 */
|
||
meow_settings_save();
|
||
return 1; /* 主题变化 → 重刷状态栏 */
|
||
} else if (chip == NCP_CHIP_BRIGHT) {
|
||
/* 快捷亮度:在「极暗(下限)」与「100」之间切换。 */
|
||
int cur = 0; meow_settings_get_int("display.brightness", &cur);
|
||
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
|
||
if (mnp_brightness_clamp(cur, vmin) < 100) {
|
||
meow_settings_set_int("display.brightness", 100);
|
||
write_brightness(100);
|
||
g_sys.brightness = 100;
|
||
} else {
|
||
meow_settings_set_int("display.brightness", vmin);
|
||
write_brightness(vmin);
|
||
g_sys.brightness = vmin;
|
||
}
|
||
paint_sb_right();
|
||
} else if (chip == NCP_CHIP_POWER) {
|
||
/* 电源菜单 chip:唤起既有系统级电源菜单(真弹对话框,动作在函数内 system();取消则返回)。
|
||
* 不涉及任何设置项(电源菜单不走 meow_settings,零旁路)。返回 2 → 调用方整帧重绘还原面板/状态栏。 */
|
||
logmsg("notif-center: power chip -> show power menu");
|
||
show_power_menu();
|
||
return 2;
|
||
}
|
||
meow_settings_save();
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。
|
||
* off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */
|
||
static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th)
|
||
{
|
||
if (off >= 0) return;
|
||
int bw = -off;
|
||
if (bw <= 0) return;
|
||
int bx = NCP_ROW_X + NCP_ROW_W + off; /* 被空出的右条左缘 */
|
||
fill_rect(bx, ry, bw, NCP_ROW_H - 6, th->error);
|
||
if (bw >= 44) {
|
||
int cx = bx + bw / 2, cy = ry + (NCP_ROW_H - 6) / 2;
|
||
ui_draw(cx - ui_width("删除", 16) / 2, cy - meow_font_line_height(16) / 2,
|
||
"删除", 0xFFFFFF, 16);
|
||
}
|
||
}
|
||
|
||
|
||
/* 画一行内容(行卡 + 图标/APP名/时间/标题/正文/进度),整体随 off 左移(⑦ 行左滑跟手)。
|
||
* i=g_rows 下标;ry=行顶(已含 scroll);off=行左移量(px,<=0)。 */
|
||
static void ncp_draw_row(int i, int ry, int off, const meow_ui_theme_t *th)
|
||
{
|
||
const ncp_row_t *r = &g_rows[i];
|
||
meow_gfx_fill_round(NCP_ROW_X + off, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container);
|
||
/* 图标 */
|
||
int icx = NCP_ICON_X + off, icy = ry + (NCP_ROW_H - 32) / 2;
|
||
if (r->img) meow_ui_img_cover(icx, icy, 32, r->img);
|
||
else {
|
||
/* 回退:主题色圆点 + 首字符 */
|
||
const char *nm = ncp_app_name(r->app_id);
|
||
ncp_dot(icx + 16, icy + 16, 16, th->primary);
|
||
char ch = nm[0] ? nm[0] : '?';
|
||
char cbuf[4] = { ch, 0, 0, 0 };
|
||
/* 首字符若是多字节(中文),取整码 */
|
||
if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0;
|
||
if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } }
|
||
int cw = ui_width(cbuf, 20);
|
||
ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20);
|
||
}
|
||
/* app 名 + 时间 */
|
||
ui_draw(NCP_TEXT_X + off, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16);
|
||
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
|
||
ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16) + off, ry + 12, tbuf, th->on_surface_variant, 16);
|
||
/* 标题 + 内容 */
|
||
ncp_text(NCP_TEXT_X + off, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W);
|
||
ncp_text(NCP_TEXT_X + off, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W);
|
||
/* 进度(progress>=0):细条 */
|
||
if (r->progress >= 0) {
|
||
int px = NCP_TEXT_X + off, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14;
|
||
fill_rect(px, py, pw, 4, th->surface_variant);
|
||
int fw = r->progress * pw / 100;
|
||
if (fw > 0) fill_rect(px, py, fw, 4, th->primary);
|
||
}
|
||
}
|
||
|
||
|
||
/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。
|
||
* ⑦:左滑中的行(g_ncp_swipe_row)按 g_ncp_swipe_off 左移,其下先画删除底(ncp_draw_delete_bg)。 */
|
||
static void ncp_draw_list(int scroll)
|
||
{
|
||
const meow_ui_theme_t *th = s_ui_theme();
|
||
int lt = ncp_list_top(), lv = ncp_list_view();
|
||
if (g_nrows == 0) {
|
||
ui_draw(NCP_ROW_X + 8, lt + 40, "暂无通知", th->on_surface_variant, 22);
|
||
return;
|
||
}
|
||
int total = g_nrows * NCP_ROW_H;
|
||
scroll = mnp_scroll_clamp(scroll, total, lv);
|
||
for (int i = 0; i < g_nrows; i++) {
|
||
int ry = lt + i * NCP_ROW_H - scroll;
|
||
if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */
|
||
int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */
|
||
if (off < 0) ncp_draw_delete_bg(ry, off, th);
|
||
ncp_draw_row(i, ry, off, th);
|
||
}
|
||
/* 滚动条 */
|
||
if (total > lv) {
|
||
int thumb_h = lv * lv / total; if (thumb_h < 24) thumb_h = 24;
|
||
int ty = lt + (lv - thumb_h) * scroll / (total - lv);
|
||
meow_gfx_fill_round(fb_xres - 8, lt, 5, lv, 3, th->surface_container);
|
||
meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant);
|
||
}
|
||
}
|
||
|
||
|
||
/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底(面板 surface_variant),
|
||
* 再画可见行 + 滚动条;只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT)),面板体其它控件 / 状态栏带
|
||
* [0,NCP_TOP) 均不触碰 —— 满足「滚动只重绘列表可视区,绝不整屏」。 */
|
||
static void ncp_repaint_list(int scroll)
|
||
{
|
||
const meow_ui_theme_t *th = s_ui_theme();
|
||
int lt = ncp_list_top(), lv = ncp_list_view();
|
||
fill_rect(0, lt, fb_xres, lv, th->surface_variant);
|
||
ncp_draw_list(scroll);
|
||
meow_dirty_add(0, lt, fb_xres, lv);
|
||
meow_ui_flush();
|
||
}
|
||
|
||
|
||
/* ⑦ 仅重绘一行(行左滑跟手/回弹/滑出专用;只刷该行矩形,不整面板、不刷状态栏)。
|
||
* 清该行带(含被滑出的右条)→ 画删除底(若 off<0)+ 该行内容位移 → 只标该行带脏。 */
|
||
static void ncp_repaint_row(int scroll, int row, int off)
|
||
{
|
||
const meow_ui_theme_t *th = s_ui_theme();
|
||
if (row < 0 || row >= g_nrows) return;
|
||
int lt = ncp_list_top();
|
||
int ry = lt + row * NCP_ROW_H - scroll;
|
||
if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */
|
||
/* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */
|
||
fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant);
|
||
if (off < 0) ncp_draw_delete_bg(ry, off, th);
|
||
ncp_draw_row(row, ry, off, th);
|
||
meow_dirty_add(0, ry, fb_xres, NCP_ROW_H);
|
||
meow_ui_flush();
|
||
}
|
||
|
||
|
||
/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段):
|
||
* 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [0, NCP_DND_SPLIT_X) → 勿扰命中。
|
||
* 右半段归「一键清除」(ncp_clear_hit,互斥);不与 5 chip(y≤130)、滑条带(y≤238)、
|
||
* 通知列表(y≥NCP_LIST_TOP=308)、状态栏(y<28)重叠。 */
|
||
static int ncp_dnd_hit(int tx, int ty)
|
||
{
|
||
return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H &&
|
||
tx >= 0 && tx < NCP_DND_SPLIT_X);
|
||
}
|
||
|
||
|
||
/* ⑧ 一键清除带命中(勿扰对称处的右半段):
|
||
* 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [NCP_DND_SPLIT_X, fb_xres) → 清除命中。 */
|
||
static int ncp_clear_hit(int tx, int ty)
|
||
{
|
||
return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H &&
|
||
tx >= NCP_DND_SPLIT_X && tx < fb_xres);
|
||
}
|
||
|
||
|
||
/* ⑥ 绘勿扰带(⑧ 收窄为左半段):label「勿扰」+ 铃铛图标 + 状态「开/关」 + 左半段右侧 s_draw_switch(48×28)。
|
||
* 右半段由 ncp_draw_clear 画「一键清除」按钮(对称)。 */
|
||
static void ncp_draw_dnd(const meow_ui_theme_t *th)
|
||
{
|
||
int y = NCP_DND_Y;
|
||
int dnd_on = g_ncp_dnd;
|
||
int cy = y + NCP_DND_H / 2;
|
||
/* 铃铛(简化:圆顶 + 顶蒂 + 底部小圆点)。 */
|
||
int bx = NCP_ROW_X + 4, bcy = cy - 2;
|
||
meow_gfx_fill_round(bx, bcy - 7, 16, 13, 7, th->on_surface_variant); /* 铃身圆顶 */
|
||
meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */
|
||
ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */
|
||
/* label + 状态 */
|
||
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18);
|
||
char st[16];
|
||
snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关");
|
||
int lw = ui_width("勿扰", 18);
|
||
ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st,
|
||
dnd_on ? th->primary : th->on_surface_variant, 18);
|
||
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */
|
||
s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th);
|
||
}
|
||
|
||
|
||
/* ⑧ 绘一键清除按钮(勿扰对称处的右半段):圆角按钮底 + 垃圾桶字形 + 「清除」文字。 */
|
||
static void ncp_draw_clear(const meow_ui_theme_t *th)
|
||
{
|
||
int y = NCP_DND_Y;
|
||
int cy = y + NCP_DND_H / 2;
|
||
int cx0 = NCP_DND_SPLIT_X + 8; /* 右半段按钮左缘 */
|
||
int cw = (fb_xres - NCP_ROW_X) - cx0; /* 右半段按钮宽 */
|
||
if (cw <= 0) return;
|
||
meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container);
|
||
/* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */
|
||
const char *lbl = "清除";
|
||
int lw = ui_width(lbl, 18);
|
||
int unit = 22; /* 图标占位宽 + 与文字的间距 */
|
||
int total = unit + lw;
|
||
int gx = cx0 + (cw - total) / 2; /* 图标左缘(居中) */
|
||
int icy = cy;
|
||
fill_rect(gx, icy - 7, 14, 2, th->on_surface_variant); /* 顶盖 */
|
||
meow_gfx_fill_round(gx + 1, icy - 3, 12, 12, 2, th->on_surface_variant);/* 桶身 */
|
||
meow_gfx_fill_round(gx - 2, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手左 */
|
||
meow_gfx_fill_round(gx + 14, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手右 */
|
||
ui_draw(gx + unit, cy - meow_font_line_height(18) / 2 + 2, lbl, th->on_surface, 18);
|
||
}
|
||
|
||
static int ncp_clear_collect_cb(const meow_notif_t *n, void *ctx)
|
||
{
|
||
ncp_clear_ctx_t *c = ctx;
|
||
if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0; /* 保留红线项 */
|
||
if (c->n >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */
|
||
c->ids[c->n++] = n->id;
|
||
return 0;
|
||
}
|
||
|
||
/* 收集可清除通知 id 后逐条 dismiss(dismiss = archive_one(dump→TF) + 释放槽 = 转储留档 + 清出显示)。
|
||
* 返回清除条数。 */
|
||
static int ncp_clear_all(void)
|
||
{
|
||
ncp_clear_ctx_t c;
|
||
memset(&c, 0, sizeof c);
|
||
meow_notif_list(NULL, ncp_clear_collect_cb, &c);
|
||
for (int i = 0; i < c.n; i++) meow_notif_dismiss(c.ids[i]);
|
||
return c.n;
|
||
}
|
||
|
||
|
||
/* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。
|
||
* 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过,
|
||
* 使脏区仅限面板体,绝不整屏重绘。 */
|
||
/* ================= 阶段D附录B:顶部媒体卡片(绘制/命中) ================= */
|
||
|
||
/* 封面缓存:artwork 路径变化才重载(不每帧解码)。 */
|
||
static void ncp_media_cover_load(const meow_media_session_t *s)
|
||
{
|
||
if (!s) return;
|
||
if (strcmp(g_ncp_media_cover_path, s->artwork) == 0) return;
|
||
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
|
||
ncp_strncpy(g_ncp_media_cover_path, s->artwork, sizeof g_ncp_media_cover_path);
|
||
if (!s->artwork[0]) return;
|
||
char path[200];
|
||
if (s->artwork[0] == '/') snprintf(path, sizeof path, "%s", s->artwork);
|
||
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", s->artwork);
|
||
g_ncp_media_cover = meow_img_load_file(path);
|
||
}
|
||
|
||
/* 实心三角(dir=+1 右向 / -1 左向);size=最大宽度 px。按扫描线 fill_rect 近似 */
|
||
static void ncp_media_tri(int cx, int cy, int size, int dir, uint32_t col)
|
||
{
|
||
int half = size / 2;
|
||
for (int dy = -half; dy <= half; dy++) {
|
||
int w = half + 1 - (dy < 0 ? -dy : dy); /* 顶宽 1 → 底宽 half+1 */
|
||
if (w <= 0) continue;
|
||
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
|
||
fill_rect(x0, cy + dy, w, 1, col);
|
||
}
|
||
}
|
||
|
||
/* 圆形按钮底 + 字形(kind:0=prev 1=play 2=next 3=pause 4=暂停态play)。 */
|
||
static void ncp_media_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
|
||
{
|
||
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
|
||
uint32_t fg = 0xFFFFFF;
|
||
if (kind == 0) { /* prev:左三角 + 左竖条 */
|
||
ncp_media_tri(cx + 4, cy, r - 4, -1, fg);
|
||
fill_rect(cx - 3, cy - (r - 5), 3, (r - 5) * 2, fg);
|
||
} else if (kind == 1) { /* play:右三角 */
|
||
ncp_media_tri(cx + 2, cy, r - 2, 1, fg);
|
||
} else if (kind == 2) { /* next:右三角 + 右竖条 */
|
||
ncp_media_tri(cx - 4, cy, r - 4, 1, fg);
|
||
fill_rect(cx + 1, cy - (r - 5), 3, (r - 5) * 2, fg);
|
||
} else if (kind == 3) { /* pause:双竖条 */
|
||
int bw = (r - 6) / 2; if (bw < 2) bw = 2;
|
||
fill_rect(cx - 7, cy - (r - 5), bw, (r - 5) * 2, fg);
|
||
fill_rect(cx + 7 - bw, cy - (r - 5), bw, (r - 5) * 2, fg);
|
||
}
|
||
}
|
||
|
||
/* 画顶部媒体卡片(封面小图 + 标题/歌手 + 播放/暂停/上下曲 + 进度条)。 */
|
||
static void ncp_draw_media_card(const meow_ui_theme_t *th)
|
||
{
|
||
const meow_media_session_t *s = meow_media_current();
|
||
if (!s || s->state == MEOW_MEDIA_STOPPED) return;
|
||
int cy = ncp_media_y();
|
||
meow_gfx_fill_round(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H, 14, th->surface_container);
|
||
ncp_media_cover_load(s);
|
||
int cover_s = 88, cover_x = NCP_MEDIA_CARD_X + 12, cover_y = cy + 12;
|
||
if (g_ncp_media_cover) {
|
||
meow_gfx_fill_round(cover_x, cover_y, cover_s, cover_s, 12, th->surface_variant);
|
||
meow_ui_img_cover(cover_x + 4, cover_y + 4, cover_s - 8, g_ncp_media_cover);
|
||
} else {
|
||
meow_gfx_fill_round(cover_x, cover_y, cover_s, cover_s, 12, th->primary);
|
||
const char *t = s->title[0] ? s->title : s->app_id;
|
||
int cw = ui_width(t, 20);
|
||
ui_draw(cover_x + (cover_s - cw) / 2, cover_y + (cover_s - meow_font_line_height(20)) / 2,
|
||
t, 0xFFFFFF, 20);
|
||
}
|
||
int tx = cover_x + cover_s + 14;
|
||
int tw = (NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R - 8) - tx; /* 文本宽限到上一首按钮左缘前 8px */
|
||
if (tw < 0) tw = 0;
|
||
ncp_text(tx, cy + 14, s->title, 18, th->on_surface, tw);
|
||
ncp_text(tx, cy + 40, s->artist[0] ? s->artist : s->album, 16, th->on_surface_variant, tw);
|
||
/* 控制按钮 */
|
||
int by = cy + NCP_MEDIA_BTN_Y;
|
||
ncp_media_btn(NCP_MEDIA_PREV_CX, by, NCP_MEDIA_BTN_R - 4, 0, th);
|
||
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 /* pause */ : 1 /* play */;
|
||
ncp_media_btn(NCP_MEDIA_PLAY_CX, by, NCP_MEDIA_BTN_R, pk, th);
|
||
ncp_media_btn(NCP_MEDIA_NEXT_CX, by, NCP_MEDIA_BTN_R - 4, 2, th);
|
||
/* 进度条 + 时间 */
|
||
uint32_t pos = meow_media_display_pos(s);
|
||
int px = tx, pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px, py = cy + NCP_MEDIA_PROG_Y;
|
||
if (pw > 0) {
|
||
fill_rect(px, py, pw, 4, th->outline);
|
||
int fw = (s->dur_ms > 0) ? (int)((uint64_t)pos * pw / s->dur_ms) : 0;
|
||
if (fw > pw) fw = pw;
|
||
if (fw > 0) fill_rect(px, py, fw, 4, th->primary);
|
||
}
|
||
}
|
||
|
||
/* 卡片命中:按钮/进度(含 seek)。返回 1=已处理(内部已下发 PLAYCTL);0=未命中。 */
|
||
static int ncp_media_card_tap(int tx, int ty)
|
||
{
|
||
const meow_media_session_t *s = meow_media_current();
|
||
if (!s || s->state == MEOW_MEDIA_STOPPED) return 0;
|
||
int cy = ncp_media_y();
|
||
if (ty < cy || ty >= cy + NCP_MEDIA_H) return 0;
|
||
int by = cy + NCP_MEDIA_BTN_Y;
|
||
int cmd = 0; int64_t seek = 0;
|
||
if (tx >= NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_PREV_CX + NCP_MEDIA_BTN_R &&
|
||
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R) cmd = 3; /* 上一首 */
|
||
else if (tx >= NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_PLAY_CX + NCP_MEDIA_BTN_R &&
|
||
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R)
|
||
cmd = (s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u; /* 播放/暂停 */
|
||
else if (tx >= NCP_MEDIA_NEXT_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_NEXT_CX + NCP_MEDIA_BTN_R &&
|
||
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R) cmd = 4; /* 下一首 */
|
||
else if (s->dur_ms > 0) { /* 进度条 seek */
|
||
int px = NCP_MEDIA_CARD_X + 12 + 88 + 14;
|
||
int pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px;
|
||
int py = cy + NCP_MEDIA_PROG_Y;
|
||
if (pw > 0 && ty >= py - 12 && ty <= py + 16 && tx >= px && tx <= px + pw) {
|
||
int frac = (tx - px) * 1000 / pw;
|
||
seek = (int64_t)s->dur_ms * frac / 1000;
|
||
cmd = 5;
|
||
}
|
||
}
|
||
if (!cmd) return 0;
|
||
int rc = meow_media_playctl(cmd, seek);
|
||
logmsg("notif-card: media tap cmd=%u seek=%lld rc=%d", cmd, (long long)seek, rc);
|
||
return 1;
|
||
}
|
||
|
||
/* 仅重绘媒体卡片带(进度/封面/按钮;不整面板、不刷状态栏)——局部刷新用。 */
|
||
static void ncp_repaint_media_card(void)
|
||
{
|
||
const meow_ui_theme_t *th = s_ui_theme();
|
||
if (!meow_media_has_source()) return;
|
||
int cy = ncp_media_y();
|
||
fill_rect(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H, th->surface_variant);
|
||
ncp_draw_media_card(th);
|
||
meow_dirty_add(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H);
|
||
meow_ui_flush();
|
||
}
|
||
|
||
static void draw_notif_center(int scroll, int redraw_sb)
|
||
{
|
||
const meow_ui_theme_t *th = s_ui_theme();
|
||
int W = fb_xres, H = fb_yres;
|
||
/* 状态栏带 [0,NCP_TOP) 仅当 redraw_sb=1(打开/主题变化)时清底,随后由下方 draw_statusbar()
|
||
* 覆盖重绘(Stage-One:draw_statusbar 只喂 SDK + 标脏顶条,实际由 SDK present() 重绘);
|
||
* redraw_sb=0(交互重绘,如拖滑条)时**绝不触碰**——保留 back_buf 中 SDK 状态栏/上次 top 脏,
|
||
* 避免被清成 surface 底仍被 blit → 状态栏数值闪烁/消失(真机①修复)。
|
||
* 面板体(状态栏之下 surface_variant)态恒整片铺。 */
|
||
if (redraw_sb) fill_rect(0, 0, W, NCP_TOP, th->surface);
|
||
fill_rect(0, NCP_TOP, W, H - NCP_TOP, th->surface_variant);
|
||
|
||
/* 把手 */
|
||
int grab_w = 44, grab_x = (W - grab_w) / 2;
|
||
meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline);
|
||
/* 标题 + 「上滑/点空白关闭」 提示 */
|
||
ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22);
|
||
ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16);
|
||
|
||
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */
|
||
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
|
||
int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
|
||
int chip_x0 = (W - total_chip) / 2;
|
||
for (int c = 0; c < NCP_CHIP_N; c++) {
|
||
int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP);
|
||
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
|
||
uint32_t bg = on ? th->primary : th->surface_container;
|
||
uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant;
|
||
meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg);
|
||
int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2;
|
||
int icon_cy = NCP_CHIP_Y + 14;
|
||
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
|
||
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
|
||
else if (c == NCP_CHIP_THEME) {
|
||
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
|
||
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
|
||
}
|
||
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
|
||
else if (c == NCP_CHIP_POWER) {
|
||
/* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */
|
||
int pc = icon_cx + SB_ICON_SIZE / 2;
|
||
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
|
||
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
|
||
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
|
||
}
|
||
char label[24];
|
||
snprintf(label, sizeof label, "%s", chip_lbl[c]);
|
||
if (val[0] && c == NCP_CHIP_BRIGHT)
|
||
snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */
|
||
ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18,
|
||
label, fg, 14);
|
||
}
|
||
|
||
/* 亮度滑条 */
|
||
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
|
||
int bval = 0; meow_settings_get_int("display.brightness", &bval);
|
||
bval = mnp_brightness_clamp(bval, bmin);
|
||
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18);
|
||
char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval);
|
||
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb,
|
||
th->on_surface_variant, 18);
|
||
s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th);
|
||
|
||
/* 音量滑条 */
|
||
int vval = 0; meow_settings_get_int("audio.volume", &vval);
|
||
vval = vol_clamp(vval);
|
||
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18);
|
||
char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval);
|
||
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl,
|
||
th->on_surface_variant, 18);
|
||
s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th);
|
||
|
||
/* 分隔线 */
|
||
fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline);
|
||
|
||
/* ⑥ 勿扰面板内开关带(list 上方,左半段) */
|
||
ncp_draw_dnd(th);
|
||
/* ⑧ 一键清除按钮(勿扰对称处的右半段) */
|
||
ncp_draw_clear(th);
|
||
|
||
/* 阶段D附录B:顶部媒体卡片(活动媒体会话时显示,固定于列表上方;列表可视区随之下移) */
|
||
ncp_draw_media_card(th);
|
||
|
||
/* 通知列表 */
|
||
ncp_draw_list(scroll);
|
||
|
||
/* 底部提示 */
|
||
ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭",
|
||
th->on_surface_variant, 16);
|
||
|
||
/* 状态栏画在面板体之上(0-27 由 SDK 全局状态栏维护,经 draw_statusbar→sb_sdk_refresh
|
||
* 喂数据+标脏顶条;不落面板体脏区)。仅 redraw_sb=1(打开/主题变化)才喂/标脏,交互时跳过
|
||
* 以保持脏区只限面板体(顶条像素留在 back_buf,由 SDK 已合成的状态栏保持)。 */
|
||
if (redraw_sb) draw_statusbar();
|
||
}
|
||
|
||
|
||
/* 触摸滑条带命中:origin 落在 (slugX, slugY) 的滑条行带内。返回 1=命中。 */
|
||
static int ncp_slider_band(int tx, int ty, int sy)
|
||
{
|
||
return (tx >= NCP_SLIDER_X - 14 && tx <= NCP_SLIDER_X + NCP_SLIDER_W + 14 &&
|
||
ty >= sy - 30 && ty <= sy + 24);
|
||
}
|
||
|
||
|
||
/* 滑条值应用(拖动实时 set_*,save_now=抬手落盘)。
|
||
* 复用设置页方案:亮度 clamp [bmin,100];音量 vol_clamp 上限 + 跳步吸附 + DAC 节流去抖。
|
||
* 返回 1=值实际变更(需重绘面板/刷状态栏);0=吸附+钳制后与当前已提交值相同(懒跳过:
|
||
* 不写 DAC/背光、不刷状态栏、不标脏)→ 拖动去抖,消除逐事件刷状态栏/DAC 的卡顿(真机②修复)。
|
||
* 亮度写背光仅 sysfs 不引额外节流;音量写 DAC 节流(meow_dac_vol_write_throttled/flush 同 s_commit)。 */
|
||
static int ncp_slider_apply(int which, int tx, int save_now)
|
||
{
|
||
if (which == 1) {
|
||
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
|
||
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, bmin, 100);
|
||
v = mnp_brightness_clamp(v, bmin); /* 下限 brightness_min(≥1)、上限 100 */
|
||
int cur = 0; meow_settings_get_int("display.brightness", &cur);
|
||
if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */
|
||
meow_settings_set_int("display.brightness", v);
|
||
write_brightness(v);
|
||
g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */
|
||
paint_sb_right();
|
||
if (save_now) meow_settings_save();
|
||
return 1;
|
||
} else {
|
||
int vmax = vol_clamp(settings_volume_max); /* 音量上限(尊重 audio.volume_max) */
|
||
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0, vmax);
|
||
v = mnp_step_round(v, settings_volume_step); /* 跳步吸附(默认 step=1 → 连续同设置页) */
|
||
v = vol_clamp(v); /* 0..MEOW_VOL_MAX + 上限 clamp */
|
||
if (!save_now && v == settings_volume_get()) return 0; /* 去抖:值未变跳过写 DAC/刷状态栏 */
|
||
meow_settings_set_int("audio.volume", v);
|
||
g_sys.vol = settings_volume_get(); /* 状态栏数值源=最近写入值(含 clamp 后) */
|
||
paint_sb_right();
|
||
if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } /* 抬手=无条件直写 DAC */
|
||
else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
|
||
/* 通知点击:dismiss(标记阅读)+ 按 route_status(FRAMEWORK_DESIGN §4)产出 notif_click_t *click。
|
||
* status 0=OK 跳转(chain 解析出链路;媒体无 target 也 status=0,chain 置空=裸启动主屏);
|
||
* status 1=NO ROUTE(源 app 在,但无 target_activity / 路由未注册);
|
||
* status 2=SOURCE NOT FOUND(源 app 不在 apps[] 或 exec 不可用)。
|
||
* 返回 1=应关闭面板(点击即关;status 1/2 由 run_menu 弹 toast,不崩)。 */
|
||
static int ncp_row_tap(int tx, int ty, int scroll, notif_click_t *click)
|
||
{
|
||
(void)tx; /* 行命中仅依赖 ty/scroll;tx 保留作接口一致(0-warning) */
|
||
int eff = ty - ncp_list_top() + scroll;
|
||
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
|
||
if (row < 0 || row >= g_nrows) return 0;
|
||
const ncp_row_t *r = &g_rows[row];
|
||
meow_notif_dismiss(r->id); /* 点击 = 阅读,撤下 */
|
||
if (click) {
|
||
click->app_idx = -1;
|
||
click->status = NOTIF_CLICK_STATUS_NOROUTE;
|
||
click->chain[0] = 0;
|
||
}
|
||
/* 源 app 定位(apps[] 内且 exec ok):找不到 → SOURCE NOT FOUND(不崩,toast) */
|
||
int src = -1;
|
||
if (g_ncp_apps)
|
||
for (int i = 0; i < g_ncp_napps; i++)
|
||
if (!strcmp(g_ncp_apps[i].id, r->app_id) && g_ncp_apps[i].ok) { src = i; break; }
|
||
if (src < 0) {
|
||
if (click) click->status = NOTIF_CLICK_STATUS_NOSRC;
|
||
return 1;
|
||
}
|
||
/* 跳转判定:M5 接 M3 —— target_activity 解析出链路则跳;媒体通知(无 target)整 app 拉起主屏;
|
||
* 其余无路由 → NO ROUTE(不崩,toast)。 */
|
||
int depth = 0;
|
||
char chain[MEOW_CHAIN_MAX] = "";
|
||
if (r->target[0] && resolve_chain(r->app_id, r->target, chain, &depth) == MEOW_OK) {
|
||
if (click) { click->app_idx = src; click->status = NOTIF_CLICK_STATUS_OK;
|
||
strncpy(click->chain, chain, MEOW_CHAIN_MAX - 1);
|
||
click->chain[MEOW_CHAIN_MAX - 1] = 0; }
|
||
} else if (!strcmp(r->category, MEOW_NOTIF_MEDIA_CAT)) {
|
||
if (click) { click->app_idx = src; click->status = NOTIF_CLICK_STATUS_OK;
|
||
click->chain[0] = 0; } /* 媒体:整 app 前台(无 activity 链则进主屏) */
|
||
} else {
|
||
if (click) click->status = NOTIF_CLICK_STATUS_NOROUTE;
|
||
}
|
||
return 1; /* 点击即关(status 0 由 run_menu 走 launch_activity/launch_app;1/2 弹 toast) */
|
||
}
|
||
|
||
|
||
/* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */
|
||
void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||
int live_open, int lox, int loy)
|
||
{
|
||
if (click) { click->app_idx = -1; click->status = NOTIF_CLICK_STATUS_NONE; click->chain[0] = 0; }
|
||
int list_scroll = 0;
|
||
unsigned char evbuf16[16];
|
||
g_ncp_apps = apps; g_ncp_napps = napps;
|
||
|
||
struct keypoll kp_power, kp_keys;
|
||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||
}
|
||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||
unsigned long long last_tick = now_ms();
|
||
unsigned long long last_act = now_ms(); (void)last_act; /* 预留 idle 计时;当前未读(0-warning) */
|
||
unsigned long long last_listchk = now_ms();
|
||
|
||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||
int drag_slider = 0; /* 0=无, 1=亮度, 2=音量 */
|
||
/* 通知列表拖动滚动(方向遵循 input.swipe_invert,与设置页列表同式;关闭手势区分见 §11.3) */
|
||
int scroll_drag = 0; /* 是否处于列表区拖动滚动手势 */
|
||
int scroll_base = 0; /* 进入滚动时锁定的 list_scroll 基准(连续跟手,防触发瞬间跳变) */
|
||
int scroll_y0 = 0; /* 按下原点纵向坐标 */
|
||
int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */
|
||
int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */
|
||
int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */
|
||
/* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off):
|
||
* row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手);
|
||
* row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。
|
||
* row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */
|
||
int row_swipe_row = -1;
|
||
int row_off = 0;
|
||
int row_settle = 0;
|
||
int row_off_target = 0;
|
||
unsigned long long row_anim_at = 0;
|
||
int row_paint = 0; /* 仅重绘该行带(行左滑专用) */
|
||
/* ⑨ 面板跟手下拉、半程可回:
|
||
* panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停);
|
||
* drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变);
|
||
* drag_y0 = 进入拖动时锁定手指 y(此后 reveal = seed + (tly - drag_y0),连续跟手/半程可回);
|
||
* drag_paint = 仅用动画重建路径重绘一帧(复用 §9 快照 blit,不整屏/不重排控件)。 */
|
||
int panel_drag = 0;
|
||
int drag_seed_reveal = 0;
|
||
int drag_y0 = 0;
|
||
int drag_paint = 0;
|
||
int closed = 0; /* 非动画路径的兜底关面板(快照不可用时瞬时退出) */
|
||
int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */
|
||
int wake_suppress = 0;
|
||
unsigned long long pwr_hold_at = 0;
|
||
int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0;
|
||
|
||
int need_sb = 1; /* 打开/主题变化时重绘状态栏;其余交互跳过(脏区只限面板体) */
|
||
|
||
/* 开关平移动画状态(§9):anim_p∈[0,NCP_ANIM_FIX],anim_target 为意图(FIX=开、0=关)。
|
||
* 仅当快照可用(anim_ok)才做平滑动画;否则降级为瞬时显示/消失(closed 兜底)。 */
|
||
int anim_p = 0, anim_target = NCP_ANIM_FIX;
|
||
int anim_last_target = NCP_ANIM_FIX; /* 记录上一步 anim_target(用于检测「意图切换」重置计时) */
|
||
unsigned long long anim_last = 0;
|
||
int anim_prev_bot = NCP_TOP; /* 上一帧面板覆盖底缘(exclusive;初始=NCP_TOP=未覆盖) */
|
||
int anim_ok = 0;
|
||
|
||
/* ⑨ live_open 入口(run_menu 热区拖动中切入):手指此刻已按下且正拖动。
|
||
* - 触摸:直接以传入原点作「已按下已锁原点」(evdev 广播,本 fd 只接后续 move/release)。
|
||
* - 进度:面板从隐藏(anim_p=0)起步,target 未定(=0),由拖动路径实时映射 anim_p;
|
||
* 松手按阈值锚定 target。默认(非 live_open)仍为 trigger 式自动展开(防御路径)。 */
|
||
if (live_open) {
|
||
td = 1; td_ok = 1; tdx = lox; tdy = loy; scroll_y0 = tdy;
|
||
anim_target = 0; anim_last_target = 0; /* 待拖动/松手锚定 */
|
||
}
|
||
|
||
/* ⑥ 读勿扰面板内开关态(开面板一次;切换在下面 tap 分支写回)。 */
|
||
{
|
||
int d = 0; meow_settings_get_bool("notify.dnd", &d);
|
||
g_ncp_dnd = d;
|
||
}
|
||
|
||
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0
|
||
* 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */
|
||
ncp_refresh_rows();
|
||
list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, ncp_list_view());
|
||
ncp_snap_init();
|
||
if (g_ncp_ph > 0) {
|
||
anim_ok = 1;
|
||
ncp_snap_bg(); /* 背景(应用列表)在面板绘制前拍 */
|
||
draw_notif_center(list_scroll, 1); /* 渲染最终位置整面板 → back_buf */
|
||
ncp_snap_panel(); /* 拍面板快照 */
|
||
/* 起点 p=0:按「上一帧为全覆盖(NCP_TOP+PH)」重建 → 整段还原为背景(面板隐藏) */
|
||
anim_prev_bot = ncp_anim_rebuild(mnp_anim_off(0, NCP_ANIM_FIX, g_ncp_ph),
|
||
NCP_TOP + g_ncp_ph);
|
||
meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP);
|
||
meow_ui_flush();
|
||
} else {
|
||
draw_notif_center(list_scroll, 1);
|
||
meow_dirty_full(); meow_ui_flush();
|
||
}
|
||
need_sb = 0;
|
||
anim_last = now_ms();
|
||
|
||
int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */
|
||
int media_prev = -1; /* 阶段D附录B:上次媒体卡片显示态(检测 出现/消失 → 整面板重排) */
|
||
int media_paint = 0; /* 阶段D附录B:仅重绘媒体卡片带(进度/封面变化,局部刷新) */
|
||
|
||
for (;;) {
|
||
unsigned long long now = now_ms();
|
||
if (sigterm_flag) break;
|
||
/* §10.5:App 前台叠加会话期间探测 App 存活。App 死(/proc/<pid>/stat 状态位 Z=僵尸)→
|
||
* 立即结束会话(回 run_menu 统一重绘背景,不 SIGCONT 死进程、不恢复死帧);launcher 不崩。
|
||
* 注:读 /proc 状态位而非 waitpid(WNOHANG)——waitpid 一旦检测到终止即回收子进程,会抢走
|
||
* app_launcher waitpid 循环的回收权(其状态/exit 码经统一路径处理),此处只读不回收。 */
|
||
if (g_notif_ctrl_pid > 0 && proc_is_zombie(g_notif_ctrl_pid)) {
|
||
logmsg("notif-center: app pid %d died during overlay -> close now", g_notif_ctrl_pid);
|
||
g_notif_ctrl_died = 1;
|
||
break;
|
||
}
|
||
int animating = anim_ok && (anim_p != anim_target);
|
||
|
||
/* 自动息屏(同设置页语义) */
|
||
if (meow_auto_slept && !meow_screen_on) {
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
int wak = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wak = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wak = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wak = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wak = 1;
|
||
}
|
||
if (!wak && screen_off_mode == 1 && tfd >= 0) {
|
||
struct pollfd spfd = { tfd, POLLIN, 0 };
|
||
if (poll(&spfd, 1, 0) > 0) {
|
||
unsigned char ev[16];
|
||
while (read(tfd, ev, sizeof ev) == 16) {
|
||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||
int32_t val = *(int32_t *)(ev + 12);
|
||
if (type == EV_KEY && code == BTN_TOUCH && val) wak = 1;
|
||
}
|
||
}
|
||
}
|
||
if (wak) {
|
||
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
|
||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||
touch_cooldown_until = now_ms() + 300;
|
||
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
||
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
|
||
scroll_invert = 0; scroll_rebound = 0; list_paint = 0;
|
||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
|
||
/* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */
|
||
anim_p = anim_target = anim_last_target = NCP_ANIM_FIX;
|
||
if (anim_ok) anim_prev_bot = NCP_TOP + g_ncp_ph;
|
||
draw_notif_center(list_scroll, 1);
|
||
if (anim_ok) ncp_snap_panel();
|
||
meow_dirty_full(); meow_ui_flush();
|
||
anim_last = now_ms();
|
||
need_sb = 0;
|
||
}
|
||
usleep(POLL_MS * 1000);
|
||
continue;
|
||
}
|
||
|
||
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||
}
|
||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||
if (wake_suppress) {
|
||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||
} else {
|
||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||
if (play && !m_prev_play) {
|
||
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
||
last_act = now;
|
||
}
|
||
if (pwr && !m_prev_pwr) {
|
||
pwr_hold_at = now; pwr_menu_state = 0; pwr_suppress_click = 0; last_act = now;
|
||
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
|
||
}
|
||
if (pwr && pwr_hold_at) {
|
||
if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) {
|
||
if (sim_no_exec) logmsg("notif-center pm: [sim-no-exec] hold %dms -> would forceoff", power_forceoff_ms);
|
||
else { logmsg("notif-center pm: hold %dms -> force poweroff", power_forceoff_ms); meow_power_action(POWER_SHUTDOWN); }
|
||
} else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) {
|
||
pwr_menu_state = 1;
|
||
logmsg("notif-center pm: hold %dms -> power menu", power_longpress_menu_ms);
|
||
show_power_menu();
|
||
last_act = now_ms();
|
||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
|
||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||
after_modal = 1; need_paint = 1;
|
||
goto ncp_keys_seeded;
|
||
}
|
||
}
|
||
if (!pwr && m_prev_pwr) {
|
||
if (pwr_suppress_click)
|
||
logmsg("notif-center pm: release after menu -> no toggle");
|
||
else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
|
||
logmsg("notif-center pm: single-click -> toggle panel");
|
||
meow_toggle_panel(); meow_auto_slept = 0; last_act = now_ms(); need_paint = 1;
|
||
} else if (pwr_woke) {
|
||
logmsg("notif-center pm: release after wake -> no toggle");
|
||
}
|
||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0;
|
||
}
|
||
}
|
||
if (vol_ramp_dir != 0) {
|
||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
||
int going = vol_ramp_tick(held);
|
||
g_sys.vol = settings_volume_get();
|
||
paint_sb_right();
|
||
need_paint = 1;
|
||
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
|
||
}
|
||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
||
ncp_keys_seeded:
|
||
;
|
||
|
||
/* 触摸 */
|
||
if (tfd >= 0 && meow_screen_on) {
|
||
/* 面板是否就位(稳态开):就位才允许控件交互;动画中抑制 chip/滑条/通知行命中
|
||
* (面板在滑,控件坐标已定但只部分可见,命中会误触——见 §9.5)。 */
|
||
int open_settled = anim_ok ? (anim_target >= NCP_ANIM_FIX && anim_p >= NCP_ANIM_FIX) : 1;
|
||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||
if (poll(&pfd, 1, 0) > 0) {
|
||
while (read(tfd, evbuf16, sizeof evbuf16) == 16) {
|
||
uint16_t type = *(uint16_t *)(evbuf16 + 8), code = *(uint16_t *)(evbuf16 + 10);
|
||
int32_t val = *(int32_t *)(evbuf16 + 12);
|
||
if (type == EV_ABS) {
|
||
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
|
||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
|
||
if (td && !td_ok && tlx >= 0 && tly >= 0) {
|
||
tdx = tlx; tdy = tly; td_ok = 1;
|
||
scroll_y0 = tdy; /* 列表拖动基准:按下原点(进滚动时才锁 scroll_base) */
|
||
/* 按下原点落在滑条带 → 开始拖动(仅面板就位) */
|
||
if (open_settled && drag_slider == 0) {
|
||
if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
|
||
else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
|
||
}
|
||
}
|
||
if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
|
||
else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
|
||
/* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop,
|
||
* 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。
|
||
* 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含
|
||
* row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大:
|
||
* 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */
|
||
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
|
||
int ddx = tlx - tdx, ddy = tly - tdy;
|
||
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
|
||
if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) {
|
||
int eff = tdy - ncp_list_top() + list_scroll;
|
||
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
|
||
/* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */
|
||
if (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
|
||
row >= 0 && row < g_nrows) {
|
||
row_swipe_row = row;
|
||
row_off = mnp_swipe_clamp(ddx, NCP_ROW_W);
|
||
row_settle = 0; row_off_target = 0;
|
||
row_anim_at = now_ms();
|
||
g_ncp_swipe_row = row; g_ncp_swipe_off = row_off;
|
||
row_paint = 1;
|
||
}
|
||
}
|
||
}
|
||
/* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */
|
||
if (row_swipe_row >= 0 && td_ok && !row_settle) {
|
||
row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W);
|
||
g_ncp_swipe_off = row_off;
|
||
row_paint = 1;
|
||
}
|
||
/* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、
|
||
* 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y,
|
||
* 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */
|
||
if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) {
|
||
int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy);
|
||
if (ady2 >= S_SCROLL_SLOP) {
|
||
int av = g_nrows * NCP_ROW_H;
|
||
int vw = ncp_list_view();
|
||
int in_over = (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
|
||
mnp_scroll_max(av, vw) > 0);
|
||
if (!in_over) {
|
||
int cur_off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph);
|
||
drag_seed_reveal = (g_ncp_ph > 0) ? (g_ncp_ph + cur_off) : 0;
|
||
drag_y0 = tly;
|
||
panel_drag = 1;
|
||
}
|
||
}
|
||
}
|
||
/* ⑨ 跟手:panel_drag 时 reveal=seed+位移 → progress 更新 anim_p;拖动期时间轴暂停。 */
|
||
if (panel_drag && td_ok) {
|
||
int reveal = mnp_drag_reveal(drag_seed_reveal, tly - drag_y0, g_ncp_ph);
|
||
int np = mnp_drag_progress(reveal, NCP_ANIM_FIX, g_ncp_ph);
|
||
if (np != anim_p) { anim_p = np; drag_paint = 1; }
|
||
}
|
||
/* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 +
|
||
* 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式);
|
||
* 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */
|
||
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
|
||
int ddy = tly - scroll_y0;
|
||
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
|
||
int total = g_nrows * NCP_ROW_H;
|
||
int view = ncp_list_view();
|
||
if (mnp_scroll_max(total, view) > 0 &&
|
||
tdy >= ncp_list_top() && tdy < NCP_LIST_BOT) {
|
||
scroll_drag = 1;
|
||
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
|
||
scroll_base = mnp_scroll_base(list_scroll, ddy, scroll_invert);
|
||
scroll_rebound = 0;
|
||
}
|
||
}
|
||
}
|
||
if (scroll_drag && td_ok) {
|
||
int total = g_nrows * NCP_ROW_H;
|
||
int view = ncp_list_view();
|
||
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
|
||
list_scroll = mnp_scroll_clamp(target, total, view);
|
||
scroll_rebound = mnp_scroll_rebound(target, mnp_scroll_max(total, view),
|
||
NCP_SCROLL_REBOUND_TOL);
|
||
list_paint = 1;
|
||
}
|
||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||
if (val && !td) {
|
||
if (now_ms() < touch_cooldown_until) { tlx = tly = -1; }
|
||
else {
|
||
td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0;
|
||
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
|
||
scroll_invert = 0; scroll_rebound = 0;
|
||
/* ⑦ 新触摸:清行左滑(防上一手势残留) */
|
||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||
/* ⑨ 新触摸:清面板跟手(防上一拖动残留) */
|
||
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
|
||
}
|
||
} else if (!val && td) {
|
||
td = 0;
|
||
if (drag_slider) { /* 滑条拖动结束 → 落盘 */
|
||
ncp_slider_apply(drag_slider, tlx >= 0 ? tlx : tdx, 1);
|
||
drag_slider = 0; need_paint = 1;
|
||
} else if (td_ok) {
|
||
int dx = tlx - tdx, dy = tly - tdy;
|
||
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||
/* 左缘左滑 = 关闭(复用全局左缘返回语义;动画中同样可打断)——
|
||
* 优先于列表滚动落定,保「左缘返回关」语义不受滚动影响 */
|
||
if (meow_edge_back(tdx, dx, dy)) {
|
||
if (anim_ok) anim_target = 0; else closed = 1;
|
||
break;
|
||
}
|
||
if (row_swipe_row >= 0) {
|
||
/* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。
|
||
* 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */
|
||
/* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */
|
||
row_off = mnp_swipe_clamp(dx, NCP_ROW_W);
|
||
g_ncp_swipe_off = row_off;
|
||
if (mnp_swipe_delete(row_off, NCP_ROW_W,
|
||
NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) {
|
||
row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */
|
||
} else {
|
||
row_off_target = 0; /* 回弹复原 */
|
||
}
|
||
row_settle = 1; row_anim_at = now_ms();
|
||
} else if (scroll_drag) {
|
||
/* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值);
|
||
* 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。
|
||
* 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过
|
||
* 下方 mnp_vertical_up 关闭判定。 */
|
||
int total = g_nrows * NCP_ROW_H;
|
||
int view = ncp_list_view();
|
||
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
|
||
list_scroll = mnp_scroll_clamp(target, total, view);
|
||
if (scroll_rebound) {
|
||
if (anim_ok) anim_target = 0; else closed = 1;
|
||
break;
|
||
}
|
||
list_paint = 1;
|
||
scroll_base = 0; scroll_y0 = 0; scroll_rebound = 0; scroll_invert = 0;
|
||
} else if (panel_drag) {
|
||
/* ⑨ 面板跟手拖动松手:按阈值锚定 target(≥40% → 补到展开;
|
||
* <40% 或反向拖回 → 回 0 收回 = 半程取消/关闭过阈值)。时间轴接管
|
||
* 把 anim_p 补到位;不误关面板、不走 tap/上滑/点空白(本手势锁定)。 */
|
||
int go_open = mnp_drag_anchor(anim_p, NCP_ANIM_FIX, NCP_DRAG_THR_PCT);
|
||
if (anim_ok) anim_target = go_open ? NCP_ANIM_FIX : 0;
|
||
else if (!go_open) closed = 1; /* 无快照降级:收回 → 直接退出 */
|
||
panel_drag = 0; drag_paint = 0; drag_seed_reveal = 0; drag_y0 = 0;
|
||
} else if (adx < 60 && ady < 60) { /* tap */
|
||
if (animating) break; /* 动画进行中:tap 忽略(控件命中会误触,见 §9.5) */
|
||
int tx = tdx, ty = tdy;
|
||
int hit = 0;
|
||
for (int c = 0; c < NCP_CHIP_COUNT; c++)
|
||
if (ncp_chip_hit(c, tx, ty)) {
|
||
int ar = ncp_chip_apply(c);
|
||
if (ar == 1) need_sb = 1; /* 主题变化 → 重刷状态栏 */
|
||
else if (ar == 2) {
|
||
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
|
||
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
|
||
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
|
||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
memset(&kd_vold, 0, sizeof kd_vold);
|
||
memset(&kd_volu, 0, sizeof kd_volu);
|
||
memset(&kd_play, 0, sizeof kd_play);
|
||
kp_poll(&kp_power, NULL, NULL);
|
||
kp_poll(&kp_keys, NULL, NULL);
|
||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
|
||
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
|
||
if (tfd >= 0) {
|
||
unsigned char ev2[16];
|
||
while (read(tfd, ev2, sizeof ev2) == 16) {}
|
||
}
|
||
}
|
||
hit = 1; need_paint = 1; break;
|
||
}
|
||
if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { hit = 1; if (ncp_slider_apply(1, tx, 1)) need_paint = 1; }
|
||
else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { hit = 1; if (ncp_slider_apply(2, tx, 1)) need_paint = 1; }
|
||
/* ⑥ 勿扰面板内开关(列表上方小 toggle):tap 即切换(整带命中) */
|
||
if (!hit && ncp_dnd_hit(tx, ty)) {
|
||
int dv = 0; meow_settings_get_bool("notify.dnd", &dv);
|
||
meow_settings_set_bool("notify.dnd", dv ? 0 : 1);
|
||
meow_settings_save();
|
||
g_ncp_dnd = dv ? 0 : 1;
|
||
ncp_refresh_rows(); /* 按 dnd 过滤重采集 */
|
||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||
ncp_list_view());
|
||
hit = 1; need_paint = 1;
|
||
}
|
||
/* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 ongoing/media/progress)。 */
|
||
if (!hit && ncp_clear_hit(tx, ty)) {
|
||
int n = ncp_clear_all();
|
||
logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n);
|
||
/* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */
|
||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||
ncp_refresh_rows();
|
||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||
ncp_list_view());
|
||
hit = 1; need_paint = 1;
|
||
}
|
||
/* 阶段D附录B:顶部媒体卡片按钮/进度(含 seek)→ 下发 PLAYCTL。
|
||
* 卡片命中固定于列表上方,先于列表行命中处理。 */
|
||
if (!hit && meow_media_has_source() && ncp_media_card_tap(tx, ty)) {
|
||
hit = 1; need_paint = 1;
|
||
} else if (!hit && ty >= ncp_list_top() && ty < NCP_LIST_BOT) {
|
||
if (ncp_row_tap(tx, ty, list_scroll, click)) {
|
||
hit = 1;
|
||
if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */
|
||
else closed = 1;
|
||
break;
|
||
}
|
||
}
|
||
if (!hit) { /* 点空白(标题/把手/提示/分隔线/列表外)→ 关闭 */
|
||
if (anim_ok) anim_target = 0; else closed = 1;
|
||
break;
|
||
}
|
||
} else if (mnp_vertical_up(dx, dy, MENU_SWIPE_THRESH)) {
|
||
if (anim_ok) anim_target = 0; else closed = 1; /* 面板内上滑 → 关闭 */
|
||
break;
|
||
} else if (anim_ok && anim_target == 0 &&
|
||
mnp_top_swipe_down(tdx, tdy, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
|
||
anim_target = NCP_ANIM_FIX; /* 收回途中顶部热区下滑 → 重新打开(平滑转向) */
|
||
}
|
||
/* 其余横滑/下滑忽略(不关闭不动作) */
|
||
}
|
||
td_ok = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (closed) break; /* 非动画兜底:上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */
|
||
|
||
/* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */
|
||
if (now - last_listchk >= 500) {
|
||
last_listchk = now;
|
||
/* 阶段D附录B:面板驻留时排空 IPC(App 上报 META 更新媒体会话)+ 媒体会话 TTL,再做展示刷新 */
|
||
meow_ipc_process();
|
||
meow_notif_tick();
|
||
meow_media_tick();
|
||
int m_on = meow_media_has_source() ? 1 : 0;
|
||
if (m_on != media_prev) {
|
||
media_prev = m_on; /* 卡片出现/消失 → 列表顶缘变化,需整面板重排 */
|
||
need_paint = 1;
|
||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, ncp_list_view());
|
||
} else if (m_on) {
|
||
media_paint = 1; /* 已有源:进度/封面随 META 变化 → 仅重绘卡片带 */
|
||
}
|
||
if (ncp_rows_changed()) {
|
||
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */
|
||
row_swipe_row = -1; row_off = 0; row_settle = 0;
|
||
row_off_target = 0; row_anim_at = 0; row_paint = 0;
|
||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
|
||
ncp_refresh_rows();
|
||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||
ncp_list_view());
|
||
need_paint = 1;
|
||
}
|
||
}
|
||
if (media_paint) { ncp_repaint_media_card(); media_paint = 0; }
|
||
|
||
/* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。
|
||
* 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */
|
||
if (anim_ok && anim_p != anim_target && !panel_drag) {
|
||
unsigned long long anow = now_ms();
|
||
int dt;
|
||
if (anim_target != anim_last_target) {
|
||
/* 意图刚切换(打开↔关闭):重置计时起点,本 tick 不计时,
|
||
* 避免稳态闲置后 anim_last 陈旧导致首帧就跳到目标(不产生动画)。 */
|
||
anim_last_target = anim_target;
|
||
anim_last = anow;
|
||
dt = 0;
|
||
} else {
|
||
dt = (int)((anow > anim_last) ? (anow - anim_last) : 0);
|
||
anim_last = anow;
|
||
}
|
||
int np = mnp_anim_step(anim_p, anim_target, NCP_ANIM_FIX,
|
||
(unsigned long long)dt, NCP_ANIM_MS);
|
||
if (np != anim_p) {
|
||
anim_p = np;
|
||
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); /* §9 快照 blit 平移(只刷面板矩形) */
|
||
}
|
||
}
|
||
if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */
|
||
|
||
/* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。
|
||
* 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。
|
||
* 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */
|
||
if (row_swipe_row >= 0 && row_settle) {
|
||
unsigned long long rnow = now_ms();
|
||
int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0);
|
||
row_anim_at = rnow;
|
||
int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE);
|
||
if (noff != row_off) {
|
||
row_off = noff;
|
||
g_ncp_swipe_off = noff;
|
||
row_paint = 1;
|
||
}
|
||
if (noff == row_off_target) {
|
||
if (row_off_target == 0) {
|
||
/* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */
|
||
ncp_repaint_row(list_scroll, row_swipe_row, 0);
|
||
row_swipe_row = -1; row_settle = 0; row_off = 0;
|
||
row_off_target = 0; row_paint = 0;
|
||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||
if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */
|
||
} else {
|
||
/* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */
|
||
if (row_swipe_row >= 0 && row_swipe_row < g_nrows)
|
||
meow_notif_dismiss(g_rows[row_swipe_row].id);
|
||
row_swipe_row = -1; row_settle = 0; row_off = 0;
|
||
row_off_target = 0; row_paint = 0;
|
||
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
|
||
ncp_refresh_rows();
|
||
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
|
||
ncp_list_view());
|
||
need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。
|
||
* need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍
|
||
* 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。
|
||
* 动画进行中:不画稳态面板(会覆盖正在滑动的帧),need_paint 保持到到位后再补画。 */
|
||
if (need_paint) {
|
||
last_act = now;
|
||
if (!(anim_ok && anim_p != anim_target)) {
|
||
draw_notif_center(list_scroll, need_sb);
|
||
need_sb = 0;
|
||
if (anim_ok) ncp_snap_panel(); /* 面板内容已变:刷新快照(供滑出动画用) */
|
||
if (after_modal) { meow_dirty_full(); after_modal = 0; }
|
||
else meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP);
|
||
meow_ui_flush();
|
||
need_paint = 0;
|
||
list_paint = 0; /* 整面板已含列表:滚动重绘标记随之清除 */
|
||
}
|
||
/* 否则动画进行中:保留 need_paint,到位后(下一稳态 tick)补画最终面板 */
|
||
}
|
||
/* 仅列表区重绘(滚动专用:只刷列表带,不整面板/不刷状态栏;同受动画门控,
|
||
* 动画进行中不画稳态,list_paint 保持到到位后再补画一次列表终值)。 */
|
||
if (list_paint) {
|
||
last_act = now;
|
||
if (!(anim_ok && anim_p != anim_target)) {
|
||
ncp_repaint_list(list_scroll);
|
||
if (anim_ok) ncp_snap_panel(); /* 列表已变:刷新面板快照(供滑出动画用) */
|
||
list_paint = 0;
|
||
}
|
||
}
|
||
/* ⑨ 面板跟手拖动重绘:只刷面板矩形 + 新露出条(§9 快照 blit 平移;不整屏/不重排控件)。
|
||
* 拖动期 anim_p 直接由手指映射,故不受下方「animating」门控(非时间轴推进),直接呈现。 */
|
||
if (drag_paint) {
|
||
last_act = now;
|
||
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok);
|
||
drag_paint = 0;
|
||
}
|
||
/* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。
|
||
* row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */
|
||
if (row_paint) {
|
||
last_act = now;
|
||
if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target))
|
||
ncp_repaint_row(list_scroll, row_swipe_row, row_off);
|
||
row_paint = 0;
|
||
}
|
||
/* ~1s 状态栏时钟 */
|
||
if (now - last_tick >= 1000) {
|
||
last_tick = now;
|
||
draw_clock();
|
||
fb_norm_buf0();
|
||
meow_ui_flush();
|
||
}
|
||
usleep(POLL_MS * 1000);
|
||
}
|
||
if (tfd >= 0) close(tfd);
|
||
kp_close(&kp_power); kp_close(&kp_keys);
|
||
ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */
|
||
ncp_rows_free();
|
||
logmsg("notification center closed");
|
||
}
|
||
|