@@ -17,6 +17,69 @@ export class AppSettings {
|
||||
private static readonly KEY_REMINDER_TICK: string = 'reminder_tick'; // 应用内提醒上次检查时间戳
|
||||
private static readonly KEY_FULL_REFETCH: string = 'full_refetch_done'; // 一次性全量重拉已完成
|
||||
private static readonly KEY_DEFAULT_VIEW: string = 'default_view'; // 打开 App 默认视图
|
||||
private static readonly KEY_POLICY_AGREED: string = 'policy_agreed'; // 是否已同意隐私政策与用户协议
|
||||
// 首启功能引导"用户已看过并关闭"的标记。键名带版本号:改动引导内容后把 vN 加 1,用户即可再看一次。
|
||||
// 注意:只在用户主动关闭("开始使用" / ✕ / 点遮罩)时才置 true,**不再"显示前就置 true"**——
|
||||
// 否则一旦某次因故没显示出来,旧标记会把功能永久锁死(v2 正是这样被锁住 → 本次升到 v3)。
|
||||
private static readonly KEY_TIPS_SHOWN: string = 'tips_shown_v3';
|
||||
|
||||
/** 隐私政策网址(华为 AGC 隐私政策托管服务生成,上架时填此地址) */
|
||||
static readonly PRIVACY_URL: string =
|
||||
'https://agreement-drcn.hispace.dbankcloud.cn/index.html?lang=zh&agreementId=2039401852717510592';
|
||||
/** 用户服务协议网址 */
|
||||
static readonly AGREEMENT_URL: string = 'https://synccalendar.yangyq.net/usa.html';
|
||||
|
||||
/**
|
||||
* 是否已同意《隐私政策》与《用户协议》(首启同意弹窗)。
|
||||
* 默认 false —— 未同意前不得申请任何系统权限、不得加载/同步数据。
|
||||
*/
|
||||
static async getPolicyAgreed(context: common.Context): Promise<boolean> {
|
||||
try {
|
||||
const store: preferences.Preferences =
|
||||
await preferences.getPreferences(context, AppSettings.STORE);
|
||||
return await store.get(AppSettings.KEY_POLICY_AGREED, false) as boolean;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async setPolicyAgreed(context: common.Context, value: boolean): Promise<void> {
|
||||
try {
|
||||
const store: preferences.Preferences =
|
||||
await preferences.getPreferences(context, AppSettings.STORE);
|
||||
await store.put(AppSettings.KEY_POLICY_AGREED, value);
|
||||
await store.flush();
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
console.error(`保存隐私政策同意状态失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 首启功能引导卡片是否已展示过(避免每次启动都弹)。
|
||||
* 默认 false —— 启动(首次同意后 / 已同意用户再次启动)时若为 false 会自动弹出一次并置为 true。
|
||||
*/
|
||||
static async getTipsShown(context: common.Context): Promise<boolean> {
|
||||
try {
|
||||
const store: preferences.Preferences =
|
||||
await preferences.getPreferences(context, AppSettings.STORE);
|
||||
return await store.get(AppSettings.KEY_TIPS_SHOWN, false) as boolean;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async setTipsShown(context: common.Context, value: boolean): Promise<void> {
|
||||
try {
|
||||
const store: preferences.Preferences =
|
||||
await preferences.getPreferences(context, AppSettings.STORE);
|
||||
await store.put(AppSettings.KEY_TIPS_SHOWN, value);
|
||||
await store.flush();
|
||||
} catch (err) {
|
||||
const e = err as BusinessError;
|
||||
console.error(`保存功能引导展示状态失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开 App 后默认展示的视图:'month' | 'week' | 'agenda'(默认 month) */
|
||||
static async getDefaultView(context: common.Context): Promise<string> {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// entry/src/main/ets/common/TipsPanel.ets
|
||||
// 首启功能引导卡片:底部弹出,Swiper 三页,左下"上一步" / 右下"下一步"(末页变"开始使用")。
|
||||
// 数据源 items 中图标为 media 下的 SVG(tip_security / tip_calendar / tip_mute)。
|
||||
// 同时被首页(首启自动弹出)与设置页(手动 revisar)复用。
|
||||
import { AppSettings } from './AppSettings';
|
||||
|
||||
/** 单张引导卡的数据 */
|
||||
interface TipItem {
|
||||
icon: Resource; // app.media 下的 SVG 矢量图标
|
||||
title: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
@Component
|
||||
export struct TipsPanel {
|
||||
@State current: number = 0;
|
||||
private controller: SwiperController = new SwiperController();
|
||||
/** 末页点击"开始使用"时回调(由宿主关闭弹层) */
|
||||
onClose: () => void = () => {};
|
||||
|
||||
private items: TipItem[] = [
|
||||
{
|
||||
icon: $r('app.media.tip_security'),
|
||||
title: '你的数据安全,由你做主',
|
||||
desc: '我们不在服务器保存任何日程或账号数据。日历只在你的设备与你自己填写的 CalDAV 服务器之间同步,' +
|
||||
'全程加密保存。开源、无后门,数据始终属于你。'
|
||||
},
|
||||
{
|
||||
icon: $r('app.media.tip_calendar'),
|
||||
title: '系统日历,也能一起管',
|
||||
desc: '手机系统日历可与 CalDAV 日历混合显示在同一时间轴;还能把系统日历本通过 CalDAV 同步备份,' +
|
||||
'一处编辑、处处更新。'
|
||||
},
|
||||
{
|
||||
icon: $r('app.media.tip_mute'),
|
||||
title: '只读与静音,自由设置',
|
||||
desc: '每个日历本都能单独设为只读或静音:只读日历不出现在新建选择中、避免误改;' +
|
||||
'静音日历照常显示,但不再推送提醒。'
|
||||
}
|
||||
];
|
||||
|
||||
build() {
|
||||
Column({ space: 0 }) {
|
||||
Swiper(this.controller) {
|
||||
ForEach(this.items, (item: TipItem, idx: number) => {
|
||||
Column({ space: 16 }) {
|
||||
Image(item.icon)
|
||||
.width(140)
|
||||
.height(140)
|
||||
.objectFit(ImageFit.Contain)
|
||||
.margin({ top: 24, bottom: 8 })
|
||||
Text(item.title)
|
||||
.fontSize(19)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
Text(item.desc)
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
.lineHeight(22)
|
||||
.padding({ left: 28, right: 28 })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}, (item: TipItem, idx: number) => `tip_${idx}`)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.indicator(new DotIndicator().selectedColor('#007DFF').color('#CFD8E3').itemWidth(6).itemHeight(6))
|
||||
.loop(false)
|
||||
.onChange((index: number): void => {
|
||||
this.current = index;
|
||||
})
|
||||
|
||||
// 底部导航:左下角"上一步" / 右下角"下一步"(末页变"开始使用")
|
||||
Row() {
|
||||
Button('上一步')
|
||||
.fontSize(15)
|
||||
.width(96)
|
||||
.height(42)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.fontColor(this.current === 0 ? $r('app.color.text_hint') : $r('app.color.text_primary'))
|
||||
.enabled(this.current > 0)
|
||||
.onClick(() => {
|
||||
if (this.current > 0) {
|
||||
this.current -= 1;
|
||||
this.controller.changeIndex(this.current, false);
|
||||
}
|
||||
})
|
||||
Button(this.current >= this.items.length - 1 ? '开始使用' : '下一步')
|
||||
.fontSize(15)
|
||||
.width(96)
|
||||
.height(42)
|
||||
.backgroundColor($r('app.color.brand'))
|
||||
.fontColor($r('app.color.button_text'))
|
||||
.onClick(() => {
|
||||
if (this.current >= this.items.length - 1) {
|
||||
this.onClose();
|
||||
} else {
|
||||
this.current += 1;
|
||||
this.controller.changeIndex(this.current, false);
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.SpaceBetween)
|
||||
.padding({ left: 20, right: 20, top: 12, bottom: 20 })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { notificationManager } from '@kit.NotificationKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { BackgroundSyncService } from '../common/BackgroundSyncService';
|
||||
import { LogUtil } from '../common/LogUtil';
|
||||
import { AppSettings } from '../common/AppSettings';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
|
||||
@@ -18,14 +19,22 @@ export default class EntryAbility extends UIAbility {
|
||||
BackgroundSyncService.initOnLaunch(this.context);
|
||||
// 服务卡片"添加"按钮跳转:把 widgetAction=add 透传给主页面(在 AppStorage 中暂存)
|
||||
this.handleWidgetParam(want);
|
||||
// 申请通知权限:日程提醒、后台同步常驻通知都依赖它(系统只弹一次授权框)
|
||||
notificationManager.isNotificationEnabled()
|
||||
.then((enabled: boolean): Promise<void> => {
|
||||
if (!enabled) {
|
||||
return notificationManager.requestEnableNotification(this.context)
|
||||
.catch((): void => {});
|
||||
// 通知权限:日程提醒、后台同步常驻通知都依赖它(系统只弹一次授权框)。
|
||||
// 必须在用户同意《隐私政策》之后才申请 —— 同意前不得申请任何权限;
|
||||
// 首次安装此处跳过,用户点"同意并继续"后由 Index.initPermissionAndLoad() 补申请。
|
||||
AppSettings.getPolicyAgreed(this.context)
|
||||
.then((agreed: boolean): Promise<void> => {
|
||||
if (!agreed) {
|
||||
LogUtil.write('未同意隐私政策:跳过通知权限申请');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.resolve();
|
||||
return notificationManager.isNotificationEnabled()
|
||||
.then((enabled: boolean): Promise<void> => {
|
||||
if (!enabled) {
|
||||
return notificationManager.requestEnableNotification(this.context).catch((): void => {});
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
})
|
||||
.catch((): void => {});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { mediaquery, router } from '@kit.ArkUI';
|
||||
import { common, abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
|
||||
import { geoLocationManager } from '@kit.LocationKit';
|
||||
import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
|
||||
import { notificationManager } from '@kit.NotificationKit';
|
||||
import { DavAccount, AccountStore, CalSource, TYPE_CALDAV } from '../common/AccountStore';
|
||||
import { DisplayEvent, CalendarDataService } from '../common/CalendarDataService';
|
||||
import { SyncEngine } from '../common/SyncEngine';
|
||||
@@ -13,6 +14,7 @@ import { LunarUtil } from '../common/LunarUtil';
|
||||
import { CardDataService } from '../common/CardDataService';
|
||||
import { ReminderService } from '../common/ReminderService';
|
||||
import { AppSettings } from '../common/AppSettings';
|
||||
import { TipsPanel } from '../common/TipsPanel';
|
||||
import { EventDb, LocalEvent, RemoteEvent } from '../common/EventDb';
|
||||
import { RruleUtil } from '../common/RruleUtil';
|
||||
import { IcsUtil } from '../common/IcsUtil';
|
||||
@@ -73,6 +75,12 @@ struct Index {
|
||||
@State dayTimeline: DayTimeline = new DayTimeline(); // 选中日(月/周视图用)
|
||||
@State agendaTimelines: AgendaTimelineGroup[] = []; // 列表视图:每天一条时间轴
|
||||
|
||||
// ---- 隐私政策与用户协议首启同意(未同意前不申请任何权限、不加载数据) ----
|
||||
@State policyChecked: boolean = false; // 是否已读取过同意状态(避免首帧闪烁)
|
||||
@State policyAgreed: boolean = false; // 是否已同意《隐私政策》与《用户协议》
|
||||
private notifAsked: boolean = false; // 通知权限是否已申请(同意后才申请)
|
||||
@State showTips: boolean = false; // 首启功能引导(整页覆盖层:遮罩 + 底部卡片 + Swiper 三页)
|
||||
|
||||
aboutToAppear(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
@@ -85,14 +93,17 @@ struct Index {
|
||||
this.selectedDate = this.startOfDay(now.getTime());
|
||||
this.rebuildPages();
|
||||
this.initLandscapeListener();
|
||||
this.initPermissionAndLoad();
|
||||
// 读取"默认视图"设置:打开 App 后按用户选择展示(月/周/列表,默认月)
|
||||
if (ctx !== undefined) {
|
||||
AppSettings.getDefaultView(ctx).then((v: string): void => {
|
||||
this.mode = v;
|
||||
if (v === 'agenda') {
|
||||
this.ensureAgendaData();
|
||||
// 首启同意门禁:未同意《隐私政策》与《用户协议》前,不申请任何系统权限、不加载/同步数据(AGC 硬性要求)
|
||||
if (ctx === undefined) {
|
||||
this.policyChecked = true;
|
||||
this.startApp(ctx);
|
||||
} else {
|
||||
AppSettings.getPolicyAgreed(ctx).then((ok: boolean): void => {
|
||||
this.policyChecked = true;
|
||||
if (ok) {
|
||||
this.startApp(ctx);
|
||||
}
|
||||
// 未同意:仅显示整页同意门禁(consentGate),不申请权限、不加载数据
|
||||
});
|
||||
}
|
||||
// 红线"播放"效果:每 30s 更新 nowMs,让当前时间红线随真实时间推进(同时刷新色块"进行中"标记)
|
||||
@@ -129,6 +140,15 @@ struct Index {
|
||||
|
||||
/** 从设置页/账号页返回时刷新(同步间隔、系统日历开关立即生效),并处理编辑账号后的待同步 */
|
||||
onPageShow(): void {
|
||||
// 未同意《隐私政策》前不加载、不同步任何数据
|
||||
if (!this.policyAgreed) {
|
||||
return;
|
||||
}
|
||||
// 首启功能引导兜底:已同意用户若在 aboutToAppear 阶段漏触发,这里再补一次(幂等:已展示过会直接跳过)
|
||||
const tipCtx = this.getUIContext().getHostContext();
|
||||
if (tipCtx !== undefined) {
|
||||
this.maybeShowTips(tipCtx);
|
||||
}
|
||||
// 卡片"添加"按钮深链:首次冷启动经 onCreate→AppStorage 落到这里消费
|
||||
this.consumeWidgetAction();
|
||||
// 无条件刷新:之前"列表为空就跳过"的条件会导致账号列表卡死在空状态,
|
||||
@@ -164,6 +184,7 @@ struct Index {
|
||||
return;
|
||||
}
|
||||
this.reloadAll();
|
||||
this.ensureNotificationPermission(context as common.UIAbilityContext);
|
||||
if (!this.permissionAsked) {
|
||||
this.permissionAsked = true;
|
||||
// 申请读取全部日历权限(用于混合展示系统日程)
|
||||
@@ -174,6 +195,101 @@ struct Index {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请通知权限(日程提醒、后台同步常驻通知都依赖它)。
|
||||
* 必须在用户同意《隐私政策》之后才调用 —— 同意前不得申请任何权限。
|
||||
*/
|
||||
private ensureNotificationPermission(context: common.UIAbilityContext): void {
|
||||
if (this.notifAsked) {
|
||||
return;
|
||||
}
|
||||
this.notifAsked = true;
|
||||
notificationManager.isNotificationEnabled()
|
||||
.then((enabled: boolean): Promise<void> => {
|
||||
if (!enabled) {
|
||||
return notificationManager.requestEnableNotification(context).catch((): void => {});
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch((): void => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同意之后(或"已同意用户"启动时)的正式初始化:
|
||||
* 申请权限、加载数据、读取默认视图,并首启弹一次功能引导卡片。
|
||||
* 首次同意(acceptPolicy)与已同意直接启动(aboutToAppear)都走这里,保证两条路径都能弹出引导。
|
||||
*/
|
||||
private startApp(ctx: common.Context | undefined): void {
|
||||
this.policyAgreed = true;
|
||||
this.initPermissionAndLoad();
|
||||
if (ctx !== undefined) {
|
||||
// 读取"默认视图"设置:打开 App 后按用户选择展示(月/周/列表,默认月)
|
||||
AppSettings.getDefaultView(ctx).then((v: string): void => {
|
||||
this.mode = v;
|
||||
if (v === 'agenda') {
|
||||
this.ensureAgendaData();
|
||||
}
|
||||
});
|
||||
this.maybeShowTips(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 首启功能引导:仅在用户从未看过时显示一次(整页覆盖层),之后不再出现。
|
||||
* 注意:这里**不写"已看过"标记** —— 标记由 hideTips() 在用户主动关闭时写入。
|
||||
* 若在此处提前写标记,一旦显示环节出问题就会把引导永久锁死(历史踩过坑)。
|
||||
*/
|
||||
private maybeShowTips(ctx: common.Context): void {
|
||||
AppSettings.getTipsShown(ctx).then((seen: boolean): void => {
|
||||
if (seen) {
|
||||
return;
|
||||
}
|
||||
// 略延迟:先让主页面完成首帧布局,并尽量错开紧随其后的权限申请系统弹窗
|
||||
setTimeout((): void => {
|
||||
this.showTips = true;
|
||||
}, 600);
|
||||
});
|
||||
}
|
||||
|
||||
/** 关闭功能引导覆盖层("开始使用" / ✕ / 点遮罩 均走这里),此刻才标记"已看过" */
|
||||
private hideTips(): void {
|
||||
this.showTips = false;
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
AppSettings.setTipsShown(ctx, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户点击"同意并继续":持久化同意标记,之后才申请权限、加载数据 */
|
||||
private acceptPolicy(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
AppSettings.setPolicyAgreed(ctx, true);
|
||||
}
|
||||
this.startApp(ctx);
|
||||
}
|
||||
|
||||
/** 用户点击"不同意并退出":不申请任何权限、不处理任何数据,直接退出应用 */
|
||||
private rejectPolicy(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
(ctx as common.UIAbilityContext).terminateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开外部链接(隐私政策 / 用户协议 / 开源仓库等) */
|
||||
private async openExternalUrl(url: string): Promise<void> {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await (ctx as common.UIAbilityContext).openLink(url);
|
||||
} catch (err) {
|
||||
this.getUIContext().getPromptAction().showToast({ message: '无法打开链接' });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 工具 ----------
|
||||
private startOfDay(ms: number): number {
|
||||
const d = new Date(ms);
|
||||
@@ -881,6 +997,23 @@ struct Index {
|
||||
.onClick(() => {
|
||||
this.addEvent();
|
||||
})
|
||||
|
||||
// 未同意《隐私政策》与《用户协议》:整页门禁盖在最上层(不申请权限、不加载数据)
|
||||
if (!this.policyChecked) {
|
||||
// 读取同意状态期间的占位(极短),避免闪一下主界面或门禁页
|
||||
Column() {
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
} else if (!this.policyAgreed) {
|
||||
this.consentGate()
|
||||
}
|
||||
|
||||
// 功能引导:整页覆盖层,放根 Stack 最后一层(盖住主界面与悬浮按钮)
|
||||
if (this.showTips) {
|
||||
this.tipsOverlay()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
@@ -892,6 +1025,126 @@ struct Index {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 首启隐私政策同意门禁(整页渲染,不用弹窗 —— 弹窗在本环境实测无法弹出)。
|
||||
* AGC 要求:明确同意前不得申请权限、不得收集/处理个人信息,故未同意时整页挡住主界面。
|
||||
*/
|
||||
@Builder
|
||||
consentGate() {
|
||||
Column({ space: 14 }) {
|
||||
Text('隐私政策与用户协议')
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('100%')
|
||||
.margin({ top: 40 })
|
||||
Text('欢迎使用「同步日历」。在使用前,请阅读并同意以下条款。')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('100%')
|
||||
Text('我们不会保存你的日程与账号数据:日历仅在你的设备与你自己填写的 CalDAV 服务器之间同步;位置信息仅在你点击“导航”时用于调用第三方地图,不会上传。')
|
||||
.fontSize(13)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.width('100%')
|
||||
Button('《隐私政策》')
|
||||
.width('100%')
|
||||
.fontSize(14)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.fontColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
this.openExternalUrl(AppSettings.PRIVACY_URL);
|
||||
})
|
||||
Button('《用户服务协议》')
|
||||
.width('100%')
|
||||
.fontSize(14)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.fontColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
this.openExternalUrl(AppSettings.AGREEMENT_URL);
|
||||
})
|
||||
Blank()
|
||||
Text('点击“同意并继续”,即表示你已阅读并同意上述两份文件。')
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
.width('100%')
|
||||
Row({ space: 10 }) {
|
||||
Button('不同意并退出')
|
||||
.fontSize(15)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.onClick(() => {
|
||||
this.rejectPolicy();
|
||||
})
|
||||
Button('同意并继续')
|
||||
.fontSize(15)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
this.acceptPolicy();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ bottom: 24 })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ left: 24, right: 24, top: 20, bottom: 12 })
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 首启功能引导:整页覆盖层(半透明遮罩 + 底部卡片),由 TipsPanel 提供 Swiper 三页与
|
||||
* 左下"上一步" / 右下"下一步"。
|
||||
* 用页面内容渲染而非 bindSheet/CustomDialog —— 本环境从"启动时的异步回调"里弹出系统弹层不可靠
|
||||
* (详见 MEMORY:CustomDialogController 静默失败;bindSheet 同样实测弹不出来)。
|
||||
*/
|
||||
@Builder
|
||||
tipsOverlay() {
|
||||
Column() {
|
||||
// 上部留白:露出被遮罩压暗的主界面,制造"从下方弹出"的层次(点空白处也可关闭)
|
||||
Column() {
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.onClick((): void => {
|
||||
this.hideTips();
|
||||
})
|
||||
|
||||
// 底部引导卡片
|
||||
Column() {
|
||||
// 右上角关闭
|
||||
Row() {
|
||||
Blank()
|
||||
Text('✕')
|
||||
.fontSize(16)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||
.onClick((): void => {
|
||||
this.hideTips();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 12, right: 12, top: 8 })
|
||||
|
||||
Column() {
|
||||
TipsPanel({ onClose: (): void => { this.hideTips(); } })
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.height('70%')
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
.borderRadius({ topLeft: 20, topRight: 20 })
|
||||
.clip(true)
|
||||
.shadow({ radius: 16, color: $r('app.color.shadow_color'), offsetX: 0, offsetY: -2 })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor('#66000000')
|
||||
}
|
||||
|
||||
@Builder
|
||||
header() {
|
||||
Row({ space: 10 }) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AppSettings } from '../common/AppSettings';
|
||||
import { BackgroundSyncService } from '../common/BackgroundSyncService';
|
||||
import { AccountStore, DavAccount } from '../common/AccountStore';
|
||||
import { ReminderService } from '../common/ReminderService';
|
||||
import { TipsPanel } from '../common/TipsPanel';
|
||||
|
||||
const INTERVAL_OPTIONS: number[] = [1, 5, 15, 30, 60];
|
||||
|
||||
@@ -32,6 +33,8 @@ struct SettingsPage {
|
||||
@State notifyEnabled: boolean = true; // 通知权限状态(提醒依赖)
|
||||
@State showFeatures: boolean = false; // 软件特性弹层
|
||||
@State showHelp: boolean = false; // 使用帮助弹层
|
||||
@State showLegal: boolean = false; // 隐私政策与用户协议弹层
|
||||
@State showTips: boolean = false; // 功能引导卡片(底部弹出)
|
||||
@State defaultView: string = 'month'; // 打开 App 默认视图:month | week | agenda
|
||||
private context?: common.Context;
|
||||
|
||||
@@ -692,6 +695,78 @@ struct SettingsPage {
|
||||
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.showTips = true;
|
||||
})
|
||||
.bindSheet($$this.showTips, this.tipsSheet(), {
|
||||
height: '72%',
|
||||
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.showLegal = true;
|
||||
})
|
||||
.bindSheet($$this.showLegal, this.legalSheet(), {
|
||||
height: 400,
|
||||
dragBar: true,
|
||||
showClose: true,
|
||||
title: { title: '隐私政策与用户协议' }
|
||||
})
|
||||
|
||||
// 日历本只读 / 静音(放在最下方;本多时列表较长)
|
||||
Column({ space: 8 }) {
|
||||
Row({ space: 10 }) {
|
||||
@@ -774,6 +849,49 @@ struct SettingsPage {
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
}
|
||||
|
||||
/** 功能引导卡片(与首页共用 TipsPanel) */
|
||||
@Builder
|
||||
tipsSheet() {
|
||||
TipsPanel({ onClose: (): void => { this.showTips = false; } })
|
||||
}
|
||||
|
||||
/** 隐私政策与用户协议弹层 */
|
||||
@Builder
|
||||
legalSheet() {
|
||||
Scroll() {
|
||||
Column({ space: 12 }) {
|
||||
Text('我们遵循"数据自持、最小必要"原则:开发者不保存你的日程与账号数据,不收集通讯录、相册、剪贴板等信息,不含任何第三方统计或广告 SDK。')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('100%')
|
||||
Button('查看《隐私政策》')
|
||||
.width('100%')
|
||||
.fontSize(15)
|
||||
.backgroundColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
this.openUrl(AppSettings.PRIVACY_URL);
|
||||
})
|
||||
Button('查看《用户服务协议》')
|
||||
.width('100%')
|
||||
.fontSize(15)
|
||||
.backgroundColor($r('app.color.brand'))
|
||||
.onClick(() => {
|
||||
this.openUrl(AppSettings.AGREEMENT_URL);
|
||||
})
|
||||
Text('《隐私政策》说明本应用如何收集、使用与保护你的信息(日历、位置等);《用户服务协议》说明使用本应用的权利与义务。两份文件随功能调整更新,最新版本以上述链接为准。')
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.align(Alignment.Top)
|
||||
.padding(16)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor($r('app.color.page_bg'))
|
||||
}
|
||||
|
||||
@Builder
|
||||
featureSheet() {
|
||||
Scroll() {
|
||||
|
||||
Reference in New Issue
Block a user