@@ -79,6 +79,22 @@ export class BookPalette {
|
||||
static colorFor(index: number): string {
|
||||
return BookPalette.colors[index % BookPalette.colors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按日历本的**稳定标识**(集合 href)派生回退色。
|
||||
*
|
||||
* 为什么不用 colorFor(索引):服务器未返回颜色时若按索引取色,一旦在服务器上
|
||||
* 新增/删除日历本导致列表顺序变化,索引就会位移,**同一个日历本的颜色会跟着变**
|
||||
* (表现为"加了个新日历本,其他日历本色块颜色全不对了")。
|
||||
* 用 href 做哈希派生则与顺序无关,增删日历本不会影响其余日历本的颜色。
|
||||
*/
|
||||
static colorForHref(href: string): string {
|
||||
let h: number = 0;
|
||||
for (let i = 0; i < href.length; i++) {
|
||||
h = (h * 31 + href.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return BookPalette.colors[h % BookPalette.colors.length];
|
||||
}
|
||||
}
|
||||
|
||||
function bytesToB64(u: Uint8Array): string {
|
||||
|
||||
@@ -66,9 +66,9 @@ export class CalendarDataService {
|
||||
name = acc.calendarHrefs.length === 1 ? acc.name : `日历本 ${i + 1}`;
|
||||
}
|
||||
s.name = `${acc.name}:${name}`;
|
||||
// 优先使用服务器端定义的颜色,未定义时按序号取调色板
|
||||
// 优先使用服务器端定义的颜色;未定义时按**集合 href** 派生(不随顺序变化,见 colorForHref 说明)
|
||||
let color: string = i < acc.calendarColors.length ? AccountStore.normalizeColor(acc.calendarColors[i]) : '';
|
||||
s.color = color !== '' ? color : BookPalette.colorFor(i);
|
||||
s.color = color !== '' ? color : BookPalette.colorForHref(acc.calendarHrefs[i]);
|
||||
s.source = 'dav';
|
||||
s.visible = true;
|
||||
// 写权限:服务器探测结果 + 手动标记(部分服务器不在 CalDAV 层拒绝写入)
|
||||
|
||||
@@ -191,6 +191,34 @@ export class DavClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化集合 href,用于「本地保存的 calendarHrefs」与「PROPFIND 返回的 href」比对。
|
||||
*
|
||||
* 为什么需要:同一集合在不同 PROPFIND 请求/不同服务器上的写法可能不同——
|
||||
* ① 绝对 URL(https://nas/calendars/A/) vs 纯路径(/calendars/A/)
|
||||
* ② URL 编码差异(/calendars/%E6%96%B0/ vs /calendars/新/)
|
||||
* ③ 末尾斜杠有无、重复斜杠
|
||||
* 直接用字符串相等比对会匹配失败 → 服务器上的颜色取不回来,界面只能退回调色板色。
|
||||
* 这里统一成「解码后的路径、无尾斜杠」再比较(路径大小写保持敏感,不转小写)。
|
||||
*/
|
||||
static normalizeHref(href: string): string {
|
||||
let s: string = href.trim();
|
||||
const m = /^[a-zA-Z]+:\/\/[^/]+/i.exec(s);
|
||||
if (m !== null) {
|
||||
s = s.substring(m[0].length);
|
||||
}
|
||||
try {
|
||||
s = decodeURIComponent(s);
|
||||
} catch (err) {
|
||||
// 解码失败(如含非法 % 序列)保持原样
|
||||
}
|
||||
s = s.replace(/\/{2,}/g, '/');
|
||||
if (s.length > 1 && s.endsWith('/')) {
|
||||
s = s.substring(0, s.length - 1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 颜色规范化:#RRGGBBAA → #RRGGBB */
|
||||
static normalizeHex(raw: string): string {
|
||||
const v: string = raw.trim();
|
||||
|
||||
@@ -516,4 +516,45 @@ export class EventDb {
|
||||
}
|
||||
await store.delete(predicates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移因「日历本序号变动」而错位的日程行(calKey = `${accId}_${序号}`)。
|
||||
*
|
||||
* 背景(本方法要修的现象):序号取自「勾选顺序」。在服务器上新增一个日历本后重新勾选,
|
||||
* 若新本不是排在末尾而是插在中间,其后所有日历本的序号都会 +1;但已落库的日程仍挂着
|
||||
* 旧 calKey,于是**日程与日历本错位**——表现为色块颜色不对、日程显示到别人的日历本下。
|
||||
* 同步虽然最终会用远端数据重建,但在同步完成前(以及同步部分失败时)错位一直可见。
|
||||
*
|
||||
* 做法:按 href 对应关系把旧 calKey 的行整体搬到新 calKey。
|
||||
* 分两阶段更新(旧键 → 临时键 → 新键),避免 A↔B 互换时后一次更新把前一次的行一起改掉。
|
||||
*
|
||||
* @param pairs [旧calKey, 新calKey] 列表;只传真正发生位移的项
|
||||
* @returns 被迁移的行数
|
||||
*/
|
||||
static async remapCalKeys(context: common.Context, pairs: Array<[string, string]>): Promise<number> {
|
||||
if (pairs.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const store = await EventDb.getDb(context);
|
||||
const stamp: number = Date.now();
|
||||
const tmpKeys: string[] = [];
|
||||
let moved: number = 0;
|
||||
// 阶段一:旧键 → 临时键
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const tmpKey: string = `__remap_${stamp}_${i}`;
|
||||
const bucket: relationalStore.ValuesBucket = { 'cal_key': tmpKey };
|
||||
const p = new relationalStore.RdbPredicates('events');
|
||||
p.equalTo('cal_key', pairs[i][0]);
|
||||
moved += await store.update(bucket, p);
|
||||
tmpKeys.push(tmpKey);
|
||||
}
|
||||
// 阶段二:临时键 → 新键
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const bucket: relationalStore.ValuesBucket = { 'cal_key': pairs[i][1] };
|
||||
const p = new relationalStore.RdbPredicates('events');
|
||||
p.equalTo('cal_key', tmpKeys[i]);
|
||||
await store.update(bucket, p);
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
}
|
||||
@@ -202,19 +202,22 @@ export class SyncEngine {
|
||||
}
|
||||
for (let i = 0; i < acc.calendarHrefs.length; i++) {
|
||||
const target: string = acc.calendarHrefs[i];
|
||||
const originMatch = /https?:\/\/[^/]+/i.exec(target);
|
||||
let path: string = originMatch !== null ? target.substring(originMatch[0].length) : target;
|
||||
if (path === '') {
|
||||
path = '/';
|
||||
}
|
||||
const norm = (s: string): string => s.endsWith('/') ? s : s + '/';
|
||||
// 用规范化后的路径比对:兼容「绝对 URL vs 纯路径」「URL 编码差异」「尾斜杠有无」,
|
||||
// 否则新加入的日历本匹配不上 → 服务器颜色取不回来,界面只能退回调色板色(表现为"颜色不对")
|
||||
const targetNorm: string = DavClient.normalizeHref(target);
|
||||
const found = entries.find((e: DavColorEntry): boolean =>
|
||||
norm(e.href) === norm(path));
|
||||
DavClient.normalizeHref(e.href) === targetNorm);
|
||||
if (found !== undefined && found.color !== '') {
|
||||
while (acc.calendarColors.length <= i) {
|
||||
acc.calendarColors.push('');
|
||||
}
|
||||
if (acc.calendarColors[i] !== found.color) {
|
||||
LogUtil.write(`日历本[${i}] 颜色更新:${acc.calendarColors[i] === '' ? '(空)' : acc.calendarColors[i]} → ${found.color}`);
|
||||
}
|
||||
acc.calendarColors[i] = found.color;
|
||||
} else if (found === undefined) {
|
||||
// 匹配失败不改动已有颜色(保留上次取到的值),但记录便于排查
|
||||
LogUtil.write(`日历本[${i}] 颜色未匹配:${target}(PROPFIND 返回 ${entries.length} 个集合)`);
|
||||
}
|
||||
// 回写各日历本写权限('1'=可写 '0'=只读)
|
||||
while (acc.calendarWritable.length <= i) {
|
||||
|
||||
Reference in New Issue
Block a user