diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 5c5f7ab..079bc27 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -427,13 +427,8 @@ struct Index { } private openEvent(e: DisplayEvent): void { - // 只读日历本或系统日历日程:无法保存修改,直接显示详情 - if (e.isSystem || !e.writable) { - this.showEventDetail(e); - return; - } - AppStorage.setOrCreate('pendingEventId', e.id); - router.pushUrl({ url: 'pages/EventEditPage' }); + // 所有日程点击都弹出详情(只读/可写/系统一致);可写日程在详情中可点"编辑"进入编辑页 + this.showEventDetail(e); } /** 只读日程详情:改为卡片式半屏弹层(与编辑页风格一致) */ @@ -729,6 +724,25 @@ struct Index { .align(Alignment.Top) .layoutWeight(1) } + // 可写日程:详情底部提供"编辑"入口(系统/只读日程无此按钮) + if (this.detailEvent !== null && !this.detailEvent.isSystem && this.detailEvent.writable) { + Button('编辑日程') + .width('100%') + .height(44) + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.button_text')) + .backgroundColor($r('app.color.brand')) + .borderRadius(12) + .onClick(() => { + const id: number = this.detailEvent !== null ? this.detailEvent.id : 0; + this.detailShow = false; + if (id > 0) { + AppStorage.setOrCreate('pendingEventId', id); + router.pushUrl({ url: 'pages/EventEditPage' }); + } + }) + } } .width('100%') .height('100%') @@ -1739,6 +1753,19 @@ struct Index { .backgroundColor($r('app.color.chip_off_bg')) } } + if (e.location !== '') { + Row({ space: 4 }) { + Text('📍') + .fontSize(11) + .fontColor($r('app.color.text_hint')) + Text(e.location) + .fontSize(12) + .fontColor($r('app.color.text_hint')) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .width('100%') + } } .alignItems(HorizontalAlign.Start) .layoutWeight(1) diff --git a/entry/src/main/ets/pages/SettingsPage.ets b/entry/src/main/ets/pages/SettingsPage.ets index 4ec3165..fcd971a 100644 --- a/entry/src/main/ets/pages/SettingsPage.ets +++ b/entry/src/main/ets/pages/SettingsPage.ets @@ -2,17 +2,15 @@ // 设置页:系统日历模式(仅显示/备份到 CalDAV)+ 混合显示开关 + 自动同步间隔 + 后台同步 import { router } from '@kit.ArkUI'; import { common } from '@kit.AbilityKit'; -import { pasteboard } from '@kit.BasicServicesKit'; import { notificationManager } from '@kit.NotificationKit'; import { AppSettings } from '../common/AppSettings'; import { BackgroundSyncService } from '../common/BackgroundSyncService'; import { AccountStore, DavAccount } from '../common/AccountStore'; import { ReminderService } from '../common/ReminderService'; -import { LogUtil } from '../common/LogUtil'; const INTERVAL_OPTIONS: number[] = [1, 5, 15, 30, 60]; -/** 日历本选项(备份目标 / 只读标记共用) */ +/** 日历本选项(备份目标 / 只读标记 / 静音标记共用) */ class BackupTarget { calKey: string = ''; label: string = ''; @@ -28,13 +26,12 @@ struct SettingsPage { @State sysMode: string = 'display'; // display | backup @State backupKey: string = ''; @State backupTargets: BackupTarget[] = []; - @State allBooks: BackupTarget[] = []; // 全部 DAV 日历本(只读标记管理用) + @State allBooks: BackupTarget[] = []; // 全部 DAV 日历本(只读/静音标记管理用) @State manualKeys: string[] = []; // 手动标记只读的 calKey @State mutedKeys: string[] = []; // 提醒静音的 calKey - @State showLogSheet: boolean = false; // 同步日志查看弹层 - @State logText: string = ''; @State notifyEnabled: boolean = true; // 通知权限状态(提醒依赖) - @State lastTestResult: string = ''; + @State showFeatures: boolean = false; // 软件特性弹层 + @State showHelp: boolean = false; // 使用帮助弹层 private context?: common.Context; aboutToAppear(): void { @@ -43,7 +40,6 @@ struct SettingsPage { return; } this.context = ctx; - LogUtil.init(ctx); AppSettings.getShowSystemCalendar(ctx).then((v: boolean): void => { this.showSystem = v; }); @@ -62,6 +58,13 @@ struct SettingsPage { AppSettings.getMutedReminderKeys(ctx).then((v: string[]): void => { this.mutedKeys = v; }); + this.refreshNotifyState(); + this.loadBackupSettings(); + this.loadAllBooks(); + } + + /** 重新检测通知权限状态 */ + private refreshNotifyState(): void { notificationManager.isNotificationEnabled() .then((v: boolean): void => { this.notifyEnabled = v; @@ -69,11 +72,9 @@ struct SettingsPage { .catch((): void => { this.notifyEnabled = false; }); - this.loadBackupSettings(); - this.loadAllBooks(); } - /** 加载全部 DAV 日历本(含探测到的写权限,只读标记管理用) */ + /** 加载全部 DAV 日历本(含探测到的写权限,只读/静音标记管理用) */ private async loadAllBooks(): Promise { if (this.context === undefined) { return; @@ -116,14 +117,6 @@ struct SettingsPage { }); } - /** 日历本当前只读状态文案 */ - private bookStateLabel(b: BackupTarget): string { - if (this.manualKeys.includes(b.calKey)) { - return '只读(手动)'; - } - return b.serverWritable ? '可写' : '只读'; - } - /** 切换日历本提醒静音:静音后该本所有日程不再发布系统提醒 */ private async toggleMuteBook(b: BackupTarget): Promise { if (this.context === undefined) { @@ -179,7 +172,6 @@ struct SettingsPage { // 手动标记只读的本不可作为备份目标 const manual: string[] = await AppSettings.getManualReadonlyKeys(this.context); this.backupTargets = targets.filter((t: BackupTarget): boolean => !manual.includes(t.calKey)); - this.backupTargets = targets; const saved: string = await AppSettings.getBackupCalKey(this.context); this.backupKey = targets.some((t: BackupTarget): boolean => t.calKey === saved) ? saved : ''; } @@ -191,7 +183,6 @@ struct SettingsPage { this.sysMode = mode; await AppSettings.setSysCalMode(this.context, mode); if (mode === 'backup' && this.backupKey === '') { - // 自动选中第一个可写日历本 if (this.backupTargets.length > 0) { this.backupKey = this.backupTargets[0].calKey; await AppSettings.setBackupCalKey(this.context, this.backupKey); @@ -287,63 +278,30 @@ struct SettingsPage { }); } - /** 打开同步日志查看弹层 */ - private openLogSheet(): void { - if (this.context === undefined) { - return; - } - this.logText = LogUtil.readAll(this.context); - this.showLogSheet = true; - } - - /** 复制全部日志到剪贴板(方便粘贴给我诊断) */ - private copyLog(): void { + /** 去系统设置开启本应用通知 */ + private async openNotifySettings(): Promise { if (this.context === undefined) { return; } try { - const data: pasteboard.PasteData = pasteboard.createData( - pasteboard.MIMETYPE_TEXT_PLAIN, LogUtil.readAll(this.context)); - pasteboard.getSystemPasteboard().setData(data) - .then((): void => { - this.getUIContext().getPromptAction().showToast({ message: '日志已复制到剪贴板' }); - }) - .catch((): void => { - this.getUIContext().getPromptAction().showToast({ message: '复制失败' }); - }); + await notificationManager.requestEnableNotification(this.context as common.UIAbilityContext); + this.refreshNotifyState(); // 返回后重新检测状态 } catch (err) { - this.getUIContext().getPromptAction().showToast({ message: '复制失败' }); + // 用户取消或未跳转,状态保持原样 } } - private clearLog(): void { - if (this.context === undefined) { + /** 打开外部链接(如开源仓库) */ + private async openUrl(url: string): Promise { + const context = this.getUIContext().getHostContext(); + if (context === undefined) { return; } - LogUtil.clear(this.context); - this.logText = '(日志已清空)'; - this.getUIContext().getPromptAction().showToast({ message: '日志已清空' }); - } - - /** 发布 1 分钟后的测试提醒,验证提醒链路 */ - private async testReminder(): Promise { - if (this.context === undefined) { - return; + try { + await (context as common.UIAbilityContext).openLink(url); + } catch (err) { + this.getUIContext().getPromptAction().showToast({ message: '无法打开链接' }); } - this.lastTestResult = await ReminderService.publishTestReminder( - this.context as common.UIAbilityContext); - } - - /** 按当前数据立即重建提醒,显示发布条数 */ - private async refreshNow(): Promise { - if (this.context === undefined) { - return; - } - const n: number = await ReminderService.refreshReminders( - this.context as common.UIAbilityContext); - this.getUIContext().getPromptAction().showToast({ - message: n > 0 ? `已发布 ${n} 个提醒` : '没有发布任何提醒(可能都过了提醒时间、被静音或通知未开启,详见日志)' - }); } build() { @@ -432,132 +390,30 @@ struct SettingsPage { .backgroundColor($r('app.color.card_bg')) .border({ width: 1, color: $r('app.color.shadow_color') }) - // 日历本只读标记(部分服务器不在 CalDAV 层拒绝写入,自动探测无法区分时手动标记) - Column({ space: 8 }) { - Row({ space: 10 }) { - Column({ space: 2 }) { - Text('日历本只读标记') - .fontSize(15) - .fontWeight(FontWeight.Medium) - .fontColor($r('app.color.text_primary')) - Text('点击日历本切换只读标记:标记后其中日程仅可查看详情,新建日程时不再显示') - .fontSize(12) - .fontColor($r('app.color.text_secondary')) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - } - .width('100%') - if (this.allBooks.length === 0) { - Text('暂无 CalDAV 日历本') - .fontSize(12) - .fontColor($r('app.color.text_hint')) - .width('100%') - } - ForEach(this.allBooks, (b: BackupTarget) => { - Row({ space: 8 }) { - Text(b.label) - .fontSize(13) - .fontColor($r('app.color.text_primary')) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .layoutWeight(1) - Text(this.bookStateLabel(b)) - .fontSize(11) - .fontColor(this.bookStateLabel(b) === '可写' - ? $r('app.color.success') : $r('app.color.error')) - .padding({ left: 8, right: 8, top: 3, bottom: 3 }) - .borderRadius(10) - .backgroundColor(this.bookStateLabel(b) === '可写' - ? $r('app.color.success_bg') : $r('app.color.error_bg')) - } - .width('100%') - .padding({ top: 6, bottom: 6 }) - .onClick(() => { - this.toggleManualBook(b); - }) - }, (b: BackupTarget) => `${b.calKey}_${this.manualKeys.includes(b.calKey) ? 1 : 0}`) - } - .alignItems(HorizontalAlign.Start) - .width('100%') - .padding(14) - .borderRadius(12) - .backgroundColor($r('app.color.card_bg')) - .border({ width: 1, color: $r('app.color.shadow_color') }) - - // 日历本提醒静音(如别人分享的日历本,静音后其中所有日程不再提醒) - Column({ space: 8 }) { - Row({ space: 10 }) { - Column({ space: 2 }) { - Text('日历本提醒静音') - .fontSize(15) - .fontWeight(FontWeight.Medium) - .fontColor($r('app.color.text_primary')) - Text('点击日历本切换静音:静音后该日历本的所有日程都不再提醒(日程仍正常显示)') - .fontSize(12) - .fontColor($r('app.color.text_secondary')) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - } - .width('100%') - if (this.allBooks.length === 0) { - Text('暂无 CalDAV 日历本') - .fontSize(12) - .fontColor($r('app.color.text_hint')) - .width('100%') - } - ForEach(this.allBooks, (b: BackupTarget) => { - Row({ space: 8 }) { - Text(b.label) - .fontSize(13) - .fontColor($r('app.color.text_primary')) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .layoutWeight(1) - Text(this.mutedKeys.includes(b.calKey) ? '已静音' : '提醒中') - .fontSize(11) - .fontColor(this.mutedKeys.includes(b.calKey) - ? $r('app.color.error') : $r('app.color.success')) - .padding({ left: 8, right: 8, top: 3, bottom: 3 }) - .borderRadius(10) - .backgroundColor(this.mutedKeys.includes(b.calKey) - ? $r('app.color.error_bg') : $r('app.color.success_bg')) - } - .width('100%') - .padding({ top: 6, bottom: 6 }) - .onClick(() => { - this.toggleMuteBook(b); - }) - }, (b: BackupTarget) => `mute_${b.calKey}_${this.mutedKeys.includes(b.calKey) ? 1 : 0}`) - } - .alignItems(HorizontalAlign.Start) - .width('100%') - .padding(14) - .borderRadius(12) - .backgroundColor($r('app.color.card_bg')) - .border({ width: 1, color: $r('app.color.shadow_color') }) - // 数据修复:手动触发一次性全量重拉(重建提醒等本地残缺字段) - Row({ space: 10 }) { - Column({ space: 2 }) { - Text('重建日程数据') - .fontSize(15) - .fontWeight(FontWeight.Medium) - .fontColor($r('app.color.text_primary')) - Text('同步时重新从服务器拉取全部日程的完整数据,修复提醒丢失等本地数据问题。耗时段落请保持 App 在前台、屏幕常亮') - .fontSize(12) - .fontColor($r('app.color.text_secondary')) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('重建日程数据') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('同步时重新从服务器拉取全部日程的完整数据,修复提醒丢失等本地数据问题。耗时段落请保持 App 在前台、屏幕常亮') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + Button('重建') + .fontSize(13) + .backgroundColor($r('app.color.brand')) + .onClick(() => { + this.askRebuildData(); + }) } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - Button('重建') - .fontSize(13) - .backgroundColor($r('app.color.brand')) - .onClick(() => { - this.askRebuildData(); - }) + .width('100%') } + .alignItems(HorizontalAlign.Start) .width('100%') .padding(14) .borderRadius(12) @@ -565,27 +421,31 @@ struct SettingsPage { .border({ width: 1, color: $r('app.color.shadow_color') }) // 混合显示系统日历 - Row({ space: 10 }) { - Column({ space: 2 }) { - Text('混合显示系统日历') - .fontSize(15) - .fontWeight(FontWeight.Medium) - .fontColor($r('app.color.text_primary')) - Text('关闭后只显示 DAV 账号的日程') - .fontSize(12) - .fontColor($r('app.color.text_secondary')) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('混合显示系统日历') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('关闭后只显示 DAV 账号的日程') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + Toggle({ type: ToggleType.Switch, isOn: this.showSystem }) + .selectedColor($r('app.color.brand')) + .onChange((isOn: boolean) => { + if (isOn !== this.showSystem) { + this.showSystem = isOn; + this.saveShowSystem(isOn); + } + }) } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - Toggle({ type: ToggleType.Switch, isOn: this.showSystem }) - .selectedColor($r('app.color.brand')) - .onChange((isOn: boolean) => { - if (isOn !== this.showSystem) { - this.showSystem = isOn; - this.saveShowSystem(isOn); - } - }) + .width('100%') } + .alignItems(HorizontalAlign.Start) .width('100%') .padding(14) .borderRadius(12) @@ -593,33 +453,37 @@ struct SettingsPage { .border({ width: 1, color: $r('app.color.shadow_color') }) // 自动同步间隔 - Row({ space: 10 }) { - Column({ space: 2 }) { - Text('自动同步间隔') - .fontSize(15) - .fontWeight(FontWeight.Medium) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('自动同步间隔') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('应用打开期间按此间隔自动同步') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + Select([{ value: '1 分钟' }, { value: '5 分钟' }, { value: '15 分钟' }, + { value: '30 分钟' }, { value: '1 小时' }] as SelectOption[]) + .selected(INTERVAL_OPTIONS.indexOf(this.intervalMinutes)) + .value(this.intervalLabel(this.intervalMinutes)) .fontColor($r('app.color.text_primary')) - Text('应用打开期间按此间隔自动同步') - .fontSize(12) - .fontColor($r('app.color.text_secondary')) + .font({ size: 14 }) + .optionFont({ size: 14 }) + .selectedOptionFont({ size: 14 }) + .onSelect((index: number) => { + if (index >= 0 && index < INTERVAL_OPTIONS.length) { + this.intervalMinutes = INTERVAL_OPTIONS[index]; + this.saveInterval(INTERVAL_OPTIONS[index]); + } + }) } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - Select([{ value: '1 分钟' }, { value: '5 分钟' }, { value: '15 分钟' }, - { value: '30 分钟' }, { value: '1 小时' }] as SelectOption[]) - .selected(INTERVAL_OPTIONS.indexOf(this.intervalMinutes)) - .value(this.intervalLabel(this.intervalMinutes)) - .fontColor($r('app.color.text_primary')) - .font({ size: 14 }) - .optionFont({ size: 14 }) - .selectedOptionFont({ size: 14 }) - .onSelect((index: number) => { - if (index >= 0 && index < INTERVAL_OPTIONS.length) { - this.intervalMinutes = INTERVAL_OPTIONS[index]; - this.saveInterval(INTERVAL_OPTIONS[index]); - } - }) + .width('100%') } + .alignItems(HorizontalAlign.Start) .width('100%') .padding(14) .borderRadius(12) @@ -627,42 +491,47 @@ struct SettingsPage { .border({ width: 1, color: $r('app.color.shadow_color') }) // 后台持续同步(长时任务) - Row({ space: 10 }) { - Column({ space: 2 }) { - Text('后台持续同步') - .fontSize(15) - .fontWeight(FontWeight.Medium) - .fontColor($r('app.color.text_primary')) - Text('开启后申请后台长时任务,App 退到后台也按设定间隔继续同步(通知栏会显示常驻通知)') - .fontSize(12) - .fontColor($r('app.color.text_secondary')) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('后台持续同步') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('开启后申请后台长时任务,App 退到后台也按设定间隔继续同步(通知栏会显示常驻通知)') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + Toggle({ type: ToggleType.Switch, isOn: this.backgroundSync }) + .selectedColor($r('app.color.brand')) + .onChange((isOn: boolean) => { + if (isOn !== this.backgroundSync) { + this.backgroundSync = isOn; + this.saveBackgroundSync(isOn); + } + }) } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - Toggle({ type: ToggleType.Switch, isOn: this.backgroundSync }) - .selectedColor($r('app.color.brand')) - .onChange((isOn: boolean) => { - if (isOn !== this.backgroundSync) { - this.backgroundSync = isOn; - this.saveBackgroundSync(isOn); - } - }) + .width('100%') } + .alignItems(HorizontalAlign.Start) .width('100%') .padding(14) .borderRadius(12) .backgroundColor($r('app.color.card_bg')) .border({ width: 1, color: $r('app.color.shadow_color') }) - // 提醒诊断:通知权限状态 + 测试提醒 + 立即刷新 + + // 通知权限:仅检测是否开启,未开启则引导去设置 Column({ space: 8 }) { Row({ space: 10 }) { Column({ space: 2 }) { Row({ space: 6 }) { - Text('提醒诊断') + Text('通知权限') .fontSize(15) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.text_primary')) - Text(this.notifyEnabled ? '通知已开启' : '通知未开启!') + Text(this.notifyEnabled ? '已开启' : '未开启') .fontSize(11) .fontColor(this.notifyEnabled ? $r('app.color.success') : $r('app.color.error')) .padding({ left: 8, right: 8, top: 3, bottom: 3 }) @@ -670,8 +539,8 @@ struct SettingsPage { .backgroundColor(this.notifyEnabled ? $r('app.color.success_bg') : $r('app.color.error_bg')) } Text(this.notifyEnabled - ? '点"测试提醒"验证链路:1 分钟后应收到通知' - : '系统设置 → 通知管理 → 找到本应用 → 允许通知,日程提醒才能弹出') + ? '日程提醒将正常以系统通知弹出' + : '未开启系统通知,日程提醒无法弹出') .fontSize(12) .fontColor($r('app.color.text_secondary')) } @@ -679,25 +548,13 @@ struct SettingsPage { .layoutWeight(1) } .width('100%') - Row({ space: 10 }) { - Button('测试提醒') + if (!this.notifyEnabled) { + Button('去开启通知') .fontSize(13) .backgroundColor($r('app.color.brand')) .onClick(() => { - this.testReminder(); + this.openNotifySettings(); }) - Button('立即刷新提醒') - .fontSize(13) - .backgroundColor($r('app.color.text_secondary')) - .onClick(() => { - this.refreshNow(); - }) - } - if (this.lastTestResult !== '') { - Text(this.lastTestResult) - .fontSize(12) - .fontColor($r('app.color.text_secondary')) - .width('100%') } } .alignItems(HorizontalAlign.Start) @@ -707,26 +564,142 @@ struct SettingsPage { .backgroundColor($r('app.color.card_bg')) .border({ width: 1, color: $r('app.color.shadow_color') }) - // 同步日志:查看/复制/清空(真机测试诊断用) - Row({ space: 10 }) { - Column({ space: 2 }) { - Text('同步日志') - .fontSize(15) - .fontWeight(FontWeight.Medium) - .fontColor($r('app.color.text_primary')) - Text('记录每次同步/推送/提醒的详细过程,测试出问题时复制日志用于分析') - .fontSize(12) - .fontColor($r('app.color.text_secondary')) + // 软件特性(点击弹层;bindSheet 挂在本卡片上,避免与其它弹层冲突) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('软件特性') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('了解这个应用的特点与隐私理念') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + Text('›') + .fontSize(18) + .fontColor($r('app.color.text_hint')) } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - Button('查看') - .fontSize(13) - .backgroundColor($r('app.color.brand')) - .onClick(() => { - this.openLogSheet(); - }) + .width('100%') } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding(14) + .borderRadius(12) + .backgroundColor($r('app.color.card_bg')) + .border({ width: 1, color: $r('app.color.shadow_color') }) + .onClick(() => { + this.showFeatures = true; + }) + .bindSheet($$this.showFeatures, this.featureSheet(), { + height: 560, + dragBar: true, + showClose: true, + title: { title: '软件特性' } + }) + + // 使用帮助(点击弹层;bindSheet 挂在本卡片上) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('使用帮助') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('查看交互逻辑与使用方法') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + Text('›') + .fontSize(18) + .fontColor($r('app.color.text_hint')) + } + .width('100%') + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding(14) + .borderRadius(12) + .backgroundColor($r('app.color.card_bg')) + .border({ width: 1, color: $r('app.color.shadow_color') }) + .onClick(() => { + this.showHelp = true; + }) + .bindSheet($$this.showHelp, this.helpSheet(), { + height: 560, + dragBar: true, + showClose: true, + title: { title: '使用帮助' } + }) + + // 日历本只读 / 静音(放在最下方;本多时列表较长) + Column({ space: 8 }) { + Row({ space: 10 }) { + Column({ space: 2 }) { + Text('日历本只读 / 静音') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.text_primary')) + Text('只读:标记后该本日程仅可查看、不再可新建;静音:该本所有日程不再提醒') + .fontSize(12) + .fontColor($r('app.color.text_secondary')) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + } + .width('100%') + if (this.allBooks.length === 0) { + Text('暂无 CalDAV 日历本') + .fontSize(12) + .fontColor($r('app.color.text_hint')) + .width('100%') + } + ForEach(this.allBooks, (b: BackupTarget) => { + Row({ space: 8 }) { + Text(b.label) + .fontSize(13) + .fontColor($r('app.color.text_primary')) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + Button(this.manualKeys.includes(b.calKey) ? '已只读' : '只读') + .fontSize(11) + .height(28) + .fontColor(this.manualKeys.includes(b.calKey) + ? $r('app.color.button_text') : $r('app.color.text_secondary')) + .backgroundColor(this.manualKeys.includes(b.calKey) + ? $r('app.color.brand') : $r('app.color.card_bg')) + .border({ + width: this.manualKeys.includes(b.calKey) ? 0 : 1, + color: $r('app.color.shadow_color') + }) + .onClick(() => { + this.toggleManualBook(b); + }) + Button(this.mutedKeys.includes(b.calKey) ? '已静音' : '静音') + .fontSize(11) + .height(28) + .fontColor(this.mutedKeys.includes(b.calKey) + ? $r('app.color.button_text') : $r('app.color.text_secondary')) + .backgroundColor(this.mutedKeys.includes(b.calKey) + ? $r('app.color.error') : $r('app.color.card_bg')) + .border({ + width: this.mutedKeys.includes(b.calKey) ? 0 : 1, + color: $r('app.color.shadow_color') + }) + .onClick(() => { + this.toggleMuteBook(b); + }) + } + .width('100%') + .padding({ top: 6, bottom: 6 }) + }, (b: BackupTarget) => `book_${b.calKey}_r${this.manualKeys.includes(b.calKey) ? 1 : 0}_m${this.mutedKeys.includes(b.calKey) ? 1 : 0}`) + } + .alignItems(HorizontalAlign.Start) .width('100%') .padding(14) .borderRadius(12) @@ -739,11 +712,6 @@ struct SettingsPage { .layoutWeight(1) .align(Alignment.Top) .scrollBar(BarState.Off) - .bindSheet($$this.showLogSheet, this.logSheetBuilder(), { - height: 560, - dragBar: true, - title: { title: '同步日志' } - }) } .width('100%') .height('100%') @@ -751,41 +719,151 @@ struct SettingsPage { } @Builder - logSheetBuilder() { - Column({ space: 10 }) { - Row({ space: 10 }) { - Button('复制全部') + featureSheet() { + Scroll() { + Column({ space: 12 }) { + Text('隐私与数据') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 数据自持:你的所有日程、待办只存放在你自己(或所在机构)的 CalDAV 服务器上,我们(开发者)不保存你的任何日程或账户数据。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 账户加密:CalDAV 账号的密码在本地以 AES-256-GCM 加密保存,只有发起同步时才临时解密使用,不会以明文留存。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 开源透明:本项目完全开源,代码可自行查看与审计,不藏任何后台上传逻辑。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Button('打开开源仓库') .fontSize(13) .backgroundColor($r('app.color.brand')) .onClick(() => { - this.copyLog(); + this.openUrl('https://gitee.com/dryangyq/SyncCalendar'); }) - Button('清空') - .fontSize(13) - .backgroundColor($r('app.color.error')) - .onClick(() => { - this.clearLog(); - }) - Button('刷新') - .fontSize(13) - .backgroundColor($r('app.color.text_secondary')) - .onClick(() => { - this.openLogSheet(); - }) - } - Scroll() { - Text(this.logText === '' ? '(日志为空)' : this.logText) - .fontSize(11) + Text('同步能力') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + .margin({ top: 4 }) + Text('• 双向同步:本地新建/修改会自动推送到服务器,服务器上的变更也会自动拉取,多设备保持一致。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 多账号多日历本统一管理,并可与手机系统日历混合展示。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 完整重复规则(RRULE)与多种提前提醒(可多选:5/10/15/30/60 分钟/1 天等)。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 只读日历本标记、他人分享日历本一键静音免打扰。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 后台持续同步(长时任务),退到后台也按设定间隔继续同步。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 日程地址一键拉起高德地图导航(系统地理编码定位,无需额外配置)。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('• 提醒走系统本地通知,不依赖厂商云推送;在系统无代理提醒配额时自动降级为应用内通知(需 App 在前台或开启后台同步)。') + .fontSize(14) .fontColor($r('app.color.text_primary')) .width('100%') } - .layoutWeight(1) + .width('100%') .align(Alignment.Top) - .scrollBar(BarState.On) + .padding(16) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.page_bg')) + } + + @Builder + helpSheet() { + Scroll() { + Column({ space: 12 }) { + Text('一、添加账号') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + Text('在「账号」页填写你的 CalDAV 服务器地址(如群晖 https://域名:5006)、用户名与密码。账号信息在本地加密保存,不会上传给开发者。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + + Text('二、查看与新建日程') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + .margin({ top: 4 }) + Text('首页支持月 / 周 / 日 / 列表视图。点「+」新建日程:选择所属日历本、开始/结束时间、重复规则、提醒(可多选),并可填写地址用于导航。点列表中任意日程都会弹出详情;详情中若有地址,点「导航 ›」即可拉起高德地图;可写日程还能点「编辑日程」修改。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + + Text('三、同步') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + .margin({ top: 4 }) + Text('App 处于打开状态时按设定间隔自动同步。开启「后台持续同步」后,退到后台也会继续同步(通知栏有常驻通知)。同步期间会保持屏幕常亮,避免熄屏中断。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + + Text('四、只读与静音') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + .margin({ top: 4 }) + Text('把某个日历本标记为「只读」后,其中日程只能查看、不再可新建(适用于他人共享给你、或服务器侧不让改的本)。把分享来的日历本「静音」,其中所有日程都不再弹提醒,避免被打扰。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + + Text('五、提醒') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + .margin({ top: 4 }) + Text('日程会在开始前按设定提前量弹出系统通知。请在系统设置中开启本应用「通知」权限(设置页可检测并一键前往开启)。提醒依赖系统代理提醒配额;若系统额度为 0,将自动改用应用内通知。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + + Text('六、系统日历与数据修复') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.text_primary')) + .width('100%') + .margin({ top: 4 }) + Text('可开启「混合显示系统日历」,把手机自带日历也一并展示;或开启「备份到 CalDAV」,把系统本地日程导入所选日历本,换机/丢失也有备份。若发现提醒丢失等异常,可点「重建日程数据」让下次同步重新拉取完整数据。') + .fontSize(14) + .fontColor($r('app.color.text_primary')) + .width('100%') + } + .width('100%') + .align(Alignment.Top) + .padding(16) } .width('100%') .height('100%') - .padding(16) .backgroundColor($r('app.color.page_bg')) } }