diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000..00bbb29 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,5 @@ +{ + "language_settings": "Language Settings", + "system_language": "System Language", + "learning_language": "Learning Language" +} diff --git a/assets/translations/ko.json b/assets/translations/ko.json new file mode 100644 index 0000000..4c426ce --- /dev/null +++ b/assets/translations/ko.json @@ -0,0 +1,5 @@ +{ + "language_settings": "언어 설정", + "system_language": "시스템 언어", + "learning_language": "학습 언어" +} diff --git a/ios/Podfile b/ios/Podfile index 231da75..d580bf7 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -41,14 +41,6 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - - # You can enable the permissions needed here. For example to enable camera - # permission, just remove the `#` character in front so it looks like this: - # - # ## dart: PermissionGroup.camera - # 'PERMISSION_CAMERA=1' - # - # Preprocessor definitions can be found at: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', @@ -67,8 +59,8 @@ post_install do |installer| ## dart: PermissionGroup.camera # 'PERMISSION_CAMERA=1', - ## dart: PermissionGroup.microphone - # 'PERMISSION_MICROPHONE=1', + # dart: PermissionGroup.microphone + 'PERMISSION_MICROPHONE=1', ## dart: PermissionGroup.speech # 'PERMISSION_SPEECH_RECOGNIZER=1', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8c7380d..592c3a6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -634,6 +634,8 @@ PODS: - AppAuth/Core (1.6.2) - AppAuth/ExternalUserAgent (1.6.2): - AppAuth/Core + - audioplayers_darwin (0.0.1): + - Flutter - BoringSSL-GRPC (0.0.24): - BoringSSL-GRPC/Implementation (= 0.0.24) - BoringSSL-GRPC/Interface (= 0.0.24) @@ -840,6 +842,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -887,6 +890,8 @@ SPEC REPOS: - Try EXTERNAL SOURCES: + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/ios" cloud_firestore: :path: ".symlinks/plugins/cloud_firestore/ios" firebase_auth: @@ -923,6 +928,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: abseil: 926fb7a82dc6d2b8e1f2ed7f3a718bce691d1e46 AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 + audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33 cloud_firestore: ba576bee785a05ff952e4da7fa4e23c196917436 Firebase: 10c8cb12fb7ad2ae0c09ffc86cd9c1ab392a0031 @@ -964,6 +970,6 @@ SPEC CHECKSUMS: Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 -PODFILE CHECKSUM: 0b2c97823421f8b156b8e4753a469ac167670df8 +PODFILE CHECKSUM: 163f6f9d5628ee98843cfda3ba4b8cb24bfcad1a COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7052ce9..9ba0168 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -476,6 +476,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ""; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -605,6 +606,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -653,6 +655,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ""; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/lib/main.dart b/lib/main.dart index 92c8077..43d18f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ void main() async { /* Open .env file */ await dotenv.load(fileName: "assets/config/.env"); await initializeDateFormatting(); + WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); await SystemChrome.setPreferredOrientations([ @@ -23,16 +24,10 @@ void main() async { runApp(EasyLocalization( // 지원 언어 리스트 - supportedLocales: const [ - Locale('ko', 'KR'), - Locale('en', 'US'), - ], - //path: 언어 파일 경로 + saveLocale: true, + useOnlyLangCode: true, + supportedLocales: const [Locale('en'), Locale('ko')], path: 'assets/translations', - //fallbackLocale supportedLocales에 설정한 언어가 없는 경우 설정되는 언어 - fallbackLocale: const Locale('en', 'US'), - //startLocale을 지정하면 초기 언어가 설정한 언어로 변경됨 - //만일 이 설정을 하지 않으면 OS 언어를 따라 기본 언어가 설정됨 - //startLocale: Locale('ko', 'KR') + fallbackLocale: const Locale('en'), child: const MainApp(initialRoute: "/"))); } diff --git a/lib/utilities/base/supported_locale.dart b/lib/utilities/base/supported_locale.dart new file mode 100644 index 0000000..3852096 --- /dev/null +++ b/lib/utilities/base/supported_locale.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +const supportedLocale = [ + Locale.fromSubtags(languageCode: 'ko'), // English + Locale.fromSubtags(languageCode: 'us'), // German +]; diff --git a/lib/viewModels/home/home_viewmodel.dart b/lib/viewModels/home/home_viewmodel.dart index 186874c..8f71592 100644 --- a/lib/viewModels/home/home_viewmodel.dart +++ b/lib/viewModels/home/home_viewmodel.dart @@ -7,6 +7,12 @@ class HomeViewModel extends GetxController { var circleNumber = 42.obs; var linialPersent = 0.9.obs; + @override + void onInit() { + super.onInit(); + update(); + } + // 발음 점수 업데이트 void updateSpeakingScore(int newScore) { speakingScore.value = newScore; @@ -27,4 +33,3 @@ class HomeViewModel extends GetxController { linialPersent.value = newPersent; } } - diff --git a/lib/viewModels/user/user_viewmodel.dart b/lib/viewModels/user/user_viewmodel.dart index 097acd4..61a515a 100644 --- a/lib/viewModels/user/user_viewmodel.dart +++ b/lib/viewModels/user/user_viewmodel.dart @@ -13,40 +13,6 @@ class UserViewModel extends GetxController { final FirebaseFirestore _firestore = FirebaseFirestore.instance; final storage = const FlutterSecureStorage(); // Instance for secure storage - // final List wordList = [ - // WordCard( - // id: 1, - // word: "강", - // speaker: "가-앙", - // video: "https://www.youtube.com/watch?v=OzHrIz-wMLA"), - // WordCard( - // id: 2, - // word: "서", - // speaker: "가-앙", - // video: "https://www.youtube.com/watch?v=OzHrIz-wMLA"), - // WordCard( - // id: 3, - // word: "희", - // speaker: "가-앙", - // video: "https://www.youtube.com/watch?v=OzHrIz-wMLA"), - // WordCard( - // id: 4, - // word: "찬", - // speaker: "차-안", - // video: "https://www.youtube.com/watch?v=OzHrIz-wMLA"), - // WordCard( - // id: 5, - // word: "캬", - // speaker: "캬", - // video: "https://www.youtube.com/watch?v=OzHrIz-wMLA"), - // ]; - - // for (final word in wordList) { - // await FirebaseFirestore.instance - // .collection('words') - // .doc(word.id.toString()) - // .set(word.toMap()); - // } // User Profile Data final Rx _firebaseUser = Rx(null); String? get uid => _firebaseUser.value?.uid; @@ -65,7 +31,6 @@ class UserViewModel extends GetxController { final circleNumber = 10.obs; final linialPersent = 0.1.obs; - final flSpots = [].obs; final DataFetcher _dataFetcher = DataFetcher(); RxDouble maxYValue = 0.0.obs; @@ -75,7 +40,11 @@ class UserViewModel extends GetxController { void onInit() { super.onInit(); fetchAndSetGraphData(); + _firebaseUser.bindStream(_auth.authStateChanges()); + + if (uid != null) getUserData(); } + void fetchAndSetGraphData() async { final data = await _dataFetcher.fetchGraphData(); flSpots.value = data; @@ -96,6 +65,7 @@ class UserViewModel extends GetxController { final doc = await _firestore.collection('users').doc(uid).get(); userData.value = doc.data() ?? {}; nickname.value = userData.value['nickname'] ?? ''; + print('nickname: ${nickname.value}'); systemLanguage.value = userData.value['systemLanguage'] ?? '한국어'; learningLanguage.value = userData.value['learningLanguage'] ?? '한국어'; @@ -168,9 +138,7 @@ class UserViewModel extends GetxController { } } - class DataFetcher { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseAuth _auth = FirebaseAuth.instance; double maxYValue = 0.0; @@ -185,7 +153,6 @@ class DataFetcher { DateTime startDate = DateTime(now.year, now.month, now.day - 29); DateFormat formatter = DateFormat('yyyyMMdd'); // Reuse the formatter - // Prepare a list of all dates to query List> futures = List.generate(31, (i) { DateTime currentDate = startDate.add(Duration(days: i)); @@ -215,6 +182,4 @@ class DataFetcher { return spots; } - } - diff --git a/lib/views/home/home_head_widget.dart b/lib/views/home/home_head_widget.dart index f4aba93..8660fca 100644 --- a/lib/views/home/home_head_widget.dart +++ b/lib/views/home/home_head_widget.dart @@ -53,6 +53,7 @@ class HomeHeaderWidget extends StatelessWidget { ), ), // Use ViewModel data + Text( isLoggedIn ? '${vm.learningLanguage.value} - ${vm.nickname.value}' diff --git a/lib/views/word/widget/word_list_widget.dart b/lib/views/word/widget/word_list_widget.dart index d665fba..2b7e989 100644 --- a/lib/views/word/widget/word_list_widget.dart +++ b/lib/views/word/widget/word_list_widget.dart @@ -39,6 +39,7 @@ class _WordListState extends State { itemCount: widget.wordDataList.length, onPageChanged: (index) { wordViewModel.currentIndex.value = index; + print('currentIndex: ${wordViewModel.currentIndex.value}'); }, itemBuilder: (context, index) { final wordData = widget.wordDataList[index]; @@ -64,14 +65,14 @@ class _WordListState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '${wordViewModel.currentIndex.value + 1}/${widget.wordDataList.length}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: ColorSystem.gray5, - ), - ), + Obx(() => Text( + '${wordViewModel.currentIndex.value + 1}/${widget.wordDataList.length}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: ColorSystem.gray5, + ), + )), // isDone 여부에 따라 다른 체크박스 아이콘을 표시합니다. isDone diff --git a/lib/views/word/widget/word_sentence_widget.dart b/lib/views/word/widget/word_sentence_widget.dart new file mode 100644 index 0000000..91c7709 --- /dev/null +++ b/lib/views/word/widget/word_sentence_widget.dart @@ -0,0 +1,55 @@ +// word_sentence_widget.dart +import 'dart:io'; + +import 'package:earlips/models/word_data_model.dart'; +import 'package:earlips/viewModels/record/record_viewmodel.dart'; +import 'package:earlips/viewModels/word/word_viewmodel.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sound/flutter_sound.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:get/get.dart'; + +class WordSentenceWidget extends StatelessWidget { + final List wordDataList; + const WordSentenceWidget({super.key, required this.wordDataList}); + + @override + Widget build(BuildContext context) { + final wordViewModel = Get.find(); // Access your ViewModel! + return GetBuilder( + init: RecordViewModel(), + builder: (viewModel) => Center( + child: Column( + children: [ + const Text('WordSentenceWidget'), + StreamBuilder( + stream: viewModel.recorder.onProgress, + builder: (context, snapshot) { + final disposition = snapshot.hasData + ? snapshot.data!.duration + : Duration.zero; + + return Text('Recorder: ${disposition.inSeconds}s'); + }), + ElevatedButton( + onPressed: () async { + if (viewModel.recorder.isRecording) { + await viewModel.stopRecording( + wordDataList[wordViewModel.currentIndex.value] + .wordCard + .word); + } else { + await viewModel.startRecording(); + } + }, + child: Icon( + viewModel.recorder.isRecording ? Icons.stop : Icons.mic, + size: 40, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/word/word_screen.dart b/lib/views/word/word_screen.dart index 65c6674..a7c18fa 100644 --- a/lib/views/word/word_screen.dart +++ b/lib/views/word/word_screen.dart @@ -2,6 +2,7 @@ import 'package:earlips/utilities/style/color_styles.dart'; import 'package:earlips/viewModels/word/word_viewmodel.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_youtube_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -12,16 +13,11 @@ class WordScreen extends StatelessWidget { final int type; const WordScreen({super.key, required this.title, required this.type}); - - // argument로 받은 type에 따라 다른 wordViewModel을 사용 - @override Widget build(BuildContext context) { final wordViewModel = Get.put(WordViewModel( type: type, )); - var arg = Get.arguments; - wordViewModel.currentIndex.value = arg; final PageController pageController = PageController(initialPage: wordViewModel.currentIndex.value); @@ -84,15 +80,17 @@ class WordScreen extends StatelessWidget { padding: EdgeInsets.all(20.0), child: YoutubeWordPlayer(), ); - } else { - return Container( - child: const CreateScriptPage(), + } else if (controller.type == 2) { + return WordSentenceWidget( + wordDataList: controller.wordList, ); + } else { + return const CreateScriptPage(); } }, ), const Spacer(), - // wordViewModel final String video로 영상 유튜브 링크를 바로 볼 수 있게 하기 + // final String video로 영상 유튜브 링크를 바로 볼 수 있게 하기 ElevatedButton( style: ButtonStyle( padding: MaterialStateProperty.all( @@ -110,13 +108,18 @@ class WordScreen extends StatelessWidget { ElevatedButton( // button style onPressed: () async { + // 단어 학습 완료 처리 => await wordViewModel.markWordAsDone(wordViewModel .wordList[wordViewModel.currentIndex.value] .wordCard); + + // 마지막 단어가 아닐 경우 뒤로가기, 마지막 단어일 경우 홈으로 이동 wordViewModel.currentIndex.value < wordViewModel.wordList.length - 1 ? Get.back() : Get.offAllNamed('/'); + + // 다음 단어로 넘어가기 if (wordViewModel.currentIndex.value < wordViewModel.wordList.length - 1) { pageController.animateToPage( @@ -124,12 +127,13 @@ class WordScreen extends StatelessWidget { duration: const Duration(milliseconds: 300), curve: Curves.ease, ); + + // currentIndex 증가 wordViewModel.currentIndex.value = wordViewModel.currentIndex.value + 1; } }, // 마지막 단어일 경우 홈으로 이동 - child: Text(wordViewModel.currentIndex.value < wordViewModel.wordList.length - 1 ? '다음 단어'