Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/notif_center.c
T

1774 lines
98 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.
/* 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 画该行位移)——M7.3 已迁 SDK
* meow_listview 左滑删除(SDK 内部持有 swipe_idx/swipe_dx,不再需要本组全局)。
* 保留在 #if 0 回退区内(随旧手绘内层一起重出)。 */
#if 0
static int g_ncp_swipe_row = -1;
static int g_ncp_swipe_off = 0;
#endif
/* ---- 阶段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 */
/* M7.0b:媒体卡片三控制按钮改用 SDK meow_iconbtn + sb_icons 矢量图标(与 Now Playing 同源),
* 取代原先手绘三角/竖条(ncp_media_tri/ncp_media_btn,用户反馈手绘有 bug)。up: 圆钮尺寸由
* meow_iconbtn 的 size 决定(ink=0.6*size),此处 prev/next 用 NCP_MEDIA_BTN_R-4、play 用
* NCP_MEDIA_BTN_R(与旧几何一致,命中仍按三圆心——ncp_media_card_tap 不变)。控件 lazy 建一次,
* owner=哨兵 0x52(不与电源菜单 0x50 / Now Playing 0x51 冲突);绘制经 meow_widget_draw。 */
#define NCP_MEDIA_ACT 0x52
#define NCP_MEDIA_PREV_R (NCP_MEDIA_BTN_R - 4) /* 18 */
#define NCP_MEDIA_NEXT_R (NCP_MEDIA_BTN_R - 4) /* 18 */
static meow_widget *g_ncp_btn_prev = NULL, *g_ncp_btn_play = NULL;
static meow_widget *g_ncp_btn_pause = NULL, *g_ncp_btn_next = NULL;
static int g_ncp_media_btn_init = 0;
/* 创建三控制按钮(owner 钉哨兵;play/pause 同位叠两钮,按状态只画其一——同 now_playing)。 */
static void ncp_media_btns_create(void)
{
meow_ui_activity_push(NCP_MEDIA_ACT);
int by = NCP_LIST_TOP + NCP_MEDIA_BTN_Y; /* 三钮圆心 y(与 ncp_draw_media_card/ncp_media_card_tap 同源) */
g_ncp_btn_prev = meow_iconbtn_create(NCP_MEDIA_PREV_CX - NCP_MEDIA_PREV_R, by - NCP_MEDIA_PREV_R,
NCP_MEDIA_PREV_R * 2, MEOW_ICON_SKIP_PREV);
g_ncp_btn_play = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PLAY_ARROW);
g_ncp_btn_pause = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PAUSE);
g_ncp_btn_next = meow_iconbtn_create(NCP_MEDIA_NEXT_CX - NCP_MEDIA_NEXT_R, by - NCP_MEDIA_NEXT_R,
NCP_MEDIA_NEXT_R * 2, MEOW_ICON_SKIP_NEXT);
/* 主播放钮保持「强调动作」观感(与 Now Playing 一致):底色=primary、图标=onPrimary;其余默认。 */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
meow_ui_activity_pop();
g_ncp_media_btn_init = 1;
}
/* 绘制三控制按钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时新圆底盖掉旧)。
* 每次刷新播放钮底色/图标(同 now_playing np_refresh_theme:跨主题切换跟手)。 */
static void ncp_draw_media_btns(const meow_media_session_t *s)
{
if (!g_ncp_media_btn_init) ncp_media_btns_create();
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
if (g_ncp_btn_prev) { meow_widget_set_selected(g_ncp_btn_prev, 0); meow_widget_draw(g_ncp_btn_prev); }
if (g_ncp_btn_next) { meow_widget_set_selected(g_ncp_btn_next, 0); meow_widget_draw(g_ncp_btn_next); }
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
meow_widget *pb = playing ? g_ncp_btn_pause : g_ncp_btn_play;
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
}
/* 短串拷贝(同 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 ? meow_str_zh("ncp.theme_light", "浅色")
: meow_str_zh("ncp.theme_dark", "深色"));
} 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, "notif_center bright_chip");
g_sys.brightness = 100;
} else {
meow_settings_set_int("display.brightness", vmin);
write_brightness(vmin, "notif_center bright_chip");
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;
}
/* ==================================================================== */
/* M7.3:通知中心内层迁 SDK 控件(芯片/双滑条/勿扰开关/通知列表/左滑删除) */
/* --------------------------------------------------------------------
* 定位:只迁「内层绘制/命中」。叠加会话骨架(SIGSTOP / 输入移交 / 恢复帧 / 顶条热区——见
* app_launcher.c run_app_notif_overlay)绝对不动,此处仅把 run_notif_center / draw_notif_center
* 中面板体的绘制与行命中交给 SDK meow_widget。SDK meow_ui.a 已定稿(c3d6371b),勿重编直接用。
*
* owner 钉哨兵 NCP_WIDGET_ACT(不冲突:0x50 电源菜单 / 0x51 锁屏·NowPlaying / 0x52 子页·媒体卡 /
* 0x53 设置页)。绘制经 meow_widget_draw / meow_listview_vs_draw;命中:通知列表走
* meow_listview_hit / meow_listview_swipe_begin·move·end(SDK 手势判定+删除/回弹动画),
* 芯片/滑条/勿扰开关沿用既有人工几何命中(与媒体卡 M7.0b 一致——叠加会话为手绘循环,不跑 SDK
* 输入线程,故不依赖 meow_widget_hit 的 owner==active 闸门)。 */
#define NCP_WIDGET_ACT 0x54
static meow_widget *g_ncp_lv = NULL; /* 通知列表:listview 虚拟化 + 行自绘 + 空态 + 左滑删除 */
static meow_widget *g_ncp_sl_bri = NULL; /* 亮度滑条 */
static meow_widget *g_ncp_sl_vol = NULL; /* 音量滑条 */
static meow_widget *g_ncp_sw_dnd = NULL; /* 勿扰开关 */
static meow_widget *g_ncp_chip[NCP_CHIP_COUNT]; /* 5 个快捷 chip(自定义画布:圆角 + 图标 + 标签) */
static int g_ncp_widget_init = 0;
static int g_ncp_need_paint = 0; /* M7.3:行左滑删除回调置位 → 主循环整面板重画 */
/* ---- 通知列表虚拟化源:count=当前行数(动态,逐帧实时查);bind 仅填 enabled 供
* meow_listview_hit 判定(行内容经 set_item_draw 自绘直读 g_rows[],无池缓存过期问题)。 ---- */
static int ncp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
static void ncp_lv_bind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
it->enabled = (idx >= 0 && idx < g_nrows) ? 1 : 0;
}
/* 文本截断(SDK meow_ui_text_width/meow_ui_text;UTF-8 安全,与旧 ncp_text 同构)。 */
static void ncp_mt_text(int x, int y, const char *s, int size, uint32_t col, int maxw)
{
if (!s) s = "";
if (meow_ui_text_width(s, size) <= maxw) { meow_ui_text(x, y, s, size, col); return; }
int ell = meow_ui_text_width("...", size);
int limit = maxw - ell;
char buf[128]; int len = 0;
while (s[len] && len < (int)sizeof buf - 8) {
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 (meow_ui_text_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; }
meow_ui_text(x, y, buf, size, col);
} else {
meow_ui_text(x, y, "...", size, col);
}
}
/* 通知行自绘(set_item_draw 覆盖默认 renderer)。x 已含左滑位移(SDK 先画右露删除区并把行 x 左移),
* 逐元素对齐旧 ncp_draw_row;颜色全取 meow_theme 语义色(M7 定案 #2 单一权威)。 */
static void ncp_lv_row_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)w; (void)state; (void)ctx; /* w=SDK 视口宽;行卡宽用 NCP_ROW_W(含 NCP_ROW_X 两侧留白) */
if (idx < 0 || idx >= g_nrows) return;
const ncp_row_t *r = &g_rows[idx];
const meow_theme *th = meow_theme_get();
meow_gfx_fill_round(x + NCP_ROW_X, y, NCP_ROW_W, h - 6, 12, th->surfaceContainer);
/* 图标 */
int icx = x + NCP_ICON_X, icy = y + (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 = meow_ui_text_width(cbuf, th->font_body);
meow_ui_text(icx + 16 - cw / 2, icy + 16 - meow_ui_text_height(th->font_body) / 2,
cbuf, th->font_body, th->onPrimary);
}
/* app 名 + 时间 */
meow_ui_text(x + NCP_TEXT_X, y + 10, ncp_app_name(r->app_id), th->font_body, th->onSurfaceVariant);
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
meow_ui_text(x + NCP_ROW_X + NCP_ROW_W - 8 - meow_ui_text_width(tbuf, th->font_body),
y + 12, tbuf, th->font_body, th->onSurfaceVariant);
/* 标题 + 内容(SDK 截断) */
ncp_mt_text(x + NCP_TEXT_X, y + 30, r->summary, th->font_body, th->onSurface, NCP_TEXT_W);
ncp_mt_text(x + NCP_TEXT_X, y + 56, r->body, th->font_body, th->onSurfaceVariant, NCP_TEXT_W);
/* 进度(progress>=0):细条 */
if (r->progress >= 0) {
int px = x + NCP_TEXT_X, pw = NCP_TEXT_W, py = y + h - 14;
meow_ui_fill_rect(px, py, pw, 4, th->surfaceVariant);
int fw = r->progress * pw / 100;
if (fw > 0) meow_ui_fill_rect(px, py, fw, 4, th->primary);
}
}
/* 左滑删除完成回调(SDK 删除/回弹动画结束后触发):撤下该通知 + 重采集 + 重排。 */
static void ncp_lv_on_delete(int idx, void *ctx)
{
(void)ctx;
logmsg("notif-center: swipe delete row %d", idx);
if (idx >= 0 && idx < g_nrows) meow_notif_dismiss(g_rows[idx].id);
ncp_refresh_rows();
/* 行数变化 → 列表重排:置位让主循环整面板重画(含媒体卡出现/消失重排 + 列表滚动重钳)。 */
g_ncp_need_paint = 1;
}
/* 芯片自绘(自定义画布 render_cb:TONAL 观感圆角 + 位图图标 + 标签,与旧 draw_notif_center chip 循环一致)。
* launcher 侧 meow_widget 不透明 → 用 meow_widget_rect 取绝对几何(同 power_menu pm_render_cb)。 */
static void ncp_chip_render(meow_widget *w, const meow_theme *th, void *ctx)
{
int c = (int)(intptr_t)ctx;
if (c < 0 || c >= NCP_CHIP_COUNT) return;
int rc[4]; meow_widget_rect(w, rc);
int x = rc[0], y = rc[1], wd = rc[2], ht = rc[3];
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
uint32_t bg = on ? th->primary : th->surfaceContainer;
uint32_t fg = on ? th->onPrimary : th->onSurfaceVariant;
meow_gfx_fill_round(x, y, wd, ht, 12, bg);
int icon_cx = x + (wd - SB_ICON_SIZE) / 2;
int icon_cy = 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) {
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); /* 顶部短柄 */
}
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
static const char *chip_id[NCP_CHIP_COUNT] = { "ncp.chip_wifi", "ncp.chip_bt",
"ncp.chip_theme", "ncp.chip_bright",
"ncp.chip_power" };
char label[24]; snprintf(label, sizeof label, "%s", meow_str_zh(chip_id[c], chip_lbl[c]));
if (val[0] && c == NCP_CHIP_BRIGHT) snprintf(label, sizeof label, "%.9s", val);
ui_draw(x + (wd - ui_width(label, 14)) / 2, y + ht - 18, label, fg, 14);
}
/* 芯片自定义画布命中(矩形;未被主循环使用——命中仍走 ncp_chip_hit 人工几何,此处仅满足 W_CUSTOM 契约)。 */
static int ncp_chip_widget_hit(meow_widget *w, int x, int y, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
return (x >= rc[0] && x < rc[0] + rc[2] && y >= rc[1] && y < rc[1] + rc[3]);
}
/* 创建 M7.3 内层控件集(owner 钉哨兵;仅首进创建一次,跨次复用——同媒体卡 M7.0b 惯用法)。 */
static void ncp_widgets_create(void)
{
meow_ui_activity_push(NCP_WIDGET_ACT);
/* 通知列表(视口 = 列表区;行高 NCP_ROW_H;空态 + 左滑删除) */
int lvy = ncp_list_top(), lvh = ncp_list_view();
g_ncp_lv = meow_listview_create(0, lvy, fb_xres, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
if (g_ncp_lv) {
meow_listview_set_vsource(g_ncp_lv, ncp_lv_count, ncp_lv_bind, NULL);
meow_listview_set_item_draw(g_ncp_lv, ncp_lv_row_draw, NULL);
meow_listview_set_item_h(g_ncp_lv, NCP_ROW_H);
meow_listview_set_empty_text(g_ncp_lv, meow_str_zh("ncp.empty", "暂无通知"));
meow_listview_set_swipe_delete(g_ncp_lv, 1, ncp_lv_on_delete, NULL);
}
/* 亮度/音量双滑条(track 中心对齐旧 s_draw_slider:NCP_SL*_Y+3) */
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);
g_ncp_sl_bri = meow_slider_create(NCP_SLIDER_X, NCP_SL1_Y - 14, NCP_SLIDER_W, bmin, 100, bval);
int vmax = vol_clamp(settings_volume_max);
int vval = vol_clamp(settings_volume_get());
g_ncp_sl_vol = meow_slider_create(NCP_SLIDER_X, NCP_SL2_Y - 14, NCP_SLIDER_W, 0, vmax, vval);
/* 勿扰开关 */
g_ncp_sw_dnd = meow_switch_create(0, 0, g_ncp_dnd ? 1 : 0);
if (g_ncp_sw_dnd) meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48,
NCP_DND_Y + NCP_DND_H / 2 - 14, 48, 28);
/* 5 个快捷 chip(自定义画布;坐标与旧 ncp_chip_x / NCP_CHIP_Y 一致) */
for (int c = 0; c < NCP_CHIP_COUNT; c++)
g_ncp_chip[c] = meow_widget_custom_create(ncp_chip_x(c), NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H,
ncp_chip_render, ncp_chip_widget_hit,
(void *)(intptr_t)c);
meow_ui_activity_pop();
g_ncp_widget_init = 1;
}
/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。
* off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */
#if 0 /* M7.3 回退位:旧手绘行删除底(SDK 左滑删除已接管此绘制,见 meow_listview swipe 渲染)。 */
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();
}
#endif /* M7.3 回退位结束(上述旧手绘 ncp_draw_delete_bg/ncp_draw_row/ncp_draw_list/
* ncp_repaint_list/ncp_repaint_row 重开本区即回退;新路径由下方 SDK 版接管)。 */
/* ---- M7.3:通知列表绘制(SDK listview 版,替换上方 #if 0 的手绘版)---- */
/* 绘制通知列表可见区(scroll=像素偏移)。同步 listview 视口矩形(媒体卡出现/消失时列表顶缘
* 变化)+ 滚动偏移,再画整个列表控件(虚拟化 + set_item_draw 行自绘 + 空态 + 左滑删除渲染
* 全由 SDK 完成)。 */
static void ncp_draw_list(int scroll)
{
if (!g_ncp_widget_init || !g_ncp_lv) return;
int lt = ncp_list_top(), lv = ncp_list_view();
meow_widget_set_rect(g_ncp_lv, 0, lt, fb_xres, lv);
meow_listview_set_scroll(g_ncp_lv, mnp_scroll_clamp(scroll, g_nrows * NCP_ROW_H, lv));
meow_widget_draw(g_ncp_lv);
}
/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底,再画 SDK listview;
* 只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT))——「滚动只重绘列表可视区,绝不整屏」。 */
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();
}
/* ⑥/⑧ 勿扰开关带命中(面板内小 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 + 状态 */
const char *dnd_lbl = meow_str_zh("ncp.dnd", "勿扰");
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, dnd_lbl, th->on_surface, 18);
char st[16];
snprintf(st, sizeof st, "%s", dnd_on ? meow_str_zh("common.on", "开") : meow_str_zh("common.off", "关"));
int lw = ui_width(dnd_lbl, 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 垂直居中)。
* M7.3:迁 SDK meow_switch(绘制经 meow_widget_draw;命中仍走 ncp_dnd_hit 整带)。 */
if (g_ncp_widget_init && g_ncp_sw_dnd) {
meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48, cy - 14, 48, 28);
meow_switch_set(g_ncp_sw_dnd, dnd_on ? 1 : 0);
meow_widget_draw(g_ncp_sw_dnd);
}
}
/* ⑧ 绘一键清除按钮(勿扰对称处的右半段):圆角按钮底 + 垃圾桶字形 + 「清除」文字。 */
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 = meow_str_zh("ncp.clear", "清除");
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);
}
/* 画顶部媒体卡片(封面小图 + 标题/歌手 + 播放/暂停/上下曲 + 进度条)。 */
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, meow_theme_get()->onPrimary, 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);
/* 控制按钮(SDK meow_iconbtn + sb_icons,与 Now Playing 一致;命中仍走 ncp_media_card_tap 三圆心) */
ncp_draw_media_btns(s);
/* 进度条 + 时间 */
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)/卡片体。返回 2=卡片体命中已发 FOCUS(7)(调用方收起面板露出
* 播放页);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) {
/* 卡片体命中(非按钮/进度):向活动媒体源下发 FOCUS(7)(=NCM_CTL_FOCUS,见 ncm
* ipc_media.c case 7 → g_req_focus_player=1 → UI 线程 push ACT_PLAYER),请求打开
* App 播放页,并消费该次点击——返回 2 让调用方收起面板露出播放页。 */
meow_media_playctl(7, 0);
logmsg("notif-card: media tap card body -> FOCUS(7) open player");
return 2;
}
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, meow_str_zh("ncp.title", "通知中心"), th->on_surface, 22);
ui_draw(W - 24 - ui_width(meow_str_zh("ncp.tap_close", "点空白关闭"), 16), NCP_TITLE_Y + 2,
meow_str_zh("ncp.tap_close", "点空白关闭"), th->on_surface_variant, 16);
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单)。M7.3:迁 SDK 自定义画布 chip
*(TONAL 观感圆角 + 位图图标 + 标签,见 ncp_chip_render;绘制经 meow_widget_draw)。 */
if (g_ncp_widget_init)
for (int c = 0; c < NCP_CHIP_N; c++)
if (g_ncp_chip[c]) meow_widget_draw(g_ncp_chip[c]);
/* 亮度滑条(M7.3:迁 SDK meow_slider;track 中心 = NCP_SL1_Y+3,与旧 s_draw_slider 对齐) */
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, meow_str_zh("ncp.brightness", "亮度"), 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);
if (g_ncp_widget_init && g_ncp_sl_bri) { meow_slider_set(g_ncp_sl_bri, bval); meow_widget_draw(g_ncp_sl_bri); }
/* 音量滑条 */
int vval = 0; meow_settings_get_int("audio.volume", &vval);
vval = vol_clamp(vval);
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, meow_str_zh("ncp.volume", "音量"), 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);
if (g_ncp_widget_init && g_ncp_sl_vol) { meow_slider_set(g_ncp_sl_vol, vval); meow_widget_draw(g_ncp_sl_vol); }
/* 分隔线 */
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(meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), 16)) / 2, NCP_FOOT_Y,
meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), 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, "notif_center bright_slider");
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; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */
/* ⑦ 单条通知行左滑删除(M7.3:迁 SDK meow_listview_set_swipe_delete + swipe_begin/move/end)。
* sdk_swipe = 本触摸已交给 SDK 行左滑手势(begin 命中可滑行);SDK 负责方向仲裁(横向=消费 /
* 纵向=让位滚动)、删除/回弹动画(meow_anim_tick 推进)与 on_delete 回调。 */
int sdk_swipe = 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;
}
/* M7.3:建内层 SDK 控件集(listview/双滑条/勿扰开关/芯片;owner 钉哨兵,仅首进一次)。 */
ncp_widgets_create();
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 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;
sdk_swipe = 0; /* 唤醒:清行左滑手势(SDK swipe 内部状态亦重置) */
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; }
}
/* M7.3:按下原点落在通知列表行 → 交 SDK 行左滑手势(begin 命中可滑行;
* 后续 move 由 SDK 做方向仲裁:横向=消费/纵向=让位滚动)。先同步 listview
* 滚动偏移(meow_listview_hit 用其内部 scroll)。 */
if (open_settled && !drag_slider && !panel_drag && g_ncp_widget_init && g_ncp_lv) {
meow_listview_set_scroll(g_ncp_lv, list_scroll);
sdk_swipe = meow_listview_swipe_begin(g_ncp_lv, tlx, tly);
}
}
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; }
/* ⑦ 行左滑(SDK swipe-delete):已交给 SDK 手势 → 喂 move。SDK 方向仲裁:
* 横向小位移超容差 → 消费(返回 1,行左移/删除区由 SDK 画,本触摸不滚/不拖面板);
* 纵向主导 → 取消本行 swipe(返回 0,sdk_swipe=0),回退到下方滚动/面板拖动。 */
if (sdk_swipe && td_ok && drag_slider == 0) {
meow_listview_set_scroll(g_ncp_lv, list_scroll);
if (!meow_listview_swipe_move(g_ncp_lv, tlx, tly)) sdk_swipe = 0;
}
/* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、
* 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y,
* 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */
if (td_ok && !drag_slider && !scroll_drag && !sdk_swipe && !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 && !sdk_swipe) {
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;
/* ⑦ 新触摸:清行左滑手势(防上一手势残留) */
sdk_swipe = 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 (sdk_swipe && g_ncp_widget_init && g_ncp_lv) {
/* ⑦ 行左滑松手(SDK swipe-delete):消费 → 删除/回弹动画交 SDK
* (meow_anim_tick 推进;删除完成后 ncp_lv_on_delete 置 g_ncp_need_paint)。
* 本手势不关面板、不走 tap/上滑/点空白。未滑动(=点击某行)→ 未消费,
* 清 sdk_swipe 后走下方 tap 分支(ncp_row_tap 命中该行 → dismiss+关面板)。 */
meow_listview_set_scroll(g_ncp_lv, list_scroll);
if (meow_listview_swipe_end(g_ncp_lv, tlx, tly)) {
sdk_swipe = 0;
td_ok = 0;
goto ncp_release_done;
}
sdk_swipe = 0;
} 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);
/* 清除后取消行左滑手势(下标会变),再重采集 + 重钳 + 整面板重画。 */
sdk_swipe = 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;
* 卡片体 → 下发 FOCUS(7) 请求打开 App 播放页 + 收起面板(返回 2)。
* 卡片命中固定于列表上方,先于列表行命中处理。 */
if (!hit && meow_media_has_source()) {
int mh = ncp_media_card_tap(tx, ty);
if (mh) {
hit = 1;
if (mh == 2) { /* 卡片体 FOCUS → 收起面板露出播放页 */
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
need_paint = 1; /* 按钮/进度:仅重绘卡片(面板保持开启) */
}
}
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;
ncp_release_done:
;
}
}
}
}
}
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):取消当前行左滑手势,避免行下标错位。⑨ 同时取消面板跟手。 */
sdk_swipe = 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 且无按下触摸 → 真正退出子循环 */
/* M7.3:行左滑落位动画已由 SDK 接管(meow_listview_swipe_end → meow_anim → swipe_anim_done
* → on_delete(idx) → ncp_lv_on_delete 置 g_ncp_need_paint)。此处不再需要手绘行位移/回弹/滑出。 */
/* 重绘(面板体区域;状态栏 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;
}
/* M7.3:行左滑跟手/回弹/滑出重绘已由 SDK 接管(swipe_anim_update 经 meow_widget_draw(lv)
* 重绘列表 + meow_anim_set_region 标该行带脏)。此处不再有 row_paint 手绘路径。 */
/* ~1s 状态栏时钟 */
if (now - last_tick >= 1000) {
last_tick = now;
draw_clock();
fb_norm_buf0();
meow_ui_flush();
}
/* M7.3:推进 SDK 动画(行左滑删除/回弹、meow_scroll fling 等)。左滑删除完成后 on_delete
* 置 g_ncp_need_paint → 下一帧整面板重画。swipe 动画推进时 SDK 已重绘列表(swipe_anim_update
* 调 meow_widget_draw(lv)),此处只把列表带标脏上屏(覆盖 SDK 经 meow_gfx 标脏的列表矩形)。 */
meow_anim_tick();
if (g_ncp_need_paint) { g_ncp_need_paint = 0; need_paint = 1; }
if (g_ncp_widget_init && g_ncp_lv && meow_listview_swipe_active(g_ncp_lv)) {
meow_dirty_add(0, ncp_list_top(), fb_xres, ncp_list_view());
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();
/* 封面缓存随面板关闭回收(对齐 Now Playing np_cleanup:每次下拉重新加载当前封面)。
* 旧码 g_ncp_media_cover_path 是文件级 static、从不重置——若首次加载失败/或 artwork 初
* 为空的瞬间进面板,其后即便封面落盘并因路径不变而被「按文本缓存」跳过重载,卡片永久
* 停留在「显示歌名」回退/旧封面。关面板即清缓存,保证下次下拉能拿到最新封面。 */
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
g_ncp_media_cover_path[0] = 0;
logmsg("notification center closed");
}