Commit a506beff7f4ef20d2016b98e901c8aeda51af822

Authored by 吴启风
1 parent 608c05b4

feat:先声sdk方法找不到问题修复;绘本接口&逻辑

android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt
... ... @@ -16,6 +16,8 @@ class MainActivity : FlutterActivity() {
16 16 override fun onCreate(savedInstanceState: Bundle?) {
17 17 super.onCreate(savedInstanceState)
18 18 Log.i("WowEnglish", "MainActivity onCreate")
  19 +
  20 + flutterEngine?.let { SingSoungMethodChannel(this, it) }
19 21 }
20 22  
21 23 override fun onResume() {
... ... @@ -46,9 +48,4 @@ class MainActivity : FlutterActivity() {
46 48 // 打开沉浸式
47 49 WindowCompat.setDecorFitsSystemWindows(window, false)*/
48 50 }
49   -
50   - override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
51   - super.configureFlutterEngine(flutterEngine)
52   - SingSoungMethodChannel(this, flutterEngine)
53   - }
54 51 }
... ...
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt
1 1 package com.kouyuxingqiu.wow_english.methodChannels
2 2  
  3 +import android.util.Log
  4 +import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper
3 5 import io.flutter.embedding.android.FlutterActivity
4 6 import io.flutter.embedding.engine.FlutterEngine
5 7 import io.flutter.plugin.common.MethodChannel
... ... @@ -30,10 +32,18 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F
30 32 )
31 33 methodChannel?.setMethodCallHandler { call, result ->
32 34 when (call.method) {
33   - "startRecord" -> {
34   - val jsonStr = call.arguments as? String ?: return@setMethodCallHandler
  35 + "initVoiceSdk" -> {
  36 + SingEngineHelper.init(activity)
  37 + }
  38 + "startVoice" -> {
  39 + val paramMap = call.arguments as HashMap<String, String>
  40 + Log.d("WQF", "SingSoungMethodChannel startVoice=${call.arguments.javaClass} paramMap=$paramMap")
  41 + paramMap["word"]?.let { SingEngineHelper.startRecord(it) }
35 42 //do nothing
36 43 }
  44 + "stopVoice" -> {
  45 + Log.d("WQF", "SingSoungMethodChannel stopVoice")
  46 + }
37 47 else -> {
38 48 result.notImplemented()
39 49 }
... ...
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt
... ... @@ -18,7 +18,7 @@ import org.json.JSONObject
18 18 import java.util.*
19 19  
20 20  
21   -class SingEngineHelper private constructor() :
  21 +object SingEngineHelper :
22 22 AudioErrorCallback, EvalReturnRequestIdCallback, OnRealTimeResultListener {
23 23  
24 24 private val TAG = "SingEngineManager"
... ... @@ -54,57 +54,59 @@ class SingEngineHelper private constructor() :
54 54 mListeners = mutableListOf()
55 55 if (mSingEngine == null) {
56 56 mSingEngine = SingEngine.newInstance(context)
57   - }
58   - Thread {
59   - try {
60   - mSingEngine?.run {
61   - // 设置测评结果监听器
62   - setListener(this@SingEngineHelper)
63   - // 设置录音器初始化错误的回调
64   - setAudioErrorCallback(this@SingEngineHelper)
65   - setEvalReturnRequestIdCallback(this@SingEngineHelper)
  57 + Thread {
  58 + try {
  59 + mSingEngine?.run {
  60 + // 设置测评结果监听器
  61 + setListener(this@SingEngineHelper)
  62 + // 设置录音器初始化错误的回调
  63 + setAudioErrorCallback(this@SingEngineHelper)
  64 + setEvalReturnRequestIdCallback(this@SingEngineHelper)
66 65 // // 设置音频格式
67 66 // setAudioType(AudioTypeEnum.WAV)
68   - // 设置引擎类型。引擎类型(在线CLOUD、 离线NATIVE、混合AUTO),默认使用在线引擎。
69   - setServerType(CoreProvideTypeEnum.CLOUD)
70   - // 设置log日志级别
71   - setLogLevel(4)
72   - // 禁用实时音量返回
73   - disableVolume()
74   - // 设置录音音频路径
75   - wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/"
76   - // 设置是否开启 VAD 功能
77   - setOpenVad(true, "vad.0.1.bin")
78   - //setOpenVad(false, null);
79   - // 设置 VAD 前置超时时间
80   - setFrontVadTime(3000)
  67 + // 设置引擎类型。引擎类型(在线CLOUD、 离线NATIVE、混合AUTO),默认使用在线引擎。
  68 + setServerType(CoreProvideTypeEnum.CLOUD)
  69 + // 设置log日志级别
  70 + setLogLevel(4)
  71 + // 禁用实时音量返回
  72 + disableVolume()
  73 + // 设置录音音频路径
  74 + wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/"
  75 + // 设置是否开启 VAD 功能
  76 + setOpenVad(true, "vad.0.1.bin")
  77 + //setOpenVad(false, null);
  78 + // 设置 VAD 前置超时时间
  79 + setFrontVadTime(3000)
81 80 // setServerTimeout(10000)
82   - // 开启错误日志保存到本地,发生错误时文件中会保存到android/data/包名/files/SSError.txt中
  81 + // 开启错误日志保存到本地,发生错误时文件中会保存到android/data/包名/files/SSError.txt中
83 82 // setOpenWriteLog(true)
84   - // 设置在线服务器地址和账号
85   - setServerAPI("wss://api.cloud.ssapi.cn")
  83 + // 设置在线服务器地址和账号
  84 + setServerAPI("wss://api.cloud.ssapi.cn")
86 85 // // 设置评测语言(针对离线评测)
87 86 // setOffLineSource(OffLineSourceEnum.SOURCE_EN)
88   - // 设置引擎初始化参数
89   - setNewCfg(
90   - buildInitJson(
91   - SingSoundConfig.APPKEY,
92   - SingSoundConfig.SECERTKEY
  87 + // 设置引擎初始化参数
  88 + setNewCfg(
  89 + buildInitJson(
  90 + SingSoundConfig.APPKEY,
  91 + SingSoundConfig.SECERTKEY
  92 + )
93 93 )
94   - )
95   - // 引擎初始化
96   - createEngine()
97   - }
  94 + // 引擎初始化
  95 + createEngine("1")
98 96  
99   - getSymbolsMap()
100   - } catch (e: Exception) {
101   - e.printStackTrace()
102   - }
103   - }.start()
  97 + Log.w(TAG, "createEngine")
  98 + }
  99 +
  100 + getSymbolsMap()
  101 + } catch (e: Exception) {
  102 + e.printStackTrace()
  103 + }
  104 + }.start()
  105 + }
104 106 }
105 107  
106 108 // 开始语音评测
107   - fun startRecord(originText: String, @EvalTargetType evalTargetType: Int?) {
  109 + fun startRecord(originText: String, @EvalTargetType evalTargetType: Int? = EvalTargetType.SENTENCE) {
108 110 try {
109 111 val request = JSONObject()
110 112 when (evalTargetType) {
... ...
lib/generated/json/course_process_entity.g.dart
... ... @@ -38,9 +38,9 @@ Map&lt;String, dynamic&gt; $CourseProcessEntityToJson(CourseProcessEntity entity) {
38 38  
39 39 CourseProcessReadings $CourseProcessReadingsFromJson(Map<String, dynamic> json) {
40 40 final CourseProcessReadings courseProcessReadings = CourseProcessReadings();
41   - final String? auditUrl = jsonConvert.convert<String>(json['auditUrl']);
42   - if (auditUrl != null) {
43   - courseProcessReadings.auditUrl = auditUrl;
  41 + final String? audioUrl = jsonConvert.convert<String>(json['audioUrl']);
  42 + if (audioUrl != null) {
  43 + courseProcessReadings.audioUrl = audioUrl;
44 44 }
45 45 final int? courseLessonId = jsonConvert.convert<int>(json['courseLessonId']);
46 46 if (courseLessonId != null) {
... ... @@ -83,7 +83,7 @@ CourseProcessReadings $CourseProcessReadingsFromJson(Map&lt;String, dynamic&gt; json)
83 83  
84 84 Map<String, dynamic> $CourseProcessReadingsToJson(CourseProcessReadings entity) {
85 85 final Map<String, dynamic> data = <String, dynamic>{};
86   - data['auditUrl'] = entity.auditUrl;
  86 + data['audioUrl'] = entity.audioUrl;
87 87 data['courseLessonId'] = entity.courseLessonId;
88 88 data['createTime'] = entity.createTime;
89 89 data['deleted'] = entity.deleted;
... ...
lib/models/course_process_entity.dart
... ... @@ -24,7 +24,7 @@ class CourseProcessEntity {
24 24  
25 25 @JsonSerializable()
26 26 class CourseProcessReadings {
27   - String? auditUrl;
  27 + String? audioUrl;
28 28 int? courseLessonId;
29 29 String? createTime;
30 30 String? deleted;
... ...
lib/pages/practice/bloc/topic_picture_bloc.dart
... ... @@ -142,7 +142,7 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
142 142 void _voiceXsTest(XSVoiceTestEvent event,Emitter<TopicPictureState> emitter) async {
143 143 EasyLoading.show(status: '录音中....');
144 144 methodChannel.invokeMethod(
145   - 'starVoice',
  145 + 'startVoice',
146 146 {'word':event.testWord,'type':event.type,'userId':event.userId.toString()}
147 147 );
148 148 _isVoicing = true;
... ...
lib/pages/reading/bloc/reading_bloc.dart
  1 +import 'package:audioplayers/audioplayers.dart';
1 2 import 'package:flutter/cupertino.dart';
  3 +import 'package:flutter/foundation.dart';
  4 +import 'package:flutter/services.dart';
2 5 import 'package:flutter_bloc/flutter_bloc.dart';
  6 +import 'package:flutter_easyloading/flutter_easyloading.dart';
3 7 import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart';
4 8  
  9 +import '../../../common/request/dao/listen_dao.dart';
  10 +import '../../../common/request/exception.dart';
  11 +import '../../../models/course_process_entity.dart';
  12 +import '../../../utils/loading.dart';
  13 +
5 14 part 'reading_event.dart';
6 15 part 'reading_state.dart';
7 16  
8 17 class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> {
9   -
10 18 final PageController pageController;
11 19  
12 20 ///当前页索引
... ... @@ -19,26 +27,68 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
19 27  
20 28 ReadingModeType get currentMode => _currentMode;
21 29  
  30 + CourseProcessEntity? _entity;
  31 +
  32 + CourseProcessEntity? get entity => _entity;
  33 +
  34 + ///正在评测
  35 + bool _isRecording = false;
  36 +
  37 + bool get isRecording => _isRecording;
  38 +
  39 + late MethodChannel methodChannel;
  40 +
  41 + late AudioPlayer audioPlayer;
  42 +
22 43 ReadingPageBloc(this.pageController) : super(ReadingPageInitial()) {
23 44 on<CurrentPageIndexChangeEvent>(_pageControllerChange);
24 45 on<CurrentModeChangeEvent>(_selectItemLoad);
25 46 // pageController.addListener(() {
26 47 // _currentPage = pageController.page!.round();
27 48 // });
  49 + on<RequestDataEvent>(_requestData);
  50 + on<ReadingPageEvent>((event, emit) {
  51 + //音频播放器
  52 + audioPlayer = AudioPlayer();
  53 + audioPlayer.onPlayerStateChanged.listen((event) {
  54 + if (event == PlayerState.completed) {
  55 + if (kDebugMode) {
  56 + print('绘本播放完成');
  57 +
  58 + }
  59 + }
  60 + });
  61 +
  62 + methodChannel = const MethodChannel('sing_sound_method_channel');
  63 + methodChannel.invokeMethod('initVoiceSdk',{});
  64 + methodChannel.setMethodCallHandler((call) async {
  65 + if (call.method == 'voiceResult') {//评测结束
  66 + // add(XSVoiceResultEvent(call.arguments));
  67 + }
  68 + });
  69 + });
  70 + on<PlayOriginalAudioEvent>(_playOriginalAudio);
  71 + on<XSVoiceTestEvent>(_voiceXsTest);
  72 + on<XSVoiceResultEvent>(_voiceXsResult);
28 73 }
29 74  
30 75 @override
31 76 Future<void> close() {
32 77 pageController.dispose();
  78 + audioPlayer.release();
  79 + audioPlayer.dispose();
33 80 return super.close();
34 81 }
35 82  
36   - void _pageControllerChange(CurrentPageIndexChangeEvent event, Emitter<ReadingPageState> emitter) async {
  83 + void _pageControllerChange(CurrentPageIndexChangeEvent event,
  84 + Emitter<ReadingPageState> emitter) async {
37 85 _currentPage = event.pageIndex;
  86 + _playOriginVoice(null);
38 87 emitter(CurrentPageIndexState());
39 88 }
40 89  
41   - void _selectItemLoad(CurrentModeChangeEvent event, Emitter<ReadingPageState> emitter) async {
  90 + void _selectItemLoad(
  91 + CurrentModeChangeEvent event, Emitter<ReadingPageState> emitter) async {
42 92 if (_currentMode == ReadingModeType.auto) {
43 93 _currentMode = ReadingModeType.manual;
44 94 } else {
... ... @@ -46,4 +96,77 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
46 96 }
47 97 emitter(CurrentModeState());
48 98 }
  99 +
  100 + ///请求数据
  101 + void _requestData(
  102 + RequestDataEvent event, Emitter<ReadingPageState> emitter) async {
  103 + try {
  104 + await loading(() async {
  105 + _entity = await ListenDao.process('1');
  106 + print("reading page entity: ${_entity!.toJson()}");
  107 + emitter(RequestDataState());
  108 + });
  109 + } catch (e) {
  110 + if (e is ApiException) {
  111 + EasyLoading.showToast(e.message ?? '请求失败,请检查网络连接');
  112 + }
  113 + }
  114 + }
  115 +
  116 + void _playOriginalAudio(PlayOriginalAudioEvent event, Emitter<ReadingPageState> emitter) async {
  117 + print("_playOriginalAudio");
  118 + _playOriginVoice(event.url);
  119 + }
  120 +
  121 + /// 播放绘本原音
  122 + void _playOriginVoice(String? audioUrl) async {
  123 + audioPlayer.stop();
  124 + final readingData = currentPageData();
  125 + if (readingData?.audioUrl != null) {
  126 + final urlStr = audioUrl ?? readingData?.audioUrl ?? '';
  127 + if (urlStr.isNotEmpty) {
  128 + audioPlayer.play(UrlSource(urlStr));
  129 + }
  130 + }
  131 + }
  132 +
  133 + int dataCount() {
  134 + // print("dataCount=${_entity?.readings?.length ?? 0}");
  135 + return _entity?.readings?.length ?? 0;
  136 + }
  137 +
  138 + CourseProcessReadings? currentPageData() {
  139 + return _entity?.readings?[_currentPage];
  140 + }
  141 +
  142 + ///先声测试
  143 + void _voiceXsTest(XSVoiceTestEvent event, Emitter<ReadingPageState> emitter) async {
  144 + startRecord(event.content);
  145 + emitter(XSVoiceTestState());
  146 + }
  147 +
  148 + void startRecord(String content) async {
  149 + if (_isRecording == true) {
  150 + return;
  151 + }
  152 + EasyLoading.show(status: '录音中....');
  153 + methodChannel.invokeMethod(
  154 + 'startVoice',
  155 + {'word':'how old are you','type':'0','userId':'1'}
  156 + );
  157 + _isRecording = true;
  158 + }
  159 +
  160 + void _voiceXsResult(XSVoiceResultEvent event,Emitter<ReadingPageState> emitter) async {
  161 + final Map args = event.message as Map;
  162 + final result = args['result'] as String;
  163 + if (result == '1') {
  164 + final overall = args['overall'].toString();
  165 + EasyLoading.showToast('测评成功,分数是$overall',duration: const Duration(seconds: 10));
  166 + } else {
  167 + EasyLoading.showToast('测评失败',duration: const Duration(seconds: 10));
  168 + }
  169 + _isRecording = false;
  170 + emitter(XSVoiceTestState());
  171 + }
49 172 }
... ...
lib/pages/reading/bloc/reading_event.dart
... ... @@ -8,4 +8,33 @@ class CurrentPageIndexChangeEvent extends ReadingPageEvent {
8 8 CurrentPageIndexChangeEvent(this.pageIndex);
9 9 }
10 10  
11   -class CurrentModeChangeEvent extends ReadingPageEvent {}
12 11 \ No newline at end of file
  12 +class CurrentModeChangeEvent extends ReadingPageEvent {}
  13 +
  14 +///请求接口获取数据
  15 +class RequestDataEvent extends ReadingPageEvent {}
  16 +
  17 +///播放原音频
  18 +class PlayOriginalAudioEvent extends ReadingPageEvent {
  19 + final String? url;
  20 + PlayOriginalAudioEvent(this.url);
  21 +}
  22 +
  23 +///初始化先声SDK
  24 +class XSVoiceInitEvent extends ReadingPageEvent {
  25 + final Map data;
  26 + XSVoiceInitEvent(this.data);
  27 +}
  28 +
  29 +///评测结果
  30 +class XSVoiceResultEvent extends ReadingPageEvent {
  31 + final dynamic message;
  32 + XSVoiceResultEvent(this.message);
  33 +}
  34 +
  35 +///先声测试
  36 +class XSVoiceTestEvent extends ReadingPageEvent {
  37 + final String content;
  38 + final String type;
  39 + final String userId;
  40 + XSVoiceTestEvent(this.content,this.type,this.userId);
  41 +}
13 42 \ No newline at end of file
... ...
lib/pages/reading/bloc/reading_state.dart
... ... @@ -9,3 +9,7 @@ class CurrentPageIndexState extends ReadingPageState {}
9 9  
10 10 /// 手动or自动播放
11 11 class CurrentModeState extends ReadingPageState {}
  12 +
  13 +class RequestDataState extends ReadingPageState {}
  14 +
  15 +class XSVoiceTestState extends ReadingPageState {}
... ...
lib/pages/reading/reading_page.dart
... ... @@ -4,6 +4,8 @@ import &#39;package:flutter_screenutil/flutter_screenutil.dart&#39;;
4 4 import 'package:wow_english/common/extension/string_extension.dart';
5 5 import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart';
6 6  
  7 +import '../../common/core/user_util.dart';
  8 +import '../../models/course_process_entity.dart';
7 9 import 'bloc/reading_bloc.dart';
8 10  
9 11 class ReadingPage extends StatelessWidget {
... ... @@ -12,7 +14,7 @@ class ReadingPage extends StatelessWidget {
12 14 @override
13 15 Widget build(BuildContext context) {
14 16 return BlocProvider(
15   - create: (_) => ReadingPageBloc(PageController()),
  17 + create: (_) => ReadingPageBloc(PageController())..add(RequestDataEvent()),
16 18 child: _ReadingPage(),
17 19 );
18 20 }
... ... @@ -22,7 +24,15 @@ class _ReadingPage extends StatelessWidget {
22 24 @override
23 25 Widget build(BuildContext context) {
24 26 return BlocListener<ReadingPageBloc, ReadingPageState>(
25   - listener: (context, state) {},
  27 + listener: (context, state) {
  28 + if (state is RequestDataState) {
  29 + // context.read<TopicPictureBloc>().add(CurrentPageIndexChangeEvent(0));
  30 + print('reading RequestDataState=$state');
  31 +
  32 + ///刷新页面
  33 + context.read<ReadingPageBloc>().add(CurrentPageIndexChangeEvent(0));
  34 + }
  35 + },
26 36 child: _readingPageView(),
27 37 );
28 38 }
... ... @@ -36,13 +46,13 @@ class _ReadingPage extends StatelessWidget {
36 46 child: Stack(
37 47 children: [
38 48 PageView.builder(
39   - itemCount: 10,
  49 + itemCount: bloc.dataCount(),
40 50 controller: bloc.pageController,
41 51 onPageChanged: (int index) {
42 52 bloc.add(CurrentPageIndexChangeEvent(index));
43 53 },
44 54 itemBuilder: (context, int index) {
45   - return _readingPagerItem();
  55 + return _readingPagerItem(bloc.entity!.readings![index]);
46 56 }),
47 57 Container(
48 58 color: Colors.transparent,
... ... @@ -76,15 +86,14 @@ class _ReadingPage extends StatelessWidget {
76 86 ),
77 87 alignment: Alignment.center,
78 88 child: Text(
79   - '${bloc.currentPage}/10',
80   -
81   - ///todo 分母需要替换成数据数组长度
  89 + '${bloc.currentPage}/${bloc.dataCount()}',
82 90 style: TextStyle(fontSize: 20.sp, color: Colors.white),
83 91 ),
84 92 ),
85 93  
86 94 Padding(
87   - padding: EdgeInsets.only(right: 15.w + ScreenUtil().bottomBarHeight),
  95 + padding: EdgeInsets.only(
  96 + right: 15.w + ScreenUtil().bottomBarHeight),
88 97 child: GestureDetector(
89 98 onTap: () {
90 99 bloc.add(CurrentModeChangeEvent());
... ... @@ -121,17 +130,26 @@ class _ReadingPage extends StatelessWidget {
121 130 margin: EdgeInsets.symmetric(horizontal: 10.w),
122 131 child: Row(
123 132 children: [
124   - Image.asset(
125   - 'voice'.assetPng,
126   - height: 40.h,
127   - width: 45.w,
  133 + GestureDetector(
  134 + onTap: () {
  135 + if (bloc.isRecording) {
  136 + return;
  137 + }
  138 + print("voice tap");
  139 + bloc.add(PlayOriginalAudioEvent(null));
  140 + },
  141 + child: Image.asset(
  142 + 'voice'.assetPng,
  143 + height: 40.h,
  144 + width: 45.w,
  145 + ),
128 146 ),
129 147 SizedBox(
130 148 width: 10.w,
131 149 ),
132 150 Expanded(
133 151 child: Text(
134   - "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorld",
  152 + bloc.currentPageData()?.word ?? '',
135 153 style: TextStyle(
136 154 color: const Color(0xFF333333), fontSize: 21.sp),
137 155 maxLines: 2,
... ... @@ -140,11 +158,18 @@ class _ReadingPage extends StatelessWidget {
140 158 SizedBox(
141 159 width: 10.w,
142 160 ),
143   - Image.asset(
144   - 'micro_phone'.assetPng,
145   - height: 47.h,
146   - width: 47.w,
147   - ),
  161 + GestureDetector(
  162 + onTap: () {
  163 + if (bloc.isRecording) {
  164 + return;
  165 + }
  166 + bloc.add(XSVoiceTestEvent(bloc.currentPageData()?.word??'', '0',UserUtil.getUser()!.id.toString()));
  167 + },
  168 + child: Image.asset(
  169 + 'micro_phone'.assetPng,
  170 + height: 47.h,
  171 + width: 47.w,
  172 + )),
148 173 SizedBox(
149 174 width: 10.w,
150 175 ),
... ... @@ -169,14 +194,12 @@ class _ReadingPage extends StatelessWidget {
169 194 );
170 195 });
171 196  
172   - Widget _readingPagerItem() =>
  197 + Widget _readingPagerItem(CourseProcessReadings readings) =>
173 198 BlocBuilder<ReadingPageBloc, ReadingPageState>(builder: (context, state) {
174 199 return Stack(
175 200 children: [
176   - Image.network(
177   - 'https://img.liblibai.com/web/648331d5a2cb5.png?image_process=format,webp&x-oss-process=image/resize,w_2980,m_lfit/format,webp',
178   - height: double.infinity,
179   - width: double.infinity),
  201 + Image.network(readings.picUrl ?? '',
  202 + height: double.infinity, width: double.infinity),
180 203 ],
181 204 );
182 205 });
... ...