diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 15275b1e7..3c367eb2a 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart' as http; @@ -18,13 +19,13 @@ import '../model/meeting_model.dart'; class MeetingDetailsController extends GetxController with GetSingleTickerProviderStateMixin { - late ScrollController scrollController; late TabController tabController; final RxInt currentIndex = 0.obs; final PlayerController playerController = PlayerController(); RxBool isTop = false.obs; RxInt durationMax = 0.obs; + RxDouble dragOffset = 210.0.w.obs; int id = 0; @@ -52,8 +53,6 @@ class MeetingDetailsController extends GetxController @override void onInit() { super.onInit(); - scrollController = ScrollController(initialScrollOffset: 0.0); - scrollController.addListener(_onScroll); tabController = TabController(length: 4, vsync: this); tabController.addListener(() { currentIndex.value = tabController.index; @@ -70,38 +69,12 @@ class MeetingDetailsController extends GetxController @override void onClose() { - scrollController.dispose(); tabController.dispose(); playerController.dispose(); _isDisposed = true; super.onClose(); } - void _onScroll() async { - double pixels = scrollController.position.pixels; - double maxScrollExtent = scrollController.position.maxScrollExtent; - if (!isTop.value && pixels > 5) { - await scrollController.animateTo( - maxScrollExtent, - duration: const Duration(milliseconds: 100), - curve: Curves.easeOut, - ); - if (pixels > maxScrollExtent - 2) { - isTop.value = true; - } - } - if (isTop.value && pixels < maxScrollExtent - 5) { - await scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 100), - curve: Curves.easeOut, - ); - if (pixels < 2) { - isTop.value = false; - } - } - } - void _getAgent() async { final GetStorage storage = GetStorage(); Map userInfo = storage.read("user_info"); @@ -113,6 +86,10 @@ class MeetingDetailsController extends GetxController final data = await SqfliteApi.getMeetingDetails(id); meetingData.value = MeetingModel.fromJson(data); meetingDetails.value = MeetingDetailsModel.fromJson(data['details']); + if (meetingDetails.value.summary.isNotEmpty) { + dragOffset.value = 0; + isTop.value = true; + } await playerController.preparePlayer( path: meetingData.value.filepath, noOfSamples: 1200, diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index d1b2ec7a5..9af120ebb 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -161,21 +161,63 @@ class MeetingDetailsView extends GetView { // 构建主体内容 Widget _buildBody(BuildContext context, bool isDarkMode) { + double initialPosition = 0; return Column( children: [ - _soundWave(isDarkMode), - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w), - margin: EdgeInsets.symmetric(vertical: 10.w), - child: Obx( - () => Row( - children: [ - _iconItem(Icons.summarize, '概述', 0), - _iconItem(Icons.message, '转写', 1), - _iconItem(Icons.article, '总结', 2), - _iconItem(Icons.lan, '思维导图', 3), - ], - ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onVerticalDragStart: (DragStartDetails dragStartDetails) { + initialPosition = dragStartDetails.localPosition.dy; + }, + onVerticalDragUpdate: (DragUpdateDetails dragUpdateDetails) { + controller.dragOffset.value += dragUpdateDetails.delta.dy; + }, + onVerticalDragEnd: (DragEndDetails dragEndDetails) { + double offset = initialPosition - dragEndDetails.localPosition.dy; + if (offset > 0) { + if (controller.isTop.value) { + controller.dragOffset.value = 0; + return; + } + if (offset > 60) { + controller.dragOffset.value = 0; + controller.isTop.value = true; + } else { + controller.dragOffset.value = 210.w; + controller.isTop.value = false; + } + } else { + if (!controller.isTop.value) { + controller.dragOffset.value = 210.w; + return; + } + if (offset < -60) { + controller.dragOffset.value = 210.w; + controller.isTop.value = false; + } else { + controller.dragOffset.value = 0; + controller.isTop.value = true; + } + } + }, + child: Column( + children: [ + _soundWave(isDarkMode), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w), + margin: EdgeInsets.symmetric(vertical: 10.w), + child: Obx( + () => Row( + children: [ + _iconItem(Icons.summarize, '概述', 0), + _iconItem(Icons.message, '转写', 1), + _iconItem(Icons.article, '总结', 2), + _iconItem(Icons.lan, '思维导图', 3), + ], + ), + ), + ), + ], ), ), Expanded( @@ -184,11 +226,15 @@ class MeetingDetailsView extends GetView { child: TabBarView( controller: controller.tabController, physics: const NeverScrollableScrollPhysics(), - children: const [ - OverviewTab(), - SpeechTab(), - SummaryTab(), - MindMapTab(), + children: [ + const OverviewTab(), + const SpeechTab(), + const SummaryTab(), + Obx( + () => controller.meetingDetails.value.summary.isNotEmpty + ? const MindMapTab() + : const SizedBox(), + ), ], ), ), @@ -198,108 +244,131 @@ class MeetingDetailsView extends GetView { } Widget _soundWave(bool isDarkMode) { - return Container( - height: 200.w, - margin: EdgeInsets.only(bottom: 10.w), - decoration: BoxDecoration( - color: Colors.grey[200]!, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(20.r), - ), - ), - child: Column( - children: [ - Container( - margin: EdgeInsets.only(bottom: 5.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.r), - ), - child: AudioFileWaveforms( - size: Size(350.w, 120.w), - playerController: controller.playerController, - playerWaveStyle: PlayerWaveStyle( - fixedWaveColor: Colors.grey[300]!, - liveWaveColor: Colors.red, - seekLineColor: Colors.red, - scaleFactor: 600, - ), - ), - ), - SizedBox( - width: 350.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StreamBuilder( - stream: controller.playerController.onCurrentDurationChanged, - builder: (context, snapshot) { - final position = snapshot.data ?? 0; - return Text( - DateUtil.formatDateMs( - position, - format: 'HH:mm:ss', - isUtc: true, + return Obx( + () => SizedBox( + height: controller.dragOffset.value < 0 + ? 0 + : controller.dragOffset.value > 210.w + ? 210.w + : controller.dragOffset.value, + child: Stack( + children: [ + Positioned( + bottom: 0, + child: Container( + width: 1.sw, + height: 200.w, + margin: EdgeInsets.only(bottom: 10.w), + decoration: BoxDecoration( + color: Colors.grey[200]!, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(20.r), + ), + ), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 5.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.r), ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, + child: AudioFileWaveforms( + size: Size(350.w, 120.w), + playerController: controller.playerController, + playerWaveStyle: PlayerWaveStyle( + fixedWaveColor: Colors.grey[300]!, + liveWaveColor: Colors.red, + seekLineColor: Colors.red, + scaleFactor: 600, + ), ), - ); - }, - ), - Obx( - () => Text( - DateUtil.formatDateMs( - controller.durationMax.value, - format: 'HH:mm:ss', - isUtc: true, ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, + SizedBox( + width: 350.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamBuilder( + stream: controller + .playerController.onCurrentDurationChanged, + builder: (context, snapshot) { + final position = snapshot.data ?? 0; + return Text( + DateUtil.formatDateMs( + position, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + fontSize: 12.sp, + ), + ); + }, + ), + Obx( + () => Text( + DateUtil.formatDateMs( + controller.durationMax.value, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + fontSize: 12.sp, + ), + ), + ), + ], + ), ), - ), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Icon( - Icons.replay_10, - color: Colors.black, - size: 30.w, - ), - StreamBuilder( - stream: controller.playerController.onPlayerStateChanged, - builder: (context, snapshot) { - bool isPlay = snapshot.data?.isPlaying ?? false; - return GestureDetector( - onTap: () { - if (isPlay) { - controller.playerController.pausePlayer(); - } else { - controller.playerController.startPlayer(); - } - }, - child: Icon( - isPlay ? Icons.pause_circle : Icons.play_circle, - color: Colors.black, - size: 48.w, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon( + Icons.replay_10, + color: Colors.black, + size: 30.w, + ), + StreamBuilder( + stream: + controller.playerController.onPlayerStateChanged, + builder: (context, snapshot) { + bool isPlay = snapshot.data?.isPlaying ?? false; + return GestureDetector( + onTap: () { + if (isPlay) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } + }, + child: Icon( + isPlay ? Icons.pause_circle : Icons.play_circle, + color: Colors.black, + size: 48.w, + ), + ); + }, + ), + Icon( + Icons.forward_10, + color: Colors.black, + size: 30.w, + ), + ], ), - ); - }, - ), - Icon( - Icons.forward_10, - color: Colors.black, - size: 30.w, + ], + ), ), - ], - ), - ], + ), + ], + ), ), ); } diff --git a/lib/modules/meeting/views/mind_map_tab.dart b/lib/modules/meeting/views/mind_map_tab.dart index 1f93e04fa..40528c592 100644 --- a/lib/modules/meeting/views/mind_map_tab.dart +++ b/lib/modules/meeting/views/mind_map_tab.dart @@ -11,7 +11,8 @@ class MindMapTab extends StatefulWidget { State createState() => _MindMapTabState(); } -class _MindMapTabState extends State { +class _MindMapTabState extends State + with AutomaticKeepAliveClientMixin { late final WebViewController _controller; final _meetingDetailsController = Get.find(); @@ -41,12 +42,14 @@ class _MindMapTabState extends State { height: 100%; margin: 0; padding: 0; + overflow: hidden; } .markmap { width: 100%; height: 100%; position: relative; + overflow: hidden; } .markmap>svg { @@ -76,6 +79,7 @@ class _MindMapTabState extends State { markmap: maxWidth: 300 colorFreezeLevel: 2 + initialExpandLevel: 3 --- ${_meetingDetailsController.meetingDetails.value.summary} @@ -90,8 +94,12 @@ ${_meetingDetailsController.meetingDetails.value.summary} ..loadHtmlString(html); } + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); return WebViewWidget( controller: _controller, );