feat(privacy): add personalized ads toggle in settings
- Add PrivacySettings schema with can_sell field (default: false) - Move privacy toggle to GeneralSettingsScreen with clear labeling - Update l10n for zh/en/zh_hant with user-friendly copy - Clean up redundant PrivacyNotificationSettingsScreen - Update profile-protocol.md to reflect new privacy schema - Fix basedpyright warnings in user.py
This commit is contained in:
@@ -325,7 +325,7 @@ class _EryaoAppState extends State<EryaoApp> {
|
||||
}
|
||||
|
||||
Future<ProfileSettingsV1> _saveProfile(ProfileSettingsV1 updated) async {
|
||||
final saved = await _profileApi.updateProfile(updated);
|
||||
final saved = await _profileApi.updateSettings(updated);
|
||||
if (!mounted) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@ class ProfileApi {
|
||||
'timezone': settings.preferences.timezone,
|
||||
'country': settings.preferences.country,
|
||||
},
|
||||
'privacy': settings.privacy,
|
||||
'privacy': {
|
||||
'can_sell': settings.privacy.canSell,
|
||||
'profile_visibility': settings.privacy.profileVisibility,
|
||||
},
|
||||
'notification': {
|
||||
'allow_notifications': settings.notification.allowNotifications,
|
||||
'allow_vibration': settings.notification.allowVibration,
|
||||
@@ -115,6 +118,17 @@ class ProfileApi {
|
||||
)
|
||||
: const PreferenceSettings();
|
||||
|
||||
final privacyRaw = settingsRaw is Map<String, dynamic>
|
||||
? settingsRaw['privacy']
|
||||
: null;
|
||||
final privacy = privacyRaw is Map<String, dynamic>
|
||||
? PrivacySettings(
|
||||
canSell: (privacyRaw['can_sell'] as bool?) ?? false,
|
||||
profileVisibility:
|
||||
(privacyRaw['profile_visibility'] as String?) ?? 'public',
|
||||
)
|
||||
: const PrivacySettings();
|
||||
|
||||
final notificationRaw = settingsRaw is Map<String, dynamic>
|
||||
? settingsRaw['notification']
|
||||
: null;
|
||||
@@ -150,10 +164,7 @@ class ProfileApi {
|
||||
avatarPath: json['avatar_path'] as String?,
|
||||
avatarUrl: json['avatar_url'] as String?,
|
||||
preferences: preferences,
|
||||
privacy: settingsRaw is Map<String, dynamic>
|
||||
? (settingsRaw['privacy'] as Map<String, dynamic>? ??
|
||||
const <String, dynamic>{})
|
||||
: const <String, dynamic>{},
|
||||
privacy: privacy,
|
||||
notification: notification,
|
||||
divinationTutorial: divinationTutorial,
|
||||
);
|
||||
|
||||
@@ -84,6 +84,23 @@ class DivinationTutorialSettings {
|
||||
}
|
||||
}
|
||||
|
||||
class PrivacySettings {
|
||||
const PrivacySettings({
|
||||
this.canSell = false,
|
||||
this.profileVisibility = 'public',
|
||||
});
|
||||
|
||||
final bool canSell;
|
||||
final String profileVisibility;
|
||||
|
||||
PrivacySettings copyWith({bool? canSell, String? profileVisibility}) {
|
||||
return PrivacySettings(
|
||||
canSell: canSell ?? this.canSell,
|
||||
profileVisibility: profileVisibility ?? this.profileVisibility,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileSettingsV1 {
|
||||
const ProfileSettingsV1({
|
||||
this.version = 1,
|
||||
@@ -92,7 +109,7 @@ class ProfileSettingsV1 {
|
||||
this.avatarPath,
|
||||
this.avatarUrl,
|
||||
this.preferences = const PreferenceSettings(),
|
||||
this.privacy = const <String, Object?>{},
|
||||
this.privacy = const PrivacySettings(),
|
||||
this.notification = const NotificationSettings(),
|
||||
this.divinationTutorial = const DivinationTutorialSettings(),
|
||||
});
|
||||
@@ -103,7 +120,7 @@ class ProfileSettingsV1 {
|
||||
final String? avatarPath;
|
||||
final String? avatarUrl;
|
||||
final PreferenceSettings preferences;
|
||||
final Map<String, Object?> privacy;
|
||||
final PrivacySettings privacy;
|
||||
final NotificationSettings notification;
|
||||
final DivinationTutorialSettings divinationTutorial;
|
||||
|
||||
@@ -114,7 +131,7 @@ class ProfileSettingsV1 {
|
||||
String? avatarPath,
|
||||
String? avatarUrl,
|
||||
PreferenceSettings? preferences,
|
||||
Map<String, Object?>? privacy,
|
||||
PrivacySettings? privacy,
|
||||
NotificationSettings? notification,
|
||||
DivinationTutorialSettings? divinationTutorial,
|
||||
}) {
|
||||
|
||||
@@ -104,6 +104,21 @@ class _GeneralSettingsScreenState extends State<GeneralSettingsScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
SectionLabel(text: l10n.settingsSectionPrivacy),
|
||||
SettingsGroupCard(
|
||||
children: [
|
||||
SettingsSwitchTile(
|
||||
icon: Icons.security_rounded,
|
||||
title: l10n.settingsDoNotSellTitle,
|
||||
value: _settings.privacy.canSell,
|
||||
tint: colors.primary,
|
||||
background: colors.surfaceContainerHighest,
|
||||
showDivider: false,
|
||||
onChanged: (value) => _updatePrivacy(canSell: value),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
SectionLabel(text: l10n.settingsSectionNotification),
|
||||
SettingsGroupCard(
|
||||
children: [
|
||||
@@ -151,6 +166,15 @@ class _GeneralSettingsScreenState extends State<GeneralSettingsScreen> {
|
||||
await widget.onSettingsChanged(_settings);
|
||||
}
|
||||
|
||||
void _updatePrivacy({bool? canSell}) {
|
||||
final newPrivacy = _settings.privacy.copyWith(canSell: canSell);
|
||||
final newSettings = _settings.copyWith(privacy: newPrivacy);
|
||||
setState(() {
|
||||
_settings = newSettings;
|
||||
});
|
||||
widget.onSettingsChanged(newSettings);
|
||||
}
|
||||
|
||||
void _updateNotification({bool? allowNotifications, bool? allowVibration}) {
|
||||
final newNotification = _settings.notification.copyWith(
|
||||
allowNotifications: allowNotifications,
|
||||
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../l10n/app_localizations.dart';
|
||||
import '../../../../shared/theme/design_tokens.dart';
|
||||
import '../../data/models/profile_settings.dart';
|
||||
import 'settings_placeholder_screen.dart';
|
||||
import '../widgets/settings_section_widgets.dart';
|
||||
|
||||
class PrivacyNotificationSettingsScreen extends StatelessWidget {
|
||||
const PrivacyNotificationSettingsScreen({super.key, required this.settings});
|
||||
|
||||
final ProfileSettingsV1 settings;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settingsPrivacyAndNotificationTitle),
|
||||
centerTitle: true,
|
||||
backgroundColor: colors.surfaceContainerLow,
|
||||
surfaceTintColor: colors.surfaceContainerLow,
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
children: [
|
||||
SectionLabel(text: l10n.settingsSectionPrivacy),
|
||||
SettingsGroupCard(
|
||||
children: [
|
||||
SettingsMenuTile(
|
||||
icon: Icons.visibility_outlined,
|
||||
title: l10n.settingsPrivacyProfileVisibility,
|
||||
subtitle: l10n.settingsPlaceholderState(
|
||||
settings.privacy.length,
|
||||
),
|
||||
tint: colors.primary,
|
||||
background: colors.surfaceContainerHighest,
|
||||
onTap: () => _openPlaceholder(
|
||||
context,
|
||||
title: l10n.settingsPrivacyProfileVisibility,
|
||||
value: l10n.settingsComingSoon,
|
||||
description: l10n.settingsPrivacyHint,
|
||||
),
|
||||
),
|
||||
SettingsMenuTile(
|
||||
icon: Icons.psychology_alt_outlined,
|
||||
title: l10n.settingsPrivacyPersonalization,
|
||||
subtitle: l10n.settingsComingSoon,
|
||||
tint: colors.primary,
|
||||
background: colors.surfaceContainerHighest,
|
||||
onTap: () => _openPlaceholder(
|
||||
context,
|
||||
title: l10n.settingsPrivacyPersonalization,
|
||||
value: l10n.settingsComingSoon,
|
||||
description: l10n.settingsPrivacyHint,
|
||||
),
|
||||
),
|
||||
SettingsMenuTile(
|
||||
icon: Icons.history_toggle_off_rounded,
|
||||
title: l10n.settingsPrivacyHistoryVisibility,
|
||||
subtitle: l10n.settingsComingSoon,
|
||||
tint: colors.primary,
|
||||
background: colors.surfaceContainerHighest,
|
||||
showDivider: false,
|
||||
onTap: () => _openPlaceholder(
|
||||
context,
|
||||
title: l10n.settingsPrivacyHistoryVisibility,
|
||||
value: l10n.settingsComingSoon,
|
||||
description: l10n.settingsPrivacyHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
SectionLabel(text: l10n.settingsSectionNotification),
|
||||
SettingsGroupCard(
|
||||
children: [
|
||||
SettingsMenuTile(
|
||||
icon: Icons.notifications_outlined,
|
||||
title: l10n.settingsNotificationSystem,
|
||||
subtitle: l10n.settingsPlaceholderState(1),
|
||||
tint: colors.secondary,
|
||||
background: colors.surfaceContainerHighest,
|
||||
onTap: () => _openPlaceholder(
|
||||
context,
|
||||
title: l10n.settingsNotificationSystem,
|
||||
value: l10n.settingsComingSoon,
|
||||
description: l10n.settingsNotificationHint,
|
||||
),
|
||||
),
|
||||
SettingsMenuTile(
|
||||
icon: Icons.campaign_outlined,
|
||||
title: l10n.settingsNotificationActivity,
|
||||
subtitle: l10n.settingsComingSoon,
|
||||
tint: colors.secondary,
|
||||
background: colors.surfaceContainerHighest,
|
||||
onTap: () => _openPlaceholder(
|
||||
context,
|
||||
title: l10n.settingsNotificationActivity,
|
||||
value: l10n.settingsComingSoon,
|
||||
description: l10n.settingsNotificationHint,
|
||||
),
|
||||
),
|
||||
SettingsMenuTile(
|
||||
icon: Icons.auto_graph_outlined,
|
||||
title: l10n.settingsNotificationResult,
|
||||
subtitle: l10n.settingsComingSoon,
|
||||
tint: colors.secondary,
|
||||
background: colors.surfaceContainerHighest,
|
||||
showDivider: false,
|
||||
onTap: () => _openPlaceholder(
|
||||
context,
|
||||
title: l10n.settingsNotificationResult,
|
||||
value: l10n.settingsComingSoon,
|
||||
description: l10n.settingsNotificationHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openPlaceholder(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String value,
|
||||
required String description,
|
||||
}) async {
|
||||
await Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => SettingsPlaceholderScreen(
|
||||
title: title,
|
||||
value: value,
|
||||
description: description,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -483,5 +483,9 @@
|
||||
"settingsInviteEmptyDescription": "Each friend who registers using your invite code gives you bonus credits",
|
||||
"settingsInviteInputLabel": "Enter invite code (optional)",
|
||||
"settingsInviteInputHint": "Enter code to bind your inviter",
|
||||
"settingsInviteInvalidCode": "Please enter a valid 6-character invite code"
|
||||
"settingsInviteInvalidCode": "Please enter a valid 6-character invite code",
|
||||
"settingsDoNotSellTitle": "Personalized Ads",
|
||||
"settingsDoNotSellDescription": "When off, your personal info won't be used for ad recommendations",
|
||||
"settingsDoNotSellEnabled": "Off",
|
||||
"settingsDoNotSellDisabled": "On"
|
||||
}
|
||||
|
||||
@@ -2300,6 +2300,30 @@ abstract class AppLocalizations {
|
||||
/// In zh, this message translates to:
|
||||
/// **'请输入有效的6位邀请码'**
|
||||
String get settingsInviteInvalidCode;
|
||||
|
||||
/// No description provided for @settingsDoNotSellTitle.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'个性化广告推荐'**
|
||||
String get settingsDoNotSellTitle;
|
||||
|
||||
/// No description provided for @settingsDoNotSellDescription.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'关闭后,我们不会将您的个人信息用于广告推荐'**
|
||||
String get settingsDoNotSellDescription;
|
||||
|
||||
/// No description provided for @settingsDoNotSellEnabled.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'已关闭'**
|
||||
String get settingsDoNotSellEnabled;
|
||||
|
||||
/// No description provided for @settingsDoNotSellDisabled.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'已开启'**
|
||||
String get settingsDoNotSellDisabled;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -1211,4 +1211,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settingsInviteInvalidCode =>
|
||||
'Please enter a valid 6-character invite code';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellTitle => 'Personalized Ads';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellDescription =>
|
||||
'When off, your personal info won\'t be used for ad recommendations';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellEnabled => 'Off';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellDisabled => 'On';
|
||||
}
|
||||
|
||||
@@ -1159,6 +1159,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settingsInviteInvalidCode => '请输入有效的6位邀请码';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellTitle => '个性化广告推荐';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellDescription => '关闭后,我们不会将您的个人信息用于广告推荐';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellEnabled => '已关闭';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellDisabled => '已开启';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, using the Han script (`zh_Hant`).
|
||||
@@ -2072,4 +2084,16 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String get settingsInviteInvalidCode => '請輸入有效的6位邀請碼';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellTitle => '個人化廣告推薦';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellDescription => '關閉後,我們不會將您的個人資訊用於廣告推薦';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellEnabled => '已關閉';
|
||||
|
||||
@override
|
||||
String get settingsDoNotSellDisabled => '已開啟';
|
||||
}
|
||||
|
||||
@@ -483,5 +483,9 @@
|
||||
"settingsInviteEmptyDescription": "每成功邀请一位好友注册,您将获得积分奖励",
|
||||
"settingsInviteInputLabel": "输入邀请码(选填)",
|
||||
"settingsInviteInputHint": "输入邀请码绑定您的邀请人",
|
||||
"settingsInviteInvalidCode": "请输入有效的6位邀请码"
|
||||
"settingsInviteInvalidCode": "请输入有效的6位邀请码",
|
||||
"settingsDoNotSellTitle": "个性化广告推荐",
|
||||
"settingsDoNotSellDescription": "关闭后,我们不会将您的个人信息用于广告推荐",
|
||||
"settingsDoNotSellEnabled": "已关闭",
|
||||
"settingsDoNotSellDisabled": "已开启"
|
||||
}
|
||||
|
||||
@@ -385,5 +385,9 @@
|
||||
"wuXingTu": "土",
|
||||
"wuXingJin": "金",
|
||||
"wuXingShui": "水",
|
||||
"retry": "重試"
|
||||
"retry": "重試",
|
||||
"settingsDoNotSellTitle": "個人化廣告推薦",
|
||||
"settingsDoNotSellDescription": "關閉後,我們不會將您的個人資訊用於廣告推薦",
|
||||
"settingsDoNotSellEnabled": "已關閉",
|
||||
"settingsDoNotSellDisabled": "已開啟"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user