feat: CSV 导入导出、导入撤销支持及多项 bug 修复

功能:
- 新增 CSV 格式导入导出支持(含 BOM 处理、引号转义、智能标题行检测)
- 导入操作支持撤销/重做
- 保存时 PATH 长度检查与警告
- 深色模式状态持久化(darkmode.txt)
- 提取 get_current_target/push_record 为共享函数,消除控制器层重复代码
- 新增 string_list_insert_at,修复撤销删除时的索引恢复
- 新增 undo_redo、error_code、import_export 单元测试

Bug 修复:
- 修复备份目录对话框和失败原因的硬编码中文字符串
- 提取 get_exe_dir 到 os_env 消除 i18n.c/ui_utils.c 重复定义
- 修复导入撤销 old_sys/old_user 内存管理(push 后置 NULL 防止重复释放)
- 修复 CSV 导出转义与导入解析不一致(移除反斜杠转义,依赖 CSV 引号机制)
- 修正 PATH 长度 8191 限制描述为 "command line safe limit"
This commit is contained in:
2026-05-03 01:52:06 +08:00
parent 720ebb535d
commit cdcfd8e0a7
27 changed files with 1823 additions and 110 deletions
+32
View File
@@ -5,6 +5,7 @@
#include "utils/ui_constants.h"
#include "ui/ui_utils.h"
#include <iup.h>
#include <string.h>
// 辅助函数:获取主对话框
Ihandle *get_main_dlg(void)
@@ -84,3 +85,34 @@ void sync_merged_list(Ihandle *dlg)
IupSetInt(list_merged, "COUNT", total);
refresh_single_list_style(list_merged);
}
// 获取当前 Tab 对应的 TargetType
TargetType get_current_target(Ihandle *dlg)
{
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
if (tabs)
{
int pos = IupGetInt(tabs, "VALUEPOS");
return (pos == 0) ? TARGET_SYSTEM : TARGET_USER;
}
return TARGET_USER;
}
// 辅助函数:创建并推送撤销记录
void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
char **old_paths, char **new_paths)
{
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return;
OpRecord record;
record.type = op_type;
record.target = get_current_target(dlg);
record.index = index;
record.count = count;
record.old_paths = old_paths;
record.new_paths = new_paths;
push_undo_record(ctx->undo_redo_mgr, &record);
}
-31
View File
@@ -14,37 +14,6 @@
#include <stdio.h>
#include <stdlib.h>
// 辅助函数:检查当前目标是系统还是用户
static TargetType get_current_target(Ihandle *dlg)
{
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
if (tabs)
{
int tab = IupGetInt(tabs, "VALUE");
return (tab == 1) ? TARGET_SYSTEM : TARGET_USER;
}
return TARGET_USER;
}
// 辅助函数:创建并推送撤销记录
static void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
char **old_paths, char **new_paths)
{
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return;
OpRecord record;
record.type = op_type;
record.target = get_current_target(dlg);
record.index = index;
record.count = count;
record.old_paths = old_paths;
record.new_paths = new_paths;
push_undo_record(ctx->undo_redo_mgr, &record);
}
// 按钮回调:新建
int btn_new_cb(Ihandle *self)
{
+88 -1
View File
@@ -3,6 +3,7 @@
#include "core/app_context.h"
#include "core/import_export.h"
#include "core/lua_config.h"
#include "core/undo_redo.h"
#include "utils/string_ext.h"
#include "utils/safe_string.h"
#include "utils/error_code.h"
@@ -12,6 +13,7 @@
#include "ui/ui_utils.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
// 按钮回调:导入
@@ -32,7 +34,7 @@ int btn_import_cb(Ihandle *self)
IupSetAttribute(filedlg, "DIALOGTYPE", "OPEN");
IupSetAttribute(filedlg, "TITLE", lua_config_get_string("label", "import_title"));
IupSetAttribute(filedlg, "FILTER", "json");
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json|文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*");
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json|CSV 文件 (*.csv)|*.csv|文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*");
IupPopup(filedlg, IUP_CENTER, IUP_CENTER);
@@ -59,6 +61,23 @@ int btn_import_cb(Ihandle *self)
return IUP_DEFAULT;
}
// 保存导入前的状态用于撤销
char **old_sys = NULL, **old_user = NULL;
int old_sys_count = ctx->sys_paths.count;
int old_user_count = ctx->user_paths.count;
if (old_sys_count > 0)
{
old_sys = (char **)malloc(old_sys_count * sizeof(char *));
for (int i = 0; i < old_sys_count; i++)
old_sys[i] = _strdup(string_list_get(&ctx->sys_paths, i));
}
if (old_user_count > 0)
{
old_user = (char **)malloc(old_user_count * sizeof(char *));
for (int i = 0; i < old_user_count; i++)
old_user[i] = _strdup(string_list_get(&ctx->user_paths, i));
}
int choice = 0;
if (has_system && has_user)
{
@@ -81,6 +100,32 @@ int btn_import_cb(Ihandle *self)
if (choice == 1 || choice == 3)
{
// 记录系统变量导入的撤销信息
char **new_sys = NULL;
if (imported.system.count > 0)
{
new_sys = (char **)malloc(imported.system.count * sizeof(char *));
for (int i = 0; i < imported.system.count; i++)
new_sys[i] = _strdup(imported.system.items[i]);
}
OpRecord rec = {0};
rec.type = OP_IMPORT;
rec.target = TARGET_SYSTEM;
rec.index = 0;
rec.count = imported.system.count;
rec.old_paths = old_sys;
rec.new_paths = new_sys;
push_undo_record(ctx->undo_redo_mgr, &rec);
// push_undo_record 会深拷贝,释放临时副本
if (new_sys)
{
for (int i = 0; i < imported.system.count; i++)
free(new_sys[i]);
free(new_sys);
}
// old_sys 已被深拷贝到撤销管理器,置 NULL 避免末尾重复释放
old_sys = NULL;
clear_string_list(&ctx->sys_paths);
for (int i = 0; i < imported.system.count; i++)
{
@@ -93,6 +138,32 @@ int btn_import_cb(Ihandle *self)
if (choice == 2 || choice == 3)
{
// 记录用户变量导入的撤销信息
char **new_user = NULL;
if (imported.user.count > 0)
{
new_user = (char **)malloc(imported.user.count * sizeof(char *));
for (int i = 0; i < imported.user.count; i++)
new_user[i] = _strdup(imported.user.items[i]);
}
OpRecord rec = {0};
rec.type = OP_IMPORT;
rec.target = TARGET_USER;
rec.index = 0;
rec.count = imported.user.count;
rec.old_paths = old_user;
rec.new_paths = new_user;
push_undo_record(ctx->undo_redo_mgr, &rec);
// push_undo_record 会深拷贝,释放临时副本
if (new_user)
{
for (int i = 0; i < imported.user.count; i++)
free(new_user[i]);
free(new_user);
}
// old_user 已被深拷贝到撤销管理器,置 NULL 避免末尾重复释放
old_user = NULL;
clear_string_list(&ctx->user_paths);
for (int i = 0; i < imported.user.count; i++)
{
@@ -103,6 +174,20 @@ int btn_import_cb(Ihandle *self)
total_imported += imported.user.count;
}
// 释放未被撤销记录使用的旧状态
if (old_sys)
{
for (int i = 0; i < old_sys_count; i++)
free(old_sys[i]);
free(old_sys);
}
if (old_user)
{
for (int i = 0; i < old_user_count; i++)
free(old_user[i]);
free(old_user);
}
// 释放导入数据
clear_string_list(&imported.system);
clear_string_list(&imported.user);
@@ -114,6 +199,8 @@ int btn_import_cb(Ihandle *self)
Ihandle *lbl_status = IupGetDialogChild(dlg, CTRL_LBL_STATUS);
if (lbl_status)
IupSetAttribute(lbl_status, "TITLE", lua_config_get_string("status", "loaded"));
refresh_undo_redo_buttons(dlg);
}
else
{
-31
View File
@@ -11,37 +11,6 @@
#include <stdio.h>
#include <stdlib.h>
// 辅助函数:检查当前目标是系统还是用户
static TargetType get_current_target(Ihandle *dlg)
{
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
if (tabs)
{
int tab = IupGetInt(tabs, "VALUE");
return (tab == 1) ? TARGET_SYSTEM : TARGET_USER;
}
return TARGET_USER;
}
// 辅助函数:创建并推送撤销记录
static void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
char **old_paths, char **new_paths)
{
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return;
OpRecord record;
record.type = op_type;
record.target = get_current_target(dlg);
record.index = index;
record.count = count;
record.old_paths = old_paths;
record.new_paths = new_paths;
push_undo_record(ctx->undo_redo_mgr, &record);
}
// 按钮回调:上移
int btn_up_cb(Ihandle *self)
{
+9
View File
@@ -2,11 +2,13 @@
#include "controller/callbacks_internal.h"
#include "core/app_context.h"
#include "core/lua_config.h"
#include "core/undo_redo.h"
#include "utils/string_ext.h"
#include "utils/safe_string.h"
#include "utils/ui_constants.h"
#include "ui/ui_utils.h"
#include <string.h>
#include <stdlib.h>
#include <windows.h>
// 搜索回调
@@ -65,10 +67,17 @@ int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y
if (txt_search)
IupSetAttribute(txt_search, "VALUE", "");
// 记录撤销信息
char *path_copy = _strdup(filename);
char *paths[1] = {path_copy};
push_record(dlg, OP_ADD, raw_data->count, 1, NULL, paths);
free(path_copy);
add_string_list(raw_data, filename);
sync_string_list_to_ui(current_list, raw_data);
IupSetInt(current_list, "VALUE", raw_data->count);
refresh_undo_redo_buttons(dlg);
}
else
{
+56 -6
View File
@@ -42,7 +42,7 @@ int btn_ok_cb(Ihandle *self)
{
Ihandle *filedlg = IupFileDlg();
IupSetAttribute(filedlg, "DIALOGTYPE", "DIR");
IupSetAttribute(filedlg, "TITLE", "选择备份目录");
IupSetAttribute(filedlg, "TITLE", _("Select backup directory"));
IupPopup(filedlg, IUP_CENTER, IUP_CENTER);
@@ -79,22 +79,72 @@ int btn_ok_cb(Ihandle *self)
if (backup_result != ERR_OK)
{
log_error("Backup failed: error code %d", backup_result);
const char *reason = "未知错误";
const char *reason = _("Unknown error");
if (backup_result == ERR_FAILED)
reason = "无法获取 AppData 路径";
reason = _("Failed to get AppData path");
else if (backup_result == ERR_FILE_NOT_FOUND)
reason = "无法创建备份目录或文件";
reason = _("Failed to create backup directory or file");
else if (backup_result == ERR_REGISTRY_FAILED)
reason = "无法读取注册表中的 PATH 值";
reason = _("Failed to read PATH from registry");
char msg[512];
snprintf(msg, sizeof(msg), "备份失败!原因:%s\n\n是否继续保存?\n(继续保存可能导致无法恢复)", reason);
snprintf(msg, sizeof(msg), _("Backup failed! Reason: %s\n\nContinue saving?\n(Continuing may prevent recovery)"), reason);
int choice = IupAlarm(_("Warning"), msg, _("Continue Saving"), _("Cancel"), NULL);
if (choice != 1)
return IUP_DEFAULT;
}
}
// PATH 长度检查
{
int sys_len = 0, user_len = 0;
for (int i = 0; i < ctx->sys_paths.count; i++)
{
const char *p = string_list_get(&ctx->sys_paths, i);
if (p) sys_len += (int)strlen(p) + 1; // +1 for semicolon
}
for (int i = 0; i < ctx->user_paths.count; i++)
{
const char *p = string_list_get(&ctx->user_paths, i);
if (p) user_len += (int)strlen(p) + 1;
}
int warnings = 0;
char warn_msg[1024] = "";
if (sys_len > 2048)
{
snprintf(warn_msg, sizeof(warn_msg),
_("System PATH length: %d characters (recommended max: 2048)\n"), sys_len);
warnings++;
}
if (user_len > 2048)
{
char tmp[256];
snprintf(tmp, sizeof(tmp),
_("User PATH length: %d characters (recommended max: 2048)\n"), user_len);
strncat(warn_msg, tmp, sizeof(warn_msg) - strlen(warn_msg) - 1);
warnings++;
}
if (sys_len + user_len > 8191)
{
char tmp[256];
snprintf(tmp, sizeof(tmp),
_("Total PATH length: %d characters (command line safe limit: 8191)\n"), sys_len + user_len);
strncat(warn_msg, tmp, sizeof(warn_msg) - strlen(warn_msg) - 1);
warnings++;
}
if (warnings > 0)
{
strncat(warn_msg, _("\nSaving may cause system instability. Continue?"),
sizeof(warn_msg) - strlen(warn_msg) - 1);
int choice = IupAlarm(_("PATH Length Warning"), warn_msg, _("Continue Saving"), _("Cancel"), NULL);
if (choice != 1)
return IUP_DEFAULT;
}
}
ErrorCode sys_ok = save_system_paths(&ctx->sys_paths);
ErrorCode user_ok = save_user_paths(&ctx->user_paths);
+137 -12
View File
@@ -5,6 +5,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
// 获取当前日期时间
@@ -103,14 +104,6 @@ static char *escape_csv_field(const char *str)
*p++ = '"';
*p++ = '"';
break;
case '\n':
*p++ = '\\';
*p++ = 'n';
break;
case '\r':
*p++ = '\\';
*p++ = 'r';
break;
default:
*p++ = str[i];
break;
@@ -169,7 +162,7 @@ static ErrorCode export_paths_to_json(const ExportData *data, FILE *fp)
// 导出 PATH 到 CSV 文件
// 格式:type,path
// type: "system""user"
// type: system 或 user
static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
{
// 写入 UTF-8 BOM
@@ -186,7 +179,7 @@ static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
char *escaped = escape_csv_field(data->system.items[i]);
if (escaped)
{
fprintf(fp, "\"system\",\"%s\"\n", escaped);
fprintf(fp, "system,%s\n", escaped);
free(escaped);
}
}
@@ -200,7 +193,7 @@ static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
char *escaped = escape_csv_field(data->user.items[i]);
if (escaped)
{
fprintf(fp, "\"user\",\"%s\"\n", escaped);
fprintf(fp, "user,%s\n", escaped);
free(escaped);
}
}
@@ -263,7 +256,7 @@ static void trim_whitespace(char *str)
start++;
char *end = str + strlen(str) - 1;
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
while (end >= start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
*end-- = '\0';
if (start != str)
@@ -289,6 +282,133 @@ static int is_json_file(const char *filepath)
return ext && _stricmp(ext, ".json") == 0;
}
// 检查文件是否为 CSV 格式(通过扩展名)
static int is_csv_file(const char *filepath)
{
const char *ext = strrchr(filepath, '.');
return ext && _stricmp(ext, ".csv") == 0;
}
// 解析 CSV 字段(处理引号包围的字段)
// 返回值:指向下一个字段的指针,或 NULL
static const char *parse_csv_field(const char *line, char *field, int field_size)
{
if (!line || !field || field_size <= 0)
return NULL;
const char *p = line;
char *out = field;
char *end = field + field_size - 1;
if (*p == '"')
{
// 引号包围的字段
p++; // 跳过开始引号
while (*p && out < end)
{
if (*p == '"')
{
if (*(p + 1) == '"')
{
// 转义的引号 ""
*out++ = '"';
p += 2;
}
else
{
// 结束引号
p++; // 跳过结束引号
break;
}
}
else
{
*out++ = *p++;
}
}
// 跳过逗号分隔符
if (*p == ',') p++;
}
else
{
// 非引号字段(按逗号分隔)
while (*p && *p != ',' && out < end)
{
*out++ = *p++;
}
if (*p == ',') p++;
}
*out = '\0';
return p;
}
// 导入 CSV 格式的 PATH 文件
static ErrorCode import_paths_from_csv(const char *filepath, ExportData *data)
{
FILE *fp = fopen(filepath, "rb");
if (!fp)
{
log_error("Failed to open file for import: %s", filepath);
return ERR_FILE_NOT_FOUND;
}
char line[4096];
int line_num = 0;
int header_skipped = 0;
while (fgets(line, sizeof(line), fp))
{
line_num++;
trim_whitespace(line);
if (line[0] == '\0')
continue;
// 跳过 UTF-8 BOM
const char *start = line;
if ((unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF)
{
start += 3;
}
// 智能检测标题行:第一行包含 "type" 和 "path" 则视为标题
if (!header_skipped)
{
header_skipped = 1;
char lower[256];
strncpy(lower, start, sizeof(lower) - 1);
lower[sizeof(lower) - 1] = '\0';
for (char *p = lower; *p; p++) *p = (char)tolower((unsigned char)*p);
if (strstr(lower, "type") && strstr(lower, "path"))
continue;
}
char type[32] = {0};
char path[4096] = {0};
start = parse_csv_field(start, type, sizeof(type));
if (!start)
continue;
parse_csv_field(start, path, sizeof(path));
if (path[0] == '\0')
continue;
if (_stricmp(type, "system") == 0)
add_string_list(&data->system, path);
else if (_stricmp(type, "user") == 0)
add_string_list(&data->user, path);
}
fclose(fp);
log_info("Imported paths from CSV file: sys=%d, user=%d, file=%s",
data->system.count, data->user.count, filepath);
return ERR_OK;
}
// 检查引号前是否有奇数个连续反斜杠(奇数个表示引号被转义)
static int is_quote_escaped(const char *quote_pos, const char *line_start)
{
@@ -311,6 +431,11 @@ ErrorCode import_paths_from_file(const char *filepath, ExportData *data)
init_string_list(&data->system);
init_string_list(&data->user);
if (is_csv_file(filepath))
{
return import_paths_from_csv(filepath, data);
}
if (!is_json_file(filepath))
{
FILE *fp = fopen(filepath, "rb");
+5 -18
View File
@@ -149,11 +149,11 @@ int undo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
break;
case OP_DELETE:
// 撤销删除:恢复被删除的路径
// 撤销删除:恢复被删除的路径到原始位置
for (int i = 0; i < rec->count; i++)
{
if (rec->old_paths[i])
add_string_list(target, rec->old_paths[i]);
string_list_insert_at(target, rec->index + i, rec->old_paths[i]);
}
break;
@@ -221,23 +221,10 @@ int redo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
break;
case OP_DELETE:
// 重做删除:重新删除路径
for (int i = 0; i < rec->count; i++)
// 重做删除:从高索引到低索引删除,避免索引偏移
for (int i = rec->count - 1; i >= 0; i--)
{
// 找到并删除对应路径
for (int j = 0; j < target->count; j++)
{
if (target->items[j] && rec->old_paths[i] &&
strcmp(target->items[j], rec->old_paths[i]) == 0)
{
free(target->items[j]);
// 移动后面的元素
for (int k = j; k < target->count - 1; k++)
target->items[k] = target->items[k + 1];
target->count--;
break;
}
}
path_manager_remove_at(target, rec->index + i);
}
break;
+24 -6
View File
@@ -7,6 +7,13 @@
#include "core/app_context.h"
#include "utils/ui_constants.h"
#include "core/lua_config.h"
#include "utils/string_ext.h"
#include "utils/os_env.h"
#include "utils/logger.h"
#include "utils/i18n.h"
#include "controller/callbacks.h"
#include "ui/main_window.h"
#include "ui/ui_utils.h"
// 需要在非管理员模式禁用的按钮列表
#define ADMIN_DISABLE_COUNT 10
@@ -15,12 +22,6 @@ static const char* ADMIN_DISABLE_BUTTONS[] = {
CTRL_BTN_UP, CTRL_BTN_DOWN, CTRL_BTN_CLEAN, CTRL_BTN_OK,
CTRL_BTN_IMPORT, CTRL_BTN_EXPORT
};
#include "utils/string_ext.h"
#include "utils/os_env.h"
#include "utils/logger.h"
#include "utils/i18n.h"
#include "controller/callbacks.h"
#include "ui/main_window.h"
/*
!编译命令:
@@ -65,6 +66,9 @@ int main(int argc, char **argv)
// 初始化国际化系统
i18n_init(NULL);
// 初始化深色模式(从配置文件加载)
init_dark_mode();
// 在管理员模式下,解决无法拖拽文件到列表框的问题 (UIPI)
// 需要加载 User32.dll 获取 ChangeWindowMessageFilter 函数
HMODULE hUser32 = LoadLibraryW(L"user32.dll");
@@ -97,6 +101,20 @@ int main(int argc, char **argv)
Ihandle *dlg = create_main_window();
// 如果深色模式已启用,应用深色主题
if (get_dark_mode())
{
const char *dark_bg = lua_config_get_string("theme", "dark_bg");
if (dark_bg && *dark_bg)
IupSetAttribute(dlg, "BGCOLOR", dark_bg);
// 更新深色模式按钮标题
Ihandle *btn_darkmode = IupGetDialogChild(dlg, CTRL_BTN_DARKMODE);
if (btn_darkmode)
IupSetAttribute(btn_darkmode, "TITLE",
_(lua_config_get_string("button", "lightmode")));
}
// 绑定上下文到对话框
IupSetAttribute(dlg, "APP_CONTEXT", (char *)ctx);
// 注册主窗口句柄,方便其他地方获取
+39
View File
@@ -1,6 +1,7 @@
#include "ui/ui_utils.h"
#include "utils/os_env.h"
#include "utils/string_ext.h"
#include "utils/logger.h"
#include "core/lua_config.h"
#include <stdio.h>
#include <string.h>
@@ -8,9 +9,41 @@
static int g_dark_mode = 0;
static void load_dark_mode(void)
{
char path[512];
get_exe_dir(path, sizeof(path));
strncat(path, "\\darkmode.txt", sizeof(path) - strlen(path) - 1);
FILE *fp = fopen(path, "r");
if (fp)
{
char val[8] = {0};
if (fgets(val, sizeof(val), fp))
g_dark_mode = (atoi(val) == 1) ? 1 : 0;
fclose(fp);
log_info("Loaded dark mode: %d", g_dark_mode);
}
}
static void save_dark_mode(void)
{
char path[512];
get_exe_dir(path, sizeof(path));
strncat(path, "\\darkmode.txt", sizeof(path) - strlen(path) - 1);
FILE *fp = fopen(path, "w");
if (fp)
{
fprintf(fp, "%d\n", g_dark_mode);
fclose(fp);
}
}
void set_dark_mode(int enabled)
{
g_dark_mode = enabled;
save_dark_mode();
}
int get_dark_mode(void)
@@ -18,6 +51,12 @@ int get_dark_mode(void)
return g_dark_mode;
}
// 初始化深色模式(启动时调用)
void init_dark_mode(void)
{
load_dark_mode();
}
// 刷新列表样式(斑马纹 + 有效性检查)
void refresh_single_list_style(Ihandle *list)
{
+14 -3
View File
@@ -1,5 +1,6 @@
#include "utils/i18n.h"
#include "utils/logger.h"
#include "utils/os_env.h"
#include "core/lua_config.h"
#include <windows.h>
#include <stdio.h>
@@ -10,7 +11,11 @@ static char locale_path[256] = {0};
static void load_saved_language(void)
{
FILE *fp = fopen("language.txt", "r");
char path[512];
get_exe_dir(path, sizeof(path));
strncat(path, "\\language.txt", sizeof(path) - strlen(path) - 1);
FILE *fp = fopen(path, "r");
if (fp != NULL)
{
char lang[16] = {0};
@@ -39,7 +44,9 @@ void i18n_init(const char *default_lang)
{
setlocale(LC_ALL, "");
snprintf(locale_path, sizeof(locale_path), "./locale");
char dir[256];
get_exe_dir(dir, sizeof(dir));
snprintf(locale_path, sizeof(locale_path), "%s/locale", dir);
bindtextdomain("messages", locale_path);
textdomain("messages");
@@ -96,7 +103,11 @@ void i18n_change_language(const char *lang)
snprintf(new_lang_path, sizeof(new_lang_path), "%s/%s/LC_MESSAGES/%s.mo", locale_path, lang, lang);
bindtextdomain("messages", new_lang_path);
FILE *fp = fopen("language.txt", "w");
char path[512];
get_exe_dir(path, sizeof(path));
strncat(path, "\\language.txt", sizeof(path) - strlen(path) - 1);
FILE *fp = fopen(path, "w");
if (fp != NULL)
{
fprintf(fp, "%s\n", lang);
+23
View File
@@ -5,10 +5,33 @@
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <direct.h>
#include <shlobj.h>
// 获取可执行文件所在目录(带缓存)
static char s_exe_dir[256] = {0};
void get_exe_dir(char *buf, size_t size)
{
if (s_exe_dir[0] != '\0')
{
strncpy(buf, s_exe_dir, size - 1);
buf[size - 1] = '\0';
return;
}
DWORD len = GetModuleFileNameA(NULL, buf, (DWORD)size);
if (len > 0 && len < size)
{
char *last_sep = strrchr(buf, '\\');
if (!last_sep) last_sep = strrchr(buf, '/');
if (last_sep) *last_sep = '\0';
}
strncpy(s_exe_dir, buf, sizeof(s_exe_dir) - 1);
s_exe_dir[sizeof(s_exe_dir) - 1] = '\0';
}
// 检查管理员权限
int check_admin(void)
{
+30
View File
@@ -92,6 +92,36 @@ void add_string_list(StringList *list, const char *str)
list->count++;
}
// 在指定索引位置插入字符串(后续元素后移)
int string_list_insert_at(StringList *list, int index, const char *str)
{
if (!list || !str || index < 0 || index > list->count)
return -1;
// 扩容检查
if (list->count >= list->capacity)
{
int new_capacity = (list->capacity == 0) ? 16 : list->capacity * 2;
char **new_items = (char **)realloc(list->items, new_capacity * sizeof(char *));
if (!new_items)
return -1;
list->items = new_items;
list->capacity = new_capacity;
}
char *dup = _strdup(str);
if (!dup)
return -1;
// 后移元素
for (int i = list->count; i > index; i--)
list->items[i] = list->items[i - 1];
list->items[index] = dup;
list->count++;
return 0;
}
// 获取指定索引的字符串(只读)
const char *string_list_get(const StringList *list, int index)
{