import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/home_controller.dart'; import '../../../routes/app_routes.dart'; import 'widgets/calendar/calendar_card.dart'; import 'widgets/calendar/calendar_task_list.dart'; import '../controllers/calendar_controller.dart'; import '../../../data/services/memory_reminder_scheduler.dart'; // 首页视图 class HomeView extends GetView { const HomeView({Key? key}) : super(key: key); // 功能卡片模块(语音记录/翻译/通话录音等)折叠态下的目标可视高度: // 固定预算而非内容自然高度,避免卡片变多时撑爆首页固定布局; // 展示不下的部分(如通话录音)在模块内部滚动查看。 static const double _extrasTargetHeight = 420; @override Widget build(BuildContext context) { final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: Colors.transparent, // 背景:顶部「左淡蓝·中浅蓝·右淡暖」横向晕染,向下渐隐到白(取自参考图真实色值) body: Stack( children: [ // 1) 顶部横向色带(浅色模式)/ 深色竖直渐变(深色模式) Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: isDarkMode ? const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF0A0F1A), Color(0xFF000000)], ) : const LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ Color(0xFFD4EAFF), // 左:淡蓝 Color(0xFFE6F3FE), // 中:浅蓝 Color(0xFFFBF2F0), // 右:淡暖(浅奶粉) ], ), ), ), ), // 2) 竖直方向把顶部色带渐隐到白(仅浅色模式) if (!isDarkMode) const Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0x00FFFFFF), // 顶部透明:露出横向色带 Color(0x00FFFFFF), Color(0xFFFFFFFF), // 向下渐变为白 ], stops: [0.0, 0.15, 0.42], ), ), ), ), // 内容(固定布局:日历展开时任务列表铺满并覆盖下方翻译卡片/AI) SafeArea( child: Padding( // 横向不留白:任务列表需铺满到屏幕边(在屏幕边裁切,阴影完整); // 其余元素各自加 16 横向内边距对齐 padding: EdgeInsets.only(top: 16.w, bottom: 8.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部标题栏(固定) Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: _buildAppBar(context, isDarkMode), ), _buildNotifyDisabledBar(isDarkMode), SizedBox(height: 16.h), // 日历 + 任务列表 + 翻译卡片/AI(随日历折叠/展开切换) Expanded(child: _buildCalendarArea(isDarkMode)), ], ), ), ), ], ), ); } /// 通知权限没开时的静态提示条。 /// /// ⚠️ 这是**唯一**能让用户知道「闹钟不会响」的地方。 /// MemoryReminderScheduler.permissionDenied 这个标志一直存在,但在此之前 /// 没有任何 UI 在读它 —— 于是没通知权限时的表现是:助手说闹钟定好了、 /// 拾忆页也确实列出来了,到点却一声不响,而且没有任何解释。 /// /// ⚠️ 刻意做成常驻的一条,**不是弹窗**:申请通知权限必须由用户在 /// 「设置 → 权限管理」里主动发起(PermissionUtil 有 48 小时合规冷却, /// 拒过一次之后两天内 App 不被允许再弹窗解释)。这里只能引导,不能代劳。 Widget _buildNotifyDisabledBar(bool isDarkMode) { if (!Get.isRegistered()) { return const SizedBox.shrink(); } return Obx(() { if (!MemoryReminderScheduler.to.permissionDenied.value) { return const SizedBox.shrink(); } return Padding( padding: EdgeInsets.only(top: 8.h, left: 16.w, right: 16.w), child: InkWell( onTap: () => Get.toNamed(Routes.permissions), borderRadius: BorderRadius.circular(8.r), child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), decoration: BoxDecoration( color: const Color(0xFFFFF4E5), borderRadius: BorderRadius.circular(8.r), ), child: Row( children: [ Icon(Icons.notifications_off_outlined, size: 16.sp, color: const Color(0xFFB26A00)), SizedBox(width: 8.w), Expanded( child: Text( 'memNotifyDisabled'.tr, style: TextStyle( fontSize: 12.sp, color: const Color(0xFFB26A00)), ), ), Icon(Icons.chevron_right, size: 16.sp, color: const Color(0xFFB26A00)), ], ), ), ), ); }); } // 日历区域:日历卡片 + 任务列表(Expanded) + 翻译卡片/AI(随展开收起) Widget _buildCalendarArea(bool isDarkMode) { final cal = Get.find(); return LayoutBuilder( builder: (context, constraints) { final double h = constraints.maxHeight; // 极小高度(超大字体/超小屏)兜底:退回可滚动布局,避免溢出 if (h < 360.h) { return _buildCalendarScrollFallback(isDarkMode); } // 月历网格最大高度夹取:留出间距 + 最小任务列表 + 卡片其余高度,避免 Expanded 负高 final double maxBody = h - 16.h - 96.h - 130.h; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: CalendarCard(maxBodyHeight: maxBody > 0 ? maxBody : null), ), SizedBox(height: 16.h), // 任务列表:填满任务区(连续高度,便于折叠/展开两层按下拉距离交叉淡入淡出)。 const Expanded(child: CalendarTaskList()), // 翻译卡片 + AI:随日历展开(dragProgress→0)从底部收起、被覆盖。 // 用固定目标高度(非内容自然高度)做插值:卡片模块内容一旦变多(如新增 // "通话录音")就不会撑爆这个不滚动的整屏布局;模块内部改用可滚动容器 // 兜底——展示不下的卡片(如通话录音)靠上下滑动查看,而不是让布局溢出。 Obx(() => ClipRect( child: SizedBox( height: _extrasTargetHeight.h * cal.dragProgress.value, child: IgnorePointer( ignoring: cal.dragProgress.value < 1.0, child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 16.w), physics: const BouncingScrollPhysics(), child: _buildExtrasBlock(isDarkMode), ), ), ), )), ], ); }, ); } // 翻译功能卡片(折叠态显示在任务列表下方;展开态被收起覆盖) // AI 球已改为全局悬浮球(core/widgets/global_ai_orb.dart),不再嵌在首页里 Widget _buildExtrasBlock(bool isDarkMode) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 16.h), _buildFeatureGrid(isDarkMode), SizedBox(height: 24.h), ], ); } // 极小屏兜底:可滚动布局(任务列表给有限高度) Widget _buildCalendarScrollFallback(bool isDarkMode) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: const CalendarCard(), ), SizedBox(height: 16.h), SizedBox(height: 280.h, child: const CalendarTaskList()), SizedBox(height: 16.h), Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: _buildFeatureGrid(isDarkMode), ), SizedBox(height: 24.h), ], ), ); } // 顶部标题栏:左侧设备管理,右侧“语音打断”开关 + 设置入口 Widget _buildAppBar(BuildContext context, bool isDarkMode) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ // 左侧:设备管理入口 _buildDeviceManager(isDarkMode), // 右侧:标语 + 设置图标 Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'YM开启科技人生', style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white70 : Colors.grey[700], ), ), SizedBox(width: 8.w), _buildSettingsEntry(isDarkMode), ], ), ], ); } // 左侧设备管理:未连接显示“添加设备”,已连接显示电量 Widget _buildDeviceManager(bool isDarkMode) { final Color pillBg = isDarkMode ? const Color(0xFF1A1A1A) : Colors.white; final Color textColor = isDarkMode ? Colors.white : const Color(0xFF222222); return Obx(() { final bool connected = controller.isFullyConnectedRx.value; return GestureDetector( behavior: HitTestBehavior.opaque, // 未连接 → 扫描页;已连接 → 我的设备 onTap: () => Get.toNamed(connected ? Routes.myDevices : Routes.devices), child: Container( padding: EdgeInsets.fromLTRB(6.w, 6.h, 14.w, 6.h), decoration: BoxDecoration( color: pillBg, borderRadius: BorderRadius.circular(24.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.08), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ // 蓝色圆形图标 Container( width: 26.w, height: 26.w, decoration: const BoxDecoration( color: Color(0xFF3B82F6), shape: BoxShape.circle, ), child: Icon( connected ? Icons.battery_full : Icons.add, color: Colors.white, size: connected ? 16.sp : 18.sp, ), ), SizedBox(width: 8.w), Text( connected ? controller.getBatteryText() : 'addDevice'.tr, style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.bold, color: textColor, ), ), ], ), ), ); }); } // 右侧“语音打断”开关(保留原有逻辑与样式) // 设置入口:头像/人形图标,点击进入设置页 Widget _buildSettingsEntry(bool isDarkMode) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Get.toNamed(Routes.settings), child: Container( width: 36.w, height: 36.w, decoration: BoxDecoration( color: isDarkMode ? const Color(0xFF2A2A2A) : Colors.white.withOpacity(0.7), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.08), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Icon( Icons.person, size: 22.sp, color: isDarkMode ? Colors.white70 : Colors.grey[700], ), ), ); } // 功能卡片区:顶部(通话录音/现场录音,依赖设备连接) + 左1大(语音记录助手) + // 右2(同声翻译/面对面翻译) + 下2(通话翻译/多媒体翻译) Widget _buildFeatureGrid(bool isDarkMode) { const double smallH = 84; // 小卡高度 const double gap = 12; // 卡片间距 return Column( children: [ // 顶区:通话录音 + 现场录音(依赖设备连接,未连接时置灰/透明) Obx(() { final bool connected = controller.isFullyConnectedRx.value; return Row( children: [ Expanded( child: _buildFeatureCard( title: '通话录音', icon: Icons.fiber_manual_record, gradient: const [Color(0xFFEF5350), Color(0xFFE53935)], height: smallH.h, isDarkMode: isDarkMode, enabled: connected, onTap: controller.openCallRecording, ), ), SizedBox(width: gap.w), Expanded( child: _buildFeatureCard( title: '现场录音', icon: Icons.mic, gradient: const [Color(0xFF26C6DA), Color(0xFF00838F)], height: smallH.h, isDarkMode: isDarkMode, onTap: controller.openFieldRecording, ), ), ], ); }), SizedBox(height: gap.h), // 上区:左侧高卡 + 右侧两张卡 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _buildFeatureCard( title: '语音记录助手', subtitle: '实时记录各种场景语音', icon: Icons.groups_2, gradient: const [Color(0xFFAB47BC), Color(0xFFEC407A)], height: (smallH * 2 + gap).h, iconOnTop: true, isDarkMode: isDarkMode, onTap: controller.openMeet, // → 会议(Routes.meeting) ), ), SizedBox(width: gap.w), Expanded( child: Column( children: [ _buildFeatureCard( title: '同声翻译', icon: Icons.record_voice_over, gradient: const [Color(0xFF4CAF50), Color(0xFF009688)], height: smallH.h, isDarkMode: isDarkMode, onTap: () => controller.openTranslationFeature('simultaneous'), ), SizedBox(height: gap.h), _buildFeatureCard( title: '面对面翻译', icon: Icons.forum, gradient: const [Color(0xFF42A5F5), Color(0xFF1E88E5)], height: smallH.h, isDarkMode: isDarkMode, onTap: () => controller.openTranslationFeature('faceToFace'), ), ], ), ), ], ), SizedBox(height: gap.h), // 下区:通话翻译 + 多媒体翻译(依赖设备连接,未连接时置灰/透明) Obx(() { final bool connected = controller.isFullyConnectedRx.value; return Row( children: [ Expanded( child: _buildFeatureCard( title: '通话翻译', icon: Icons.phone_in_talk, gradient: const [Color(0xFFFFA726), Color(0xFFFB8C00)], height: smallH.h, isDarkMode: isDarkMode, enabled: connected, onTap: () => controller.openTranslationFeature('call'), ), ), SizedBox(width: gap.w), Expanded( child: _buildFeatureCard( title: '多媒体翻译', icon: Icons.photo_camera, gradient: const [Color(0xFF5C6BC0), Color(0xFF3949AB)], height: smallH.h, isDarkMode: isDarkMode, onTap: () => controller.openTranslationFeature('audioVideo'), ), ), ], ); }), ], ); } // 通用功能卡片 Widget _buildFeatureCard({ required String title, String? subtitle, required IconData icon, required List gradient, required double height, required bool isDarkMode, required VoidCallback onTap, bool iconOnTop = false, bool enabled = true, // false 时整卡变透明(未连接设备) }) { final Color cardBg = isDarkMode ? const Color(0xFF1A1A1A) : Colors.white; final Color titleColor = isDarkMode ? Colors.white : const Color(0xFF222222); final Color subColor = isDarkMode ? Colors.white60 : const Color(0xFF888888); final Widget iconBox = Container( width: 42.w, height: 42.w, decoration: BoxDecoration( gradient: LinearGradient( colors: gradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12.r), ), child: Icon(icon, color: Colors.white, size: 22.sp), ); return Opacity( opacity: enabled ? 1.0 : 0.4, // 未连接:整卡变透明(仍可点击 → 弹"请连接设备") child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( height: height, padding: EdgeInsets.all(14.w), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDarkMode ? 0.06 : 0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: iconOnTop ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ iconBox, const Spacer(), Text( title, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, color: titleColor, ), ), if (subtitle != null) ...[ SizedBox(height: 4.h), Text( subtitle, style: TextStyle(fontSize: 12.sp, color: subColor), ), ], ], ) : Row( children: [ iconBox, SizedBox(width: 12.w), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, softWrap: false, style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.bold, color: titleColor, ), ), if (subtitle != null) ...[ SizedBox(height: 2.h), Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, softWrap: false, style: TextStyle(fontSize: 11.sp, color: subColor), ), ], ], ), ), ], ), ), ), ); } }