diff --git a/assets/images/phone.svg b/assets/images/phone.svg new file mode 100644 index 0000000..c12e06c --- /dev/null +++ b/assets/images/phone.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/safehouse.svg b/assets/images/safehouse.svg new file mode 100644 index 0000000..6c21f93 --- /dev/null +++ b/assets/images/safehouse.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/logo/logo.svg b/assets/logo/logo.svg new file mode 100644 index 0000000..cc6115b --- /dev/null +++ b/assets/logo/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/voice/voice_1.mp3 b/assets/voice/voice_1.mp3 new file mode 100644 index 0000000..f4426c1 Binary files /dev/null and b/assets/voice/voice_1.mp3 differ diff --git a/assets/voice/voice_2.mp3 b/assets/voice/voice_2.mp3 new file mode 100644 index 0000000..db16fa7 Binary files /dev/null and b/assets/voice/voice_2.mp3 differ diff --git a/assets/voice/voice_3.mp3 b/assets/voice/voice_3.mp3 new file mode 100644 index 0000000..4641ed5 Binary files /dev/null and b/assets/voice/voice_3.mp3 differ diff --git a/assets/voice/voice_4.mp3 b/assets/voice/voice_4.mp3 new file mode 100644 index 0000000..e75e4d7 Binary files /dev/null and b/assets/voice/voice_4.mp3 differ diff --git a/lib/core/CustomDialogs.dart b/lib/core/CustomDialogs.dart new file mode 100644 index 0000000..38c694c --- /dev/null +++ b/lib/core/CustomDialogs.dart @@ -0,0 +1,57 @@ +// custom_dialogs.dart +import 'package:flutter/material.dart'; + +class CustomDialogs { + // 일반 다이얼로그 생성 + static void showCustomDialog( + BuildContext context, { + required String title, + required String content, + String? confirmText = '확인', + String? cancelText = '취소', + VoidCallback? onConfirm, + VoidCallback? onCancel, + bool isTextField = false, + }) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: isTextField + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(content), + TextField( + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: '시간(분)', + ), + ), + ], + ) + : Text(content), + actions: [ + if (onCancel != null) + TextButton( + onPressed: () { + onCancel(); + Navigator.of(context).pop(); + }, + child: Text(cancelText!), + ), + if (onConfirm != null) + ElevatedButton( + onPressed: () { + onConfirm(); + Navigator.of(context).pop(); + }, + child: Text(confirmText!), + ), + ], + ); + }, + ); + } +} diff --git a/lib/core/SplashScreen.dart b/lib/core/SplashScreen.dart index 50b0521..f3e0221 100644 --- a/lib/core/SplashScreen.dart +++ b/lib/core/SplashScreen.dart @@ -1,7 +1,9 @@ +import 'package:blueberry_flutter_template/feature/onboarding/OnboardingScreen.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'TopScreen.dart'; @@ -28,6 +30,9 @@ class _SplashScreenState extends ConsumerState { /// 앱 초기화를 단계별로 수행하는 함수 Future _initializeApp() async { + final prefs = await SharedPreferences.getInstance(); + final hasSeenOnboarding = prefs.getBool('hasSeenOnboarding') ?? false; + try { // 단계별로 로딩 상태를 업데이트 // 단계별 로딩은 사용하지 않음. @@ -39,7 +44,10 @@ class _SplashScreenState extends ConsumerState { await Future.delayed(const Duration(seconds: 3)); // 초기화 완료 후 메인 화면으로 전환 if (mounted) { - context.goNamed(TopScreen.name); + hasSeenOnboarding + ? context.goNamed(OnboardingScreen.name) // 온보딩 화면을 본 경우 + : context.goNamed(OnboardingScreen.name) // 온보딩 화면을 보지 않은 경우 + ; } } catch (e) { // 초기화 중 발생한 오류 처리 diff --git a/lib/feature/map/PoliceMapScreen.dart b/lib/feature/map/PoliceMapScreen.dart index ff8cec3..bb506c4 100644 --- a/lib/feature/map/PoliceMapScreen.dart +++ b/lib/feature/map/PoliceMapScreen.dart @@ -1,10 +1,8 @@ import 'dart:async'; + import 'package:blueberry_flutter_template/feature/map/provider/LocationProvider.dart'; import 'package:blueberry_flutter_template/feature/map/provider/PermissionProvider.dart'; import 'package:blueberry_flutter_template/feature/map/provider/PoliceStationProvider.dart'; -import 'package:blueberry_flutter_template/feature/map/widget/GoogleMapWidget.dart'; -import 'package:blueberry_flutter_template/feature/map/widget/PermissionDeniedWidget.dart'; -import 'package:blueberry_flutter_template/feature/map/widget/PoliceStationListWidget.dart'; import 'package:blueberry_flutter_template/feature/map/widget/SendMessageWidget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -47,38 +45,40 @@ class _PoliceMapScreenState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(8.0), child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: policeStationsAsyncValue.when( - data: (policeStations) => GoogleMapWidget( - googleMapControllerCompleter: - _googleMapControllerCompleter, - locationState: locationState, - policeStationsAsyncValue: policeStationsAsyncValue, - ), - loading: () => - const Center(child: CircularProgressIndicator()), - error: (error, stack) => - Center(child: Text('Error: $error')), - ), - ), + // Expanded( + // child: policeStationsAsyncValue.when( + // data: (policeStations) => GoogleMapWidget( + // googleMapControllerCompleter: + // _googleMapControllerCompleter, + // locationState: locationState, + // policeStationsAsyncValue: policeStationsAsyncValue, + // ), + // loading: () => + // const Center(child: CircularProgressIndicator()), + // error: (error, stack) => + // Center(child: Text('Error: $error')), + // ), + // ), const SizedBox(height: 8), - Expanded( - child: PoliceStationListWidget( - googleMapControllerCompleter: - _googleMapControllerCompleter, - locationState: locationState, - policeStationsAsyncValue: policeStationsAsyncValue, - ), - ), - SendMessage(locationState: locationState), + // Expanded( + // child: PoliceStationListWidget( + // googleMapControllerCompleter: + // _googleMapControllerCompleter, + // locationState: locationState, + // policeStationsAsyncValue: policeStationsAsyncValue, + // ), + // ), + Center(child: SendMessage(locationState: locationState)), ], ), ), ); - } else { - return PermissionDeniedWidget(permissionStatus: permissionStatus); } + // else { + // return PermissionDeniedWidget(permissionStatus: permissionStatus); + // } }(), ); } diff --git a/lib/feature/mypage/MyPageScreen.dart b/lib/feature/mypage/MyPageScreen.dart index fdc0b69..6b71b6a 100644 --- a/lib/feature/mypage/MyPageScreen.dart +++ b/lib/feature/mypage/MyPageScreen.dart @@ -1,3 +1,4 @@ +import 'package:blueberry_flutter_template/core/CustomDialogs.dart'; import 'package:flutter/material.dart'; class MyPageScreen extends StatelessWidget { @@ -8,27 +9,23 @@ class MyPageScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('마이페이지'), - centerTitle: true, - ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text( - "nickname", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 40.0), ElevatedButton( onPressed: () { // 발송 문자 지정 버튼의 기능 구현 + CustomDialogs.showCustomDialog( + context, + title: '발송 문자 지정', + content: '구현 예정입니다.', + onConfirm: () { + // 발송 문자 선택 로직 구현 + }, + ); }, child: const Text('발송 문자 지정'), ), @@ -36,6 +33,14 @@ class MyPageScreen extends StatelessWidget { ElevatedButton( onPressed: () { // 주변 범죄 위험도 확인 버튼의 기능 구현 + CustomDialogs.showCustomDialog( + context, + title: '발송 문자 지정', + content: '구현 예정입니다.', + onConfirm: () { + // 발송 문자 선택 로직 구현 + }, + ); }, child: const Text('주변 범죄 위험도 확인'), ), @@ -43,6 +48,14 @@ class MyPageScreen extends StatelessWidget { ElevatedButton( onPressed: () { // 자동 대화 시간 설정 버튼의 기능 구현 + CustomDialogs.showCustomDialog( + context, + title: '발송 문자 지정', + content: '구현 예정입니다.', + onConfirm: () { + // 발송 문자 선택 로직 구현 + }, + ); }, child: const Text('자동 대화 시간 설정'), ), diff --git a/lib/feature/onboarding/OnboardingScreen.dart b/lib/feature/onboarding/OnboardingScreen.dart index 60a76a5..d04748d 100644 --- a/lib/feature/onboarding/OnboardingScreen.dart +++ b/lib/feature/onboarding/OnboardingScreen.dart @@ -2,13 +2,14 @@ import 'package:blueberry_flutter_template/core/TopScreen.dart'; import 'package:blueberry_flutter_template/feature/onboarding/widgets/OnboardingPageViewBuilder.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'OnboardingData.dart'; import 'widgets/OnboardingDotWidget.dart'; import 'widgets/OnboardingPageButton.dart'; class OnboardingScreen extends StatefulWidget { - static const String name = '/onboarding'; + static const String name = 'OnboardingScreen'; const OnboardingScreen({super.key}); @@ -38,8 +39,10 @@ class _OnboardingScreenState extends State { }); } - void _onNextPressed() { + void _onNextPressed() async { if (_currentPage == OnboardingData.pageDataList.length - 1) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('hasSeenOnboarding', true); context.goNamed(TopScreen.name); } else { _pageController.nextPage( diff --git a/lib/feature/voiceOutput/widget/VoiceOutputWidget.dart b/lib/feature/voiceOutput/widget/VoiceOutputWidget.dart index be4e304..415220e 100644 --- a/lib/feature/voiceOutput/widget/VoiceOutputWidget.dart +++ b/lib/feature/voiceOutput/widget/VoiceOutputWidget.dart @@ -1,5 +1,8 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:blueberry_flutter_template/gen/assets.gen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../provider/VoiceOutputProvider.dart'; class VoiceOutputWidget extends ConsumerWidget { @@ -27,7 +30,7 @@ class VoiceOutputWidget extends ConsumerWidget { children: data .map((text) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: VoiceOutputRow(text: text), + child: VoiceOutputRow(text: text, index: data.indexOf(text)), )) .toList(), ), @@ -37,8 +40,10 @@ class VoiceOutputWidget extends ConsumerWidget { class VoiceOutputRow extends StatelessWidget { final String text; + final int index; + final player = AudioPlayer(); - const VoiceOutputRow({super.key, required this.text}); + VoiceOutputRow({super.key, required this.text, required this.index}); @override Widget build(BuildContext context) { @@ -56,7 +61,10 @@ class VoiceOutputRow extends StatelessWidget { ), const SizedBox(width: 8), ElevatedButton( - onPressed: () {}, + onPressed: () { + final source = AssetSource("voice/voice_${index+1}.mp3"); + player.play(source); + }, child: const Text("button"), ), ], diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 5a2eadd..44ad98c 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -127,6 +127,9 @@ class $AssetsLoginPageImagesGen { class $AssetsLogoGen { const $AssetsLogoGen(); + /// File path: assets/logo/logo.svg + String get logo => 'assets/logo/logo.svg'; + /// File path: assets/logo/logo_1.png AssetGenImage get logo1 => const AssetGenImage('assets/logo/logo_1.png'); @@ -146,14 +149,38 @@ class $AssetsLogoGen { AssetGenImage get mbtiLogo => const AssetGenImage('assets/logo/mbti_logo.webp'); + /// File path: assets/logo/voice_logo.png + AssetGenImage get voiceLogo => + const AssetGenImage('assets/logo/voice_logo.png'); + /// List of all assets - List get values => - [logo1, logo2, logo3, logo4, logo5, mbtiLogo]; + List get values => + [logo, logo1, logo2, logo3, logo4, logo5, mbtiLogo, voiceLogo]; +} + +class $AssetsVoiceGen { + const $AssetsVoiceGen(); + + /// File path: assets/voice/voice_1.mp3 + String get voice1 => 'assets/voice/voice_1.mp3'; + + /// File path: assets/voice/voice_2.mp3 + String get voice2 => 'assets/voice/voice_2.mp3'; + + /// File path: assets/voice/voice_3.mp3 + String get voice3 => 'assets/voice/voice_3.mp3'; + + /// File path: assets/voice/voice_4.mp3 + String get voice4 => 'assets/voice/voice_4.mp3'; + + /// List of all assets + List get values => [voice1, voice2, voice3, voice4]; } class Assets { Assets._(); + static const String aEnv = '.env'; static const $Assets300x420Gen a300x420 = $Assets300x420Gen(); static const $Assets600x400Gen a600x400 = $Assets600x400Gen(); static const $Assets700x150Gen a700x150 = $Assets700x150Gen(); @@ -162,6 +189,10 @@ class Assets { static const $AssetsLoginPageImagesGen loginPageImages = $AssetsLoginPageImagesGen(); static const $AssetsLogoGen logo = $AssetsLogoGen(); + static const $AssetsVoiceGen voice = $AssetsVoiceGen(); + + /// List of all assets + static List get values => [aEnv]; } class AssetGenImage { diff --git a/lib/router/RouterProvider.dart b/lib/router/RouterProvider.dart index ef0abb5..6798b8f 100644 --- a/lib/router/RouterProvider.dart +++ b/lib/router/RouterProvider.dart @@ -1,3 +1,4 @@ +import 'package:blueberry_flutter_template/feature/onboarding/OnboardingScreen.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -26,8 +27,14 @@ final routerProvider = Provider((ref) { name: TopScreen.name, builder: (context, state) => ResponsiveLayoutBuilder(context, const TopScreen()), - routes: [GoRoute(path: '')], + routes: [], ), + GoRoute( + path: '/onboarding', + name: OnboardingScreen.name, + builder: (context, state) => + ResponsiveLayoutBuilder(context, const OnboardingScreen()), + ) ], ); }); diff --git a/pubspec.yaml b/pubspec.yaml index 7ebe1c4..4375f35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -110,7 +110,12 @@ dependencies: geolocator: ^12.0.0 flutter_dotenv: ^5.1.0 - # apiKey + # shared preference + shared_preferences: ^2.2.3 + + # audio player + audioplayers: ^6.0.0 + dev_dependencies: @@ -121,6 +126,7 @@ dev_dependencies: json_serializable: ^6.8.0 build_runner: ^2.4.11 + flutter: uses-material-design: true assets: @@ -133,6 +139,11 @@ flutter: - assets/logo/ - assets/icon/ - assets/login_page_images/ + - assets/voice/ + - assets/voice/voice_1.mp3 + - assets/voice/voice_2.mp3 + - assets/voice/voice_3.mp3 + - assets/voice/voice_4.mp3 # Run Commands to generate splash view # 1) flutter clean