732 lines
22 KiB
Dart
732 lines
22 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 日签卡片AR 3D展示页面
|
||
/// 创建时间: 2026-06-04
|
||
/// 更新时间: 2026-06-04
|
||
/// 作用: 使用设备传感器+3D变换模拟AR空间中悬浮的日签卡片
|
||
/// 上次更新: 初始创建,伪AR效果(陀螺仪+透视+景深+光影)
|
||
/// ============================================================
|
||
|
||
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:math' as math;
|
||
import 'dart:ui' as ui;
|
||
import 'package:flutter/rendering.dart';
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:sensors_plus/sensors_plus.dart';
|
||
import 'package:share_plus/share_plus.dart';
|
||
|
||
import '../../../../core/theme/app_theme.dart';
|
||
import '../../../../core/theme/app_radius.dart';
|
||
import '../../../../core/theme/app_spacing.dart';
|
||
import '../../../../core/utils/logger.dart';
|
||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||
import '../../../../shared/widgets/feedback/app_toast.dart';
|
||
import '../../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||
import '../models/daily_card_models.dart';
|
||
import 'widgets/card_renderer.dart';
|
||
|
||
// ============================================================
|
||
// AR视图入口参数
|
||
// ============================================================
|
||
|
||
/// AR视图所需的卡片数据封装
|
||
class DailyCardArParams {
|
||
const DailyCardArParams({
|
||
required this.content,
|
||
required this.style,
|
||
required this.date,
|
||
required this.weekday,
|
||
});
|
||
|
||
final DailyCardContent content;
|
||
final CardStylePreset style;
|
||
final String date;
|
||
final String weekday;
|
||
}
|
||
|
||
// ============================================================
|
||
// AR主题预设 — 6套AR空间氛围主题
|
||
// ============================================================
|
||
|
||
enum ArTheme {
|
||
cosmic('🌌', '宇宙深空'),
|
||
aurora('🌈', '极光幻境'),
|
||
sunset('🌅', '落日余晖'),
|
||
forest('🌿', '森林秘境'),
|
||
ocean('🌊', '深海探幽'),
|
||
crystal('💎', '水晶殿堂');
|
||
|
||
const ArTheme(this.emoji, this.label);
|
||
final String emoji;
|
||
final String label;
|
||
|
||
List<Color> get colors => switch (this) {
|
||
ArTheme.cosmic => [
|
||
const Color(0xFF0a0a2e),
|
||
const Color(0xFF1a1a4e),
|
||
const Color(0xFF0d0d3b),
|
||
],
|
||
ArTheme.aurora => [
|
||
const Color(0xFF0a2a3a),
|
||
const Color(0xFF1a4a5a),
|
||
const Color(0xFF0d3d4d),
|
||
],
|
||
ArTheme.sunset => [
|
||
const Color(0xFF2d1b30),
|
||
const Color(0xFF4a2535),
|
||
const Color(0xFF351820),
|
||
],
|
||
ArTheme.forest => [
|
||
const Color(0xFF0a1f15),
|
||
const Color(0xFF153022),
|
||
const Color(0xFF0d2819),
|
||
],
|
||
ArTheme.ocean => [
|
||
const Color(0xFF051525),
|
||
const Color(0xFF0a2540),
|
||
const Color(0xFF071d32),
|
||
],
|
||
ArTheme.crystal => [
|
||
const Color(0xFF1a1a2e),
|
||
const Color(0xFF16213e),
|
||
const Color(0xFF121230),
|
||
],
|
||
};
|
||
}
|
||
|
||
// ============================================================
|
||
// 页面主体
|
||
// ============================================================
|
||
|
||
class DailyCardArView extends ConsumerStatefulWidget {
|
||
const DailyCardArView({
|
||
super.key,
|
||
required this.params,
|
||
});
|
||
|
||
final DailyCardArParams params;
|
||
|
||
@override
|
||
ConsumerState<DailyCardArView> createState() => _DailyCardArViewState();
|
||
}
|
||
|
||
class _DailyCardArViewState extends ConsumerState<DailyCardArView>
|
||
with TickerProviderStateMixin {
|
||
// ---- 传感器数据 ----
|
||
double _tiltX = 0.0; // X轴倾斜(俯仰)
|
||
double _tiltY = 0.0; // Y轴倾斜(偏航)
|
||
StreamSubscription<AccelerometerEvent>? _accelSubscription;
|
||
bool _sensorAvailable = false;
|
||
|
||
// ---- 手势控制 ----
|
||
double _rotateX = 0.0;
|
||
double _rotateY = 0.0;
|
||
double _scale = 1.0;
|
||
Offset? _lastPanPosition;
|
||
|
||
// ---- 动画控制器 ----
|
||
late AnimationController _lightController;
|
||
late AnimationController _floatController;
|
||
late AnimationController _particleController;
|
||
|
||
// ---- 状态 ----
|
||
ArTheme _currentTheme = ArTheme.cosmic;
|
||
bool _showControls = true;
|
||
bool _autoRotate = true;
|
||
final GlobalKey _repaintKey = GlobalKey();
|
||
|
||
// ---- 常量 ----
|
||
static const double _maxTilt = 0.5; // 最大倾斜弧度(~28度)
|
||
static const double _perspective = 0.003; // 透视强度
|
||
static const double _maxGestureAngle = 0.8;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_initSensors();
|
||
_lightController = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(seconds: 4),
|
||
)..repeat();
|
||
_floatController = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(seconds: 3),
|
||
)..repeat(reverse: true);
|
||
_particleController = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(seconds: 8),
|
||
)..repeat();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_accelSubscription?.cancel();
|
||
_lightController.dispose();
|
||
_floatController.dispose();
|
||
_particleController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
// ---- 传感器初始化 ----
|
||
|
||
void _initSensors() {
|
||
try {
|
||
_accelSubscription =
|
||
accelerometerEventStream(samplingPeriod: const Duration(milliseconds: 60))
|
||
.listen(_onAccelerometerData, onError: (e) {
|
||
Log.w('AR视图: 加速度传感器不可用,切换到纯动画模式');
|
||
_sensorAvailable = false;
|
||
});
|
||
_sensorAvailable = true;
|
||
Log.i('AR视图: 加速度传感器已启动');
|
||
} catch (e) {
|
||
Log.w('AR视图: 传感器初始化失败: $e');
|
||
_sensorAvailable = false;
|
||
}
|
||
}
|
||
|
||
void _onAccelerometerData(AccelerometerEvent event) {
|
||
if (!mounted || !_autoRotate) return;
|
||
// 将加速度计数据映射到倾斜角度 (归一化到[-1, 1]再乘以最大倾斜)
|
||
final rawX = (event.x / 10.0).clamp(-1.0, 1.0);
|
||
final rawY = (event.y / 10.0).clamp(-1.0, 1.0);
|
||
setState(() {
|
||
_tiltX = rawX * _maxTilt;
|
||
_tiltY = rawY * _maxTilt;
|
||
});
|
||
}
|
||
|
||
// ---- 手势处理 ----
|
||
|
||
void _onScaleStart(ScaleStartDetails details) {
|
||
_lastPanPosition = details.localFocalPoint;
|
||
}
|
||
|
||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||
// 缩放处理
|
||
setState(() {
|
||
_scale = (_scale * details.scale).clamp(0.6, 2.0);
|
||
});
|
||
// 旋转处理(原 pan 逻辑)
|
||
if (_lastPanPosition == null) return;
|
||
final delta = details.localFocalPoint - _lastPanPosition!;
|
||
final size = MediaQuery.of(context).size;
|
||
setState(() {
|
||
_rotateY += (delta.dx / size.width) * _maxGestureAngle;
|
||
_rotateX -= (delta.dy / size.height) * _maxGestureAngle;
|
||
_rotateX = _rotateX.clamp(-_maxGestureAngle, _maxGestureAngle);
|
||
_rotateY = _rotateY.clamp(-_maxGestureAngle, _maxGestureAngle);
|
||
});
|
||
_lastPanPosition = details.localFocalPoint;
|
||
}
|
||
|
||
void _onScaleEnd(ScaleEndDetails details) {
|
||
_lastPanPosition = null;
|
||
}
|
||
|
||
void _onDoubleTap() {
|
||
HapticFeedback.lightImpact();
|
||
setState(() {
|
||
_rotateX = 0.0;
|
||
_rotateY = 0.0;
|
||
_scale = 1.0;
|
||
_tiltX = 0.0;
|
||
_tiltY = 0.0;
|
||
});
|
||
}
|
||
|
||
// ---- 截图功能 ----
|
||
|
||
Future<void> _captureAndShare() async {
|
||
HapticFeedback.mediumImpact();
|
||
try {
|
||
final boundary = _repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
||
if (boundary == null) {
|
||
AppToast.show('❌ 截图失败');
|
||
return;
|
||
}
|
||
final image = await boundary.toImage(pixelRatio: 3.0);
|
||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||
if (byteData == null) {
|
||
AppToast.show('❌ 截图失败');
|
||
return;
|
||
}
|
||
final bytes = byteData.buffer.asUint8List();
|
||
final tempDir = Directory.systemTemp;
|
||
final file = File(
|
||
'${tempDir.path}/xianyan_ar_${DateTime.now().millisecondsSinceEpoch}.png',
|
||
);
|
||
await file.writeAsBytes(bytes);
|
||
await SharePlus.instance.share(
|
||
ShareParams(files: [XFile(file.path)], text: '🎴 闲言 · AR日签卡片'),
|
||
);
|
||
Log.i('AR视图: 截图分享成功');
|
||
} catch (e) {
|
||
Log.e('AR视图: 截图失败', e);
|
||
AppToast.show('❌ 分享失败');
|
||
}
|
||
}
|
||
|
||
// ---- 主题切换 ----
|
||
|
||
void _cycleTheme() {
|
||
HapticFeedback.selectionClick();
|
||
const themes = ArTheme.values;
|
||
final idx = (themes.indexOf(_currentTheme) + 1) % themes.length;
|
||
setState(() => _currentTheme = themes[idx]);
|
||
}
|
||
|
||
// ---- UI构建 ----
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = AppTheme.ext(context);
|
||
|
||
return CupertinoPageScaffold(
|
||
navigationBar: CupertinoNavigationBar(
|
||
leading: const AdaptiveBackButton(),
|
||
middle: Text(
|
||
'${_currentTheme.emoji} AR 日签',
|
||
style: TextStyle(
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w600,
|
||
color: ext.textPrimary,
|
||
),
|
||
),
|
||
trailing: GestureDetector(
|
||
onTap: () => setState(() => _showControls = !_showControls),
|
||
child: Icon(
|
||
_showControls ? CupertinoIcons.eye_slash : CupertinoIcons.eye,
|
||
color: ext.textSecondary,
|
||
size: 20,
|
||
),
|
||
),
|
||
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
|
||
border: null,
|
||
),
|
||
child: SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
// 深色背景层
|
||
Positioned.fill(child: _buildBackground(ext)),
|
||
// AR内容层
|
||
Positioned.fill(child: _buildArContent()),
|
||
// 控制面板
|
||
if (_showControls)
|
||
Positioned(
|
||
left: AppSpacing.md,
|
||
right: AppSpacing.md,
|
||
bottom: AppSpacing.lg + MediaQuery.of(context).padding.bottom,
|
||
child: _buildControlPanel(ext),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// AR空间背景 — 渐变+粒子+景深模糊
|
||
// ============================================================
|
||
|
||
Widget _buildBackground(AppThemeExtension ext) {
|
||
return AnimatedBuilder(
|
||
animation: _particleController,
|
||
builder: (context, child) {
|
||
final t = _particleController.value;
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
gradient: RadialGradient(
|
||
center: Alignment(
|
||
0.3 + math.sin(t * math.pi * 2) * 0.2,
|
||
-0.3 + math.cos(t * math.pi * 2) * 0.15,
|
||
),
|
||
radius: 1.4,
|
||
colors: _currentTheme.colors,
|
||
),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
// 星空粒子层
|
||
...List.generate(20, (i) => _buildParticle(i, t)),
|
||
// 底部环境光晕
|
||
Positioned(
|
||
bottom: -100,
|
||
left: -100,
|
||
right: -100,
|
||
child: Container(
|
||
height: 300,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: _currentTheme.colors[1].withValues(alpha: 0.3),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildParticle(int index, double t) {
|
||
final seed = index * 137.508; // 黄金角度分布
|
||
final baseX = math.sin(seed) * 0.5 + 0.5;
|
||
final baseY = math.cos(seed * 1.3) * 0.5 + 0.5;
|
||
final phase = (t + index * 0.1) % 1.0;
|
||
final opacity = (math.sin(phase * math.pi * 2) * 0.3 + 0.2).clamp(0.0, 1.0);
|
||
final size = 1.0 + index % 3;
|
||
|
||
return Positioned(
|
||
left: baseX * MediaQuery.of(context).size.width,
|
||
top: baseY * MediaQuery.of(context).size.height,
|
||
child: AnimatedOpacity(
|
||
opacity: opacity,
|
||
duration: const Duration(milliseconds: 200),
|
||
child: Container(
|
||
width: size,
|
||
height: size,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: Colors.white.withValues(alpha: opacity * 0.6),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// AR核心内容 — 3D悬浮卡片
|
||
// ============================================================
|
||
|
||
Widget _buildArContent() {
|
||
// 合成最终旋转角度:传感器倾斜 + 手势旋转
|
||
final totalRotateX = _tiltX + _rotateX;
|
||
final totalRotateY = _tiltY + _rotateY;
|
||
|
||
return GestureDetector(
|
||
onScaleStart: _onScaleStart,
|
||
onScaleUpdate: _onScaleUpdate,
|
||
onScaleEnd: _onScaleEnd,
|
||
onDoubleTap: _onDoubleTap,
|
||
behavior: HitTestBehavior.opaque,
|
||
child: AnimatedBuilder(
|
||
animation: Listenable.merge([
|
||
_lightController,
|
||
_floatController,
|
||
]),
|
||
builder: (context, _) {
|
||
final floatOffset = _floatController.value * 8.0;
|
||
final lightPhase = _lightController.value * math.pi * 2;
|
||
|
||
return Center(
|
||
child: Transform(
|
||
alignment: FractionalOffset.center,
|
||
transform: Matrix4.identity()
|
||
..setEntry(3, 2, _perspective)
|
||
..rotateX(totalRotateX)
|
||
..rotateY(totalRotateY),
|
||
child: Transform.translate(
|
||
offset: Offset(0, floatOffset),
|
||
child: Transform.scale(
|
||
scale: _scale,
|
||
child: RepaintBoundary(
|
||
key: _repaintKey,
|
||
child: _build3DCard(lightPhase),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// 3D卡片 — 阴影+光影+内容+景深边框
|
||
// ============================================================
|
||
|
||
Widget _build3DCard(double lightPhase) {
|
||
final cardWidth = MediaQuery.of(context).size.width * 0.82;
|
||
final radius = AppRadius.of(context);
|
||
|
||
return Container(
|
||
width: cardWidth,
|
||
constraints: const BoxConstraints(minHeight: 380, maxHeight: 520),
|
||
child: Stack(
|
||
children: [
|
||
// 卡片阴影(跟随倾斜动态变化)
|
||
Positioned.fill(
|
||
child: Transform(
|
||
transform: Matrix4.identity()
|
||
..translateByDouble(0.0, 12.0, 0.0, 1.0)
|
||
..scaleByDouble(0.95, 0.95, 0.95, 1.0),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(radius.xl),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.5),
|
||
blurRadius: 40,
|
||
offset: const Offset(0, 20),
|
||
spreadRadius: 4,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
// 主卡片容器
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(radius.xl),
|
||
child: Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
// 卡片内容
|
||
CardRenderer(
|
||
key: ValueKey(widget.params.style.id),
|
||
content: widget.params.content,
|
||
style: widget.params.style,
|
||
date: widget.params.date,
|
||
weekday: widget.params.weekday,
|
||
),
|
||
// 光影流动覆盖层
|
||
_buildLightOverlay(lightPhase, radius),
|
||
// 边缘高光(模拟玻璃反光)
|
||
_buildEdgeHighlight(radius),
|
||
// 景深前景模糊边缘
|
||
_buildDepthVignette(),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---- 光影流动效果 ----
|
||
|
||
Widget _buildLightOverlay(double lightPhase, AppRadiusData radius) {
|
||
return IgnorePointer(
|
||
child: ShaderMask(
|
||
shaderCallback: (bounds) => LinearGradient(
|
||
begin: Alignment(
|
||
math.sin(lightPhase) * 1.5,
|
||
math.cos(lightPhase) * 1.5 - 0.5,
|
||
),
|
||
end: Alignment(
|
||
math.sin(lightPhase + math.pi) * 1.5,
|
||
math.cos(lightPhase + math.pi) * 1.5 + 0.5,
|
||
),
|
||
colors: [
|
||
Colors.white.withValues(alpha: 0.08),
|
||
Colors.transparent,
|
||
Colors.white.withValues(alpha: 0.04),
|
||
Colors.transparent,
|
||
],
|
||
stops: const [0.0, 0.4, 0.6, 1.0],
|
||
).createShader(bounds),
|
||
blendMode: BlendMode.overlay,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(radius.xl),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---- 边缘高光 ----
|
||
|
||
Widget _buildEdgeHighlight(AppRadiusData radius) {
|
||
return IgnorePointer(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(radius.xl),
|
||
border: Border.all(
|
||
color: Colors.white.withValues(alpha: 0.12),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---- 景深暗角 ----
|
||
|
||
Widget _buildDepthVignette() {
|
||
return IgnorePointer(
|
||
child: ShaderMask(
|
||
shaderCallback: (bounds) => RadialGradient(
|
||
radius: 0.8,
|
||
colors: [
|
||
Colors.transparent,
|
||
Colors.black.withValues(alpha: 0.15),
|
||
],
|
||
).createShader(bounds),
|
||
blendMode: BlendMode.multiply,
|
||
child: Container(color: Colors.transparent),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// 底部控制面板 — Cupertino风格
|
||
// ============================================================
|
||
|
||
Widget _buildControlPanel(AppThemeExtension ext) {
|
||
final radius = AppRadius.of(context);
|
||
|
||
return GlassContainer(
|
||
depth: GlassDepth.elevated,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
_buildCtrlButton(
|
||
icon: CupertinoIcons.photo_camera,
|
||
label: '截图',
|
||
ext: ext,
|
||
radius: radius,
|
||
onTap: _captureAndShare,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
_buildCtrlButton(
|
||
icon: CupertinoIcons.photo,
|
||
label: _currentTheme.label,
|
||
ext: ext,
|
||
radius: radius,
|
||
onTap: _cycleTheme,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
_buildToggleBtn(
|
||
active: _autoRotate,
|
||
iconOn: CupertinoIcons.arrow_2_circlepath,
|
||
iconOff: CupertinoIcons.hand_draw,
|
||
label: _sensorAvailable ? '自动' : '手动',
|
||
ext: ext,
|
||
radius: radius,
|
||
onTap: () {
|
||
HapticFeedback.selectionClick();
|
||
setState(() => _autoRotate = !_autoRotate);
|
||
},
|
||
),
|
||
const Spacer(),
|
||
_buildCtrlButton(
|
||
icon: CupertinoIcons.arrow_counterclockwise,
|
||
label: '重置',
|
||
ext: ext,
|
||
radius: radius,
|
||
onTap: _onDoubleTap,
|
||
),
|
||
],
|
||
),
|
||
// 提示文字
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: AppSpacing.xs),
|
||
child: Text(
|
||
'拖拽旋转 · 双击重置 · 捏合缩放',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: ext.textHint,
|
||
letterSpacing: 0.5,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCtrlButton({
|
||
required IconData icon,
|
||
required String label,
|
||
required AppThemeExtension ext,
|
||
required AppRadiusData radius,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgSecondary.withValues(alpha: 0.7),
|
||
borderRadius: BorderRadius.circular(radius.md),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, size: 14, color: ext.iconSecondary),
|
||
const SizedBox(width: 4),
|
||
Text(label,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: ext.textSecondary,
|
||
)),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildToggleBtn({
|
||
required bool active,
|
||
required IconData iconOn,
|
||
required IconData iconOff,
|
||
required String label,
|
||
required AppThemeExtension ext,
|
||
required AppRadiusData radius,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: active ? ext.accent.withValues(alpha: 0.2) : ext.bgSecondary.withValues(alpha: 0.7),
|
||
borderRadius: BorderRadius.circular(radius.md),
|
||
border: active
|
||
? Border.all(color: ext.accent.withValues(alpha: 0.4), width: 0.5)
|
||
: null,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
active ? iconOn : iconOff,
|
||
size: 14,
|
||
color: active ? ext.accent : ext.iconSecondary,
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(label,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: active ? FontWeight.w600 : FontWeight.w500,
|
||
color: active ? ext.accent : ext.textSecondary,
|
||
)),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|