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