0.0.8:系统日历账户显示名可切换 + 清理判定改为 bookId 身份校验
背景:应用往系统日历写数据时,账户显示名固定为「同步日历 · <本名>」。多账号用户
无法看出某个日历本属于哪个账号;若把"应用名+账号名"都写上又太长,本名被挤压看不清。
方案见《镜像账户命名方案-去掉应用名前缀.md》,影响评估见《0.0.8升级影响评估.md》。
平台硬约束(已核实 @ohos.calendarManager):
**没有"修改账户"的 API**(只有 create/delete/get/getAll;CalendarConfig 仅
enableReminder+color;CalendarAccount.name 为 readonly)→ displayName 只能在
创建时定,改名必须"删账户 → 重建"。
实现:
1. **三种显示名格式**(分隔符统一半角点 `·` U+00B7、**无空格**):
1 = `同步日历·<本名>`
2 = `同步日历·<账号>·<本名>`
3 = `<账号>·<本名>` ★默认
⚠️ **不提供"仅本名"**:裸本名与用户原有同名日历本无法区分 → 有误删风险;
每个镜像账户名必须至少带一个归属标识(同步日历/账号名/二者)。
displayName 上限 64 字符 → 超长自动截断。
2. ⭐ **一键重命名** `MirrorRenamer.reapplyDisplayNames`,顺序**不可调换**:
① `MirrorInbound.reconcile()` 先回读(把用户在系统日历里手动新建/改动的
收进本地库并置 dirty)—— 因为 deleteCalendar 会删掉账户下**所有**日程,
含用户手动加的,不回读就会丢数据;回读失败则**中止**重命名(宁可不改)。
② `removeAccountsByBookIds` 删旧账户(按 bookId 精确定位,与名字无关;
含"清空日程后重试"降级)
③ `syncNow()` 按新格式重建并全量写回
独立成模块的原因:MirrorInbound 已 import SystemCalendarMirror,反向 import 会成环。
3. ⭐⭐ **清理判定改为「bookId 身份校验」**(`isOurMirrorAccount`)—— 用户指出的关键改进:
**两道闸**:① name 必须以 `synccalendar.mirror.` 开头(挡掉别人家的账户);
② 从 name 切出 `accId`(取最后一个 `-` 之前)并确认在账号库中。
⭐ **附加安全阀**:账号库读不到 → **一律不认为是我们的**(宁可不清,不可误删)。
`displayName` 兜底**降级为仅格式 1/2 可用**(格式 3 = `账号·本名` 与别的 App
账户可能同名,参与兜底会误删)。
⇒ **删不删看 accId 是不是我们的;名字只用来显示。** 判定与名字彻底解耦。
4. `AppSettings` 新增 `mirror_display_format`(1|2|3,默认 3;非法值回落 3)。
5. 设置页新增「系统日历中的显示名称」:三种格式胶囊选择 + **用真实账号名/本名实时预览**
+ 「应用」按钮(未变化时禁用、失败自动回滚选项)+ 检测到旧格式时的弱提示
+ 说明"期间日程会短暂消失几秒"。
6. `ensureCalendar` 按格式创建(已有账户**不在此处改名** —— 改名是破坏性操作,
必须由用户显式点「应用」触发);`syncBook` 增加 format 参数并逐层传递。
7. `describeCal` / `purgeOrphans` 参数语义更新(aggressive → allowDisplayMark)。
**不做自动迁移**(用户决定):升级后老账户名字不变、日程不动,用户想换才在设置页点。
理由:自动迁移期间日程会短暂消失,老用户可能正在开会/开车,看到"日程全没了"会惊慌;
且默认格式是主观偏好,强改所有人等于替用户做决定。
验证:
- 编译通过(BUILD SUCCESSFUL);包内版本 800/0.0.8。
- 离线用例覆盖:三种格式生成(确认半角点、无空格)、64 字符截断、
身份判定五种情形(含"别的 App 同名账户不被删""accId 失效的孤儿仍被清"
"读不到账号时一律不删""格式 3 兜底失效")—— **全部通过**。
- 手机未连接 → 未装机(待用户插线后安装测试)。
This commit is contained in:
@@ -11,6 +11,7 @@ import { AccountStore, DavAccount } from '../common/AccountStore';
|
||||
import { ReminderService } from '../common/ReminderService';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { BackupTargetCheck, BookRef, MirrorResult, PurgeResult, SystemCalendarMirror } from '../common/SystemCalendarMirror';
|
||||
import { MirrorRenamer, RenameResult } from '../common/MirrorRenamer';
|
||||
import { DocViewer } from '../common/DocViewer';
|
||||
|
||||
const INTERVAL_OPTIONS: number[] = [1, 5, 15, 30, 60];
|
||||
@@ -51,6 +52,12 @@ struct SettingsPage {
|
||||
@State mirrorCandidates: BackupTarget[] = []; // 可镜像的本(已排除"系统日历备份目标本",防回灌)
|
||||
@State mirrorInbound: boolean = false; // 是否把系统日历的改动回写 CalDAV(默认关)
|
||||
@State accountDump: string = ''; // 系统日历账户清单(诊断用,清理后填充)
|
||||
// ⭐ 系统日历账户显示名格式:1=`同步日历·本名` 2=`同步日历·账号·本名` 3=`账号·本名`(默认)
|
||||
@State displayFormat: number = 3;
|
||||
@State displayFormatApplied: number = 3; // 已生效的格式(用于判断"应用"按钮是否可点)
|
||||
@State formatRunning: boolean = false; // 正在按新格式重建镜像
|
||||
@State hasLegacyNames: boolean = false; // 是否还有旧格式账户(给出"可更换"弱提示)
|
||||
@State formatPreview: string = ''; // 实时预览示例
|
||||
private context?: common.Context;
|
||||
|
||||
aboutToAppear(): void {
|
||||
@@ -118,6 +125,71 @@ struct SettingsPage {
|
||||
AppSettings.getMirrorInbound(ctx).then((v: boolean): void => {
|
||||
this.mirrorInbound = v;
|
||||
});
|
||||
// ⭐ 显示名格式:加载当前值 + 实时预览 + 检查是否还有旧格式账户
|
||||
AppSettings.getMirrorDisplayFormat(ctx).then((v: number): void => {
|
||||
this.displayFormat = v;
|
||||
this.displayFormatApplied = v;
|
||||
this.refreshFormatPreview();
|
||||
});
|
||||
MirrorRenamer.hasLegacyNamedAccounts(ctx).then((v: boolean): void => {
|
||||
this.hasLegacyNames = v;
|
||||
});
|
||||
}
|
||||
|
||||
/** 用用户**实际的第一个账号名 + 第一个本名**拼出示例,让他所见即所得 */
|
||||
private refreshFormatPreview(): void {
|
||||
const ctx = this.context;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const f: number = this.displayFormat;
|
||||
SystemCalendarMirror.selectedBookRefs(ctx).then((refs: BookRef[]): void => {
|
||||
if (refs.length > 0) {
|
||||
this.formatPreview = SystemCalendarMirror.displayNameFor(refs[0], f);
|
||||
} else {
|
||||
// 没有可用本时用占位示例
|
||||
const dummy = new BookRef();
|
||||
dummy.name = '个人日程';
|
||||
dummy.accountName = '工作';
|
||||
this.formatPreview = SystemCalendarMirror.displayNameFor(dummy, f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** ⭐ 按新格式重建镜像(回读→删→重建),期间日程短暂消失几秒 */
|
||||
private async applyDisplayFormat(): Promise<void> {
|
||||
const ctx = this.context;
|
||||
if (ctx === undefined || this.formatRunning) {
|
||||
return;
|
||||
}
|
||||
if (this.displayFormat === this.displayFormatApplied) {
|
||||
return; // 未变化,不需要重建
|
||||
}
|
||||
this.formatRunning = true;
|
||||
try {
|
||||
// 先落盘(reapplyDisplayNames 会按现有配置重建)
|
||||
await AppSettings.setMirrorDisplayFormat(ctx, this.displayFormat);
|
||||
const res: RenameResult = await MirrorRenamer.reapplyDisplayNames(
|
||||
ctx as common.UIAbilityContext);
|
||||
if (res.ok) {
|
||||
this.displayFormatApplied = this.displayFormat;
|
||||
this.hasLegacyNames = false;
|
||||
this.getUIContext().getPromptAction().showToast({
|
||||
message: `已更新系统日历中的名称(${res.mirrored} 个日历本)`
|
||||
});
|
||||
} else {
|
||||
// 失败 → 回滚选项与配置,避免"设置显示已改、实际没改"的错觉
|
||||
this.displayFormat = this.displayFormatApplied;
|
||||
await AppSettings.setMirrorDisplayFormat(ctx, this.displayFormatApplied);
|
||||
this.getUIContext().getPromptAction().showToast({ message: res.message });
|
||||
}
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
this.displayFormat = this.displayFormatApplied;
|
||||
this.getUIContext().getPromptAction().showToast({ message: `更新失败:${e.message}` });
|
||||
}
|
||||
this.formatRunning = false;
|
||||
this.refreshFormatPreview();
|
||||
}
|
||||
|
||||
/** 重新检测通知权限状态 */
|
||||
@@ -936,6 +1008,67 @@ struct SettingsPage {
|
||||
.width('100%')
|
||||
.padding({ top: 4 })
|
||||
|
||||
// ⭐ 系统日历中的显示名称(0.0.8):三种格式,切换需重建镜像
|
||||
Column({ space: 6 }) {
|
||||
Text('系统日历中的显示名称')
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('100%')
|
||||
if (this.hasLegacyNames) {
|
||||
Text('当前名称是旧格式,你可以在下方更换显示方式')
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.brand_text'))
|
||||
.width('100%')
|
||||
}
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach([
|
||||
{ v: 1, label: '同步日历·本名' },
|
||||
{ v: 2, label: '同步日历·账号·本名' },
|
||||
{ v: 3, label: '账号·本名' }
|
||||
], (opt: Record<string, string | number>): void => {
|
||||
Text(opt.label as string)
|
||||
.fontSize(12)
|
||||
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
|
||||
.margin({ right: 8, bottom: 6 })
|
||||
.borderRadius(14)
|
||||
.backgroundColor(this.displayFormat === (opt.v as number)
|
||||
? $r('app.color.brand') : $r('app.color.card_bg'))
|
||||
.fontColor(this.displayFormat === (opt.v as number)
|
||||
? $r('app.color.button_text') : $r('app.color.text_primary'))
|
||||
.border({
|
||||
width: 1,
|
||||
color: this.displayFormat === (opt.v as number)
|
||||
? $r('app.color.brand') : $r('app.color.shadow_color')
|
||||
})
|
||||
.onClick(() => {
|
||||
this.displayFormat = opt.v as number;
|
||||
this.refreshFormatPreview();
|
||||
})
|
||||
}, (opt: Record<string, string | number>): string => `fmt_${opt.v}`)
|
||||
}
|
||||
.width('100%')
|
||||
Text(`示例:${this.formatPreview}`)
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.width('100%')
|
||||
Text('修改后需重新写入系统日历,期间日程会短暂消失几秒')
|
||||
.fontSize(11)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
.width('100%')
|
||||
Button(this.formatRunning ? '正在应用…'
|
||||
: (this.displayFormat === this.displayFormatApplied ? '已应用' : '应用'))
|
||||
.fontSize(13)
|
||||
.width('100%')
|
||||
.enabled(!this.formatRunning && this.displayFormat !== this.displayFormatApplied)
|
||||
.onClick(() => {
|
||||
this.applyDisplayFormat();
|
||||
})
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.width('100%')
|
||||
.padding({ top: 8 })
|
||||
|
||||
Button(this.mirrorRunning ? '正在执行…' : '立即执行镜像')
|
||||
.fontSize(14)
|
||||
.width('100%')
|
||||
|
||||
Reference in New Issue
Block a user