rtc_prd/airhub_app/lib/pages/profile/settings_sub_pages.dart
seaislee1209 f26627a83f fix: 修复页面渐隐过大 + MP3进度条拖动 + notification字段修复
- 修复设置子页面/帮助页/喂养指南顶部渐隐区域过大(12%→5%)导致首行文字过淡
- 修复4首预设音乐(卡皮巴拉系列)因ID3标签过大导致进度条无法拖动
- 修复notification_page中notif.detail→notif.content字段名错误
- 新增测试生成的故事和音频文件

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 11:28:49 +08:00

599 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:airhub_app/theme/design_tokens.dart';
import 'package:airhub_app/widgets/animated_gradient_background.dart';
class SettingsContentPage extends StatelessWidget {
final String title;
final String date;
final List<Widget> children;
const SettingsContentPage({
super.key,
required this.title,
required this.date,
required this.children,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
const AnimatedGradientBackground(),
Column(
children: [
_buildHeader(context),
Expanded(
child: ShaderMask(
shaderCallback: (Rect rect) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black, Colors.black, Colors.transparent],
stops: [0.0, 0.05, 0.95, 1.0],
).createShader(rect);
},
blendMode: BlendMode.dstIn,
child: SingleChildScrollView(
padding: EdgeInsets.only(
top: 20,
left: 24,
right: 24,
bottom: 40 + MediaQuery.of(context).padding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...children,
const SizedBox(height: 40),
Center(
child: Text(
'更新日期:$date',
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 13,
),
),
),
],
),
),
),
),
],
),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 20,
left: AppSpacing.lg,
right: AppSpacing.lg,
bottom: AppSpacing.md,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.iconBtnBg,
borderRadius: BorderRadius.circular(AppRadius.button),
),
child: const Icon(
Icons.arrow_back_ios_new,
color: AppColors.textPrimary,
size: 18,
),
),
),
Text(title, style: AppTextStyles.title),
const SizedBox(width: 40), // Balance
],
),
);
}
}
// Helper methods to generate text styles
Widget buildSectionTitle(String text) {
return Padding(
padding: const EdgeInsets.only(top: 32, bottom: 12),
child: Text(
text,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
);
}
Widget buildParagraph(String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
text,
textAlign: TextAlign.justify,
style: const TextStyle(
fontSize: 15,
height: 1.6,
color: Color(0xFF374151),
),
),
);
}
Widget buildBulletList(List<String> items) {
return Padding(
padding: const EdgeInsets.only(bottom: 16, left: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: items
.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('', style: TextStyle(fontSize: 15, height: 1.6)),
Expanded(
child: Text(
item,
style: const TextStyle(
fontSize: 15,
height: 1.6,
color: Color(0xFF374151),
),
),
),
],
),
),
)
.toList(),
),
);
}
// Pre-defined pages content factories
class AgreementPage extends StatelessWidget {
const AgreementPage({super.key});
@override
Widget build(BuildContext context) {
return SettingsContentPage(
title: '用户协议',
date: '2025年1月15日',
children: [
buildParagraph('欢迎您使用 Airhub 产品及服务!'),
buildParagraph(
'特别提示: 在您开始使用 Airhub 产品(以下简称"本产品")及相关服务之前,请您务必仔细阅读本《用户协议》(以下简称"本协议")。特别是涉及免除或者限制责任的条款、法律适用和争议解决条款等,请您重点阅读。',
),
buildSectionTitle('1. 服务说明'),
buildParagraph(
'1.1 Airhub Team以下简称"我们"向用户提供包括但不限于设备连接控制、AI 语音交互、角色记忆存储、云端同步等服务(以下简称"本服务")。',
),
buildParagraph('1.2 本服务的具体内容由我们根据实际情况提供,我们有权随时变更、中断或终止部分或全部服务。'),
buildParagraph('1.3 用户理解并同意,本服务仅供用户个人非商业性质的使用。用户不得利用本服务进行销售或其他商业用途。'),
buildSectionTitle('2. 账号注册与使用'),
buildParagraph('2.1 用户在使用本服务时需要注册一个 Airhub 账号。用户应保证注册信息的真实性、准确性和完整性。'),
buildParagraph('2.2 用户有责任妥善保管注册账号信息及密码安全。因用户保管不善可能导致账号被盗及其后果,由用户自行承担。'),
buildParagraph(
'2.3 如发现任何未经授权使用您账号登录、使用本服务的情况,您应立即通知我们。您理解我们对您的任何请求采取行动需要合理时间,我们对在采取行动前已经产生的后果不承担责任。',
),
buildSectionTitle('3. 用户行为规范'),
buildParagraph('用户在使用本服务过程中,应当遵守法律法规,不得从事下列行为:'),
buildBulletList([
'发布、传送、传播、储存危害国家安全、破坏社会稳定、违反公序良俗的内容;',
'发布、传送、传播、储存侮辱、诽谤、淫秽、暴力、赌博等违法违规内容;',
'利用 AI 功能生成虚假信息、诈骗信息或用于非法用途;',
'对 AI 角色进行性骚扰、辱骂或诱导生成不当内容;',
'进行任何危害计算机网络安全的行为,包括但不限于攻击、侵入他人系统。',
]),
buildSectionTitle('4. 个人信息保护'),
buildParagraph(
'4.1 保护用户个人信息是我们的基本原则。我们将按照本协议及《隐私政策》的规定收集、使用、存储和分享您的个人信息。',
),
// ... simplified for brevity, following the pattern
],
);
}
}
class PrivacyPage extends StatelessWidget {
const PrivacyPage({super.key});
@override
Widget build(BuildContext context) {
return SettingsContentPage(
title: '隐私政策',
date: '2025年1月15日',
children: [
buildParagraph('Airhub (以下简称"我们")非常重视您的隐私。本隐私政策(以下简称"本政策")旨在向您说明我们在您使用 Airhub 产品及服务时如何收集、使用、保存、共享和转让您的个人信息,以及您所享有的相关权利。'),
buildParagraph('请您在使用我们的服务前,仔细阅读并了解本政策。'),
buildSectionTitle('1. 我们如何收集您的个人信息'),
buildParagraph('为了向您提供优质的服务,我们会按照合法、正当、必要的原则收集您的信息:'),
buildBulletList([
'账号注册信息:当您注册 Airhub 账号时,我们会收集您的手机号码或电子邮箱地址,用于验证身份及为您提供服务。',
'设备连接信息:当您使用 Airhub 硬件设备时,我们会收集设备的 MAC 地址、SN 序列号、固件版本、IP 地址、Wi-Fi 信号强度等信息,以便实现设备连接、控制及固件升级功能。',
'语音交互数据:当您使用语音功能与 AI 角色互动时,我们会收集您的语音指令及对话内容。这些数据将用于生成 AI 回复并优化模型效果。您可以选择不保留历史对话记录。',
'角色记忆数据:您的 AI 角色养成数据(如亲密度、性格标签、记忆库)存储于云端,以便支持跨设备无缝迁移体验。',
'日志信息:为保障服务安全及运行稳定,我们会收集您的操作日志、错误日志等。',
]),
buildSectionTitle('2. 我们如何使用您的个人信息'),
buildParagraph('我们将收集的信息用于以下用途:'),
buildBulletList([
'提供各项服务包括设备配网、远程控制、AI 语音对话等核心功能。',
'产品优化:分析用户使用习惯,改善产品功能和用户体验。',
'安全保障:监测账号异常状态,防范欺诈风险,保障系统安全。',
'个性化推荐:基于您的角色记忆,为您提供更符合您偏好的 AI 个性化回复。',
]),
buildSectionTitle('3. 信息的共享、转让与公开披露'),
buildParagraph('3.1 共享:我们不会向任何第三方共享您的个人信息,但以下情况除外:'),
buildBulletList([
'获得您的明确同意;',
'为了实现核心功能需要与合作伙伴(如云服务提供商、语音识别技术提供商)共享必要信息;',
'法律法规规定的情形。',
]),
buildParagraph('3.2 转让:我们不会将您的个人信息转让给任何第三方,除非发生合并、收购或破产清算,我们将要求受让方继续受本政策约束。'),
buildSectionTitle('4. 信息的存储与保护'),
buildParagraph('4.1 存储地点:我们依照法律法规的规定,将收集的个人信息存储于中华人民共和国境内。'),
buildParagraph('4.2 存储期限:我们仅在实现服务目的所必需的时间内保留您的个人信息。账号注销后,我们将对您的个人信息进行删除或匿名化处理。'),
buildParagraph('4.3 安全措施:我们采用 SSL 加密传输、AES 数据加密存储、严格的访问权限控制等技术措施保护您的信息安全。'),
buildSectionTitle('5. 您的权利'),
buildParagraph('5.1 访问与更正:您有权登录 APP 查阅或修改您的个人信息。'),
buildParagraph('5.2 删除:您可以通过【我的-设置-账号安全】申请注销账号。注销后,我们将删除您的所有数据且不可恢复。'),
buildParagraph('5.3 撤回同意:您可以通过设备系统设置关闭相关权限(如麦克风权限),撤回您的授权。'),
buildSectionTitle('6. 联系我们'),
buildParagraph('如您对本隐私政策有任何疑问或投诉,请发送邮件至 privacy@airhub.com 联系我们。'),
],
);
}
}
class CollectionListPage extends StatelessWidget {
const CollectionListPage({super.key});
@override
Widget build(BuildContext context) => SettingsContentPage(
title: '个人信息收集清单',
date: '2025年1月15日',
children: [
buildParagraph('为了向您提供 Airhub 的核心服务,我们需要收集以下类型的个人信息。我们将严格遵守法律法规,保护您的个人信息安全。'),
_buildInfoCard('基础功能服务', [
{'label': '收集信息类型', 'value': '手机号码、登录密码'},
{'label': '使用目的', 'value': '用于账号注册、登录、找回密码及身份认证'},
{'label': '收集场景', 'value': '用户注册或登录 APP 时'},
]),
_buildInfoCard('硬件连接与控制', [
{'label': '收集信息类型', 'value': 'Wi-Fi信息(SSID/BSSID)、蓝牙信息、设备序列号(SN)、MAC地址'},
{'label': '使用目的', 'value': '用于发现附近设备、建立蓝牙/Wi-Fi连接、设备配网及固件升级'},
{'label': '收集场景', 'value': '绑定设备、连接设备或使用设备控制功能时'},
]),
_buildInfoCard('AI 语音交互业务', [
{'label': '收集信息类型', 'value': '语音录音、对话文本、交互时间'},
{'label': '使用目的', 'value': '将语音转换为文本以理解指令、生成 AI 回复、优化语音识别模型'},
{'label': '收集场景', 'value': '使用语音对话功能与 AI 角色互动时'},
]),
_buildInfoCard('应用安全保障', [
{'label': '收集信息类型', 'value': '设备IMSI/IMEI、Android ID、IP地址、操作日志'},
{'label': '使用目的', 'value': '风控验证、安全防范、故障排查与分析'},
{'label': '收集场景', 'value': 'APP 运行期间(包括后台运行)'},
]),
],
);
static Widget _buildInfoCard(String title, List<Map<String, String>> rows) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFECCFA8).withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5E3C).withOpacity(0.06),
offset: const Offset(0, 4),
blurRadius: 16,
),
BoxShadow(
color: Colors.black.withOpacity(0.02),
offset: const Offset(0, 1),
blurRadius: 4,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Left warm accent border strip
Container(
width: 4,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFECCFA8), Color(0xFFC99672)],
),
),
),
// Card content
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title with icon
Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: const Color(0xFFECCFA8).withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.shield_outlined,
size: 16,
color: Color(0xFFC99672),
),
),
const SizedBox(width: 10),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
fontSize: 16,
),
),
],
),
const SizedBox(height: 16),
// Info rows
...rows.asMap().entries.map((entry) {
final isLast = entry.key == rows.length - 1;
return Container(
padding: EdgeInsets.only(bottom: isLast ? 0 : 10),
margin: EdgeInsets.only(bottom: isLast ? 0 : 12),
decoration: isLast
? null
: BoxDecoration(
border: Border(
bottom: BorderSide(
color: const Color(0xFFECCFA8).withOpacity(0.15),
style: BorderStyle.solid,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.value['label']!,
style: const TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
entry.value['value']!,
style: const TextStyle(
color: Color(0xFF374151),
fontWeight: FontWeight.w500,
fontSize: 14,
height: 1.4,
),
),
],
),
);
}),
],
),
),
),
],
),
),
),
);
}
}
class SharingListPage extends StatelessWidget {
const SharingListPage({super.key});
@override
Widget build(BuildContext context) => SettingsContentPage(
title: '第三方信息共享清单',
date: '2025年1月15日',
children: [
buildParagraph('为保障 Airhub 的相关功能实现与应用安全稳定运行我们可能会接入第三方提供的软件开发包SDK或服务。我们将审慎评估合作方的安全保障能力并要求其遵守严格的保密协议。'),
_buildShareCard(
company: '阿里云计算有限公司',
sdkType: '阿里云对象存储/API网关 SDK',
purpose: '用于存储角色记忆数据、用户头像及语音文件提供云端API服务稳定性。',
data: '设备信息(IP地址)、网络连接状态。',
method: 'SDK 本机采集',
privacy: 'https://terms.aliyun.com/legal-agreement/terms',
),
_buildShareCard(
company: '深圳市腾讯计算机系统有限公司',
sdkType: '微信开放平台 SDK',
purpose: '用于支持微信账号一键登录、分享内容至微信朋友圈或会话。',
data: '设备标识信息、微信个人授权信息(昵称、头像)。',
method: 'SDK 本机采集',
privacy: 'https://weixin.qq.com/cgi-bin/readtemplate?t=weixin_agreement',
),
_buildShareCard(
company: '深圳市和讯华谷信息技术有限公司',
sdkType: '极光推送 (JPush) SDK',
purpose: '用于实现消息推送功能,向您发送设备状态更新通知或系统公告。',
data: '设备标识符(IMEI/MAC/Android ID/IDFA)、网络信息、应用安装列表。',
method: 'SDK 本机采集',
privacy: 'https://www.jiguang.cn/license/privacy',
),
],
);
static Widget _buildShareCard({
required String company,
required String sdkType,
required String purpose,
required String data,
required String method,
required String privacy,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFECCFA8).withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5E3C).withOpacity(0.06),
offset: const Offset(0, 4),
blurRadius: 16,
),
BoxShadow(
color: Colors.black.withOpacity(0.02),
offset: const Offset(0, 1),
blurRadius: 4,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Left warm accent border strip
Container(
width: 4,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFECCFA8), Color(0xFFC99672)],
),
),
),
// Card content
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
padding: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: const Color(0xFFECCFA8).withOpacity(0.15),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
company,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 16,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFECCFA8).withOpacity(0.15),
const Color(0xFFC99672).withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(6),
),
child: Text(
sdkType,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFFB07D5A),
),
),
),
],
),
),
// Rows
_buildShareRow('使用目的', purpose),
_buildShareRow('收集数据', data),
_buildShareRow('共享方式', method),
_buildShareRow('隐私政策', privacy),
],
),
),
),
],
),
),
),
);
}
static Widget _buildShareRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 70,
child: Text(
label,
style: const TextStyle(
color: Color(0xFF6B7280),
fontSize: 13,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
color: Color(0xFF374151),
fontSize: 13,
height: 1.5,
),
),
),
],
),
);
}
}