サイドメニューの改善、ログイン直後にマップが表示されない問題、屋内でGPS信号が受信できない場合のポップアップ追加

This commit is contained in:
2024-04-20 17:30:26 +09:00
parent 33bd2b97a1
commit 4d40a10f9a
5 changed files with 194 additions and 33 deletions

View File

@ -22,6 +22,7 @@ PODS:
- Flutter - Flutter
- isar_flutter_libs (1.0.0): - isar_flutter_libs (1.0.0):
- Flutter - Flutter
- MTBBarcodeScanner (5.0.11)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
@ -31,6 +32,9 @@ PODS:
- Flutter - Flutter
- pointer_interceptor_ios (0.0.1): - pointer_interceptor_ios (0.0.1):
- Flutter - Flutter
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
@ -56,6 +60,7 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/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`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -63,6 +68,7 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - FMDB
- MTBBarcodeScanner
- ReachabilitySwift - ReachabilitySwift
EXTERNAL SOURCES: EXTERNAL SOURCES:
@ -94,6 +100,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
pointer_interceptor_ios: pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios" :path: ".symlinks/plugins/pointer_interceptor_ios/ios"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite: sqflite:
@ -113,10 +121,12 @@ SPEC CHECKSUMS:
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375 pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 50a33e1d72bd59ee092a519a35d107502757ebed sqflite: 50a33e1d72bd59ee092a519a35d107502757ebed

View File

@ -693,6 +693,11 @@ class BuyPointCamera extends StatelessWidget {
class QRCodeScannerPage extends StatefulWidget { class QRCodeScannerPage extends StatefulWidget {
QRCodeScannerPage({Key? key, required this.destination}) : super(key: key);
Destination destination;
@override @override
_QRCodeScannerPageState createState() => _QRCodeScannerPageState(); _QRCodeScannerPageState createState() => _QRCodeScannerPageState();
} }

View File

@ -104,6 +104,13 @@ class DestinationController extends GetxController {
// ゴール地点でのロジックの制御rogainingCountedがtrueの場合、つまりポイントがカウントされている場合にのみ、ゴール処理を実行できます。 // ゴール地点でのロジックの制御rogainingCountedがtrueの場合、つまりポイントがカウントされている場合にのみ、ゴール処理を実行できます。
// UI の更新rogainingCountedの状態に基づいて、適切なメッセージやボタンを表示することができます。 // 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 //==== Akira .. GPS信号シミュレーション用 ===== ここから、2024-4-5
@ -149,6 +156,48 @@ class DestinationController extends GetxController {
//==== Akira .. GPS信号シミュレーション用 ======= ここまで //==== 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) { 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 @override
void onInit() async { void onInit() async {
super.onInit(); super.onInit();
startGPSCheckTimer();
// MapControllerの初期化完了を待機するフラグを設定
WidgetsBinding.instance.addPostFrameCallback((_) {
isMapControllerReady = true;
});
// 要検討:エラーメッセージを表示するなどの適切な処理を追加することを検討してください。 // 要検討:エラーメッセージを表示するなどの適切な処理を追加することを検討してください。
// //
// locationController からデバイスの受け取るGPS情報を取得し、 // locationController からデバイスの受け取るGPS情報を取得し、
@ -1035,13 +1099,17 @@ class DestinationController extends GetxController {
}); });
startGame(); startGame();
checkGPSDataReceived();
} }
// コントローラーのクローズ時に呼び出されるライフサイクルメソッドです。 // コントローラーのクローズ時に呼び出されるライフサイクルメソッドです。
// //
@override @override
void onClose() { void onClose() {
gpsCheckTimer?.cancel();
locationController.stopPositionStream(); locationController.stopPositionStream();
super.onClose();
} }
// 位置情報の更新を処理する関数です。 // 位置情報の更新を処理する関数です。
@ -1062,11 +1130,25 @@ class DestinationController extends GetxController {
if (position != null) { if (position != null) {
currentLat = position.latitude; currentLat = position.latitude;
currentLon = position.longitude; currentLon = position.longitude;
lastValidGPSLocation = LatLng(currentLat, currentLon);
okToUseGPS = true; okToUseGPS = true;
lastGPSDataReceivedTime = DateTime.now();
hasReceivedGPSData = true;
} else { } else {
checkGPSDataReceived();
// 信号強度が低い場合、最後に取得した高いまたは中程度の位置情報を使用 // 信号強度が低い場合、最後に取得した高いまたは中程度の位置情報を使用
// 但し、最初から高精度のものがない場合、どうするか? // 但し、最初から高精度のものがない場合、どうするか?
// //
// GPSデータが受信できない場合、最後に有効なGPSデータを使用
position = LocationMarkerPosition(
latitude: lastValidGPSLocation.latitude,
longitude: lastValidGPSLocation.longitude,
accuracy: 0,
);
/*
if (lastValidLat != 0.0 && lastValidLon != 0.0) { if (lastValidLat != 0.0 && lastValidLon != 0.0) {
currentLat = lastValidLat; currentLat = lastValidLat;
currentLon = lastValidLon; currentLon = lastValidLon;
@ -1085,6 +1167,7 @@ class DestinationController extends GetxController {
backgroundColor: Colors.yellow, backgroundColor: Colors.yellow,
); );
} }
*/
} }
if (okToUseGPS && position != null) { if (okToUseGPS && position != null) {
@ -1103,7 +1186,7 @@ class DestinationController extends GetxController {
Duration difference = lastGPSCollectedTime.difference(DateTime.now()) Duration difference = lastGPSCollectedTime.difference(DateTime.now())
.abs(); .abs();
// 最後にGPS信号を取得した時刻から10秒以上経過、かつ10m以上経過普通に歩くスピード // 最後にGPS信号を取得した時刻から10秒以上経過、かつ10m以上経過普通に歩くスピード
if (difference.inSeconds >= 10 || distanceToDest >= 10) { if (difference.inSeconds >= 10 && distanceToDest >= 10) {
// print( // print(
// "^^^^^^^^ GPS data collected ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}, ^^^ ${position.latitude}, ${position.longitude}"); // "^^^^^^^^ GPS data collected ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}, ^^^ ${position.latitude}, ${position.longitude}");
@ -1259,6 +1342,8 @@ class DestinationController extends GetxController {
void fixMapBound(String token) { void fixMapBound(String token) {
//String _token = indexController.currentUser[0]["token"]; //String _token = indexController.currentUser[0]["token"];
indexController.switchPage(AppPages.INDEX); indexController.switchPage(AppPages.INDEX);
if (isMapControllerReady) {
LocationService.getLocationsExt(token).then((value) { LocationService.getLocationsExt(token).then((value) {
if (value != null) { if (value != null) {
//print("--- loc ext is - $value ----"); //print("--- loc ext is - $value ----");
@ -1274,6 +1359,12 @@ class DestinationController extends GetxController {
centerMapToCurrentLocation(); centerMapToCurrentLocation();
} }
}); });
} else {
// MapControllerの初期化が完了していない場合は、遅延して再試行
Future.delayed(Duration(milliseconds: 100), () {
fixMapBound(token);
});
}
} }

View File

@ -6,6 +6,9 @@ import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/services/auth_service.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
// SafeAreaウィジェットを使用して、画面の安全領域内にメニューを表示しています。
// Columnウィジェットを使用して、メニューアイテムを縦に並べています。
//
class DrawerPage extends StatelessWidget { class DrawerPage extends StatelessWidget {
DrawerPage({Key? key}) : super(key: key); DrawerPage({Key? key}) : super(key: key);
@ -13,9 +16,21 @@ class DrawerPage extends StatelessWidget {
// 要検討URLの起動に失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 // 要検討URLの起動に失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。
// //
/*
void _launchURL(url) async { void _launchURL(url) async {
if (!await launchUrl(url)) throw 'Could not launch $url'; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -26,6 +41,9 @@ class DrawerPage extends StatelessWidget {
// space to fit everything. // space to fit everything.
child: Column( child: Column(
children: [ children: [
// 最初のアイテムは、ユーザーのログイン状態に応じて表示が変わります。
// ユーザーがログインしていない場合は、"drawer_title".trというテキストを表示します。
// ユーザーがログインしている場合は、ユーザーのメールアドレスを表示します。
Container( Container(
height: 100, height: 100,
color: Colors.amber, 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 Obx(() => indexController.currentUser.isEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.login), leading: const Icon(Icons.login),
@ -63,6 +84,8 @@ class DrawerPage extends StatelessWidget {
Get.toNamed(AppPages.LOGIN); Get.toNamed(AppPages.LOGIN);
}, },
)), )),
// パスワード変更のアイテムは、ユーザーがログインしている場合にのみ表示されます。
// "change_password".trというテキストを表示し、タップするとAppPages.CHANGE_PASSWORDにナビゲートします。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.password), leading: const Icon(Icons.password),
@ -75,6 +98,8 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
// サインアップのアイテムは、ユーザーがログインしていない場合にのみ表示されます。
// "sign_up".trというテキストを表示し、タップするとAppPages.REGISTERにナビゲートします。
indexController.currentUser.isEmpty indexController.currentUser.isEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.person), leading: const Icon(Icons.person),
@ -87,6 +112,8 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
// リセットのアイテムは、ユーザーがログインしている場合にのみ表示されます。
// タップすると、確認ダイアログを表示し、ユーザーがリセットを確認するとDestinationControllerのresetRogaining()メソッドを呼び出してゲームデータをリセットします。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.password), leading: const Icon(Icons.password),
@ -95,7 +122,7 @@ class DrawerPage extends StatelessWidget {
// 要検討:リセット操作の確認メッセージをローカライズすることを検討してください。 // 要検討:リセット操作の確認メッセージをローカライズすることを検討してください。
// //
Get.defaultDialog( Get.defaultDialog(
title: "よろしいですか、リセットしますか?", title: "リセットしますがよろしいですか?",
middleText: "これにより、すべてのゲーム データが削除され、すべての状態が削除されます", middleText: "これにより、すべてのゲーム データが削除され、すべての状態が削除されます",
textConfirm: "確認する", textConfirm: "確認する",
textCancel: "キャンセルする", textCancel: "キャンセルする",
@ -114,11 +141,20 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
// アカウント削除のアイテムは、ユーザーがログインしている場合にのみ表示されます。
// "delete_account".trというテキストを表示し、タップするとAuthService.deleteUser()を呼び出してアカウントを削除し、AppPages.TRAVELにナビゲートします。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.delete_forever), leading: const Icon(Icons.delete_forever),
title: Text("delete_account".tr), title: Text("delete_account".tr),
onTap: () { onTap: () {
Get.defaultDialog(
title: "アカウントを削除しますがよろしいですか?",
middleText: "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます",
textConfirm: "確認する",
textCancel: "キャンセルする",
onCancel: () => Get.back(),
onConfirm: () {
String token = indexController.currentUser[0]['token']; String token = indexController.currentUser[0]['token'];
AuthService.deleteUser(token).then((value) { AuthService.deleteUser(token).then((value) {
if (value.isNotEmpty) { if (value.isNotEmpty) {
@ -129,11 +165,15 @@ class DrawerPage extends StatelessWidget {
} }
}); });
}, },
);
},
) )
: const SizedBox( : const SizedBox(
width: 0, width: 0,
height: 0, height: 0,
), ),
// ユーザーデータ削除のアイテムは、ユーザーがログインしている場合にのみ表示されます。
// タップすると、AuthService.deleteUser()を呼び出してユーザーデータを削除します。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
// 要検討:アカウント削除のリクエストが失敗した場合のエラーハンドリングを追加することをお勧めします。 // 要検討:アカウント削除のリクエストが失敗した場合のエラーハンドリングを追加することをお勧めします。
@ -141,11 +181,19 @@ class DrawerPage extends StatelessWidget {
leading: const Icon(Icons.delete_forever), leading: const Icon(Icons.delete_forever),
title: Text("ユーザーデータを削除する".tr), title: Text("ユーザーデータを削除する".tr),
onTap: () { onTap: () {
Get.defaultDialog(
title: "アカウントを削除しますがよろしいですか?",
middleText: "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます",
textConfirm: "確認する",
textCancel: "キャンセルする",
onCancel: () => Get.back(),
onConfirm: () {
String token = indexController.currentUser[0]['token']; String token = indexController.currentUser[0]['token'];
AuthService.deleteUser(token).then((value) { AuthService.deleteUser(token).then((value) {
Get.snackbar("ユーザーデータを削除する", Get.snackbar("ユーザーデータを削除する",
"データを削除するためにユーザーの同意が設定されています アプリとサーバーでユーザーデータが削除されました"); "データを削除するためにユーザーの同意が設定されています アプリとサーバーでユーザーデータが削除されました");
}); });
});
}, },
) )
: const SizedBox( : const SizedBox(
@ -167,18 +215,23 @@ class DrawerPage extends StatelessWidget {
// title: Text("point_rank".tr), // title: Text("point_rank".tr),
// onTap: (){}, // onTap: (){},
// ), // ),
// "rog_web".trというテキストのアイテムは、ユーザーがログインしている場合にのみ表示されます。
// タップすると、_launchURL()メソッドを呼び出して外部のウェブサイトを開きます。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.featured_video), leading: const Icon(Icons.featured_video),
title: Text("rog_web".tr), title: Text("rog_web".tr),
onTap: () { onTap: () {
_launchURL("https://www.gifuai.net/?page_id=17397"); _launchURL("https://www.gifuai.net/?page_id=60043");
}, },
) )
: const SizedBox( : const SizedBox(
width: 0, width: 0,
height: 0, height: 0,
), ),
// "privacy".trというテキストのアイテムは、常に表示されます。
// タップすると、_launchURL()メソッドを呼び出してプライバシーポリシーのURLを開きます。
ListTile( ListTile(
leading: const Icon(Icons.privacy_tip), leading: const Icon(Icons.privacy_tip),
title: Text("privacy".tr), title: Text("privacy".tr),

View File

@ -37,6 +37,7 @@ class LocationController extends GetxController {
// 位置情報のストリームを保持する変数です。StreamSubscription<Position>型で宣言されています。 // 位置情報のストリームを保持する変数です。StreamSubscription<Position>型で宣言されています。
LatLng? lastValidLocation; LatLng? lastValidLocation;
DateTime lastGPSDataReceivedTime = DateTime.now(); // 最後にGPSデータを受け取った時刻
// GPSシミュレーション用のメソッドを追加 // GPSシミュレーション用のメソッドを追加
void setSimulationMode(bool value) { void setSimulationMode(bool value) {
@ -354,6 +355,7 @@ class LocationController extends GetxController {
accuracy: position.accuracy, accuracy: position.accuracy,
); );
*/ */
lastGPSDataReceivedTime = DateTime.now(); // 最後にGPS信号を受け取った時刻
locationMarkerPositionStreamController.add(position); locationMarkerPositionStreamController.add(position);
} }
} }