diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fbf71f5..fa9de80 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + + + + + + + + diff --git a/assets/images/output-onlinegiftools.gif b/assets/images/output-onlinegiftools.gif new file mode 100644 index 0000000..ceb92af Binary files /dev/null and b/assets/images/output-onlinegiftools.gif differ diff --git a/assets/images/sound.png b/assets/images/sound.png new file mode 100644 index 0000000..b86dbf1 Binary files /dev/null and b/assets/images/sound.png differ diff --git a/assets/images/sound_wave.gif b/assets/images/sound_wave.gif new file mode 100644 index 0000000..e83822d Binary files /dev/null and b/assets/images/sound_wave.gif differ diff --git a/lib/models/word_card_model.dart b/lib/models/word_card_model.dart index b20e37d..bbc99ad 100644 --- a/lib/models/word_card_model.dart +++ b/lib/models/word_card_model.dart @@ -4,6 +4,8 @@ class WordCard { final String speaker; final String video; final int type; + final List intensities; + final List pattern; WordCard({ required this.id, @@ -11,6 +13,8 @@ class WordCard { required this.speaker, required this.video, required this.type, + required this.intensities, + required this.pattern, }); Map toMap() { @@ -20,16 +24,25 @@ class WordCard { 'speaker': speaker, 'video': video, 'type': type, + 'intensities': intensities.join(','), + 'pattern': pattern.join(','), }; } factory WordCard.fromDocument(Map doc) { + List parseToIntList(String numbers) { + return numbers.split(',').map((s) => int.parse(s.trim())).toList(); + } + return WordCard( id: doc['id'], word: doc['word'], speaker: doc['speaker'], video: doc['video'], type: doc['type'], + intensities: doc['intensities'] != null ? parseToIntList(doc['intensities']) : [], + pattern: doc['pattern'] != null ? parseToIntList(doc['pattern']) : [], + ); } @@ -39,6 +52,8 @@ class WordCard { String? speaker, String? video, int? type, + List? intensities, + List? pattern, }) { return WordCard( id: id ?? this.id, @@ -46,6 +61,8 @@ class WordCard { speaker: speaker ?? this.speaker, video: video ?? this.video, type: type ?? this.type, + intensities: intensities ?? this.intensities, + pattern: pattern ?? this.pattern, ); } } diff --git a/lib/views/script/create_script_screen.dart b/lib/views/script/create_script_screen.dart index 7536c23..e698a46 100644 --- a/lib/views/script/create_script_screen.dart +++ b/lib/views/script/create_script_screen.dart @@ -4,8 +4,8 @@ import 'package:earlips/viewModels/script/create_script_viewmodel.dart'; import 'package:get/get.dart'; class CreateScriptPage extends StatelessWidget { - final String? title; // 선택적으로 제목을 받음 - final String? text; // 선택적으로 텍스트를 받음 + final String? title; + final String? text; const CreateScriptPage({super.key, this.title, this.text}); @@ -40,66 +40,36 @@ class CreateScriptPage extends StatelessWidget { Column( // 기존의 Column 구조를 Stack 내에 배치합니다. children: [ - Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: text != null - ? // 텍스트가 제공되면 이를 사용하여 Container를 구성 - Container( - width: Get.width - 40, - margin: const EdgeInsets.all(10.0), - padding: const EdgeInsets.all(20.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15.0), - color: Colors.white, - ), - child: Text( - text!, - style: const TextStyle(fontSize: 16), - ), - ) - : // 텍스트가 제공되지 않으면 기본 TextField를 사용 - TextField( - controller: model.writedTextController, - expands: true, - maxLines: null, - decoration: InputDecoration( - hintText: 'live_script_hint'.tr, - fillColor: Colors.white, - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15.0), - borderSide: BorderSide.none, - ), - ), - textAlignVertical: TextAlignVertical.top, - ), - ), + _TopBoxs(number: text == null ? 1 : 2,), + _ScriptBox(text: text), + model.isRecording ? Image.asset( + "assets/images/sound_wave.gif" , + width: 180, + height: 90, + ) : Container( + width: 180, + height: 90, ), - Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.fromLTRB(25, 20, 25, 100), - child: Container( - padding: const EdgeInsets.all(20.0), - width: Get.width * 0.8, + + Container( + margin: const EdgeInsets.only(bottom: 40.0), + child: Align( + alignment: Alignment.bottomCenter, + child: Ink( decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15.0), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 1, - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], + color: model.isRecording ? Colors.red : Colors.blue, + borderRadius: BorderRadius.circular(40), ), - child: SingleChildScrollView( - child: Text( - model.voicedTextController.text, - style: const TextStyle(fontSize: 16), + child: InkWell( + borderRadius: BorderRadius.circular(40), + onTap: model.toggleRecording, + child: Padding( + padding: const EdgeInsets.all(20), + child: Icon( + model.isRecording ? Icons.stop : Icons.mic, + size: 30, + color: Colors.white, + ), ), ), ), @@ -107,37 +77,138 @@ class CreateScriptPage extends StatelessWidget { ), ], ), - Positioned( - // Positioned 위젯으로 사용자 정의 FloatingActionButton을 배치합니다. - bottom: 20, - left: 0, - right: 0, - child: Align( - alignment: Alignment.bottomCenter, - child: Ink( - decoration: BoxDecoration( - color: model.isRecording ? Colors.red : Colors.blue, - borderRadius: BorderRadius.circular(40), - ), - child: InkWell( - borderRadius: BorderRadius.circular(40), - onTap: model.toggleRecording, - child: Padding( - padding: const EdgeInsets.all(20), - child: Icon( - model.isRecording ? Icons.stop : Icons.mic, - size: 30, - color: Colors.white, - ), - ), - ), - ), - ), + + ], + ), + ), + ), + ); + } +} + + +class _TopBoxs extends StatelessWidget { + const _TopBoxs({super.key, this.number}); + final int? number; + + @override + Widget build(BuildContext context) { + Color leftColor = number == 1 ? Color(0xFF62ACDE) : Color(0xFFE8E8E8); + Color rightColor = number == 2 ? Color(0xFF62ACDE) : Color(0xFFE8E8E8); + Color leftTextColor = number == 1 ? Colors.white : Colors.white; // 오른쪽 텍스트 색깔을 다르게 설정 + Color rightTextColor = number == 2 ? Colors.white : Colors.white; + + return Container( + margin: const EdgeInsets.only(top: 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: leftColor, + borderRadius: BorderRadius.circular(7.0), + ), + height: 35, + width: Get.width * 0.5 - 24, + child: Text("대본 생성 및 녹음", style: TextStyle( + color: leftTextColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ),), + ), + SizedBox(width: 8), + Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: rightColor, + borderRadius: BorderRadius.circular(7.0), + ), + height: 35, + width: Get.width * 0.5 - 24, + child: Text("대본 기반 녹음", style: TextStyle( + color: rightTextColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ),), + ), + ], + ), + ); + } +} + +class _ScriptBox extends StatelessWidget { + const _ScriptBox({super.key, this.text}); + final String? text; + + @override + Widget build(BuildContext context) { + final model = context.watch(); + return Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: text != null + ? // 텍스트가 제공되면 이를 사용하여 Container를 구성 + Container( + width: Get.width - 40, + margin: const EdgeInsets.all(10.0), + padding: const EdgeInsets.all(20.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.0), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + spreadRadius: 0, + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Text( + text!, + style: const TextStyle(fontSize: 16), + ), + ) + : // 텍스트가 제공되지 않으면 기본 TextField를 사용 + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.0), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + spreadRadius: 0, + blurRadius: 20, + offset: const Offset(0, 4), ), ], ), + margin: const EdgeInsets.all(10.0), + width: Get.width - 40, + child: TextField( + controller: model.writedTextController, + expands: true, + maxLines: null, + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(15.0), + hintText: 'live_script_hint'.tr, + fillColor: Colors.white, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15.0), + borderSide: BorderSide.none, + ), + + ), + textAlignVertical: TextAlignVertical.top, + ), ), ), ); } } + + diff --git a/lib/views/script/learning_session_screen.dart b/lib/views/script/learning_session_screen.dart index faf89b4..2bb9a33 100644 --- a/lib/views/script/learning_session_screen.dart +++ b/lib/views/script/learning_session_screen.dart @@ -101,52 +101,51 @@ class _LearningSessionScreenState extends State { } Widget _buildParagraphContainer(Paragraph paragraph) { - return Container( - width: Get.width * 0.9, - height: 94.0, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15.0), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.05), - spreadRadius: 0.1, - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - alignment: Alignment.centerLeft, - margin: EdgeInsets.only(left: 16.0,top: 16.0,bottom: 8.0), - child: Text( - paragraph.dateFormat == "Example" ? "script example" : "${DateFormat('yyyy년 MM월 dd일 ').format(DateFormat('yyyy/MM/dd').parse(paragraph.dateFormat!))}진행한 학습", - style: TextStyle( - fontSize: 11, - color: Color(0xFF6E6A7C), + return InkWell( + onTap: () { + Get.to(() => CreateScriptPage( + title: paragraph.title, text: paragraph.text)); + }, + child: Container( + width: Get.width * 0.9, + height: 100.0, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.0), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + spreadRadius: 0.1, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + alignment: Alignment.centerLeft, + margin: EdgeInsets.only(left: 16.0,top: 16.0,bottom: 8.0), + child: Text( + paragraph.dateFormat == "Example" ? "script example" : "${DateFormat('yyyy년 MM월 dd일 ').format(DateFormat('yyyy/MM/dd').parse(paragraph.dateFormat!))}진행한 학습", + style: TextStyle( + fontSize: 11, + color: Color(0xFF6E6A7C), + ), ), ), - ), - Container( - margin: const EdgeInsets.only(right: 12.0, top: 12.0), - child: SvgPicture.asset("assets/icons/book.svg", - width: 24, height: 24), - ) - ], - ), - - InkWell( - onTap: () { - Get.to(() => CreateScriptPage( - title: paragraph.title, text: paragraph.text)); - }, - child: Container( + Container( + margin: const EdgeInsets.only(right: 12.0, top: 12.0), + child: SvgPicture.asset("assets/icons/book.svg", + width: 24, height: 24), + ) + ], + ), + Container( alignment: Alignment.centerLeft, margin: EdgeInsets.only(left: 16.0,bottom: 8.0, right: 16.0), child: Text( @@ -158,34 +157,34 @@ class _LearningSessionScreenState extends State { ), ), ), - ), - Container( - margin: const EdgeInsets.only(left: 16, right: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset("assets/icons/TimeCircle.svg", - width: 14, - color: const Color(0xFF5EC4E5)), - const SizedBox(width: 4), - Text( - paragraph.timeFormat!, - style: const TextStyle( - fontSize: 11, - color: Color(0xFF5EC4E5), - fontWeight: FontWeight.bold, + Container( + margin: const EdgeInsets.only(left: 16, right: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset("assets/icons/TimeCircle.svg", + width: 14, + color: const Color(0xFFC4C4D9)), + const SizedBox(width: 4), + Text( + paragraph.timeFormat!, + style: const TextStyle( + fontSize: 11, + color: Color(0xFFAAAABB), + fontWeight: FontWeight.bold, + ), ), - ), - ], - ), - _tagText(paragraph.text.length.toString()), - ], + ], + ), + _tagText(paragraph.text.length.toString()), + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/views/word/widget/sentence_alert_widget.dart b/lib/views/word/widget/sentence_alert_widget.dart index 42f7f30..e90eef8 100644 --- a/lib/views/word/widget/sentence_alert_widget.dart +++ b/lib/views/word/widget/sentence_alert_widget.dart @@ -4,6 +4,7 @@ import 'package:earlips/viewModels/word/word_viewmodel.dart'; import 'package:earlips/views/word/widget/highlight_mistake_text_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:earlips/views/word/widget/word_vibration_widget.dart'; Future SentenceAlertWidget(RecordViewModel model, WordViewModel wordViewModel, PageController pageController) { @@ -137,7 +138,10 @@ Future SentenceAlertWidget(RecordViewModel model, fontSize: 14, ), ), + WordVibrationWidget(), + WordVibrationWidget(), ], + ) ], ), diff --git a/lib/views/word/widget/word_result_dialog_widget.dart b/lib/views/word/widget/word_result_dialog_widget.dart index 21e3a1c..0446657 100644 --- a/lib/views/word/widget/word_result_dialog_widget.dart +++ b/lib/views/word/widget/word_result_dialog_widget.dart @@ -5,6 +5,7 @@ import 'package:earlips/views/word/widget/fail_word_dialog_widget.dart'; import 'package:earlips/views/word/widget/sucess_word_dialog_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:earlips/views/word/widget/word_vibration_widget.dart'; Future WordResultDialogWidget(RecordViewModel model, WordViewModel wordViewModel, PageController pageController) { @@ -48,6 +49,7 @@ Future WordResultDialogWidget(RecordViewModel model, color: ColorSystem.black, fontSize: 16, )), + ], ), ), diff --git a/lib/views/word/widget/word_sentence_widget.dart b/lib/views/word/widget/word_sentence_widget.dart index 97f4a8b..d5c75a1 100644 --- a/lib/views/word/widget/word_sentence_widget.dart +++ b/lib/views/word/widget/word_sentence_widget.dart @@ -6,6 +6,7 @@ import 'package:earlips/views/word/widget/sentence_guide_widget.dart'; import 'package:earlips/views/word/widget/word_result_dialog_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; // Import GetX library +import 'package:earlips/views/word/widget/word_vibration_widget.dart'; class WordSentenceWidget extends StatelessWidget { final List wordDataList; @@ -29,56 +30,50 @@ class WordSentenceWidget extends StatelessWidget { return Center( child: Column( children: [ - type == 2 - ? const Column( - children: [ - PronunciationGuidelinesWidget( - loudness: 50, - variance: 1, - ), - SizedBox( - height: 60, + if(type!= 0)Container( + child: WordVibrationWidget() + ), + Container( + margin: type != 0 ? EdgeInsets.only(top: Get.height * 0.1) : EdgeInsets.zero, + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + margin: EdgeInsets.only(bottom: 0), + child: Ink( + decoration: BoxDecoration( + color: model.isRecording.value ? Colors.red : Colors.blue, + borderRadius: BorderRadius.circular(40), + ), + child: InkWell( + borderRadius: BorderRadius.circular(40), + onTap: () async { + if (model.isRecording.value) { + await model.sendTextAndAudio( + wordDataList[wordViewModel.currentIndex.value] + .wordCard + .word, + type); + if (type != 0) { + // 타입이 2일 경우, 문장 교정 정보를 보여주는 대화상자를 표시 + SentenceAlertWidget( + model, wordViewModel, pageController); + } else { + WordResultDialogWidget( + model, wordViewModel, pageController); + } + } else { + // 녹음 시작 + model.sendTextAndAudio('content', 0); + } + }, + child: Padding( + padding: const EdgeInsets.all(20), + child: Icon( + model.isRecording.value ? Icons.stop : Icons.mic, + size: 30, + color: Colors.white, + ), ), - ], - ) - : const SizedBox( - height: 20, - ), - Align( - alignment: Alignment.bottomCenter, - child: Ink( - decoration: BoxDecoration( - color: model.isRecording.value ? Colors.red : Colors.blue, - borderRadius: BorderRadius.circular(40), - ), - child: InkWell( - borderRadius: BorderRadius.circular(40), - onTap: () async { - if (model.isRecording.value) { - await model.sendTextAndAudio( - wordDataList[wordViewModel.currentIndex.value] - .wordCard - .word, - type); - if (type == 2) { - // 타입이 2일 경우, 문장 교정 정보를 보여주는 대화상자를 표시 - SentenceAlertWidget( - model, wordViewModel, pageController); - } else { - WordResultDialogWidget( - model, wordViewModel, pageController); - } - } else { - // 녹음 시작 - model.sendTextAndAudio('content', 0); - } - }, - child: Padding( - padding: const EdgeInsets.all(20), - child: Icon( - model.isRecording.value ? Icons.stop : Icons.mic, - size: 30, - color: Colors.white, ), ), ), diff --git a/lib/views/word/widget/word_vibration_widget.dart b/lib/views/word/widget/word_vibration_widget.dart new file mode 100644 index 0000000..2d7f648 --- /dev/null +++ b/lib/views/word/widget/word_vibration_widget.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:vibration/vibration.dart'; +import 'package:earlips/viewModels/word/word_viewmodel.dart'; + + +class WordVibrationWidget extends StatelessWidget { + const WordVibrationWidget({super.key, this.externalPattern, this.externalIntensities}); + + final List? externalPattern; + final List? externalIntensities; + + @override + Widget build(BuildContext context) { + final wordViewModel = Get.find(); + + return Obx(() { + if (wordViewModel.currentIndex.value >= 0 && wordViewModel.currentIndex.value < wordViewModel.wordList.length) { + final currentWordCard = wordViewModel.wordList[wordViewModel.currentIndex.value].wordCard; + final List? pattern = externalPattern != null ? externalPattern : currentWordCard.pattern; + final List? intensities = externalIntensities != null ? externalIntensities : currentWordCard.intensities; + + + print('pattern: $pattern'); + print('intensities: $intensities'); + + return Container( + alignment: Alignment.topLeft, + margin: const EdgeInsets.only(top: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + child: Container( + width: Get.width * 0.33, + child: Row( + children: [ + Text('진동으로 들어보기'), + const SizedBox(width: 10), + Icon(Icons.vibration), + ], + ), + ), + onPressed: () { + Vibration.vibrate( + pattern: pattern!, + intensities: intensities!, + ); + }, + ), + ], + ), + ); + } else { + return Center(child: Text('Invalid index or word list empty')); + } + }); + } +} diff --git a/lib/views/word/word_screen.dart b/lib/views/word/word_screen.dart index 6f418a5..51b37dc 100644 --- a/lib/views/word/word_screen.dart +++ b/lib/views/word/word_screen.dart @@ -5,6 +5,7 @@ import 'package:earlips/views/phoneme/phoneme_detail_Screen.dart'; import 'package:earlips/views/word/widget/blue_back_appbar.dart'; import 'package:earlips/views/word/widget/word_list_widget.dart'; import 'package:earlips/views/word/widget/word_sentence_widget.dart'; +import 'package:earlips/views/word/widget/word_vibration_widget.dart'; import 'package:earlips/views/word/widget/word_youtube_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -142,20 +143,15 @@ class WordScreen extends StatelessWidget { // wordViewModel final String video로 영상 유튜브 링크를 바로 볼 수 있게 하기 GetBuilder( builder: (controller) { - if (controller.wordList.isEmpty) { - return const Center(child: Text("No data available")); - } - // ----------------------------------- 음소 교정 - if (controller.type == 0) { - // --- 한글일때.. + if (controller.type <= 2) { + return Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ // 유투브 영상 나옴 const YoutubeWordPlayer(), - // - const SizedBox( + if(controller.type == 0)SizedBox( height: 100, ), WordSentenceWidget( @@ -163,64 +159,17 @@ class WordScreen extends StatelessWidget { wordDataList: controller.wordList, type: controller.type, ), - const SizedBox( - height: 40, - ), - ], - ), - ); - } else if (controller.type == 1) { - // ----------------------------------- 단어 교정 - return Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - // 유투브 영상 나옴 - // const YoutubeWordPlayer(), - // - // Text( - // controller.wordList[0].wordCard.speaker, - // style: const TextStyle( - // fontSize: 16, - // fontWeight: FontWeight.w600, - // color: ColorSystem.black, - // ), - // ), - const SizedBox(height: 20), - PhoneticButtons( - phoneticString: - controller.wordList[0].wordCard.speaker), - // const SizedBox( - height: 50, - ), - WordSentenceWidget( - pageController: pageController, - wordDataList: controller.wordList, - type: controller.type, - ), - const SizedBox( - height: 40, + height: 30, ), ], ), ); - } else if (controller.type == 2) { - return Column( - children: [ - WordSentenceWidget( - pageController: pageController, - wordDataList: controller.wordList, - type: controller.type, - ), - const SizedBox( - height: 60, - ), - ], - ); - } else { - return const LearningSessionScreen(isStudyMode: true); + } + else { + return LearningSessionScreen(isStudyMode: true); + } }, ), diff --git a/pubspec.yaml b/pubspec.yaml index 8d8cbc7..80c0e45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: video_player: ^2.8.2 youtube_player_flutter: ^8.1.2 flutter_spinkit: ^5.2.1 + vibration: ^1.8.4 dev_dependencies: flutter_test: @@ -58,6 +59,8 @@ flutter: - assets/data/ - assets/images/home/ - assets/images/study/ + - assets/images/output-onlinegiftools.gif + - assets/images/sound_wave.gif # - assets/sounds/speech_to_text_listening.m4r # - assets/sounds/speech_to_text_cancel.m4r # - assets/sounds/speech_to_text_stop.m4r