You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
573 lines
21 KiB
573 lines
21 KiB
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<HomeController> {
|
|
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<MemoryReminderScheduler>()) {
|
|
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<CalendarController>();
|
|
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<Color> 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),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
}
|
|
|