Files
xianyan/lib/features/mine/settings/presentation/privacy/permission_management_page.dart
Developer ae1df22732 feat: v6.10.3 多语言翻译补全 + 17项功能修复
- 引导页协议多语言支持(languageId传递)
- 登录页双书名号修复 + 注册页协议勾选
- 个人中心页面多语言(18个翻译键)
- 网络断开提示增加关闭/刷新按钮
- 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索
- iOS快捷按钮重复修复(删除Info.plist静态定义)
- 测试账号123456警告提示
- 扫码登录自动跳转(HTTP轮询+WebSocket双通道)
- 登录页老用户按钮改次要色
- Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0)
- macOS标题栏跟随软件夜间模式
- 平台兼容分发渠道弹窗
- 软件著作权图片+交叉水印
- 桌面小部件平台兼容说明默认收起
- iOS/macOS图标更新+名称确认为闲言
- 12个语言文件补全roleNative+7个分发渠道翻译字段
2026-06-02 04:50:32 +08:00

1046 lines
34 KiB
Dart

/// ============================================================
/// 闲言APP — 权限管理页面
/// 创建时间: 2026-05-07
/// 更新时间: 2026-05-31
/// 作用: 展示所有权限状态,支持请求/跳转系统设置,区分虚拟权限和真实权限
/// 上次更新: 添加多语言支持,使用翻译键替代硬编码中文
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' show RefreshIndicator;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import '../../../../../../core/services/auth/permission_service.dart';
import '../../../../../../core/theme/app_theme.dart';
import '../../../../../../core/theme/app_spacing.dart';
import '../../../../../../core/theme/app_typography.dart';
import '../../../../../../core/theme/app_radius.dart';
import '../../../../../../shared/widgets/containers/deferred_builder.dart';
import '../../../../../../shared/widgets/containers/glass_container.dart';
import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../../../../../l10n/translation_resolver.dart';
import '../../../../../l10n/types/t_settings_permission.dart';
/// 权限状态 Provider
class PermissionStatusNotifier
extends Notifier<AsyncValue<Map<AppPermission, AppPermissionStatus>>> {
@override
AsyncValue<Map<AppPermission, AppPermissionStatus>> build() {
Future.microtask(_check).catchError((Object e, StackTrace st) {
state = AsyncValue.error(e, st);
});
return const AsyncValue.loading();
}
Future<void> _check() async {
try {
final result = await PermissionService.checkAllStatus();
state = AsyncValue.data(result);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
Future<void> refresh() async {
state = const AsyncValue.loading();
await _check();
}
}
final permissionStatusProvider =
NotifierProvider<
PermissionStatusNotifier,
AsyncValue<Map<AppPermission, AppPermissionStatus>>
>(PermissionStatusNotifier.new);
class PermissionManagementPage extends ConsumerStatefulWidget {
const PermissionManagementPage({super.key});
@override
ConsumerState<PermissionManagementPage> createState() =>
_PermissionManagementPageState();
}
class _PermissionManagementPageState
extends ConsumerState<PermissionManagementPage> {
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final statusAsync = ref.watch(permissionStatusProvider);
final perm = ref.watch(translationsProvider).settings.permission;
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
leading: const AdaptiveBackButton(),
middle: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.lock_shield_fill, size: 18, color: ext.accent),
const SizedBox(width: 6),
Text(
perm.pageTitle,
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
],
),
backgroundColor: ext.bgElevated.withValues(alpha: 0.85),
border: null,
),
child: SafeArea(
child: statusAsync.when(
loading: () => const Center(child: CupertinoActivityIndicator()),
error: (e, _) => Center(
child: Text(
perm.loadFailed,
style: AppTypography.body.copyWith(color: ext.textSecondary),
),
),
data: (statuses) => RefreshIndicator(
onRefresh: () =>
ref.read(permissionStatusProvider.notifier).refresh(),
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
children: [
_buildHeader(ext, perm),
const SizedBox(height: AppSpacing.md),
_buildUsageChart(ext, perm),
const SizedBox(height: AppSpacing.md),
_buildSectionTitle(
ext,
perm.appPermissionSection,
perm.appPermissionSubtitle,
),
const SizedBox(height: AppSpacing.sm),
..._buildRealPermissions(statuses, perm),
const SizedBox(height: AppSpacing.md),
_buildSectionTitle(
ext,
perm.systemCapabilitySection,
perm.systemCapabilitySubtitle,
),
const SizedBox(height: AppSpacing.sm),
..._buildVirtualPermissions(statuses, perm),
const SizedBox(height: AppSpacing.md),
_buildDisclaimer(ext, perm),
const SizedBox(height: AppSpacing.xl),
],
),
),
),
),
);
}
/// 顶部说明区
Widget _buildHeader(AppThemeExtension ext, TSettingsPermission perm) {
return GlassContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
CupertinoIcons.info_circle_fill,
size: 20,
color: ext.accent,
),
const SizedBox(width: AppSpacing.sm),
Text(
perm.headerTitle,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
perm.headerDesc,
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
),
],
),
);
}
/// 分区标题
Widget _buildSectionTitle(
AppThemeExtension ext,
String title,
String subtitle,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
child: Row(
children: [
Text(
title,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: AppSpacing.xs),
Text(
subtitle,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
);
}
/// 真实权限列表
List<Widget> _buildRealPermissions(
Map<AppPermission, AppPermissionStatus> statuses,
TSettingsPermission perm,
) {
return AppPermission.values
.where((p) => !p.isVirtual && p.isPlatformRelevant)
.map(
(p) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: _PermissionCard(
permission: p,
status: statuses[p] ?? AppPermissionStatus.notDetermined,
onRequest: () => _requestPermission(p),
onOpenSettings: () => PermissionService.openSettings(),
perm: perm,
),
),
)
.toList();
}
/// 虚拟权限列表
List<Widget> _buildVirtualPermissions(
Map<AppPermission, AppPermissionStatus> statuses,
TSettingsPermission perm,
) {
return AppPermission.values
.where((p) => p.isVirtual)
.map(
(p) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: p == AppPermission.shake
? _ShakePermissionCard(
permission: p,
status: statuses[p] ?? AppPermissionStatus.granted,
onToggle: (enabled) async {
await PermissionService.setShakeEnabled(enabled);
ref.read(permissionStatusProvider.notifier).refresh();
},
perm: perm,
)
: _PermissionCard(
permission: p,
status: statuses[p] ?? AppPermissionStatus.granted,
onRequest: () {},
onOpenSettings: () {},
perm: perm,
),
),
)
.toList();
}
/// 底部免责声明
Widget _buildDisclaimer(AppThemeExtension ext, TSettingsPermission perm) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
child: Text(
perm.disclaimer,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
fontSize: 11,
),
textAlign: TextAlign.center,
),
);
}
/// 请求权限
Future<void> _requestPermission(AppPermission perm) async {
final granted = await PermissionService.requestPermission(context, perm);
if (granted) {
PermissionService.recordUsage(perm);
ref.read(permissionStatusProvider.notifier).refresh();
}
}
/// 权限使用频率饼图
Widget _buildUsageChart(AppThemeExtension ext, TSettingsPermission perm) {
final stats = PermissionService.getUsageStats();
final permissions = AppPermission.values
.where((p) => p.isPlatformRelevant)
.toList();
final chartData = <_PermissionChartEntry>[];
for (final ap in permissions) {
final data = stats[ap.name];
final count = data != null ? (data['count'] as int? ?? 0) : 0;
if (count > 0) {
chartData.add(_PermissionChartEntry(permission: ap, count: count));
}
}
if (chartData.isEmpty) {
return GlassContainer(
child: Column(
children: [
Row(
children: [
Icon(
CupertinoIcons.chart_pie_fill,
size: 20,
color: ext.accent,
),
const SizedBox(width: AppSpacing.sm),
Text(
perm.usageStats,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.md),
Text(
perm.noUsageData,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
);
}
chartData.sort((a, b) => b.count.compareTo(a.count));
final totalCount = chartData.fold<int>(0, (sum, e) => sum + e.count);
return GlassContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(CupertinoIcons.chart_pie_fill, size: 20, color: ext.accent),
const SizedBox(width: AppSpacing.sm),
Text(
perm.usageStats,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
'${perm.totalCountPrefix}$totalCount${perm.totalCountSuffix}',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
const SizedBox(height: AppSpacing.md),
SizedBox(
height: 160,
child: Row(
children: [
SizedBox(
width: 160,
height: 160,
child: DeferredBuilder(builder: (context) => SfCircularChart(
series: <CircularSeries<_PermissionChartEntry, String>>[
DoughnutSeries<_PermissionChartEntry, String>(
animationDuration: 0,
dataSource: chartData,
xValueMapper: (_PermissionChartEntry d, _) =>
d.permission.label(context),
yValueMapper: (_PermissionChartEntry d, _) =>
d.count.toDouble(),
pointColorMapper: (_PermissionChartEntry d, _) =>
d.permission.color,
innerRadius: '38%',
dataLabelSettings: DataLabelSettings(
isVisible: true,
textStyle: AppTypography.caption2.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w700,
),
),
dataLabelMapper: (_PermissionChartEntry d, _) {
final percentage = d.count / totalCount * 100;
return percentage >= 8
? '${percentage.toStringAsFixed(0)}%'
: '';
},
),
],
),
)),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: chartData.take(5).map((data) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: data.permission.color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
data.permission.label(context),
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
'${data.count}${perm.timesSuffix}',
style: AppTypography.caption2.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
);
}).toList(),
),
),
],
),
),
],
),
);
}
}
/// 权限卡片组件 — 增强版
///
/// 顶部: 图标(40x40圆角容器) + 名称 + 标签(必要/可选/系统级)
/// 中间: 详细说明文字
/// 底部: 使用场景列表(每行一个小圆点 + 场景文字)
/// 统计: 最近使用时间 + 使用次数 + 使用频率
/// 右侧: 操作按钮(请求/去设置/已授权箭头/系统级信息图标)
class _PermissionCard extends StatelessWidget {
const _PermissionCard({
required this.permission,
required this.status,
required this.onRequest,
required this.onOpenSettings,
required this.perm,
});
final AppPermission permission;
final AppPermissionStatus status;
final VoidCallback onRequest;
final VoidCallback onOpenSettings;
final TSettingsPermission perm;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final usageStat = PermissionService.getPermissionUsage(permission);
return GlassContainer(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTopRow(context, ext),
const SizedBox(height: AppSpacing.sm),
_buildDescription(context, ext),
if (permission.usageScenes(context).isNotEmpty) ...[
const SizedBox(height: AppSpacing.sm),
_buildUsageScenes(context, ext),
],
if (usageStat != null) ...[
const SizedBox(height: AppSpacing.sm),
_buildUsageStats(ext, usageStat),
],
],
),
),
const SizedBox(width: AppSpacing.sm),
_buildActionButton(ext),
],
),
);
}
/// 顶部行: 图标 + 名称 + 标签 + 状态
Widget _buildTopRow(BuildContext context, AppThemeExtension ext) {
return Row(
children: [
_buildIcon(ext),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
permission.label(context),
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: AppSpacing.xs),
_buildCategoryBadge(ext),
const SizedBox(width: AppSpacing.xs),
_buildStatusBadge(ext),
],
);
}
/// 图标容器
Widget _buildIcon(AppThemeExtension ext) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: permission.color.withValues(alpha: 0.15),
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: Icon(permission.icon, size: 20, color: permission.color),
),
);
}
/// 分类标签: 必要/可选/系统级
Widget _buildCategoryBadge(AppThemeExtension ext) {
if (permission.isVirtual) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF5856D6).withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.gear_solid,
size: 8,
color: Color(0xFF5856D6),
),
const SizedBox(width: 2),
Text(
perm.badgeSystem,
style: AppTypography.caption1.copyWith(
color: const Color(0xFF5856D6),
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
final isReq = permission.isRequired;
final color = isReq ? const Color(0xFFFF3B30) : const Color(0xFF8E8E93);
final text = isReq ? perm.badgeRequired : perm.badgeOptional;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Text(
text,
style: AppTypography.caption1.copyWith(
color: color,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
);
}
/// 授权状态标签
Widget _buildStatusBadge(AppThemeExtension ext) {
final (color, icon, text) = switch (status) {
AppPermissionStatus.granted => (
const Color(0xFF34C759),
CupertinoIcons.checkmark_circle_fill,
perm.statusGranted,
),
AppPermissionStatus.denied => (
const Color(0xFFFF9500),
CupertinoIcons.exclamationmark_circle_fill,
perm.statusDenied,
),
AppPermissionStatus.permanentlyDenied => (
const Color(0xFFFF3B30),
CupertinoIcons.xmark_circle_fill,
perm.statusPermanentlyDenied,
),
AppPermissionStatus.notDetermined => (
ext.textHint,
CupertinoIcons.question_circle_fill,
perm.statusNotDetermined,
),
AppPermissionStatus.restricted => (
const Color(0xFFFF9500),
CupertinoIcons.lock_fill,
perm.statusRestricted,
),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 10, color: color),
const SizedBox(width: 2),
Text(
text,
style: AppTypography.caption1.copyWith(
color: color,
fontSize: 9,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
/// 详细说明文字
Widget _buildDescription(BuildContext context, AppThemeExtension ext) {
return Text(
permission.description(context),
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
height: 1.4,
),
);
}
/// 使用场景列表
Widget _buildUsageScenes(BuildContext context, AppThemeExtension ext) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: permission.usageScenes(context).map((scene) {
return Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 4,
height: 4,
margin: const EdgeInsets.only(top: 5, right: 6),
decoration: BoxDecoration(
color: permission.color.withValues(alpha: 0.6),
shape: BoxShape.circle,
),
),
Expanded(
child: Text(
scene,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
fontSize: 11,
height: 1.3,
),
),
),
],
),
);
}).toList(),
);
}
/// 使用统计信息
Widget _buildUsageStats(AppThemeExtension ext, PermissionUsageStat stat) {
final freqColor = switch (stat.frequencyLabel) {
'' ||
'High' ||
'Hoch' ||
'Alta' ||
'Élevée' ||
'' ||
'높음' ||
'Высокая' ||
'उच्च' ||
'উচ্চ' => const Color(0xFFFF3B30),
'' ||
'Med' ||
'Mittel' ||
'Media' ||
'Moyenne' ||
'보통' ||
'Средняя' ||
'मध्यम' ||
'মধ্যম' => const Color(0xFFFF9500),
'' ||
'Low' ||
'Niedrig' ||
'Baja' ||
'Basse' ||
'낮음' ||
'Низкая' ||
'निम्न' ||
'নিম্ন' => const Color(0xFF34C759),
_ => ext.textHint,
};
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgSecondary.withValues(alpha: 0.5),
borderRadius: AppRadius.smBorder,
),
child: Row(
children: [
Icon(CupertinoIcons.chart_bar, size: 12, color: ext.textHint),
const SizedBox(width: 4),
Text(
'${perm.recentUsagePrefix}${stat.relativeLastUsed}',
style: AppTypography.caption2.copyWith(
color: ext.textHint,
fontSize: 10,
),
),
const SizedBox(width: AppSpacing.sm),
Text(
'${stat.count}${perm.timesSuffix}',
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: AppSpacing.sm),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: freqColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(3),
),
child: Text(
stat.frequencyLabel,
style: AppTypography.caption2.copyWith(
color: freqColor,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
),
],
),
);
}
/// 右侧操作按钮
Widget _buildActionButton(AppThemeExtension ext) {
if (permission.isVirtual) {
return CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: const Size(32, 32),
onPressed: null,
child: Icon(CupertinoIcons.info_circle, size: 18, color: ext.textHint),
);
}
if (status.isGranted) {
return CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: const Size(32, 32),
onPressed: onOpenSettings,
child: Icon(
CupertinoIcons.chevron_right,
size: 16,
color: ext.textHint,
),
);
}
if (status == AppPermissionStatus.permanentlyDenied) {
return CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
minimumSize: const Size(32, 32),
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
onPressed: onOpenSettings,
child: Text(
perm.btnGoSettings,
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
);
}
return CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
minimumSize: const Size(32, 32),
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
onPressed: onRequest,
child: Text(
perm.btnRequest,
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
);
}
}
/// 饼图数据条目
class _PermissionChartEntry {
const _PermissionChartEntry({required this.permission, required this.count});
final AppPermission permission;
final int count;
}
/// 摇一摇权限卡片 — 带开关
class _ShakePermissionCard extends StatefulWidget {
const _ShakePermissionCard({
required this.permission,
required this.status,
required this.onToggle,
required this.perm,
});
final AppPermission permission;
final AppPermissionStatus status;
final ValueChanged<bool> onToggle;
final TSettingsPermission perm;
@override
State<_ShakePermissionCard> createState() => _ShakePermissionCardState();
}
class _ShakePermissionCardState extends State<_ShakePermissionCard> {
late bool _isEnabled;
@override
void initState() {
super.initState();
_isEnabled = PermissionService.isShakeEnabled;
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return GlassContainer(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: widget.permission.color.withValues(alpha: 0.15),
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: Icon(
widget.permission.icon,
size: 20,
color: widget.permission.color,
),
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
widget.permission.label(context),
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: AppSpacing.xs),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFF5856D6).withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.hand_draw,
size: 8,
color: Color(0xFF5856D6),
),
const SizedBox(width: 2),
Text(
widget.perm.badgeOptional,
style: AppTypography.caption1.copyWith(
color: const Color(0xFF5856D6),
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(width: AppSpacing.xs),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color:
(_isEnabled
? const Color(0xFF34C759)
: const Color(0xFFFF9500))
.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_isEnabled
? CupertinoIcons.checkmark_circle_fill
: CupertinoIcons.xmark_circle_fill,
size: 10,
color: _isEnabled
? const Color(0xFF34C759)
: const Color(0xFFFF9500),
),
const SizedBox(width: 2),
Text(
_isEnabled
? widget.perm.shakeEnabled
: widget.perm.shakeDisabled,
style: AppTypography.caption1.copyWith(
color: _isEnabled
? const Color(0xFF34C759)
: const Color(0xFFFF9500),
fontSize: 9,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
widget.permission.description(context),
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
height: 1.4,
),
),
if (widget.permission.usageScenes(context).isNotEmpty) ...[
const SizedBox(height: AppSpacing.sm),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.permission.usageScenes(context).map((
scene,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 4,
height: 4,
margin: const EdgeInsets.only(top: 5, right: 6),
decoration: BoxDecoration(
color: widget.permission.color.withValues(
alpha: 0.6,
),
shape: BoxShape.circle,
),
),
Expanded(
child: Text(
scene,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
fontSize: 11,
height: 1.3,
),
),
),
],
),
);
}).toList(),
),
],
],
),
),
const SizedBox(width: AppSpacing.sm),
CupertinoSwitch(
value: _isEnabled,
activeTrackColor: ext.accent,
onChanged: (value) {
setState(() => _isEnabled = value);
widget.onToggle(value);
},
),
],
),
);
}
}