diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a8e7760..b72a62a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -22,6 +22,7 @@ PODS: - Flutter - isar_flutter_libs (1.0.0): - Flutter + - MTBBarcodeScanner (5.0.11) - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -31,6 +32,9 @@ PODS: - Flutter - pointer_interceptor_ios (0.0.1): - Flutter + - qr_code_scanner (0.2.0): + - Flutter + - MTBBarcodeScanner - ReachabilitySwift (5.0.0) - shared_preferences_foundation (0.0.1): - Flutter @@ -56,6 +60,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) + - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -63,6 +68,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - FMDB + - MTBBarcodeScanner - ReachabilitySwift EXTERNAL SOURCES: @@ -94,6 +100,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" pointer_interceptor_ios: :path: ".symlinks/plugins/pointer_interceptor_ios/ios" + qr_code_scanner: + :path: ".symlinks/plugins/qr_code_scanner/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: @@ -113,10 +121,12 @@ SPEC CHECKSUMS: image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 + MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375 + qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 50a33e1d72bd59ee092a519a35d107502757ebed diff --git a/lib/pages/camera/camera_page.dart b/lib/pages/camera/camera_page.dart index 7ad678f..b40314d 100644 --- a/lib/pages/camera/camera_page.dart +++ b/lib/pages/camera/camera_page.dart @@ -693,6 +693,11 @@ class BuyPointCamera extends StatelessWidget { class QRCodeScannerPage extends StatefulWidget { + + QRCodeScannerPage({Key? key, required this.destination}) : super(key: key); + + Destination destination; + @override _QRCodeScannerPageState createState() => _QRCodeScannerPageState(); } diff --git a/lib/pages/destination/destination_controller.dart b/lib/pages/destination/destination_controller.dart index d35f6b1..577ee28 100644 --- a/lib/pages/destination/destination_controller.dart +++ b/lib/pages/destination/destination_controller.dart @@ -104,6 +104,13 @@ class DestinationController extends GetxController { // ゴール地点でのロジックの制御:rogainingCountedがtrueの場合、つまりポイントがカウントされている場合にのみ、ゴール処理を実行できます。 // UI の更新:rogainingCountedの状態に基づいて、適切なメッセージやボタンを表示することができます。 + bool isMapControllerReady = false; + + LatLng lastValidGPSLocation = LatLng(0, 0); + DateTime lastGPSDataReceivedTime = DateTime.now(); + DateTime lastPopupShownTime = DateTime.now().subtract(Duration(minutes: 10)); + bool isPopupShown = false; + bool hasReceivedGPSData = false; /* //==== Akira .. GPS信号シミュレーション用 ===== ここから、2024-4-5 @@ -149,6 +156,48 @@ class DestinationController extends GetxController { //==== Akira .. GPS信号シミュレーション用 ======= ここまで */ + void showGPSDataNotReceivedPopup() { + Get.dialog( + AlertDialog( + title: Text('GPS信号が受信できません'), + content: Text('GPS信号が受信できる場所に移動してください。'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('OK'), + ), + ], + ), + ); + } + + // 最後に有効なGPSデータを受け取ってから10分以上経過している場合にのみメッセージを表示するようにします。 + // + void checkGPSDataReceived() { + if (!hasReceivedGPSData) { + // GPS信号を全く受信していない。 + if (!isPopupShown) { + // ポップアップしていない。 + showGPSDataNotReceivedPopup(); + lastPopupShownTime = DateTime.now(); + isPopupShown = true; + } + } else { + if (DateTime.now().difference(lastGPSDataReceivedTime).inSeconds >= 600) { + // 前回GPS信号を受信してから10分経過。 + if (!isPopupShown && DateTime.now().difference(lastPopupShownTime).inMinutes >= 3) { + // 前回ポップアップしてから3分経過してなければ + showGPSDataNotReceivedPopup(); + lastPopupShownTime = DateTime.now(); + isPopupShown = true; + } + } else { + isPopupShown = false; + } + } + + } + // 日時をフォーマットされた文字列に変換する関数です。 // String getFormatedTime(DateTime datetime) { @@ -1014,12 +1063,27 @@ class DestinationController extends GetxController { } } + Timer? gpsCheckTimer; // 一定間隔でGPSデータの受信状態をチェックするタイマー + + void startGPSCheckTimer() { + gpsCheckTimer = Timer.periodic(Duration(seconds: 5), (timer) { + checkGPSDataReceived(); + }); + } + // コントローラーの初期化時に呼び出されるライフサイクルメソッドです。 // @override void onInit() async { super.onInit(); + startGPSCheckTimer(); + + // MapControllerの初期化完了を待機するフラグを設定 + WidgetsBinding.instance.addPostFrameCallback((_) { + isMapControllerReady = true; + }); + // 要検討:エラーメッセージを表示するなどの適切な処理を追加することを検討してください。 // // locationController からデバイスの受け取るGPS情報を取得し、 @@ -1035,13 +1099,17 @@ class DestinationController extends GetxController { }); startGame(); + + checkGPSDataReceived(); } // コントローラーのクローズ時に呼び出されるライフサイクルメソッドです。 // @override void onClose() { + gpsCheckTimer?.cancel(); locationController.stopPositionStream(); + super.onClose(); } // 位置情報の更新を処理する関数です。 @@ -1062,11 +1130,25 @@ class DestinationController extends GetxController { if (position != null) { currentLat = position.latitude; currentLon = position.longitude; + lastValidGPSLocation = LatLng(currentLat, currentLon); okToUseGPS = true; + lastGPSDataReceivedTime = DateTime.now(); + hasReceivedGPSData = true; + } else { + checkGPSDataReceived(); + // 信号強度が低い場合、最後に取得した高いまたは中程度の位置情報を使用 // 但し、最初から高精度のものがない場合、どうするか? // + // GPSデータが受信できない場合、最後に有効なGPSデータを使用 + position = LocationMarkerPosition( + latitude: lastValidGPSLocation.latitude, + longitude: lastValidGPSLocation.longitude, + accuracy: 0, + ); + + /* if (lastValidLat != 0.0 && lastValidLon != 0.0) { currentLat = lastValidLat; currentLon = lastValidLon; @@ -1085,6 +1167,7 @@ class DestinationController extends GetxController { backgroundColor: Colors.yellow, ); } + */ } if (okToUseGPS && position != null) { @@ -1103,7 +1186,7 @@ class DestinationController extends GetxController { Duration difference = lastGPSCollectedTime.difference(DateTime.now()) .abs(); // 最後にGPS信号を取得した時刻から10秒以上経過、かつ10m以上経過(普通に歩くスピード) - if (difference.inSeconds >= 10 || distanceToDest >= 10) { + if (difference.inSeconds >= 10 && distanceToDest >= 10) { // print( // "^^^^^^^^ GPS data collected ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}, ^^^ ${position.latitude}, ${position.longitude}"); @@ -1259,21 +1342,29 @@ class DestinationController extends GetxController { void fixMapBound(String token) { //String _token = indexController.currentUser[0]["token"]; indexController.switchPage(AppPages.INDEX); - LocationService.getLocationsExt(token).then((value) { - if (value != null) { - //print("--- loc ext is - $value ----"); - LatLngBounds bnds = LatLngBounds( - LatLng(value[1], value[0]), LatLng(value[3], value[2])); - //print("--- bnds is - $bnds ----"); - indexController.mapController.fitBounds( - bnds, - ); - indexController.currentBound.clear(); - indexController.currentBound.add(bnds); - indexController.loadLocationsBound(); - centerMapToCurrentLocation(); - } - }); + + if (isMapControllerReady) { + LocationService.getLocationsExt(token).then((value) { + if (value != null) { + //print("--- loc ext is - $value ----"); + LatLngBounds bnds = LatLngBounds( + LatLng(value[1], value[0]), LatLng(value[3], value[2])); + //print("--- bnds is - $bnds ----"); + indexController.mapController.fitBounds( + bnds, + ); + indexController.currentBound.clear(); + indexController.currentBound.add(bnds); + indexController.loadLocationsBound(); + centerMapToCurrentLocation(); + } + }); + } else { + // MapControllerの初期化が完了していない場合は、遅延して再試行 + Future.delayed(Duration(milliseconds: 100), () { + fixMapBound(token); + }); + } } diff --git a/lib/pages/drawer/drawer_page.dart b/lib/pages/drawer/drawer_page.dart index e8f1c61..ff2a740 100644 --- a/lib/pages/drawer/drawer_page.dart +++ b/lib/pages/drawer/drawer_page.dart @@ -6,6 +6,9 @@ import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/auth_service.dart'; import 'package:url_launcher/url_launcher.dart'; +// SafeAreaウィジェットを使用して、画面の安全領域内にメニューを表示しています。 +// Columnウィジェットを使用して、メニューアイテムを縦に並べています。 +// class DrawerPage extends StatelessWidget { DrawerPage({Key? key}) : super(key: key); @@ -13,9 +16,21 @@ class DrawerPage extends StatelessWidget { // 要検討:URLの起動に失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 // + /* void _launchURL(url) async { if (!await launchUrl(url)) throw 'Could not launch $url'; } + */ + + void _launchURL(String urlString) async { + Uri url = Uri.parse(urlString); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } else { + throw 'Could not launch $url'; + } + } + @override Widget build(BuildContext context) { @@ -26,6 +41,9 @@ class DrawerPage extends StatelessWidget { // space to fit everything. child: Column( children: [ + // 最初のアイテムは、ユーザーのログイン状態に応じて表示が変わります。 + // ユーザーがログインしていない場合は、"drawer_title".trというテキストを表示します。 + // ユーザーがログインしている場合は、ユーザーのメールアドレスを表示します。 Container( height: 100, color: Colors.amber, @@ -47,6 +65,9 @@ class DrawerPage extends StatelessWidget { ), )), ), + // 次に、IndexControllerのcurrentUserリストが空かどうかに応じて、ログインまたはログアウトのアイテムを表示します。 + // currentUserリストが空の場合は、"login".trというテキストのログインアイテムを表示し、タップするとAppPages.LOGINにナビゲートします。 + // currentUserリストが空でない場合は、"logout".trというテキストのログアウトアイテムを表示し、タップするとindexController.logout()を呼び出してログアウトし、AppPages.LOGINにナビゲートします。 Obx(() => indexController.currentUser.isEmpty ? ListTile( leading: const Icon(Icons.login), @@ -63,6 +84,8 @@ class DrawerPage extends StatelessWidget { Get.toNamed(AppPages.LOGIN); }, )), + // パスワード変更のアイテムは、ユーザーがログインしている場合にのみ表示されます。 + // "change_password".trというテキストを表示し、タップするとAppPages.CHANGE_PASSWORDにナビゲートします。 indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.password), @@ -75,6 +98,8 @@ class DrawerPage extends StatelessWidget { width: 0, height: 0, ), + // サインアップのアイテムは、ユーザーがログインしていない場合にのみ表示されます。 + // "sign_up".trというテキストを表示し、タップするとAppPages.REGISTERにナビゲートします。 indexController.currentUser.isEmpty ? ListTile( leading: const Icon(Icons.person), @@ -87,6 +112,8 @@ class DrawerPage extends StatelessWidget { width: 0, height: 0, ), + // リセットのアイテムは、ユーザーがログインしている場合にのみ表示されます。 + // タップすると、確認ダイアログを表示し、ユーザーがリセットを確認するとDestinationControllerのresetRogaining()メソッドを呼び出してゲームデータをリセットします。 indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.password), @@ -95,7 +122,7 @@ class DrawerPage extends StatelessWidget { // 要検討:リセット操作の確認メッセージをローカライズすることを検討してください。 // Get.defaultDialog( - title: "よろしいですか、リセットしますか?", + title: "リセットしますがよろしいですか?", middleText: "これにより、すべてのゲーム データが削除され、すべての状態が削除されます", textConfirm: "確認する", textCancel: "キャンセルする", @@ -114,26 +141,39 @@ class DrawerPage extends StatelessWidget { width: 0, height: 0, ), + // アカウント削除のアイテムは、ユーザーがログインしている場合にのみ表示されます。 + // "delete_account".trというテキストを表示し、タップするとAuthService.deleteUser()を呼び出してアカウントを削除し、AppPages.TRAVELにナビゲートします。 indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.delete_forever), title: Text("delete_account".tr), onTap: () { - String token = indexController.currentUser[0]['token']; - AuthService.deleteUser(token).then((value) { - if (value.isNotEmpty) { - indexController.logout(); - Get.toNamed(AppPages.TRAVEL); - Get.snackbar("accounted_deleted".tr, - "account_deleted_message".tr); - } - }); + Get.defaultDialog( + title: "アカウントを削除しますがよろしいですか?", + middleText: "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます", + textConfirm: "確認する", + textCancel: "キャンセルする", + onCancel: () => Get.back(), + onConfirm: () { + String token = indexController.currentUser[0]['token']; + AuthService.deleteUser(token).then((value) { + if (value.isNotEmpty) { + indexController.logout(); + Get.toNamed(AppPages.TRAVEL); + Get.snackbar("accounted_deleted".tr, + "account_deleted_message".tr); + } + }); + }, + ); }, ) : const SizedBox( width: 0, height: 0, ), + // ユーザーデータ削除のアイテムは、ユーザーがログインしている場合にのみ表示されます。 + // タップすると、AuthService.deleteUser()を呼び出してユーザーデータを削除します。 indexController.currentUser.isNotEmpty ? ListTile( // 要検討:アカウント削除のリクエストが失敗した場合のエラーハンドリングを追加することをお勧めします。 @@ -141,13 +181,21 @@ class DrawerPage extends StatelessWidget { leading: const Icon(Icons.delete_forever), title: Text("ユーザーデータを削除する".tr), onTap: () { - String token = indexController.currentUser[0]['token']; - AuthService.deleteUser(token).then((value) { - Get.snackbar("ユーザーデータを削除する", + Get.defaultDialog( + title: "アカウントを削除しますがよろしいですか?", + middleText: "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます", + textConfirm: "確認する", + textCancel: "キャンセルする", + onCancel: () => Get.back(), + onConfirm: () { + String token = indexController.currentUser[0]['token']; + AuthService.deleteUser(token).then((value) { + Get.snackbar("ユーザーデータを削除する", "データを削除するためにユーザーの同意が設定されています アプリとサーバーでユーザーデータが削除されました"); - }); - }, - ) + }); + }); + }, + ) : const SizedBox( width: 0, height: 0, @@ -167,18 +215,23 @@ class DrawerPage extends StatelessWidget { // title: Text("point_rank".tr), // onTap: (){}, // ), + + // "rog_web".trというテキストのアイテムは、ユーザーがログインしている場合にのみ表示されます。 + // タップすると、_launchURL()メソッドを呼び出して外部のウェブサイトを開きます。 indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.featured_video), title: Text("rog_web".tr), onTap: () { - _launchURL("https://www.gifuai.net/?page_id=17397"); + _launchURL("https://www.gifuai.net/?page_id=60043"); }, ) : const SizedBox( width: 0, height: 0, ), + // "privacy".trというテキストのアイテムは、常に表示されます。 + // タップすると、_launchURL()メソッドを呼び出してプライバシーポリシーのURLを開きます。 ListTile( leading: const Icon(Icons.privacy_tip), title: Text("privacy".tr), diff --git a/lib/utils/location_controller.dart b/lib/utils/location_controller.dart index 27da7c9..402f766 100644 --- a/lib/utils/location_controller.dart +++ b/lib/utils/location_controller.dart @@ -37,6 +37,7 @@ class LocationController extends GetxController { // 位置情報のストリームを保持する変数です。StreamSubscription型で宣言されています。 LatLng? lastValidLocation; + DateTime lastGPSDataReceivedTime = DateTime.now(); // 最後にGPSデータを受け取った時刻 // GPSシミュレーション用のメソッドを追加 void setSimulationMode(bool value) { @@ -354,6 +355,7 @@ class LocationController extends GetxController { accuracy: position.accuracy, ); */ + lastGPSDataReceivedTime = DateTime.now(); // 最後にGPS信号を受け取った時刻 locationMarkerPositionStreamController.add(position); } }