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'))
+9 -1
View File
@@ -406,4 +406,12 @@ 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."
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 ""
+9 -1
View File
@@ -406,4 +406,12 @@ msgstr "请先选择要删除的项"
#: src/controller/callbacks_basic.c
msgid "This path already exists and will not be added again."
msgstr "该路径已存在,不会重复添加。"
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")));
}
+40 -21
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);
}
}
@@ -60,7 +79,7 @@ void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list)
if (!list_ui || !str_list) return;
IupSetAttribute(list_ui, "REMOVEITEM", "ALL");
for (int i = 0; i < str_list->count; i++)
{
const char *item = string_list_get(str_list, i);
@@ -78,6 +97,6 @@ void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list)
}
}
IupSetInt(list_ui, "COUNT", str_list->count);
refresh_single_list_style(list_ui);
}
}