feat(ui): 添加深色模式支持

- 新增深色/浅色模式切换按钮,位于主窗口底部
- 在配置文件中定义主题颜色(浅色/深色背景、列表背景、前景色)
- 更新 UI 工具函数以支持动态主题切换,包括列表斑马纹适配
- 添加翻译条目(Dark Mode/Light Mode)并更新编译脚本
- 修改主窗口创建逻辑,集成主题切换回调
- 调整列表背景色属性从 BACKCOLOR 改为 BGCOLOR 以保持一致性
This commit is contained in:
2026-05-02 01:32:56 +08:00
parent 3df2988915
commit 720ebb535d
14 changed files with 233 additions and 27 deletions
+1
View File
@@ -17,6 +17,7 @@ int btn_ok_cb(Ihandle *self);
int btn_cancel_cb(Ihandle *self);
int btn_help_cb(Ihandle *self);
int btn_lang_cb(Ihandle *self);
int darkmode_cb(Ihandle *self);
// 撤销/重做回调
int btn_undo_cb(Ihandle *self);
+4
View File
@@ -17,4 +17,8 @@ void refresh_single_list_style(Ihandle *list);
// 会先清空列表然后重新添加所有项,最后刷新样式
void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list);
// 深色模式状态管理
void set_dark_mode(int enabled);
int get_dark_mode(void);
#endif // UI_UTILS_H
+1
View File
@@ -35,6 +35,7 @@
#define CTRL_BTN_CANCEL "BTN_CANCEL"
#define CTRL_BTN_HELP "BTN_HELP"
#define CTRL_BTN_LANG "BTN_LANG"
#define CTRL_BTN_DARKMODE "BTN_DARKMODE"
// 撤销/重做按钮
#define CTRL_BTN_UNDO "BTN_UNDO"
Binary file not shown.
Binary file not shown.
+14
View File
@@ -26,6 +26,18 @@ local config = {
backcolor = "255 255 255"
},
-- 主题颜色
theme = {
light_bg = "240 240 240",
light_list_bg = "255 255 255",
light_list_alt = "245 245 245",
light_fg = "0 0 0",
dark_bg = "30 30 30",
dark_list_bg = "40 40 40",
dark_list_alt = "50 50 50",
dark_fg = "220 220 220",
},
-- 按钮设置(使用英文原文,供 gettext 翻译)
button = {
rastersize = "100x32",
@@ -43,6 +55,8 @@ local config = {
help = "Help",
undo = "Undo",
redo = "Redo",
darkmode = "Dark Mode",
lightmode = "Light Mode",
},
-- 标签文本(使用英文原文,供 gettext 翻译)
+67
View File
@@ -0,0 +1,67 @@
import struct, re, os
def compile_po_to_mo(po_path, mo_path):
with open(po_path, 'r', encoding='utf-8') as f:
content = f.read()
entries = []
for match in re.finditer(
r'msgid "(.+?)"\s*\nmsgstr "(.*?)"',
content, re.DOTALL
):
msgid = match.group(1)
msgstr = match.group(2)
if msgid:
entries.append((msgid, msgstr))
if not entries:
print(f'No entries in {po_path}')
return
N = len(entries)
header_sz = 28
table_sz = N * 8 * 2
orig_off = header_sz + table_sz
orig_data = b''
orig_offsets = []
for eid, estr in entries:
orig_offsets.append(len(orig_data))
orig_data += eid.encode('utf-8') + b'\x00'
trans_off = orig_off + len(orig_data)
trans_data = b''
trans_offsets = []
for eid, estr in entries:
trans_offsets.append(len(trans_data))
trans_data += estr.encode('utf-8') + b'\x00'
buf = bytearray()
buf += struct.pack('<I', 0x950412de)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', N)
buf += struct.pack('<I', orig_off)
buf += struct.pack('<I', trans_off)
buf += struct.pack('<I', 0)
buf += struct.pack('<I', 0)
for i in range(N):
buf += struct.pack('<II', len(entries[i][0].encode('utf-8')), orig_offsets[i])
for i in range(N):
buf += struct.pack('<II', len(entries[i][1].encode('utf-8')), trans_offsets[i])
buf += orig_data
buf += trans_data
os.makedirs(os.path.dirname(mo_path), exist_ok=True)
with open(mo_path, 'wb') as f:
f.write(buf)
print(f'{po_path} -> {mo_path} ({N} strings)')
base = os.path.dirname(os.path.abspath(__file__))
root = os.path.dirname(base)
compile_po_to_mo(os.path.join(root, 'po', 'zh_CN.po'), os.path.join(root, 'locale', 'zh_CN', 'LC_MESSAGES', 'zh_CN.mo'))
compile_po_to_mo(os.path.join(root, 'po', 'en_US.po'), os.path.join(root, 'locale', 'en_US', 'LC_MESSAGES', 'en_US.mo'))
compile_po_to_mo(os.path.join(root, 'po', 'zh_CN.po'), os.path.join(root, 'build', 'locale', 'zh_CN', 'LC_MESSAGES', 'zh_CN.mo'))
compile_po_to_mo(os.path.join(root, 'po', 'en_US.po'), os.path.join(root, 'build', 'locale', 'en_US', 'LC_MESSAGES', 'en_US.mo'))
+8
View File
@@ -407,3 +407,11 @@ msgstr "Please select an item to delete first"
#: src/controller/callbacks_basic.c
msgid "This path already exists and will not be added again."
msgstr "This path already exists and will not be added again."
#: src/ui/main_window.c
msgid "Dark Mode"
msgstr "Dark Mode"
#: src/ui/main_window.c
msgid "Light Mode"
msgstr "Light Mode"
+8
View File
@@ -124,3 +124,11 @@ msgstr ""
#: src/controller/callbacks_nav.c
msgid "Redo completed"
msgstr ""
#: src/ui/main_window.c
msgid "Dark Mode"
msgstr ""
#: src/ui/main_window.c
msgid "Light Mode"
msgstr ""
+8
View File
@@ -407,3 +407,11 @@ msgstr "请先选择要删除的项"
#: src/controller/callbacks_basic.c
msgid "This path already exists and will not be added again."
msgstr "该路径已存在,不会重复添加。"
#: src/ui/main_window.c
msgid "Dark Mode"
msgstr "深色模式"
#: src/ui/main_window.c
msgid "Light Mode"
msgstr "浅色模式"
+33
View File
@@ -226,3 +226,36 @@ int dlg_k_any_cb(Ihandle *self, int c)
}
return IUP_DEFAULT;
}
// 深色模式切换回调
int darkmode_cb(Ihandle *self)
{
Ihandle *dlg = IupGetDialog(self);
int dark = !get_dark_mode();
set_dark_mode(dark);
IupSetAttribute(dlg, "BGCOLOR", dark
? lua_config_get_string("theme", "dark_bg")
: lua_config_get_string("theme", "light_bg"));
IupSetAttribute(self, "TITLE", _(dark
? lua_config_get_string("button", "lightmode")
: lua_config_get_string("button", "darkmode")));
Ihandle *lists[] = {
IupGetDialogChild(dlg, CTRL_LIST_SYS),
IupGetDialogChild(dlg, CTRL_LIST_USER),
IupGetDialogChild(dlg, CTRL_LIST_MERGED),
NULL
};
for (int i = 0; lists[i]; i++)
{
const char *list_bg = dark
? lua_config_get_string("theme", "dark_list_bg")
: lua_config_get_string("theme", "light_list_bg");
IupSetAttribute(lists[i], "BGCOLOR", list_bg);
refresh_single_list_style(lists[i]);
}
return IUP_DEFAULT;
}
+29
View File
@@ -63,6 +63,14 @@ static const char *get_string_default(const char *section, const char *key)
return "取消";
if (strcmp(key, "help") == 0)
return "帮助(?)";
if (strcmp(key, "undo") == 0)
return "撤销";
if (strcmp(key, "redo") == 0)
return "重做";
if (strcmp(key, "darkmode") == 0)
return "深色模式";
if (strcmp(key, "lightmode") == 0)
return "浅色模式";
}
else if (strcmp(section, "label") == 0)
{
@@ -74,11 +82,32 @@ static const char *get_string_default(const char *section, const char *key)
return "系统变量 (System)";
if (strcmp(key, "tab_user") == 0)
return "用户变量 (User)";
if (strcmp(key, "tab_merged") == 0)
return "合并预览";
if (strcmp(key, "export_title") == 0)
return "导出 PATH";
if (strcmp(key, "import_title") == 0)
return "导入 PATH";
}
else if (strcmp(section, "theme") == 0)
{
if (strcmp(key, "light_bg") == 0)
return "240 240 240";
if (strcmp(key, "light_list_bg") == 0)
return "255 255 255";
if (strcmp(key, "light_list_alt") == 0)
return "245 245 245";
if (strcmp(key, "light_fg") == 0)
return "0 0 0";
if (strcmp(key, "dark_bg") == 0)
return "30 30 30";
if (strcmp(key, "dark_list_bg") == 0)
return "40 40 40";
if (strcmp(key, "dark_list_alt") == 0)
return "50 50 50";
if (strcmp(key, "dark_fg") == 0)
return "220 220 220";
}
else if (strcmp(section, "layout") == 0)
{
if (strcmp(key, "vbox_gap") == 0)
+18 -4
View File
@@ -1,4 +1,5 @@
#include "ui/main_window.h"
#include "ui/ui_utils.h"
#include "controller/callbacks.h"
#include "controller/callbacks_internal.h"
#include "core/lua_config.h"
@@ -14,7 +15,7 @@ static Ihandle *create_path_list(const char *name)
IupSetAttribute(list, "NAME", name);
IupSetAttribute(list, "EXPAND", "YES");
IupSetAttribute(list, "ITEMPADDING", lua_config_get_string("list", "item_padding"));
IupSetAttribute(list, "BACKCOLOR", lua_config_get_string("list", "backcolor"));
IupSetAttribute(list, "BGCOLOR", lua_config_get_string("list", "backcolor"));
IupSetAttribute(list, "BORDER", "YES");
IupSetAttribute(list, "CANFOCUS", "YES");
IupSetAttribute(list, "HLINE", "NO");
@@ -47,10 +48,10 @@ Ihandle *create_main_window(void)
IupSetAttribute(list_merged, "NAME", CTRL_LIST_MERGED);
IupSetAttribute(list_merged, "EXPAND", "YES");
IupSetAttribute(list_merged, "ITEMPADDING", lua_config_get_string("list", "item_padding"));
IupSetAttribute(list_merged, "BACKCOLOR", lua_config_get_string("list", "backcolor"));
IupSetAttribute(list_merged, "BGCOLOR", lua_config_get_string("list", "backcolor"));
IupSetAttribute(list_merged, "BORDER", "YES");
IupSetAttribute(list_merged, "HLINE", "NO");
IupSetAttribute(list_merged, "ACTIVE", "NO"); // 只读
// 不设置 ACTIVE=NO,否则会禁用滚动;合并列表无编辑回调,已是只读
// 创建搜索框
Ihandle *txt_search = IupText(NULL);
@@ -113,6 +114,11 @@ Ihandle *create_main_window(void)
IupSetAttribute(btn_lang, "NAME", CTRL_BTN_LANG);
IupSetCallback(btn_lang, "ACTION", (Icallback)btn_lang_cb);
// 创建深色模式切换按钮
Ihandle *btn_darkmode = IupButton(_(lua_config_get_string("button", "darkmode")), NULL);
IupSetAttribute(btn_darkmode, "NAME", CTRL_BTN_DARKMODE);
IupSetCallback(btn_darkmode, "ACTION", (Icallback)darkmode_cb);
// 设置按钮回调
IupSetCallback(btn_new, "ACTION", (Icallback)btn_new_cb);
IupSetCallback(btn_edit, "ACTION", (Icallback)btn_edit_cb);
@@ -140,6 +146,7 @@ Ihandle *create_main_window(void)
IupSetAttribute(btn_undo, "RASTERSIZE", btn_size);
IupSetAttribute(btn_redo, "RASTERSIZE", btn_size);
IupSetAttribute(btn_lang, "RASTERSIZE", btn_size);
IupSetAttribute(btn_darkmode, "RASTERSIZE", btn_size);
// 创建操作按钮垂直布局
Ihandle *vbox_btns = IupVbox(
@@ -183,7 +190,7 @@ Ihandle *create_main_window(void)
IupSetAttribute(btn_help, "RASTERSIZE", btn_size);
// 创建底部按钮水平布局
Ihandle *hbox_bottom = IupHbox(lbl_status, IupFill(), btn_help, btn_lang, btn_ok, btn_cancel, NULL);
Ihandle *hbox_bottom = IupHbox(lbl_status, IupFill(), btn_help, btn_darkmode, btn_lang, btn_ok, btn_cancel, NULL);
IupSetAttribute(hbox_bottom, "GAP", lua_config_get_string("layout", "hbox_gap"));
IupSetAttribute(hbox_bottom, "MARGIN", lua_config_get_string("layout", "hbox_margin"));
IupSetAttribute(hbox_bottom, "ALIGNMENT", lua_config_get_string("layout", "hbox_alignment"));
@@ -255,4 +262,11 @@ void refresh_main_window_ui(Ihandle *main_dlg)
Ihandle *btn_lang = IupGetDialogChild(main_dlg, CTRL_BTN_LANG);
if (btn_lang)
IupSetAttribute(btn_lang, "TITLE", _("Language"));
// 深色模式按钮文字根据当前模式更新
Ihandle *btn_darkmode = IupGetDialogChild(main_dlg, CTRL_BTN_DARKMODE);
if (btn_darkmode)
IupSetAttribute(btn_darkmode, "TITLE", get_dark_mode()
? _(lua_config_get_string("button", "lightmode"))
: _(lua_config_get_string("button", "darkmode")));
}
+37 -18
View File
@@ -1,10 +1,23 @@
#include "ui/ui_utils.h"
#include "utils/os_env.h"
#include "utils/string_ext.h"
#include "core/lua_config.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static int g_dark_mode = 0;
void set_dark_mode(int enabled)
{
g_dark_mode = enabled;
}
int get_dark_mode(void)
{
return g_dark_mode;
}
// 刷新列表样式(斑马纹 + 有效性检查)
void refresh_single_list_style(Ihandle *list)
{
@@ -12,30 +25,42 @@ void refresh_single_list_style(Ihandle *list)
return;
int count = IupGetInt(list, "COUNT");
// 读取主题颜色,带默认值保护
const char *alt_color = g_dark_mode
? lua_config_get_string("theme", "dark_list_alt")
: lua_config_get_string("theme", "light_list_alt");
const char *bg_color = g_dark_mode
? lua_config_get_string("theme", "dark_list_bg")
: lua_config_get_string("theme", "light_list_bg");
const char *fg_default = g_dark_mode
? lua_config_get_string("theme", "dark_fg")
: lua_config_get_string("theme", "light_fg");
// 防止 NULL 或空字符串,使用硬编码默认值
if (!alt_color || !*alt_color) alt_color = g_dark_mode ? "50 50 50" : "245 245 245";
if (!bg_color || !*bg_color) bg_color = g_dark_mode ? "40 40 40" : "255 255 255";
if (!fg_default || !*fg_default) fg_default = g_dark_mode ? "220 220 220" : "0 0 0";
for (int i = 1; i <= count; i++)
{
char *item = IupGetAttributeId(list, "", i);
if (!item)
continue;
// 默认颜色:黑字
char fg_color[32] = "0 0 0";
char fg_color[32];
sprintf(fg_color, "%s", fg_default);
// 1. 检查有效性
if (!is_path_valid(item))
{
// 无效路径:红色
sprintf(fg_color, "255 0 0");
}
sprintf(fg_color, "255 0 0"); // 无效路径:红色
// 2. 检查重复 (只检查当前项之前的项,如果之前出现过,当前项标橙)
// 2. 检查重复
for (int j = 1; j < i; j++)
{
char *prev_item = IupGetAttributeId(list, "", j);
if (prev_item && _stricmp(item, prev_item) == 0) // Windows 路径不区分大小写
if (prev_item && _stricmp(item, prev_item) == 0)
{
// 重复路径:橙色
sprintf(fg_color, "255 128 0");
sprintf(fg_color, "255 128 0"); // 重复路径:橙色
break;
}
}
@@ -43,14 +68,8 @@ void refresh_single_list_style(Ihandle *list)
IupSetAttributeId(list, "ITEMFGCOLOR", i, fg_color);
// 斑马纹背景
if (i % 2 == 0)
{
IupSetAttributeId(list, "ITEMBGCOLOR", i, "245 245 245");
}
else
{
IupSetAttributeId(list, "ITEMBGCOLOR", i, "255 255 255");
}
IupSetAttributeId(list, "ITEMBGCOLOR", i,
(i % 2 == 0) ? alt_color : bg_color);
}
}