Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29898d5dff | ||
|
|
c711cdb7d5 | ||
|
|
2f124eada0 |
+2
-2
@@ -2,8 +2,8 @@
|
||||
"app": {
|
||||
"bundleName": "synccalendar.yangyq.net",
|
||||
"vendor": "yangyq",
|
||||
"versionCode": 800,
|
||||
"versionName": "0.0.8",
|
||||
"versionCode": 900,
|
||||
"versionName": "0.0.9",
|
||||
"buildVersion": "1",
|
||||
"icon": "$media:layered_image",
|
||||
"label": "$string:app_name"
|
||||
|
||||
@@ -122,7 +122,23 @@ export class SyncEngine {
|
||||
private static async syncAccountInner(context: common.Context, acc: DavAccount): Promise<number> {
|
||||
const auth: string = DavClient.authHeader(acc.username, acc.password);
|
||||
// 0) 刷新服务器端日历本颜色(每次同步都校正)
|
||||
await SyncEngine.refreshCalendarColors(acc, auth);
|
||||
// ⭐ 有变化就立刻落盘:镜像 listBooks 从磁盘重新 loadAll,
|
||||
// 不落盘的话镜像 setConfig 的永远是旧颜色(系统日历颜色不跟服务器变)。
|
||||
const colorChanged: boolean = await SyncEngine.refreshCalendarColors(acc, auth);
|
||||
if (colorChanged) {
|
||||
try {
|
||||
const all: DavAccount[] = await AccountStore.loadAll(context);
|
||||
const idx: number = all.findIndex((a: DavAccount): boolean => a.id === acc.id);
|
||||
if (idx >= 0) {
|
||||
all[idx] = acc;
|
||||
await AccountStore.saveAll(context, all);
|
||||
LogUtil.write('日历本颜色/权限有变化,已落盘(镜像将使用新颜色)');
|
||||
}
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`颜色/权限落盘失败(不影响同步): ${e.message}`);
|
||||
}
|
||||
}
|
||||
// 1) 推送该账号日历本下的本地修改
|
||||
await SyncEngine.pushDirtyForAccount(context, acc, auth);
|
||||
// 2) 拉取远端变更:REPORT 仅取 etag;新增/变化的资源用 GET 补拉完整 ICS。
|
||||
@@ -363,8 +379,13 @@ export class SyncEngine {
|
||||
/**
|
||||
* 刷新服务器端日历本颜色:PROPFIND getcolor → 按集合路径匹配更新 acc.calendarColors
|
||||
* 失败静默(颜色不影响数据正确性)
|
||||
* ⭐ 返回**是否有实际变化**(颜色或写权限)——调用方据此落盘。
|
||||
* (0.0.9 修复:此前变化只留在内存、从不 saveAll ⇒ 镜像 listBooks 从磁盘读到的
|
||||
* 永远是旧颜色 ⇒ setConfig 旧色 ⇒ 系统日历颜色永远不跟服务器变;
|
||||
* 而账号设置页是当场 PROPFIND 的,所以"页面上是新色、系统日历是旧色"。)
|
||||
*/
|
||||
static async refreshCalendarColors(acc: DavAccount, auth: string): Promise<void> {
|
||||
static async refreshCalendarColors(acc: DavAccount, auth: string): Promise<boolean> {
|
||||
let changed: boolean = false;
|
||||
let entries: DavColorEntry[] = [];
|
||||
try {
|
||||
entries = await DavClient.propfindColors(acc.serverUrl, auth);
|
||||
@@ -382,9 +403,11 @@ export class SyncEngine {
|
||||
if (found !== undefined && found.color !== '') {
|
||||
while (acc.calendarColors.length <= i) {
|
||||
acc.calendarColors.push('');
|
||||
changed = true; // 长度补齐也视作变化(旧账号可能还没这一位)
|
||||
}
|
||||
if (acc.calendarColors[i] !== found.color) {
|
||||
LogUtil.write(`日历本[${i}] 颜色更新:${acc.calendarColors[i] === '' ? '(空)' : acc.calendarColors[i]} → ${found.color}`);
|
||||
changed = true;
|
||||
}
|
||||
acc.calendarColors[i] = found.color;
|
||||
} else if (found === undefined) {
|
||||
@@ -396,17 +419,23 @@ export class SyncEngine {
|
||||
acc.calendarWritable.push('1');
|
||||
}
|
||||
const calName: string = i < acc.calendarNames.length ? acc.calendarNames[i] : `日历本${i}`;
|
||||
let writable: string;
|
||||
if (found !== undefined && found.privilegeKnown) {
|
||||
// 服务器声明了权限:直接采用
|
||||
acc.calendarWritable[i] = found.writable ? '1' : '0';
|
||||
writable = found.writable ? '1' : '0';
|
||||
LogUtil.write(`日历本[${i}]「${calName}」写权限(privilege):${found.writable ? '可写' : '只读'}`);
|
||||
} else {
|
||||
// 服务器未声明权限(如部分 Synology 配置)或 PROPFIND 未匹配到该本:真实写探测
|
||||
const w: boolean = await DavClient.probeWritable(target, auth);
|
||||
acc.calendarWritable[i] = w ? '1' : '0';
|
||||
writable = w ? '1' : '0';
|
||||
LogUtil.write(`日历本[${i}]「${calName}」写权限(探测):${w ? '可写' : '只读'}`);
|
||||
}
|
||||
if (acc.calendarWritable[i] !== writable) {
|
||||
changed = true;
|
||||
}
|
||||
acc.calendarWritable[i] = writable;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** Map<key, 行数组> 追加一行(没有该 key 就建数组) */
|
||||
|
||||
@@ -1146,7 +1146,9 @@ export class SystemCalendarMirror {
|
||||
try {
|
||||
await cal.setConfig({ enableReminder: true, color: ref.color });
|
||||
} catch (err) {
|
||||
// 设色失败不影响镜像本身
|
||||
// 设色失败不影响镜像本身,但要留痕(否则"颜色不变"无从排查)
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`镜像账户设色失败 ${accountName}: ${e.message}`);
|
||||
}
|
||||
return cal;
|
||||
}
|
||||
@@ -1159,7 +1161,8 @@ export class SystemCalendarMirror {
|
||||
try {
|
||||
await cal.setConfig({ enableReminder: true, color: ref.color });
|
||||
} catch (err) {
|
||||
// 同上
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`镜像账户设色失败(新建) ${accountName}: ${e.message}`);
|
||||
}
|
||||
return cal;
|
||||
}
|
||||
|
||||
@@ -293,6 +293,51 @@ struct SettingsPage {
|
||||
return this.allBooks.length - this.mirrorCandidates.length;
|
||||
}
|
||||
|
||||
/** 已勾选的可镜像本数量(mirrorKeys 里可能残留已不在候选里的 bookId,按候选口径统计) */
|
||||
private mirrorSelectedCount(): number {
|
||||
return this.mirrorCandidates.filter((b: BackupTarget): boolean =>
|
||||
this.mirrorKeys.indexOf(b.bookId) >= 0).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「全选 / 全不选」要镜像的日历本。
|
||||
* ⭐ 批量落盘只写一次 preferences、缓存只失效一次(与逐个 toggle 的关键区别);
|
||||
* 全不选时逐本删除对应系统日历账户(与取消单个勾选同一语义)。
|
||||
* 半选状态下点按 = 全选。
|
||||
*/
|
||||
private async toggleMirrorAll(): Promise<void> {
|
||||
if (this.context === undefined || this.mirrorCandidates.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selectAll: boolean = this.mirrorSelectedCount() < this.mirrorCandidates.length;
|
||||
if (selectAll) {
|
||||
const ids: string[] = this.mirrorCandidates.map((b: BackupTarget): string => b.bookId);
|
||||
this.mirrorKeys = ids;
|
||||
await AppSettings.setMirrorBookIds(this.context, ids);
|
||||
this.getUIContext().getPromptAction().showToast({
|
||||
message: `已全选 ${ids.length} 个日历本,点「立即执行镜像」写入系统日历`
|
||||
});
|
||||
} else {
|
||||
// 逐本删除系统日历账户;逐个独立 try,失败不影响其余
|
||||
for (const k of this.mirrorKeys) {
|
||||
try {
|
||||
await SystemCalendarMirror.removeById(this.context, k);
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
console.error(`全不选:删除镜像账户失败 bookId=${k}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
this.mirrorKeys = [];
|
||||
await AppSettings.setMirrorBookIds(this.context, []);
|
||||
this.getUIContext().getPromptAction().showToast({
|
||||
message: '已取消全部勾选,并清除对应的系统日历账户'
|
||||
});
|
||||
}
|
||||
// ⭐ 勾选本集合变化 → 提醒侧"已镜像本"缓存失效并重排提醒
|
||||
ReminderService.invalidateMirrorCache();
|
||||
await ReminderService.refreshReminders(this.context as common.UIAbilityContext);
|
||||
}
|
||||
|
||||
/** 该日历本是否只读:服务器无写权限(探测结果)或用户手动标记只读 —— 与首页色块/只读标记同一口径 */
|
||||
private isBookReadonly(b: BackupTarget): boolean {
|
||||
return !b.serverWritable || this.manualKeys.includes(b.calKey);
|
||||
@@ -978,6 +1023,35 @@ struct SettingsPage {
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
.width('100%')
|
||||
}
|
||||
// ⭐ 全选 / 全不选:半选状态下点按 = 全选
|
||||
Row({ space: 6 }) {
|
||||
Text(`全选(已选 ${this.mirrorSelectedCount()}/${this.mirrorCandidates.length})`)
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
Toggle({
|
||||
type: ToggleType.Checkbox,
|
||||
isOn: this.mirrorCandidates.length > 0
|
||||
&& this.mirrorSelectedCount() === this.mirrorCandidates.length
|
||||
})
|
||||
.selectedColor($r('app.color.brand'))
|
||||
.onChange((on: boolean): void => {
|
||||
// 半选时勾选框显示为关,onChange(on=true) 走全选;全选时取消走全不选
|
||||
if (on === (this.mirrorSelectedCount() === this.mirrorCandidates.length)) {
|
||||
return; // 状态未变化(个别系统会重复回调),忽略
|
||||
}
|
||||
this.toggleMirrorAll();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 2, bottom: 2 })
|
||||
.onClick(() => {
|
||||
// 整行可点:直接按"是否已全选"翻转,比依赖 Checkbox 的 onChange 更可靠
|
||||
this.toggleMirrorAll();
|
||||
})
|
||||
|
||||
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
|
||||
ForEach(this.mirrorCandidates, (b: BackupTarget): void => {
|
||||
Row({ space: 6 }) {
|
||||
|
||||
Reference in New Issue
Block a user