/**
* Things URL Scheme 工具函数
* 用于构建Things URL并进行参数编码
*/
/**
* 参数名映射:将驼峰式参数名转换为Things URL Scheme的参数名
*/
const PARAM_NAME_MAP = {
// add/add-project 通用
checklistItems: 'checklist-items',
listId: 'list-id',
headingId: 'heading-id',
showQuickEntry: 'show-quick-entry',
creationDate: 'creation-date',
completionDate: 'completion-date',
// add-project 特定
areaId: 'area-id',
todos: 'to-dos',
// update 通用
authToken: 'auth-token',
prependNotes: 'prepend-notes',
appendNotes: 'append-notes',
addTags: 'add-tags',
prependChecklistItems: 'prepend-checklist-items',
appendChecklistItems: 'append-checklist-items',
};
/**
* URL编码字符串
* @param {string} str - 要编码的字符串
* @returns {string} 编码后的字符串
*/
function encodeParam(str) {
if (str === null || str === undefined) {
return '';
}
return encodeURIComponent(String(str));
}
/**
* 将对象转换为URL查询字符串
* @param {object} params - 参数对象
* @returns {string} 查询字符串
*/
function buildQueryString(params) {
const queryParts = [];
for (let [key, value] of Object.entries(params)) {
// 跳过未定义和null的值
if (value === undefined || value === null) {
continue;
}
// 转换参数名
const paramName = PARAM_NAME_MAP[key] || key;
// 处理数组类型
if (Array.isArray(value)) {
// 数组用换行符连接
value = value.join('\n');
}
// 处理布尔值
if (typeof value === 'boolean') {
value = value ? 'true' : 'false';
}
// 空字符串也要保留(用于清除字段)
queryParts.push(`${paramName}=${encodeParam(value)}`);
}
return queryParts.join('&');
}
/**
* 构建Things URL
* @param {string} command - Things命令名称
* @param {object} params - 参数对象
* @returns {string} 完整的Things URL
*/
export function buildThingsUrl(command, params = {}) {
const baseUrl = `things:///${command}`;
const queryString = buildQueryString(params);
if (!queryString) {
return baseUrl;
}
return `${baseUrl}?${queryString}`;
}
/**
* 验证日期字符串格式
* @param {string} dateStr - 日期字符串
* @returns {boolean} 是否有效
*/
export function isValidDateString(dateStr) {
// 支持的格式:
// - today, tomorrow, evening, anytime, someday
// - yyyy-mm-dd
// - yyyy-mm-dd@HH:mm
// - 自然语言(英文)
const specialValues = ['today', 'tomorrow', 'evening', 'anytime', 'someday'];
if (specialValues.includes(dateStr.toLowerCase())) {
return true;
}
// ISO日期格式
const isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
if (isoDatePattern.test(dateStr)) {
return true;
}
// 日期时间格式
const dateTimePattern = /^\d{4}-\d{2}-\d{2}@\d{2}:\d{2}$/;
if (dateTimePattern.test(dateStr)) {
return true;
}
// 其他情况视为自然语言,由Things处理
return true;
}
/**
* 验证待办事项ID格式
* @param {string} id - ID字符串
* @returns {boolean} 是否有效
*/
export function isValidThingsId(id) {
// Things ID通常是UUID格式
const uuidPattern = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
return uuidPattern.test(id);
}
/**
* 验证内置列表ID
* @param {string} id - ID字符串
* @returns {boolean} 是否有效
*/
export function isValidBuiltInListId(id) {
const builtInIds = [
'inbox',
'today',
'anytime',
'upcoming',
'someday',
'logbook',
'tomorrow',
'deadlines',
'repeating',
'all-projects',
'logged-projects',
];
return builtInIds.includes(id.toLowerCase());
}
/**
* 构建JSON导入的URL(需要特殊处理)
* @param {Array} data - JSON数据数组
* @param {object} options - 其他选项
* @returns {string} Things URL
*/
export function buildJsonUrl(data, options = {}) {
// JSON需要先序列化再编码
const jsonString = JSON.stringify(data);
const params = {
data: jsonString,
...options,
};
return buildThingsUrl('json', params);
}
/**
* 解析Things URL的回调参数
* @param {string} callbackUrl - x-success回调URL
* @returns {object} 解析后的参数
*/
export function parseCallbackParams(callbackUrl) {
try {
const url = new URL(callbackUrl);
const params = {};
for (const [key, value] of url.searchParams) {
// 解码参数
params[key] = decodeURIComponent(value);
}
// 处理特殊参数
if (params['x-things-id']) {
// 可能是逗号分隔的ID列表
params.ids = params['x-things-id'].split(',');
}
if (params['x-things-ids']) {
// JSON格式的ID数组
try {
params.ids = JSON.parse(params['x-things-ids']);
} catch (e) {
// 解析失败,保持原值
}
}
return params;
} catch (error) {
console.error('解析回调URL失败:', error);
return {};
}
}
/**
* 创建待办事项对象(用于JSON导入)
* @param {object} attributes - 待办事项属性
* @param {string} operation - 操作类型: 'create' 或 'update'
* @param {string} id - 待办事项ID(更新时需要)
* @returns {object} 待办事项对象
*/
export function createTodoObject(attributes, operation = 'create', id = null) {
const todo = {
type: 'to-do',
attributes,
};
if (operation === 'update') {
todo.operation = 'update';
if (!id) {
throw new Error('更新操作需要提供ID');
}
todo.id = id;
}
return todo;
}
/**
* 创建项目对象(用于JSON导入)
* @param {object} attributes - 项目属性
* @param {string} operation - 操作类型: 'create' 或 'update'
* @param {string} id - 项目ID(更新时需要)
* @returns {object} 项目对象
*/
export function createProjectObject(attributes, operation = 'create', id = null) {
const project = {
type: 'project',
attributes,
};
if (operation === 'update') {
project.operation = 'update';
if (!id) {
throw new Error('更新操作需要提供ID');
}
project.id = id;
}
return project;
}
/**
* 创建标题对象(用于JSON导入的项目内)
* @param {string} title - 标题文本
* @param {boolean} archived - 是否存档
* @returns {object} 标题对象
*/
export function createHeadingObject(title, archived = false) {
return {
type: 'heading',
attributes: {
title,
archived,
},
};
}
/**
* 创建清单项对象
* @param {string} title - 清单项文本
* @param {boolean} completed - 是否完成
* @param {boolean} canceled - 是否取消
* @returns {object} 清单项对象
*/
export function createChecklistItemObject(title, completed = false, canceled = false) {
return {
type: 'checklist-item',
attributes: {
title,
completed,
canceled,
},
};
}