4 Commits

Author SHA1 Message Date
7976f4c75b Fixed Android issues 2024-09-10 16:16:04 +09:00
8ed6a4e8bf Fix Android issues 2024-09-10 08:12:33 +09:00
a22c2ea730 Entry の表示改善 2024-09-08 21:18:45 +09:00
e6328f84b1 Fix iOS background issue 2024-09-08 19:28:08 +09:00
33 changed files with 1704 additions and 1134 deletions

View File

@ -1 +1,16 @@
期限切れのイベントは表示しない。==> メッセージで期限切れであることを表示すること
不参加の過去エントリーは削除できるようにする
バグ:
履歴の写真:アクセスエラー
バックアップをイベントごとに保存・レストア
ログインした際に、イベントが選択されていなければ、イベントを選択するように促す。
事前チェックインした写真が履歴に表示されない。
ユーザー名間違えたらログインできなくなる。
起動時に最後の参加イベントが過去日だったら、
チェックポイントをクリアする。
当日なら、参加処理?をしてタイトルを変える。
チーム構成とエントリーの相関が難しいのでは??

8
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,8 @@
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}

View File

@ -1,6 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.dvox.gifunavi">
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
@ -11,16 +10,16 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="34" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application
android:label="岐阜ナビ"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
android:name="com.dvox.gifunavi.MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
@ -28,7 +27,7 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
>
tools:ignore="Instantiatable">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
@ -50,8 +49,7 @@
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyAUBI1ablMKuJwGj2-kSuEhvYxvB1A-mOE"/>
 <service
android:name=".LocationService"
android:enableOnBackInvokedCallback="true"
android:name="com.dvox.gifunavi.LocationService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />

View File

@ -0,0 +1,5 @@
package com.dvox.gifunavi_git
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -12,7 +12,6 @@ import CoreMotion
) -> Bool {
//GeneratedPluginRegistrant.register(with: self)
//return super.application(application, didFinishLaunchingWithOptions: launchOptions)
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let motionChannel = FlutterMethodChannel(name: "net.sumasen.gifunavi/motion",
binaryMessenger: controller.binaryMessenger)
@ -34,58 +33,17 @@ import CoreMotion
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
//
// 姿
//
// sendMotionData使Flutter
private func startMotionUpdates(result: @escaping FlutterResult) {
if motionManager.isDeviceMotionAvailable {
motionManager.deviceMotionUpdateInterval = 0.1
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] (motion, error) in
guard let self = self else { return }
if let error = error {
motionManager.startDeviceMotionUpdates(to: .main) { (motion, error) in
DispatchQueue.main.async {
result(FlutterError(code: "MOTION_ERROR",
message: error.localizedDescription,
details: nil))
}
return
}
guard let motion = motion else {
DispatchQueue.main.async {
result(FlutterError(code: "NO_MOTION_DATA",
message: "No motion data available",
details: nil))
}
return
}
DispatchQueue.main.async {
let motionData: [String: Any] = [
"attitude": [
"roll": motion.attitude.roll,
"pitch": motion.attitude.pitch,
"yaw": motion.attitude.yaw
],
"gravity": [
"x": motion.gravity.x,
"y": motion.gravity.y,
"z": motion.gravity.z
],
"userAcceleration": [
"x": motion.userAcceleration.x,
"y": motion.userAcceleration.y,
"z": motion.userAcceleration.z
]
]
self.sendMotionData(motionData)
// UI
let appState = UIApplication.shared.applicationState
//
}
}
result(nil) //
result(nil)
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "Device motion is not available.",
@ -93,12 +51,6 @@ import CoreMotion
}
}
private func sendMotionData(_ data: [String: Any]) {
let motionChannel = FlutterMethodChannel(name: "net.sumasen.gifunavi/motion",
binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
motionChannel.invokeMethod("onMotionData", arguments: data)
}
private func stopMotionUpdates(result: @escaping FlutterResult) {
motionManager.stopDeviceMotionUpdates()
result(nil)

View File

@ -20,35 +20,32 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="岐阜ナビ" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WPs-nj-CIV">
<rect key="frame" x="36" y="123.99999999999999" width="321" height="38.333333333333329"/>
<rect key="frame" x="46" y="122.99999999999999" width="301" height="38.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="32"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="NPO 岐阜aiネットワーク" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TM1-SD-6RA">
<rect key="frame" x="36" y="717.66666666666663" width="321" height="20.333333333333371"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" text="NPO 岐阜aiネットワーク" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TM1-SD-6RA">
<rect key="frame" x="46" y="708" width="301" height="40"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="hCH-Iu-4S2">
<rect key="frame" x="36" y="212.33333333333334" width="321" height="405.33333333333326"/>
<constraints>
<constraint firstAttribute="width" secondItem="hCH-Iu-4S2" secondAttribute="height" multiplier="270:341" id="jg6-gh-iGg"/>
</constraints>
<rect key="frame" x="46" y="241.33333333333334" width="301" height="341.33333333333326"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="WPs-nj-CIV" secondAttribute="trailing" constant="20" id="1UA-eg-bma"/>
<constraint firstAttribute="trailingMargin" secondItem="hCH-Iu-4S2" secondAttribute="trailing" constant="20" id="3lP-Fl-Xzw"/>
<constraint firstAttribute="trailingMargin" secondItem="TM1-SD-6RA" secondAttribute="trailing" constant="20" id="E8j-R8-JVy"/>
<constraint firstItem="xbc-2k-c8Z" firstAttribute="top" secondItem="TM1-SD-6RA" secondAttribute="bottom" constant="80" id="MKZ-EI-0XJ"/>
<constraint firstItem="hCH-Iu-4S2" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="20" id="N5f-Kk-hnZ"/>
<constraint firstItem="TM1-SD-6RA" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="20" id="OUN-wm-60h"/>
<constraint firstItem="WPs-nj-CIV" firstAttribute="top" secondItem="Ydg-fD-yQy" secondAttribute="bottom" constant="65" id="aPG-JX-zf5"/>
<constraint firstItem="hCH-Iu-4S2" firstAttribute="top" secondItem="WPs-nj-CIV" secondAttribute="bottom" constant="50" id="jKu-Hc-2ln"/>
<constraint firstItem="WPs-nj-CIV" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="20" id="u9d-2i-Jy1"/>
<constraint firstItem="xbc-2k-c8Z" firstAttribute="top" secondItem="TM1-SD-6RA" secondAttribute="bottom" constant="70" id="DbA-tx-JAk"/>
<constraint firstAttribute="trailingMargin" secondItem="hCH-Iu-4S2" secondAttribute="trailing" constant="30" id="DsC-fI-z7h"/>
<constraint firstAttribute="trailingMargin" secondItem="TM1-SD-6RA" secondAttribute="trailing" constant="30" id="Spt-qL-bHo"/>
<constraint firstItem="WPs-nj-CIV" firstAttribute="top" secondItem="Ydg-fD-yQy" secondAttribute="bottom" constant="64" id="Wf4-J5-CP7"/>
<constraint firstItem="hCH-Iu-4S2" firstAttribute="top" secondItem="WPs-nj-CIV" secondAttribute="bottom" constant="80" id="eIO-ZH-rp5"/>
<constraint firstItem="hCH-Iu-4S2" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="30" id="oZr-ky-01l"/>
<constraint firstItem="WPs-nj-CIV" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="30" id="pEC-jM-hHf"/>
<constraint firstItem="TM1-SD-6RA" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="30" id="pRM-dy-ucg"/>
<constraint firstAttribute="trailingMargin" secondItem="WPs-nj-CIV" secondAttribute="trailing" constant="30" id="ugk-x4-Wgc"/>
</constraints>
</view>
</viewController>

View File

@ -1,8 +1,8 @@
import Flutter
import UIKit
import Flutter
import CoreMotion
@main
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private let motionManager = CMMotionManager()
@ -10,11 +10,8 @@ import CoreMotion
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
//GeneratedPluginRegistrant.register(with: self)
//return super.application(application, didFinishLaunchingWithOptions: launchOptions)
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let motionChannel = FlutterMethodChannel(name: "net.sumasen.gifunavi/motion",
let motionChannel = FlutterMethodChannel(name: "com.yourcompany.app/motion",
binaryMessenger: controller.binaryMessenger)
motionChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
@ -34,58 +31,17 @@ import CoreMotion
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
//
// 姿
//
// sendMotionData使Flutter
private func startMotionUpdates(result: @escaping FlutterResult) {
if motionManager.isDeviceMotionAvailable {
motionManager.deviceMotionUpdateInterval = 0.1
motionManager.startDeviceMotionUpdates(to: .main) { (motion, error) in
guard let self = self else { return }
if let error = error {
DispatchQueue.main.async {
result(FlutterError(code: "MOTION_ERROR",
message: error.localizedDescription,
details: nil))
}
return
}
guard let motion = motion else {
DispatchQueue.main.async {
result(FlutterError(code: "NO_MOTION_DATA",
message: "No motion data available",
details: nil))
}
return
}
DispatchQueue.main.async {
let motionData: [String: Any] = [
"attitude": [
"roll": motion.attitude.roll,
"pitch": motion.attitude.pitch,
"yaw": motion.attitude.yaw
],
"gravity": [
"x": motion.gravity.x,
"y": motion.gravity.y,
"z": motion.gravity.z
],
"userAcceleration": [
"x": motion.userAcceleration.x,
"y": motion.userAcceleration.y,
"z": motion.userAcceleration.z
]
]
self.sendMotionData(motionData)
// UI
let appState = UIApplication.shared.applicationState
//
}
}
result(nil) //
result(nil)
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "Device motion is not available.",
@ -93,12 +49,6 @@ import CoreMotion
}
}
private func sendMotionData(_ data: [String: Any]) {
let motionChannel = FlutterMethodChannel(name: "net.sumasen.gifunavi/motion",
 binaryMessenger: (window?.rootViewController as! FlutterViewController).binaryMessenger)
motionChannel.invokeMethod("onMotionData", arguments: data)
}
private func stopMotionUpdates(result: @escaping FlutterResult) {
motionManager.stopDeviceMotionUpdates()
result(nil)

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:io';
//import 'dart:convert';
//import 'dart:developer';
import 'package:flutter_map/flutter_map.dart';
import 'package:gifunavi/model/gps_data.dart';
//import 'package:gifunavi/pages/home/home_page.dart';
import 'package:gifunavi/utils/database_gps.dart';
@ -89,6 +88,9 @@ Future<void> saveGameState() async {
pref.setBool(
"rogaining_counted", destinationController.rogainingCounted.value);
pref.setBool("ready_for_goal", DestinationController.ready_for_goal);
// 最後のゲーム日時を保存
pref.setString('lastGameDate', DateTime.now().toIso8601String());
}
@ -109,6 +111,20 @@ Future<void> restoreGame() async {
if (indexController.currentUser.isNotEmpty &&
indexController.currentUser[0]["user"]["id"] == savedUserId) {
// 最後のゲーム日時を取得
final lastGameDateString = pref.getString('lastGameDate');
if (lastGameDateString != null) {
final lastGameDate = DateTime.parse(lastGameDateString);
final now = DateTime.now();
// 最後のゲームが昨日以前の場合
if (lastGameDate.isBefore(DateTime(now.year, now.month, now.day))) {
// ゲームの状態をクリア
await resetGameState();
return; // ここで関数を終了
}
}
final dateString = pref.getString('eventDate');
if (dateString != null) {
final parsedDate = DateTime.parse(dateString);
@ -144,6 +160,24 @@ Future<void> restoreGame() async {
}
}
// ゲームの状態をリセットする関数
Future<void> resetGameState() async {
SharedPreferences pref = await SharedPreferences.getInstance();
await pref.remove("is_in_rog");
await pref.remove("rogaining_counted");
await pref.remove("ready_for_goal");
DestinationController destinationController = Get.find<DestinationController>();
destinationController.isInRog.value = false;
destinationController.rogainingCounted.value = false;
DestinationController.ready_for_goal = false;
// チェックポイントをクリア
destinationController.deleteDBDestinations();
debugPrint("Game state has been reset due to outdated last game date");
}
/*
void restoreGame_new() async {
SharedPreferences pref = await SharedPreferences.getInstance();
@ -199,16 +233,6 @@ void _showEventSelectionWarning() {
);
}
// main.dart の上部に追加
const bool isDebugMode = true; // リリース時にfalseに変更
// 各ファイルで使用
void debugLog(String message) {
if (isDebugMode) {
debugPrint('DEBUG: $message');
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -238,89 +262,20 @@ void main() async {
}
}
Future <void> _initializeControllers() async {
final stopwatch = Stopwatch()..start();
const timeout = Duration(seconds: 30); // タイムアウト時間を10秒から30秒に延長
try {
while (!_areAllControllersRegistered()) {
if (stopwatch.elapsed > timeout) {
throw TimeoutException('Controller initialization timed out');
}
await Future.delayed(const Duration(milliseconds: 100));
}
final LocationController _locationController = Get.find<LocationController>();
final IndexController _indexController = Get.find<IndexController>();
final DestinationController _destinationController = Get.find<DestinationController>();
final PermissionController _permissionController = Get.find<PermissionController>();
print('All controllers initialized successfully');
} catch (e) {
print('Error initializing controllers: $e');
_handleInitializationError();
}
}
bool _areAllControllersRegistered() {
return Get.isRegistered<LocationController>() &&
Get.isRegistered<IndexController>() &&
Get.isRegistered<DestinationController>() &&
Get.isRegistered<PermissionController>();
}
void _handleInitializationError() {
// エラーハンドリングのロジックをここに実装
// 例: エラーダイアログの表示、アプリの再起動など
print("_handleInitializationError");
}
Future<void> initServices() async {
print('Starting services ...');
try {
await Get.putAsync(() => ApiService().init());
//await _initApiService();
//await Get.putAsync(() => ApiService().init());
await _initApiService();
debugPrint("1: start ApiService");
if (Platform.isIOS ) {
// コントローラーを初期化
/*
Get.put(IndexController(apiService: Get.find<ApiService>()), permanent: true);
Get.put(SettingsController(), permanent: true);
Get.put(DestinationController(), permanent: true);
Get.put(LocationController(), permanent: true);
*/
// すべてのコントローラーとサービスを非同期で初期化
Get.lazyPut(() => IndexController(apiService: Get.find<ApiService>()));
debugPrint("2: start IndexController");
Get.lazyPut(() => MapController());
debugPrint("2: start MapController");
// その他のコントローラーを遅延初期化
Get.lazyPut(() => SettingsController());
debugPrint("2: start SettingsController");
Get.lazyPut(() => DestinationController());
debugPrint("3: start DestinationController");
Get.lazyPut(() => LocationController());
debugPrint("4: start LocationController");
//await _initializeControllers();
debugPrint("2: Controllers initialized");
}else {
// すべてのコントローラーとサービスを非同期で初期化
Get.lazyPut(() => IndexController(apiService: Get.find<ApiService>()));
debugPrint("2: start IndexController");
// その他のコントローラーを遅延初期化
Get.lazyPut(() => SettingsController());
debugPrint("2: start SettingsController");
Get.lazyPut(() => DestinationController());
debugPrint("3: start DestinationController");
Get.lazyPut(() => LocationController());
debugPrint("4: start LocationController");
Get.lazyPut(() => PermissionController());
}
// 非同期処理を並列実行
await Future.wait([
@ -331,13 +286,13 @@ Future<void> initServices() async {
print('=== 5. Initialized TimeZone...');
print('=== 6. CacheProvider started...');
Get.put(PermissionController());
await _checkPermissions();
debugPrint("7: start PermissionController");
//await PermissionController.checkAndRequestPermissions();
}catch(e){
print('Error initializing : $e');
rethrow;
}
print('All services started...');
@ -359,17 +314,7 @@ Future<void> _initCacheProvider() async {
}
Future<void> _checkPermissions() async {
int attempts = 0;
while (Get.context == null && attempts < 10) {
await Future.delayed(const Duration(milliseconds: 100));
attempts++;
}
if (Get.context != null) {
await PermissionController.checkAndRequestPermissions();
} else {
print('Context is still null, cannot check permissions');
}
//await PermissionController.checkAndRequestPermissions(); // main._checkPermissions
}
Future<void> _initApiService() async {
@ -549,9 +494,7 @@ Future<void> startBackgroundTracking() async {
try {
// 位置情報の権限が許可されているかを確認
WidgetsBinding.instance.addPostFrameCallback((_) {
PermissionController.checkAndRequestPermissions();
});
await PermissionController.checkAndRequestPermissions();
} catch (e) {
print('Error starting background tracking: $e');
}
@ -646,29 +589,10 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
Future <void> _initializeControllers() async {
while (!Get.isRegistered<LocationController>() ) {
print("LocationController is not up... ");
await Future.delayed(const Duration(milliseconds: 100));
}
while (!Get.isRegistered<IndexController>() ) {
print("IndexController is not up...");
await Future.delayed(const Duration(milliseconds: 100));
}
while (!Get.isRegistered<DestinationController>() ) {
print("DestinationController is not up...");
await Future.delayed(const Duration(milliseconds: 100));
}
/*
while (!Get.isRegistered<PermissionController>() ) {
print("PermissionController is not up... ");
await Future.delayed(const Duration(milliseconds: 100));
}
*/
while (!Get.isRegistered<LocationController>() ||
!Get.isRegistered<IndexController>() ||
!Get.isRegistered<DestinationController>() ) {
print("LocationController status = Get.isRegistered<LocationController>() ");
!Get.isRegistered<DestinationController>() ||
!Get.isRegistered<PermissionController>()) {
await Future.delayed(const Duration(milliseconds: 100));
}
@ -676,7 +600,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
_locationController = Get.find<LocationController>();
_indexController = Get.find<IndexController>();
_destinationController = Get.find<DestinationController>();
//_permissionController = Get.find<PermissionController>();
_permissionController = Get.find<PermissionController>();
_isControllerInitialized = true;
}
@ -763,11 +687,14 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}
void _checkMemoryUsage() async {
/*
final memoryInfo = await _getMemoryInfo();
//debugPrint('Current memory usage: ${memoryInfo['used']} MB');
if (memoryInfo['used']! > 100) { // 100MB以上使用している場合
_performMemoryCleanup();
}
*/
}
Future<Map<String, int>> _getMemoryInfo() async {
@ -881,7 +808,15 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
try {
await _initializeControllers();
if (!Get.isRegistered<IndexController>()) {
_indexController = Get.find<IndexController>();
}
if (!Get.isRegistered<LocationController>()) {
_locationController = Get.find<LocationController>();
}
if (!Get.isRegistered<DestinationController>()) {
_destinationController = Get.find<DestinationController>();
}
switch (state) {
case AppLifecycleState.resumed:
@ -923,7 +858,6 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}
Future<void> _onResumed() async {
debugPrint("==(Status Changed)==> RESUMED");
try {
if (!_isControllerInitialized) {
@ -954,76 +888,43 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}
Future<void> _onInactive() async {
try {
debugPrint("==(Status Changed)==> INACTIVE");
if (!_isControllerInitialized) {
await _initializeControllers();
}
if (Platform.isIOS && !_destinationController.isRunningBackgroundGPS) {
debugPrint(" ==(Status Changed)==> INACTIVE. 非アクティブ処理。");
_locationController.stopPositionStream();
_destinationController.isRunningBackgroundGPS = true;
await startBackgroundTracking();
} else if (Platform.isAndroid &&
!_destinationController.isRunningBackgroundGPS) {
} else if (Platform.isAndroid && !_destinationController.isRunningBackgroundGPS) {
// Android特有の処理があれば追加
debugPrint(" ==(Status Changed)==> INACTIVE. 非アクティブ処理。");
}else{
debugPrint("==(Status Changed)==> INACTIVE 不明状態");
}
await saveGameState();
} catch (e) {
print('Error in _onInactive: $e');
}
}
Future<void> _onPaused() async {
try {
debugPrint(" ==(Status Changed)==> PAUSED. バックグラウンド処理。");
if (!_isControllerInitialized) {
await _initializeControllers();
}
if (Platform.isAndroid &&
!_destinationController.isRunningBackgroundGPS) {
debugPrint(
" ==(Status Changed)==> PAUSED. Android バックグラウンド処理。");
if (Platform.isAndroid && !_destinationController.isRunningBackgroundGPS) {
debugPrint(" ==(Status Changed)==> PAUSED. Android バックグラウンド処理。");
_locationController.stopPositionStream();
const platform = MethodChannel('location');
await platform.invokeMethod('startLocationService');
_destinationController.isRunningBackgroundGPS = true;
}
await saveGameState();
} catch (e) {
print('Error in _onPaused: $e');
}
}
Future<void> _onDetached() async {
try {
if (!_isControllerInitialized) {
await _initializeControllers();
}
debugPrint(" ==(Status Changed)==> DETACHED アプリは終了します。");
await saveGameState();
// アプリ終了時の追加処理
} catch (e) {
print('Error in _onDetached: $e');
}
}
Future<void> _onHidden() async {
try {
if (!_isControllerInitialized) {
await _initializeControllers();
}
debugPrint(" ==(Status Changed)==> Hidden アプリが隠れた");
await saveGameState();
} catch (e) {
print('Error in _onHidden: $e');
}
}
@override
@ -1051,4 +952,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
enableLog: true,
);
}
}

View File

@ -179,7 +179,7 @@ class _AuthPageState extends ConsumerState<AuthPage> {
Widget buildControlls() {
if (_isLoginProgress) {
return const Center(
child: CircularProgressIndicator(), // Auth_page
child: CircularProgressIndicator(),
);
}

View File

@ -418,7 +418,7 @@ class CameraPage extends StatelessWidget {
},
child: Text("finish_goal".tr))
: const Center(
child: CircularProgressIndicator(), // Camera page
child: CircularProgressIndicator(),
)
: Container())
],

View File

@ -44,6 +44,9 @@ import 'package:path_provider/path_provider.dart';
// 目的地に関連する状態管理とロジックを担当するクラスです。
//
class DestinationController extends GetxController {
Timer? _checkForCheckinTimer;
final int _checkInterval = 3000; // ミリ秒単位でチェック間隔を設定
late LocationSettings locationSettings; // 位置情報の設定を保持する変数です。
//late TeamController teamController = TeamController();
@ -1019,15 +1022,16 @@ class DestinationController extends GetxController {
// 2024-8-24 ... 佐伯呼び出しが必要なのか?
//
Future<void> checkForCheckin() async {
//print("--- Start of checkForCheckin function ---");
dbService.updateDatabase();
await Future.delayed(const Duration(milliseconds: 3000));
if (!game_started) {
game_started = true;
dbService.updateDatabase();
}
try {
// ここで、エラー
if( indexController.locations.isNotEmpty ) {
indexController.locations[0].features.forEach((fs) async {
for (var fs in indexController.locations[0].features) {
//indexController.locations[0].features.forEach((fs) async {
GeoJSONMultiPoint mp = fs!.geometry as GeoJSONMultiPoint;
LatLng pt = LatLng(mp.coordinates[0][1], mp.coordinates[0][0]);
@ -1045,10 +1049,11 @@ class DestinationController extends GetxController {
await startTimerLocation(fs, distFs);
// Note: You cannot break out of forEach. If you need to stop processing, you might have to reconsider using forEach.
}
});
}
if (gps_push_started == false) {
unawaited(pushGPStoServer());
pushGPStoServer();
//unawaited(pushGPStoServer());
}
}
//print("--- 123 ---- $skip_gps----");
@ -1071,14 +1076,19 @@ class DestinationController extends GetxController {
// "^^^^^^^^ ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}");
try {
gps_push_started = true;
ExternalService().pushGPS();
await ExternalService().pushGPS();
} catch (e) {
print("An error occurred in pushGPStoServer: $e");
//print("An error occurred: $e");
//await pushGPStoServer();
} finally {
if (gps_push_started) {
Future.delayed(Duration(minutes: 5), pushGPStoServer);
}
//print("--- End of pushGPStoServer function, calling recursively ---");
await Future.delayed(const Duration(seconds: 5 * 60));
await pushGPStoServer();
//await Future.delayed(const Duration(seconds: 5 * 60));
//await pushGPStoServer();
}
}
@ -1126,7 +1136,7 @@ class DestinationController extends GetxController {
}
Future<String?> _saveImageToGallery(String imagePath) async {
final status = await PermissionController.checkStoragePermission(); // destinationController._saveImageToGallery
final status = await PermissionController.checkStoragePermission();
if(!status){
await PermissionController.requestStoragePermission();
}
@ -1332,6 +1342,13 @@ class DestinationController extends GetxController {
@override
void onInit() async {
super.onInit();
startCheckForCheckinTimer();
/*
WidgetsBinding.instance.addPostFrameCallback((_) async {
await PermissionController.checkAndRequestPermissions();
});
*/
startGPSCheckTimer();
@ -1409,6 +1426,18 @@ class DestinationController extends GetxController {
//checkGPSDataReceived();
}
void startCheckForCheckinTimer() {
_checkForCheckinTimer = Timer.periodic(Duration(milliseconds: _checkInterval), (_) {
checkForCheckin();
});
}
void stopCheckForCheckinTimer() {
_checkForCheckinTimer?.cancel();
_checkForCheckinTimer = null;
}
void restartGPS(){
// GPSデータのListenを再開する処理を追加
Future.delayed(const Duration(seconds: 5), () {
@ -1421,8 +1450,9 @@ class DestinationController extends GetxController {
//
@override
void onClose() {
gpsCheckTimer?.cancel();
locationController.stopPositionStream();
stopCheckForCheckinTimer();
//gpsCheckTimer?.cancel();
//locationController.stopPositionStream();
super.onClose();
}
@ -1639,7 +1669,6 @@ class DestinationController extends GetxController {
final token = indexController.userToken;
if (token != null && token.isNotEmpty) {
await indexController.loadUserDetailsForToken(token);
await Future.delayed(Duration(milliseconds: 500)); // 短い遅延を追加
fixMapBound(token);
}else {
Get.toNamed(AppPages.LOGIN)!.then((value) {
@ -1660,9 +1689,6 @@ class DestinationController extends GetxController {
});
}
// MapControllerの準備が整うまで待機
await indexController.waitForMapControllerReady();
// 地図のイベントリスナーを設定
indexController.mapController.mapEventStream.listen((MapEvent mapEvent) {
if (mapEvent is MapEventMoveEnd) {
@ -1675,12 +1701,11 @@ class DestinationController extends GetxController {
// 地図の境界を修正する関数です。
//
Future<void> fixMapBound(String token) async {
await indexController.waitForMapControllerReady();
void fixMapBound(String token) {
//String _token = indexController.currentUser[0]["token"];
indexController.switchPage(AppPages.INDEX);
if (indexController.isMapControllerReady.value) {
if (isMapControllerReady) {
LocationService.getLocationsExt(token).then((value) {
if (value != null) {
//print("--- loc ext is - $value ----");
@ -1742,18 +1767,11 @@ class DestinationController extends GetxController {
// 地図を現在位置に中央揃えする関数です。
//
void centerMapToCurrentLocation() {
final indexController = Get.find<IndexController>();
if (indexController.isMapControllerReady.value && indexController.isMapControllerReady.value) {
// ... 現在位置へのセンタリングのロジック ...
indexController.mapController.move(LatLng(currentLat, currentLon), 17.0);
} else {
debugPrint('Map controller is not ready for centering');
}
//print("center is ${currentLat}, ${currentLon}");
// Akira ... 状況によって呼ぶか呼ばないか
//if (currentLat != 0 || currentLon != 0) {
//indexController.mapController.move(LatLng(currentLat, currentLon), 17.0);
//}
if (currentLat != 0 || currentLon != 0) {
indexController.mapController.move(LatLng(currentLat, currentLon), 17.0);
}
}
// 接続状態が変更されたときに呼び出される関数です。
@ -1926,7 +1944,7 @@ class DestinationController extends GetxController {
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(), // Destination COntroller
child: CircularProgressIndicator(),
);
});
}

View File

@ -8,9 +8,11 @@ import 'package:gifunavi/model/team.dart';
import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/services/api_service.dart';
import '../index/index_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:timezone/timezone.dart' as tz;
import '../../model/user.dart';
class EntryController extends GetxController {
late ApiService _apiService;
@ -29,6 +31,10 @@ class EntryController extends GetxController {
final activeEvents = <Event>[].obs; //有効なイベントリスト
final teamMembers = <User>[].obs;
final hasError = false.obs;
final errorMessage = "".obs;
@override
void onInit() async {
super.onInit();
@ -41,7 +47,7 @@ class EntryController extends GetxController {
_apiService = await Get.putAsync(() => ApiService().init());
} catch (e) {
print('Error initializing ApiService: $e');
Get.snackbar('Error', 'Failed to initialize API service');
Get.snackbar('Error', 'APIサービスの初期化に失敗しました');
}
}
@ -60,14 +66,16 @@ class EntryController extends GetxController {
initializeEditMode(currentEntry.value!);
} else {
// 新規作成モードの場合、最初のイベントを選択
if (events.isNotEmpty) {
if (activeEvents.isNotEmpty) {
selectedEvent.value = activeEvents.first;
selectedDate.value = activeEvents.first.startDatetime;
}
}
} catch(e) {
print('Error initializing data: $e');
Get.snackbar('Error', 'Failed to load initial data');
// エラー状態を設定
hasError.value = true;
Get.snackbar('Error', '初期データの読み込みに失敗しました');
} finally {
isLoading.value = false;
}
@ -98,12 +106,64 @@ class EntryController extends GetxController {
}
}
void updateTeam(Team? value) {
Future<void> fetchTeamMembers(int teamId) async {
try {
final members = await _apiService.getTeamMembers(teamId);
teamMembers.assignAll(members);
} catch (e) {
print('Error fetching team members: $e');
Get.snackbar('Error', 'Failed to fetch team members');
}
}
List<NewCategory> getFilteredCategories() {
if (selectedTeam.value == null) return [];
if (teamMembers.isEmpty) {
// ソロの場合
String baseCategory = selectedTeam.value!.members.first.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else if (teamMembers.length == 1) {
// チームメンバーが1人の場合ソロ
String baseCategory = teamMembers.first.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else {
// 複数人の場合
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
}
}
bool isElementarySchoolOrYounger(User user) {
final now = DateTime.now();
final age = now.year - user.dateOfBirth!.year;
return age <= 12;
}
void updateTeam(Team? value) async {
selectedTeam.value = value;
if (value != null) {
await fetchTeamMembers(value.id);
final filteredCategories = getFilteredCategories();
if (filteredCategories.isNotEmpty) {
selectedCategory.value = filteredCategories.first;
} else {
selectedCategory.value = null;
}
} else {
teamMembers.clear();
selectedCategory.value = null;
}
}
void updateTeam_old(Team? value) {
selectedTeam.value = value;
if (value != null) {
selectedCategory.value = value.category;
}
}
//void updateTeam(Team? value) => selectedTeam.value = value;
void updateCategory(NewCategory? value) => selectedCategory.value = value;
//void updateDate(DateTime value) => selectedDate.value = value;

View File

@ -7,6 +7,7 @@ import 'package:gifunavi/model/event.dart';
import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/model/team.dart';
import 'package:intl/intl.dart';
import 'package:gifunavi/widgets/error_widget.dart';
import 'package:timezone/timezone.dart' as tz;
@ -31,6 +32,17 @@ class EntryDetailPage extends GetView<EntryController> {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.hasError.value) {
return CustomErrorWidget(
errorMessage: controller.errorMessage.value,
onRetry: () => controller.loadInitialData(),
);
}
try {
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
@ -41,9 +53,22 @@ class EntryDetailPage extends GetView<EntryController> {
label: 'イベント',
items: controller.activeEvents,
selectedId: controller.selectedEvent.value?.id,
onChanged: (eventId) {
final event = controller.activeEvents.firstWhereOrNull((
e) => e.id == eventId);
if (event != null) {
controller.updateEvent(event);
} else {
print('Event with id $eventId not found');
// 必要に応じてエラー処理を追加
}
},
/*
onChanged: (eventId) => controller.updateEvent(
controller.activeEvents.firstWhere((e) => e.id == eventId)
),
*/
getDisplayName: (event) => event.eventName,
getId: (event) => event.id,
),
@ -52,9 +77,21 @@ class EntryDetailPage extends GetView<EntryController> {
label: 'チーム',
items: controller.teams,
selectedId: controller.selectedTeam.value?.id,
onChanged: (teamId) {
final team = controller.teams.firstWhereOrNull((t) =>
t.id == teamId);
if (team != null) {
controller.updateTeam(team);
} else {
print('Team with id $teamId not found');
// 必要に応じてエラー処理を追加
}
},
/*
onChanged: (teamId) => controller.updateTeam(
controller.teams.firstWhere((t) => t.id == teamId)
),
*/
getDisplayName: (team) => team.teamName,
getId: (team) => team.id,
),
@ -78,7 +115,9 @@ class EntryDetailPage extends GetView<EntryController> {
title: const Text('日付'),
subtitle: Text(
controller.selectedDate.value != null
? DateFormat('yyyy-MM-dd').format(tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo')))
? DateFormat('yyyy-MM-dd').format(tz.TZDateTime.from(
controller.selectedDate.value!,
tz.getLocation('Asia/Tokyo')))
: '日付を選択してください',
),
onTap: () async {
@ -86,25 +125,37 @@ class EntryDetailPage extends GetView<EntryController> {
Get.snackbar('Error', 'Please select an event first');
return;
}
final tz.TZDateTime now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventStart = tz.TZDateTime.from(controller.selectedEvent.value!.startDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventEnd = tz.TZDateTime.from(controller.selectedEvent.value!.endDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime now = tz.TZDateTime.now(tz
.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventStart = tz.TZDateTime.from(
controller.selectedEvent.value!.startDatetime, tz
.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventEnd = tz.TZDateTime.from(
controller.selectedEvent.value!.endDatetime, tz
.getLocation('Asia/Tokyo'));
final tz.TZDateTime initialDate = controller.selectedDate.value != null
? tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo'))
final tz.TZDateTime initialDate = controller.selectedDate
.value != null
? tz.TZDateTime.from(controller.selectedDate.value!,
tz.getLocation('Asia/Tokyo'))
: (now.isAfter(eventStart) ? now : eventStart);
// 選択可能な最初の日付を設定(今日かイベント開始日のうち、より後の日付)
final tz.TZDateTime firstDate = now.isAfter(eventStart) ? now : eventStart;
final tz.TZDateTime firstDate = now.isAfter(eventStart)
? now
: eventStart;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate.isAfter(firstDate) ? initialDate : firstDate,
initialDate: initialDate.isAfter(firstDate)
? initialDate
: firstDate,
firstDate: firstDate,
lastDate: eventEnd,
);
if (picked != null) {
controller.updateDate(tz.TZDateTime.from(picked, tz.getLocation('Asia/Tokyo')));
controller.updateDate(tz.TZDateTime.from(
picked, tz.getLocation('Asia/Tokyo')));
}
},
),
@ -151,6 +202,12 @@ class EntryDetailPage extends GetView<EntryController> {
),
),
);
} catch (e) {
print('Error in EntryDetailPage: $e');
return const Center(
child: Text('エラーが発生しました。もう一度お試しください。'),
);
}
}),
);
}
@ -166,15 +223,42 @@ class EntryDetailPage extends GetView<EntryController> {
return DropdownButtonFormField<int>(
decoration: InputDecoration(labelText: label),
value: selectedId,
items: items.map((item) => DropdownMenuItem<int>(
items: items.isNotEmpty ? items.map((item) => DropdownMenuItem<int>(
//items: items.map((item) => DropdownMenuItem<int>(
value: getId(item),
child: Text(getDisplayName(item)),
)).toList(),
onChanged: onChanged,
)).toList() : null,
onChanged: (value) {
if (value != null) {
onChanged(value);
}
},
//onChanged: onChanged,
);
}
Widget _buildCategoryDropdown() {
final eligibleCategories = controller.getFilteredCategories();
return DropdownButtonFormField<NewCategory>(
decoration: InputDecoration(labelText: 'カテゴリ'),
value: controller.selectedCategory.value,
items: eligibleCategories.isNotEmpty ? eligibleCategories.map((category) => DropdownMenuItem<NewCategory>(
//items: eligibleCategories.map((category) => DropdownMenuItem<NewCategory>(
value: category,
child: Text(category.categoryName),
)).toList() : null,
onChanged: (value) {
if (value != null) {
controller.updateCategory(value);
}
},
//onChanged: (value) => controller.updateCategory(value),
);
}
Widget _buildCategoryDropdown_old() {
final eligibleCategories = controller.categories.where((c) =>
c.baseCategory == controller.selectedCategory.value?.baseCategory
).toList();

View File

@ -7,7 +7,7 @@ import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:timezone/timezone.dart' as tz;
import '../../model/entry.dart';
import 'package:gifunavi/model/entry.dart';
class EntryListPage extends GetView<EntryController> {
const EntryListPage({super.key});
@ -38,8 +38,14 @@ class EntryListPage extends GetView<EntryController> {
itemCount: sortedEntries.length,
itemBuilder: (context, index) {
final entry = sortedEntries[index];
final now = DateTime.now();
final isEntryInFuture = _compareDatesOnly(entry.date!, now) >= 0;
//final now = DateTime.now();
//print("now=$now");
//final isEntryInFuture = _compareDatesOnly(entry.date!, now) >= 0;
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
// 日付のみを比較(時間を無視)
final isEntryInFuture = _compareDatesOnly(entryDate, now) >= 0;
//final isEntryInFuture = entry.date!.isAfter(now) || entry.date!.isAtSameMomentAs(now);
@ -143,7 +149,7 @@ class EntryListPage_old extends GetView<EntryController> {
),
body: Obx((){
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator()); // EntryList
return const Center(child: CircularProgressIndicator());
}
// エントリーを日付昇順にソート

View File

@ -6,8 +6,6 @@ import 'package:gifunavi/pages/gps/gps_controller.dart';
import 'package:gifunavi/pages/history/history_controller.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:gifunavi/pages/gps/gps_controller.dart';
import 'package:gifunavi/pages/history/history_controller.dart';
import 'package:intl/intl.dart';
class EventResultPage extends StatefulWidget {

View File

@ -27,7 +27,7 @@ class _HistoryPageState extends State<HistoryPage> {
future: db.getDestinations(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); //History page
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
@ -167,7 +167,7 @@ class CustomWidget extends StatelessWidget {
print('Error loading image path: ${snapshot.error}');
return const Icon(Icons.error);
} else {
return const CircularProgressIndicator(); // History page 2
return const CircularProgressIndicator();
}
},
);

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:get/get.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
@ -9,6 +8,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:gifunavi/model/destination.dart';
import 'package:gifunavi/model/entry.dart';
@ -34,8 +34,6 @@ import 'package:timezone/data/latest.dart' as tz;
import '../permission/permission.dart';
//enum MapState { initializing, ready, error }
class IndexController extends GetxController with WidgetsBindingObserver {
List<GeoJSONFeatureCollection> locations = <GeoJSONFeatureCollection>[].obs;
List<GeoJSONFeature> currentFeature = <GeoJSONFeature>[].obs;
@ -62,16 +60,10 @@ class IndexController extends GetxController with WidgetsBindingObserver {
var isCustomAreaSelected = false.obs;
RxBool isMapControllerReady = RxBool(false); // MapControllerの初期化状態を管理するフラグ
//final mapControllerReadyStream = Stream<bool>.value(false); // MapControllerの初期化状態を通知するためのストリーム
// 複数の状態変数_isLoading, _isMapInitialized, indexController.isMapControllerReadyを一元管理します。
//final Rx<MapState> mapState = MapState.initializing.obs;
final Completer<void> mapControllerCompleter = Completer<void>();
late MapController mapController; // = Rx<MapController?>(null);
final RxBool isMapControllerReady = false.obs; // MapControllerの初期化状態を管理するフラグ
//MapController mapController = MapController();
MapController mapController = MapController();
MapController rogMapController = MapController();
LogManager logManager = LogManager();
@ -104,43 +96,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
final selectedEventName = 'add_location'.tr.obs;
RxBool isLoadingLocations = false.obs;
// ユーザーの参加状況を追跡する
final RxBool isUserParticipating = false.obs;
// ユーザーのゴール到達状況を追跡する
final RxBool hasUserReachedGoal = false.obs;
void initMapController() {
mapController = MapController();
// MapEventMoveEndイベントを使用して初期化完了を検出
mapController.mapEventStream.listen((event) {
if (event is MapEventMoveEnd && !isMapControllerReady.value) {
// MapEventMoveEndイベントが発生したら、MapControllerの準備完了とみなす
if (!isMapControllerReady.value) {
print('MapController is ready');
isMapControllerReady.value = true;
if (!mapControllerCompleter.isCompleted) {
mapControllerCompleter.complete();
}
}
}
});
}
// ユーザーの参加状況を更新するメソッド
void updateUserParticipationStatus(bool isParticipating) {
isUserParticipating.value = isParticipating;
update(); // GetX の update() メソッドを呼び出してUIを更新
}
// ユーザーのゴール到達状況を更新するメソッド
void updateUserGoalStatus(bool hasReachedGoal) {
hasUserReachedGoal.value = hasReachedGoal;
update(); // GetX の update() メソッドを呼び出してUIを更新
}
RxBool isLoadingLocations = true.obs;
void setSelectedEventName(String eventName) {
selectedEventName.value = eventName;
@ -284,16 +240,15 @@ class IndexController extends GetxController with WidgetsBindingObserver {
@override
void onInit() {
try {
super.onInit();
//mapController = MapController();
//ever(isMapControllerReady, (_) => _onMapControllerReady());
initMapController();
try {
initConnectivity();
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
//WidgetsBinding.instance.addPostFrameCallback((_) async {
// await PermissionController.checkAndRequestPermissions();
//});
WidgetsBinding.instance.addObserver(this);
_startLocationService(); // アプリ起動時にLocationServiceを開始する
@ -304,7 +259,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
tz.initializeTimeZones();
//teamController = Get.find<TeamController>();
//loadLocations();
loadLocations();
}catch(e,stacktrace){
print('Error in IndexController.onInit: $e');
@ -315,33 +270,18 @@ class IndexController extends GetxController with WidgetsBindingObserver {
}
}
void _onMapControllerReady() {
if (isMapControllerReady.value) {
loadLocations();
}
}
Future<void> loadLocations() async {
if (isLoadingLocations.value) return;
isLoadingLocations.value = true;
try {
debugPrint('IndexController: Starting to load locations');
await waitForMapControllerReady(); // loadLocations
debugPrint('IndexController: Permission granted, loading locations');
await waitForMapControllerReady();
String eventCode = currentUser.isNotEmpty ? currentUser[0]["user"]["event_code"] ?? "" : "";
await loadLocationsBound(eventCode);
debugPrint('IndexController: Locations loaded successfully');
} catch (e) {
print('Error loading locations: $e');
// エラーハンドリングを追加(例:スナックバーでユーザーに通知)
Get.snackbar('エラー', '位置情報の取得に失敗しました: ${e.toString()}');
} finally {
isLoadingLocations.value = false;
}
}
void _updateConnectionStatus(List<ConnectivityResult> results) {
@ -557,20 +497,17 @@ class IndexController extends GetxController with WidgetsBindingObserver {
//
Future<void> login(String email, String password) async {
try {
isLoading.value = true;
final value = await AuthService.login(email, password);
if (value.isNotEmpty && value['token'] != null) {
await changeUser(value);
await _initializeUserData();
Get.offAllNamed(AppPages.INDEX);
} else {
Get.snackbar('ログイン失敗', 'メールアドレスまたはパスワードが間違っています');
Get.snackbar('Login Failed', 'Invalid credentials');
}
} catch (e) {
print('Login error: $e');
Get.snackbar('ログイン失敗', 'エラーが発生しました。もう一度お試しください。');
} finally {
isLoading.value = false;
Get.snackbar('Login Failed', 'An error occurred. Please try again.');
}
}
@ -960,7 +897,6 @@ class IndexController extends GetxController with WidgetsBindingObserver {
return;
}
try {
// MapControllerの初期化が完了するまで待機
await waitForMapControllerReady();
@ -1043,8 +979,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
"please zoom in",
backgroundColor: Colors.yellow,
colorText: Colors.white,
icon: const Icon(Icons.assistant_photo_outlined, size: 40.0,
color: Colors.blue),
icon: const Icon(Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue),
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
);
@ -1081,10 +1016,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
// エラーが発生した場合のリトライ処理や適切なエラーメッセージの表示を行う
// 例えば、一定時間後に再度loadLocationsBound()を呼び出すなど
}
} catch (e) {
print('Error in loadLocationsBound: $e');
//Get.snackbar('エラー', '位置情報の読み込みに失敗しました。もう一度お試しください。');
}
}
// バウンドが有効かどうかを確認するヘルパーメソッド
@ -1097,44 +1029,20 @@ class IndexController extends GetxController with WidgetsBindingObserver {
bounds.southWest.latitude < bounds.northEast.latitude;
}
static const int maxRetries = 5;
int retryCount = 0;
//===Akira 追加:2024-4-6 #2800
// 要検討MapControllerの初期化が完了するまで待機していますが、タイムアウトを設定することを検討してください。
// 初期化に時間がかかりすぎる場合、ユーザーにわかりやすいメッセージを表示するようにしてください。
//
Future<void> waitForMapControllerReady() async {
if (isMapControllerReady.value) return;
//await isMapControllerReady.firstWhere((value) => value == true);
const timeout = Duration(seconds: 30);
try {
await Future.any([
mapControllerCompleter.future,
Future.delayed(timeout).then((_) => throw TimeoutException('MapController initialization timed out')),
]);
isMapControllerReady.value = true;
} catch (e) {
print('Warning waiting for MapController: $e');
// タイムアウト後もマップが機能している場合は、強制的に準備完了とマーク
if (!isMapControllerReady.value) {
isMapControllerReady.value = true;
if (!mapControllerCompleter.isCompleted) {
mapControllerCompleter.complete();
}
}
await Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 100));
return !isMapControllerReady.value;
});
}
}
//===Akira 追加:2024-4-6 #2800
void retryMapInitialization() {
// マップの再初期化ロジック
initMapController();
// 他の必要な再初期化処理
}
void setBound(LatLngBounds bounds) {
currentBound.clear();
currentBound.add(bounds);

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/drawer/drawer_page.dart';
@ -35,45 +34,22 @@ class IndexPage extends StatefulWidget {
}
class _IndexPageState extends State<IndexPage> {
final IndexController indexController = Get.find<IndexController>();
final DestinationController destinationController = Get.find<DestinationController>();
@override
void initState() {
super.initState();
_checkPermissionAndInitialize();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _ensureControllersAreInitialized();
//await PermissionController.checkAndRequestPermissions();
});
}
Future<void> _checkPermissionAndInitialize() async {
// 位置情報の許可が得られた場合の処理
await _initializeMap();
Future<void> _ensureControllersAreInitialized() async {
while (!Get.isRegistered<IndexController>() ||
!Get.isRegistered<DestinationController>() ||
!Get.isRegistered<LocationController>()) {
await Future.delayed(const Duration(milliseconds: 100));
}
Future<void> _initializeMap() async {
await indexController.loadLocations();
setState(() {}); // 状態を更新してUIを再構築
}
void _showLocationServiceDisabledOrDeniedError() {
Get.snackbar(
'エラー',
'位置情報サービスが無効か、許可されていません。設定を確認してください。',
duration: const Duration(seconds: 5),
snackPosition: SnackPosition.BOTTOM,
mainButton: TextButton(
child: const Text('設定'),
onPressed: () => Geolocator.openLocationSettings(),
),
);
}
void _showPermissionDeniedError() {
Get.snackbar(
'エラー',
'位置情報の許可が必要です。設定から許可してください。',
duration: const Duration(seconds: 5),
snackPosition: SnackPosition.BOTTOM,
);
}
void checkEventAndNavigate() async {
@ -136,11 +112,11 @@ class _IndexPageState extends State<IndexPage> {
void _showEventSelectionWarning() {
Get.dialog(
AlertDialog(
title: const Text('警告'),
content: const Text('イベントを選択してください。'),
title: Text('警告'),
content: Text('イベントを選択してください。'),
actions: [
TextButton(
child: const Text('OK'),
child: Text('OK'),
onPressed: () => Get.back(),
),
],
@ -148,27 +124,24 @@ class _IndexPageState extends State<IndexPage> {
);
}
// class IndexPage extends GetView<IndexController> {
// IndexPage({Key? key}) : super(key: key);
// IndexControllerとDestinationControllerのインスタンスを取得しています。
//
final LocationController locationController = Get.find<LocationController>();
final IndexController indexController = Get.find<IndexController>();
final DestinationController destinationController =
Get.find<DestinationController>();
// buildメソッドは、ウィジェットのUIを構築するメソッドです。
// ここでは、WillPopScopeウィジェットを使用して、端末の戻るボタンが押された際の動作を制御しています。
//
@override
Widget build(BuildContext context) {
/*
return PopScope(
canPop: false,
child: Scaffold(
*/
return Scaffold(
//
// Scaffoldウィジェットを使用して、アプリのメインページのレイアウトを構築しています。
//
@ -177,8 +150,24 @@ class _IndexPageState extends State<IndexPage> {
title: Obx(() => Text(indexController.selectedEventName.value)),
//title: Text("add_location".tr),
actions: [
// IconButton(
// onPressed: () {
// DatabaseService ds = DatabaseService();
// ds.updateDatabase();
// },
// icon: const Icon(Icons.ten_k_sharp)),
//
// AppBarには、タイトルとアクションアイコンが含まれています。
// アクションアイコンには、GPSデータの表示、履歴の表示、マップの更新、検索などの機能が含まれています。
//
IconButton(
onPressed: () async {
// GpsDatabaseHelper db = GpsDatabaseHelper.instance;
// List<GpsData> data = await db.getGPSData(
// indexController.currentUser[0]["user"]['team_name'],
// indexController.currentUser[0]["user"]["event_code"]);
// print("GPS data is ${data.length}");
Get.toNamed(AppPages.GPS);
},
icon: const Icon(Icons.telegram)),
@ -212,22 +201,102 @@ class _IndexPageState extends State<IndexPage> {
),
),
//CatWidget(indexController: indexController,),
//
// デバッグ時のみリロードボタンの横にGPS信号レベルの設定ボタンを設置し、
// タップすることでGPS信号の強弱をシミュレーションできるようにする
// Akira 2024-4-5
//
/*
Obx(() {
if (locationController.isSimulationMode) {
return DropdownButton<String>(
value: locationController.getSimulatedSignalStrength(),
onChanged: (value) {
//debugPrint("DropDown changed!");
locationController.setSimulatedSignalStrength(value!);
},
items: ['low', 'medium', 'high', 'real']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
} else {
return Container();
}
}),
*/
],
),
// bottomNavigationBar: BottomAppBar(
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: <Widget>[
// Obx(
// () => destinationController.isInRog.value == true
// ? IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.run_circle,
// size: 44,
// color: Colors.green,
// ))
// : IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.run_circle,
// size: 44,
// color: Colors.black12,
// )),
// ),
// Padding(
// padding:
// const EdgeInsets.only(right: 10.0, top: 4.0, bottom: 4.0),
// child: InkWell(
// child: Obx(() => destinationController
// .isGpsSelected.value ==
// true
// ? Padding(
// padding: const EdgeInsets.only(
// right: 10.0, top: 4.0, bottom: 4.0),
// child: InkWell(
// child: const Image(
// image:
// AssetImage('assets/images/route3_off.png'),
// width: 35,
// height: 35,
// ),
// onTap: () {
// //indexController.switchPage(AppPages.TRAVEL);
// },
// ),
// )
// : Padding(
// padding: const EdgeInsets.only(
// right: 10.0, top: 4.0, bottom: 4.0),
// child: InkWell(
// child: const Image(
// image:
// AssetImage('assets/images/route2_on.png'),
// width: 35,
// height: 35,
// ),
// onTap: () {
// //indexController.switchPage(AppPages.TRAVEL);
// },
// ),
// ))),
// ),
// ],
// ),
// ),
body: SafeArea(
child:Obx(() {
if (indexController.isLoadingLocations.value) {
return const Center(child: CircularProgressIndicator()); // Index Controller
} else {
return indexController.mode.value == 0
? const MapWidget()
: const ListWidget();
}
}),
),
//
// マップモードとリストモードを切り替えるためのボタンです。
//
floatingActionButton: FloatingActionButton(
onPressed: () {
indexController.toggleMode();
@ -243,6 +312,22 @@ class _IndexPageState extends State<IndexPage> {
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
//
// bodyには、SafeAreaウィジェットを使用して、画面の安全な領域内にUIを構築しています。
//
body: SafeArea(
child: Column(
children: [
Expanded(
child: Obx(
() => indexController.mode.value == 0
? const MapWidget()
: const ListWidget(),
))
],
),
),
),
);
}
}

View File

@ -182,7 +182,7 @@ class _LoginPageState extends State<LoginPage> {
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40)),
child: const CircularProgressIndicator(), // Login page
child: const CircularProgressIndicator(),
)
: Column(
children: [
@ -199,19 +199,24 @@ class _LoginPageState extends State<LoginPage> {
.tr,
backgroundColor: Colors.red,
colorText: Colors.white,
icon: const Icon(Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue),
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
icon: const Icon(
Icons
.assistant_photo_outlined,
size: 40.0,
color: Colors.blue),
snackPosition:
SnackPosition.TOP,
duration: const Duration(
seconds: 3),
);
return;
}
try {
//indexController.isLoading.value = true;
indexController.login(emailController.text, passwordController.text);
} catch (e) {
print('Error during login: $e');
// エラーハンドリングは IndexController 内で行われるため、ここでは何もしない
}
indexController.isLoading.value =
true;
indexController.login(
emailController.text,
passwordController.text
);
},
color: Colors.indigoAccent[400],
shape: RoundedRectangleBorder(

View File

@ -2,87 +2,23 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
class PermissionController {
static bool? _locationPermissionGranted;
/*
bool? _isRequestingPermission=false;
final Completer<PermissionController> _permissionCompleter = Completer<PermissionController>();
*/
static bool _isRequestingPermission = false;
static Completer<bool>? _permissionCompleter;
static Future<bool> checkAndRequestPermissions_new() async {
try {
if (_locationPermissionGranted != null) {
return _locationPermissionGranted!;
static Future<bool> checkAndRequestPermissions() async {
if (_isRequestingPermission) {
return _permissionCompleter!.future;
}
var serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
Get.snackbar(
'エラー', '位置情報サービスが無効です。設定から有効にしてください。');
// 位置情報サービスが無効の場合、ユーザーに有効化を促す
return false;
}
debugPrint("====> Geolocator.checkPermission");
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
Get.snackbar('エラー', '位置情報の許可が拒否されました。');
return false;
}
}
if (permission == LocationPermission.deniedForever) {
Get.snackbar('エラー',
'位置情報の許可が永久に拒否されました。設定から許可してください。');
return false;
}
_locationPermissionGranted = true;
return true;
} catch (e) {
print('Error checking permissions: $e');
Get.snackbar('エラー', '権限の確認中にエラーが発生しました。');
return false;
}
}
static void resetPermissionCache() {
_locationPermissionGranted = null;
}
static Future<bool> _requestLocationPermission() async {
BuildContext? context;
int attempts = 0;
const maxAttempts = 10;
debugPrint("====> _requestLocationPermission");
// コンテキストが利用可能になるまで待機するロジック
while (Get.context == null && attempts < maxAttempts) {
context = Get.context;
if (context == null) {
await Future.delayed(const Duration(milliseconds: 500));
attempts++;
}
}
if (Get.context == null) {
print('Context is still null after waiting, cannot proceed with permission check');
return false;
}
_isRequestingPermission = true;
_permissionCompleter = Completer<bool>();
try {
bool hasPermissions = await _checkLocationPermissions();
@ -100,16 +36,18 @@ class PermissionController {
SystemNavigator.pop();
}
}
return hasPermissions;
_isRequestingPermission = false;
_permissionCompleter!.complete(hasPermissions);
} catch (e) {
print('Error in permission request: $e');
return false;
_isRequestingPermission = false;
_permissionCompleter!.complete(false);
}
return _permissionCompleter!.future;
}
static Future<bool> _checkLocationPermissions() async {
final locationPermission = await Permission.location.status;
final whenInUsePermission = await Permission.locationWhenInUse.status;
@ -180,30 +118,20 @@ class PermissionController {
(whenInUsePermission == PermissionStatus.granted || alwaysPermission == PermissionStatus.granted);
}
static Future<bool> checkAndRequestPermissions() async {
/*
static Future<bool> checkAndRequestPermissions_old() async {
if (_isRequestingPermission) {
return _permissionCompleter!.future;
}
_isRequestingPermission = true;
_permissionCompleter = Completer<bool>();
*/
try {
if (_locationPermissionGranted != null) {
return _locationPermissionGranted!;
}
bool hasPermissions = await _checkLocationPermissions();
if (!hasPermissions) {
bool userAgreed = await showLocationDisclosure();
if (userAgreed) {
hasPermissions = await _requestAllLocationPermissions();
_locationPermissionGranted = true;
debugPrint("Finish checkAndRequestPermissions...");
return true;
} else {
print('User did not agree to location usage');
hasPermissions = false;
@ -212,18 +140,15 @@ class PermissionController {
}
}
//_isRequestingPermission = false;
//_permissionCompleter!.complete(hasPermissions);
_isRequestingPermission = false;
_permissionCompleter!.complete(hasPermissions);
} catch( e ) {
print('Error in permission request: $e');
//_isRequestingPermission = false;
//_permissionCompleter!.complete(false);
_isRequestingPermission = false;
_permissionCompleter!.complete(false);
}
debugPrint("Finish checkAndRequestPermissions...");
//return _permissionCompleter!.future;
return false;
return _permissionCompleter!.future;
}
static Future<void> requestAllLocationPermissions() async {
@ -242,14 +167,25 @@ class PermissionController {
}
static Future<bool> showLocationDisclosure() async {
if (Platform.isIOS) {
return await _showLocationDisclosureIOS();
} else if (Platform.isAndroid) {
return await _showLocationDisclosureAndroid();
} else {
// その他のプラットフォームの場合はデフォルトの処理を行う
return await _showLocationDisclosureIOS();
}
}
static Future<bool> _showLocationDisclosureIOS() async {
if (Get.context == null) {
print('Context is null, cannot show dialog');
return false;
}
//if (Get.isDialogOpen ?? false) {
// print('A dialog is already open');
// return false;
//}
if (Get.isDialogOpen ?? false) {
print('A dialog is already open');
return false;
}
try {
final result = await Get.dialog<bool>(
@ -289,6 +225,41 @@ class PermissionController {
}
}
static Future<bool> _showLocationDisclosureAndroid() async {
return await showDialog<bool>(
context: Get.overlayContext ?? Get.context ?? (throw Exception('No valid context found')),
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('位置情報の使用について'),
content: const SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('このアプリでは、以下の目的で位置情報を使用します:'),
Text('• チェックポイントの自動チェックイン(アプリが閉じているときも含む)'),
Text('• 移動履歴の記録(バックグラウンドでも継続)'),
Text('• 現在地周辺の情報表示'),
Text('\nバックグラウンドでも位置情報を継続的に取得します。'),
Text('これにより、バッテリーの消費が増加する可能性があります。'),
Text('同意しない場合には、アプリは終了します。'),
],
),
),
actions: <Widget>[
TextButton(
child: const Text('同意しない'),
onPressed: () => Navigator.of(context).pop(false),
),
TextButton(
child: const Text('同意する'),
onPressed: () => Navigator.of(context).pop(true),
),
],
);
},
) ?? false;
}
static void showPermissionDeniedDialog(String title,String message) {
Get.dialog(
AlertDialog(

View File

@ -9,12 +9,6 @@ class SettingsController extends GetxController {
var autoReturnDisabled = false.obs;
final MapResetController mapResetController = Get.put(MapResetController());
@override
void onInit() {
super.onInit();
ever(timerDuration, (_) => resetIdleTimer());
}
void updateTimerDuration(int seconds) {
timerDuration.value = Duration(seconds: seconds);
}

View File

@ -329,13 +329,6 @@ class TeamController extends GetxController {
}
List<NewCategory> getFilteredCategories_old() {
//List<User> teamMembers = getCurrentTeamMembers();
return categories.where((category) {
return isCategoryValid(category, teamMembers);
}).toList();
}
List<NewCategory> getFilteredCategories() {
if (teamMembers.isEmpty && currentUser.value != null) {
// ソロの場合
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
@ -347,6 +340,24 @@ class TeamController extends GetxController {
}
}
List<NewCategory> getFilteredCategories() {
if (teamMembers.isEmpty && currentUser.value != null) {
// ソロの場合
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else if (teamMembers.length == 1) {
// チームメンバーが1人の場合ソロ
String baseCategory = teamMembers.first.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else {
// 複数人の場合
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
}
}
bool isElementarySchoolOrYounger(User user) {
final now = DateTime.now();
final age = now.year - user.dateOfBirth!.year;
@ -379,17 +390,7 @@ class TeamController extends GetxController {
// メンバーリストの最新状態を取得
await fetchTeamMembers(selectedTeam.value!.id);
List<NewCategory> eligibleCategories = [];
if (teamMembers.isEmpty || teamMembers.length == 1) {
if (currentUser.value != null) {
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
} else {
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
List<NewCategory> eligibleCategories = getFilteredCategories();
// 同じ時間のカテゴリを優先的に選択
NewCategory? newCategory = eligibleCategories.firstWhereOrNull((c) => c.time == oldTime);

View File

@ -120,15 +120,19 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
}
final filteredCategories = controller.getFilteredCategories();
final categoriesToDisplay = filteredCategories.isEmpty
? controller.categories
: filteredCategories;
// 選択されているカテゴリが表示リストに含まれていない場合、最初の項目を選択する
if (controller.selectedCategory.value == null ||
!filteredCategories.contains(controller.selectedCategory.value)) {
controller.updateCategory(filteredCategories.isNotEmpty
? filteredCategories.first
: null);
}
// 選択されているカテゴリが表示リストに含まれていない場合、最初の項目を選択する
if (controller.selectedCategory.value == null ||
!categoriesToDisplay.contains(controller.selectedCategory.value)) {
controller.updateCategory(categoriesToDisplay.isNotEmpty
? categoriesToDisplay.first
!filteredCategories.contains(controller.selectedCategory.value)) {
controller.updateCategory(filteredCategories.isNotEmpty
? filteredCategories.first
: null);
}
@ -147,7 +151,7 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
),
),
const SizedBox(height: 16),
if (categoriesToDisplay.isEmpty)
if (filteredCategories.isEmpty)
const Text('カテゴリデータを読み込めませんでした。',
style: TextStyle(color: Colors.red))
else
@ -156,7 +160,7 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
decoration: const InputDecoration(
labelText: 'カテゴリ'),
value: controller.selectedCategory.value,
items: categoriesToDisplay.map((category) =>
items: filteredCategories.map((category) =>
DropdownMenuItem(
value: category,
child: Text(category.categoryName),

View File

@ -36,8 +36,7 @@ import 'package:gifunavi/pages/entry/event_entries_binding.dart';
import 'package:gifunavi/pages/register/user_detail_page.dart';
import 'package:gifunavi/pages/entry/event_result_page.dart';
import '../model/entry.dart';
import 'package:gifunavi/model/entry.dart';
part 'app_routes.dart';

View File

@ -61,7 +61,7 @@ class ApiService extends GetxService{
final indexController = Get.find<IndexController>();
if (indexController.currentUser.isNotEmpty) {
token = indexController.currentUser[0]['token'] ?? '';
//print("Get token = $token");
print("Get token = $token");
}else{
token = "";
}
@ -77,7 +77,7 @@ class ApiService extends GetxService{
final indexController = Get.find<IndexController>();
if (indexController.currentUser.isNotEmpty) {
token = indexController.currentUser[0]['token'] ?? '';
//print("Get token = $token");
print("Get token = $token");
}else{
token = "";
}
@ -117,6 +117,42 @@ class ApiService extends GetxService{
}
Future<List<Team>> getTeams_old() async {
init();
final token = await getToken2();
try {
final response = await http.get(
Uri.parse('$baseUrl/teams/'),
headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"},
);
if (response.statusCode == 200) {
// UTF-8でデコード
final decodedResponse = utf8.decode(response.bodyBytes);
//print('User Response body: $decodedResponse');
List<dynamic> teamsJson = json.decode(decodedResponse);
List<Team> teams = [];
for (var teamJson in teamsJson) {
//print('\nTeam Data:');
//_printDataComparison(teamJson, Team);
teams.add(Team.fromJson(teamJson));
}
return teams;
} else {
throw Exception('Failed to load teams. Status code: ${response.statusCode}');
}
} catch (e, stackTrace) {
print('Error in getTeams: $e');
print('Stack trace: $stackTrace');
rethrow;
}
}
Future<List<NewCategory>> getCategories() async {
init();
getToken();
@ -129,7 +165,7 @@ class ApiService extends GetxService{
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
//print('User Response body: $decodedResponse');
print('User Response body: $decodedResponse');
List<dynamic> categoriesJson = json.decode(decodedResponse);
List<NewCategory> categories = [];
@ -164,6 +200,26 @@ class ApiService extends GetxService{
)).then((data) => NewCategory.fromJson(data));
}
Future<NewCategory> getZekkenNumber_old(int categoryId) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/categories-viewset/$categoryId/get_zekken_number/'),
headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
final categoriesJson = json.decode(decodedResponse);
return NewCategory.fromJson(categoriesJson);
} else {
throw Exception('Failed to increment category number');
}
} catch (e) {
throw Exception('Error incrementing category number: $e');
}
}
Future<User> getCurrentUser() async {
init();
final token = getToken();
@ -268,6 +324,30 @@ class ApiService extends GetxService{
)).then((data) => Team.fromJson(data));
}
Future<Team> createTeam_old(String teamName, int categoryId) async {
init();
getToken();
final response = await http.post(
Uri.parse('$baseUrl/teams/'),
headers: {
'Authorization': 'Token $token',
"Content-Type": "application/json; charset=UTF-8",
},
body: json.encode({
'team_name': teamName,
'category': categoryId,
}),
);
if (response.statusCode == 201) {
final decodedResponse = utf8.decode(response.bodyBytes);
return Team.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to create team');
}
}
Future<Team> updateTeam(int teamId, String teamName, int categoryId) async {
final token = await getToken2();
return _handleRequest(() => http.put(
@ -277,6 +357,31 @@ class ApiService extends GetxService{
)).then((data) => Team.fromJson(data));
}
Future<Team> updateTeam_old(int teamId, String teamName, int categoryId) async {
init();
getToken();
final response = await http.put(
Uri.parse('$baseUrl/teams/$teamId/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'team_name': teamName,
'category': categoryId,
}),
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
return Team.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to update team');
}
}
Future<void> deleteTeam(int teamId) async {
final token = await getToken2();
await _handleRequest(() => http.delete(
@ -285,6 +390,24 @@ class ApiService extends GetxService{
));
}
Future<void> deleteTeamold_(int teamId) async {
init();
getToken();
final response = await http.delete(
Uri.parse('$baseUrl/teams/$teamId/'),
headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'},
);
if( response.statusCode == 400) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
throw Exception('まだメンバーが残っているので、チームを削除できません。');
}else if (response.statusCode != 204) {
throw Exception('Failed to delete team');
}
}
Future<List<User>> getTeamMembers(int teamId) async {
final token = await getToken2();
return _handleRequest(() => http.get(
@ -293,6 +416,26 @@ class ApiService extends GetxService{
)).then((data) => (data as List).map((json) => User.fromJson(json)).toList());
}
Future<List<User>> getTeamMembers_old(int teamId) async {
init();
getToken();
final response = await http.get(
Uri.parse('$baseUrl/teams/$teamId/members/'),
headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
List<dynamic> membersJson = json.decode(decodedResponse);
return membersJson.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load team members');
}
}
Future<User> createTeamMember(int teamId, String? email, String? firstname, String? lastname, DateTime? dateOfBirth, bool? female) async {
final token = await getToken2();
String? formattedDateOfBirth = dateOfBirth != null ? DateFormat('yyyy-MM-dd').format(dateOfBirth) : null;
@ -309,6 +452,45 @@ class ApiService extends GetxService{
)).then((data) => User.fromJson(data));
}
Future<User> createTeamMember_old(int teamId, String? email, String? firstname, String? lastname, DateTime? dateOfBirth,bool? female) async {
init();
getToken();
// emailが値を持っている場合の処理
if (email != null && email.isNotEmpty) {
firstname ??= "dummy";
lastname ??= "dummy";
dateOfBirth ??= DateTime.now();
female ??= false;
}
String? formattedDateOfBirth;
if (dateOfBirth != null) {
formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth);
}
final response = await http.post(
Uri.parse('$baseUrl/teams/$teamId/members/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'email': email,
'firstname': firstname,
'lastname': lastname,
'date_of_birth': formattedDateOfBirth,
'female': female,
}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final decodedResponse = utf8.decode(response.bodyBytes);
return User.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to create team member');
}
}
Future<User> updateTeamMember(int teamId, int? memberId, String firstname, String lastname, DateTime? dateOfBirth, bool? female) async {
final token = await getToken2();
@ -325,6 +507,37 @@ class ApiService extends GetxService{
)).then((data) => User.fromJson(data));
}
Future<User> updateTeamMember_old(int teamId,int? memberId, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async {
init();
getToken();
String? formattedDateOfBirth;
if (dateOfBirth != null) {
formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth);
}
final response = await http.put(
Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'firstname': firstname,
'lastname': lastname,
'date_of_birth': formattedDateOfBirth,
'female': female,
}),
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
return User.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to update team member');
}
}
Future<void> deleteTeamMember(int teamId, int memberId) async {
final token = await getToken2();
await _handleRequest(() => http.delete(
@ -333,6 +546,20 @@ class ApiService extends GetxService{
));
}
Future<void> deleteTeamMember_old(int teamId,int memberId) async {
init();
getToken();
final response = await http.delete(
Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'),
headers: {'Authorization': 'Token $token'},
);
if (response.statusCode != 204) {
throw Exception('Failed to delete team member');
}
}
Future<void> deleteAllTeamMembers(int teamId) async {
final response = await http.delete(
Uri.parse('$baseUrl/teams/$teamId/members/destroy_all/?confirm=true'),
@ -367,6 +594,24 @@ class ApiService extends GetxService{
)).then((data) => (data as List).map((json) => Entry.fromJson(json)).toList());
}
Future<List<Entry>> getEntries_old() async {
init();
getToken();
final response = await http.get(
Uri.parse('$baseUrl/entry/'),
headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
List<dynamic> entriesJson = json.decode(decodedResponse);
return entriesJson.map((json) => Entry.fromJson(json)).toList();
} else {
throw Exception('Failed to load entries');
}
}
Future<List<Entry>> getTeamEntries(int teamId) async {
try {
@ -387,21 +632,6 @@ class ApiService extends GetxService{
}
}
Future<Entry> updateEntryStatus(int entryId, bool hasParticipated, bool hasGoaled) async {
final token = await getToken2();
return _handleRequest(() => http.patch(
Uri.parse('$baseUrl/entries/$entryId/update-status/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8'
},
body: json.encode({
'hasParticipated': hasParticipated,
'hasGoaled': hasGoaled,
}),
)).then((data) => Entry.fromJson(data));
}
Future<List<Event>> getEvents() async {
init();
getToken();
@ -439,6 +669,39 @@ class ApiService extends GetxService{
)).then((data) => Entry.fromJson(data));
}
Future<Entry> createEntry_old(int teamId, int eventId, int categoryId, DateTime date,String zekkenNumber) async {
init();
getToken();
String? formattedDate;
formattedDate = DateFormat('yyyy-MM-dd').format(date);
final response = await http.post(
Uri.parse('$baseUrl/entry/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'team': teamId,
'event': eventId,
'category': categoryId,
'date': formattedDate,
'zekken_number':zekkenNumber,
}),
);
if (response.statusCode == 201) {
final decodedResponse = utf8.decode(response.bodyBytes);
return Entry.fromJson(json.decode(decodedResponse));
} else {
final decodedResponse = utf8.decode(response.bodyBytes);
print("decodedResponse = $decodedResponse");
final errorInfo = json.decode(decodedResponse);
throw Exception(errorInfo['error']);
}
}
Future<void> updateUserInfo(int userId, Entry entry) async {
final token = await getToken2();
@ -456,6 +719,40 @@ class ApiService extends GetxService{
));
}
Future<void> updateUserInfo_old(int userId, Entry entry) async {
init();
getToken();
final entryId = entry.id;
DateTime? date = entry.date;
String? formattedDate;
formattedDate = DateFormat('yyyy-MM-dd').format(date!);
final response = await http.put(
Uri.parse('$baseUrl/userinfo/$userId/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'zekken_number': entry.zekkenNumber,
'event_code': entry.event.eventName,
'group': entry.team.category.categoryName,
'team_name': entry.team.teamName,
'date': formattedDate,
}),
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
final updatedUserInfo = json.decode(decodedResponse);
//Get.find<IndexController>().updateUserInfo(updatedUserInfo);
} else {
throw Exception('Failed to update entry');
}
}
Future<Entry> updateEntry(int entryId, int teamId, int eventId, int categoryId, DateTime date,int zekkenNumber) async {
init();

View File

@ -87,7 +87,8 @@ class AuthService {
return changePassword;
}
static Future<Map<String, dynamic>> login(String email, String password) async {
static Future<Map<String, dynamic>> login(
String email, String password) async {
//print("------- in logged email $email pwd $password ###### --------");
Map<String, dynamic> cats = {};
String serverUrl = ConstValues.currentServer();
@ -115,12 +116,17 @@ class AuthService {
var errorBody = json.decode(utf8.decode(response.bodyBytes));
errorMessage = errorBody['non_field_errors']?[0] ?? 'パスワードが正しくありません。';
}
throw Exception(errorMessage);
Get.snackbar(
"エラー",
errorMessage,
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
);
cats = {};
}
} catch( e ){
print('Error in login: $e');
throw e; // エラーを上位に伝播させる
/*
print('Error in login: $e');
Get.snackbar("通信エラーがおきました", "サーバーと通信できませんでした",
backgroundColor: Colors.red,
@ -136,8 +142,7 @@ class AuthService {
duration: const Duration(seconds: 3),
//backgroundColor: Colors.yellow,
);
*/
//cats = {};
cats = {};
}
return cats;
}
@ -166,7 +171,7 @@ class AuthService {
Map<String, dynamic> cats = {};
String serverUrl = ConstValues.currentServer();
String url = '$serverUrl/api/register/';
//debugPrint('++++++++$url');
debugPrint('++++++++$url');
final http.Response response = await http.post(
Uri.parse(url),
headers: <String, String>{

View File

@ -1,8 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/services/api_service.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:gifunavi/model/rog.dart';
@ -35,8 +33,6 @@ class ExternalService {
Future<Map<String, dynamic>> startRogaining() async {
final IndexController indexController = Get.find<IndexController>();
//final TeamController teamController = Get.find<TeamController>();
final ApiService apiService = Get.find<ApiService>();
final EntryController entryController = Get.find<EntryController>();
debugPrint("== startRogaining ==");
@ -72,21 +68,9 @@ class ExternalService {
} else {
debugPrint("== startRogaining processing==");
try {
// 新しい API 呼び出しを使用
final updatedEntry = await apiService.updateEntryStatus(
entryController.currentEntry.value!.id!,
true, // hasParticipated を true に設定
false // hasGoaled は変更しない
);
// start_rogaining を本サーバーに送る。 TODO
// // 'has_participated': true ... Server 側の修正が必要 TODO
String serverUrl = ConstValues.currentServer();
String url = '$serverUrl/gifuroge/start_from_rogapp';
//print('++++++++$url');
print('++++++++$url');
final http.Response response = await http.post(
Uri.parse(url),
headers: <String, String>{
@ -94,7 +78,6 @@ class ExternalService {
},
body: jsonEncode(
<String, String>{'team_name': team, 'event_code': eventCode}),
// 'has_participated': true ... Server 側の修正が必要 TODO
);
print("---- start rogianing api status ---- ${response.statusCode}");
@ -102,12 +85,6 @@ class ExternalService {
if (response.statusCode == 200) {
res = json.decode(utf8.decode(response.bodyBytes));
//print('----_res : $res ----');
// ローカルのユーザーデータを更新
indexController.updateUserParticipationStatus(true);
}
}catch (e) {
print('Error in startRogaining: $e');
Get.snackbar('エラー', 'ロゲイニングの開始に失敗しました');
}
}
return res;
@ -249,8 +226,6 @@ class ExternalService {
final IndexController indexController = Get.find<IndexController>();
final DestinationController destinationController =
Get.find<DestinationController>();
final ApiService apiService = Get.find<ApiService>();
final EntryController entryController = Get.find<EntryController>();
// チームIDを取得
@ -278,14 +253,6 @@ class ExternalService {
final im1Bytes = File(image).readAsBytesSync();
String im1_64 = base64Encode(im1Bytes);
try {
// 新しい API呼び出しを使用
final updatedEntry = await apiService.updateEntryStatus(
entryController.currentEntry.value!.id!,
true, // hasParticipated を true に設定
true // hasGoaled を true に設定
);
final http.Response response = await http.post(
Uri.parse(url1),
headers: <String, String>{
@ -308,8 +275,7 @@ class ExternalService {
//print('++++++++$url');
if (response.statusCode == 201) {
try {
Map<String, dynamic> res = json.decode(
utf8.decode(response.bodyBytes));
Map<String, dynamic> res = json.decode(utf8.decode(response.bodyBytes));
// print('----_res : $res ----');
// print('---- image url ${res["goalimage"]} ----');
final http.Response response2 = await http.post(
@ -323,7 +289,6 @@ class ExternalService {
'goal_time': goalTime,
'image': res["goalimage"].toString().replaceAll(
'http://localhost:8100', serverUrl)
// 'has_goaled': true ... サーバー側の修正が必要 TODO
//'http://rogaining.sumasen.net')
}),
);
@ -340,21 +305,13 @@ class ExternalService {
//print('----- response2 is $response2 --------');
if (response2.statusCode == 200) {
res2 = json.decode(utf8.decode(response2.bodyBytes));
// ローカルのユーザーデータを更新
indexController.updateUserGoalStatus(true);
} else {
res2 = json.decode(utf8.decode(response2.bodyBytes));
// ここはどうする? TODO
}
} catch(e){
print( "Error {$e}" );
}
}
}catch(e) {
print("Error in makeGoal: $e");
Get.snackbar('エラー', 'ゴール処理に失敗しました');
}
//}
destinationController.resetRogaining(isgoal: true);
return res2;
@ -413,6 +370,15 @@ class ExternalService {
//int userId = indexController.currentUser[0]["user"]["id"];
//print("--- Pressed -----");
if( indexController.currentUser[0]["user"]==null ){
return Future.value(false);
}
if( indexController.currentUser[0]["user"]['team_name']==null ){
return Future.value(false);
}
if( indexController.currentUser[0]["user"]["event_code"]==null ){
return Future.value(false);
}
String team = indexController.currentUser[0]["user"]['team_name'];
//print("--- _team : ${_team}-----");
String eventCode = indexController.currentUser[0]["user"]["event_code"];

View File

@ -17,19 +17,6 @@ class MotionService {
}
}
Future<dynamic> _handleMotionData(MethodCall call) async {
switch (call.method) {
case 'onMotionData':
final Map<String, dynamic> motionData = call.arguments;
// ここでモーションデータを処理します
print('Received motion data: $motionData');
// 例: データを状態管理システムに渡す、UIを更新する等
break;
default:
print('Unknown method ${call.method}');
}
}
static Future<void> stopMotionUpdates() async {
if (Platform.isIOS) {
try {

View File

@ -32,8 +32,6 @@ class LocationController extends GetxController {
// Reactive variable to hold the current position
Rx<Position?> currentPosition = Rx<Position?>(null);
// 現在の位置情報を保持するReactive変数です。Rx<Position?>型で宣言されています。
final locationMarkerPositionStreamController = StreamController<LocationMarkerPosition?>.broadcast();
// Subscription to the position stream
StreamSubscription<Position>? positionStream;
@ -156,8 +154,8 @@ class LocationController extends GetxController {
//===== Akira Added 2024-4-9 end
//final locationMarkerPositionStreamController =
// StreamController<LocationMarkerPosition?>.broadcast();
final locationMarkerPositionStreamController =
StreamController<LocationMarkerPosition?>.broadcast();
// 位置マーカーの位置情報を送信するためのStreamControllerです。
// StreamController<LocationMarkerPosition?>型で宣言されています。
@ -204,7 +202,6 @@ class LocationController extends GetxController {
}
startPositionStream();
debugPrint("Started startPositionStream");
} catch( e ){
print('Error initializing location service: $e');
}
@ -238,12 +235,7 @@ class LocationController extends GetxController {
accuracy: position.accuracy,
),
);
},
onError: (error) {
debugPrint('Error in Geolocator stream: $error');
locationMarkerPositionStreamController.addError(error);
},
);
});
}
void startPositionStream_old() async {

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CustomErrorWidget extends StatelessWidget {
final String errorMessage;
final VoidCallback onRetry;
const CustomErrorWidget({
Key? key,
required this.errorMessage,
required this.onRetry,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'エラーが発生しました',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(errorMessage),
const SizedBox(height: 16),
ElevatedButton(
onPressed: onRetry,
child: const Text('再試行'),
),
],
),
);
}
}

View File

@ -1,87 +1,385 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/utils/location_controller.dart';
import 'package:gifunavi/pages/permission/permission.dart';
import 'package:gifunavi/pages/settings/settings_binding.dart';
import 'package:gifunavi/model/destination.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/settings/settings_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/utils/database_helper.dart';
import 'package:gifunavi/utils/location_controller.dart';
import 'package:gifunavi/utils/text_util.dart';
import 'package:gifunavi/widgets/base_layer_widget.dart';
import 'package:gifunavi/widgets/game_state_view.dart';
import 'package:gifunavi/widgets/bottom_sheet_new.dart';
import 'package:gifunavi/widgets/current_position_widget.dart';
import 'package:gifunavi/widgets/game_state_view.dart';
import 'package:gifunavi/pages/settings/settings_controller.dart';
class MapWidget extends StatelessWidget {
class MapResetController {
void Function()? resetIdleTimer;
}
class MapWidget extends StatefulWidget {
const MapWidget({super.key});
@override
State<MapWidget> createState() => _MapWidgetState();
}
class _MapWidgetState extends State<MapWidget> with WidgetsBindingObserver {
//class _MapWidgetState extends State<MapWidget> {
final IndexController indexController = Get.find<IndexController>();
final DestinationController destinationController = Get.find<DestinationController>();
final DestinationController destinationController =
Get.find<DestinationController>();
final LocationController locationController = Get.find<LocationController>();
final SettingsController settingsController = Get.find<SettingsController>();
MapWidget({Key? key}) : super(key: key) {
_initializeControllers();
}
late MapController mapController;
final Completer<MapController> mapControllerCompleter = Completer<MapController>();
void _initializeControllers() {
indexController.initMapController();
StreamSubscription? subscription;
Timer? _timer;
bool curr_marker_display = false;
final Map<LatLng, Marker> _markerCache = {};
List<Marker> _markers = [];
@override
void initState() {
super.initState();
// 追加
/*
WidgetsBinding.instance.addPostFrameCallback((_) {
PermissionController.checkAndRequestPermissions();
});
*/
debugPrint('MapWidget: initState called');
SettingsBinding().dependencies(); // これを追加
_startIdleTimer();
mapController = MapController();
indexController.mapController = mapController;
// added by Akira
WidgetsBinding.instance.addObserver(this);
_startIdleTimer();
ever(indexController.isMapControllerReady, (_) {
if (indexController.isMapControllerReady.value) {
_initMarkers();
// マップの操作イベントをリッスンして、_resetTimerを呼び出す
mapController.mapEventStream.listen((MapEvent mapEvent) {
if (mapEvent is MapEventMove || mapEvent is MapEventFlingAnimation) {
_resetTimer();
}
});
// MapControllerの初期化が完了するまで待機
WidgetsBinding.instance.addPostFrameCallback((_) {
debugPrint("MapControllerの初期化が完了");
setState(() {
indexController.isMapControllerReady.value = true;
});
// MapControllerの初期化が完了したら、IndexControllerのonInitを呼び出す
//indexController.checkPermission();
//PermissionController.checkAndRequestPermissions();
});
late MapResetController mapResetController = MapResetController();
mapResetController.resetIdleTimer = _resetIdleTimer;
Get.put(mapResetController);
// Add this debug subscription
subscription = locationController.locationMarkerPositionStreamController.stream.listen(
(LocationMarkerPosition? position) {
if (position != null) {
//debugPrint('Location update received: lat=${position.latitude}, lon=${position.longitude}');
} else {
debugPrint('Received null location update');
}
},
onError: (error) {
debugPrint('Error in location stream: $error');
},
onDone: () {
debugPrint('Location stream closed');
},
);
// indexController.mapController = MapController(initCompleter: mapControllerCompleter);
}
void _startIdleTimer() {
if (!settingsController.autoReturnDisabled.value) {
Future.delayed(settingsController.timerDuration.value, _centerMapOnUser);
}
}
void _centerMapOnUser() {
destinationController.centerMapToCurrentLocation();
void _resetIdleTimer() {
debugPrint("_resetIdleTimer...");
_timer?.cancel();
_startIdleTimer();
}
@override
void dispose() {
debugPrint('MapWidget: dispose called');
WidgetsBinding.instance.removeObserver(this); // added
mapController.dispose();
_timer?.cancel();
super.dispose();
}
// added by Akira
/*
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
debugPrint("MapWidget:didChangeAppLifecycleState...state=${state}");
if (state == AppLifecycleState.resumed) {
_resetTimer();
}
}
*/
// _centerMapOnUser を10秒間でコール
Future<void> _startIdleTimer() async {
//debugPrint("_startIdleTimer ....");
SettingsController settingsController;
// SettingsControllerが利用可能になるまで待機
while (true) {
try {
settingsController = Get.find<SettingsController>();
break; // SettingsControllerが見つかったらループを抜ける
} catch (e) {
// SettingsControllerがまだ利用可能でない場合は少し待ってから再試行
await Future.delayed(const Duration(milliseconds: 100));
}
}
if (!settingsController.autoReturnDisabled.value) {
_timer = Timer(settingsController.timerDuration.value, _centerMapOnUser);
}
}
// タイマーをリセットして_startIdleTimer をコール
void _resetTimer() {
//debugPrint("_resetTimer ....");
_timer?.cancel();
_startIdleTimer();
}
// マッぷを現在位置を中心にする。
void _centerMapOnUser() {
//debugPrint("_centerMapOnUser ....");
if (mounted) {
//debugPrint("_centerMapOnUser => centering ....");
destinationController.centerMapToCurrentLocation();
}
}
Future<void> _initMarkers() async {
indexController.markers.value = await _getMarkers();
List<Marker> markers = await _getMarkers();
setState(() {
_markers = markers;
});
}
Future<List<Marker>> _getMarkers() async {
debugPrint('Getting markers...');
if (indexController.isLoadingLocations.value) {
await indexController.waitForLocationsToLoad();
await Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 100));
return indexController.isLoadingLocations.value;
});
}
debugPrint('Getting markers...');
List<Marker> markers = [];
if (indexController.locations.isNotEmpty && indexController.locations[0].features.isNotEmpty) {
for (var feature in indexController.locations[0].features) {
// マーカーの作成ロジック
// 実際のマーカー作成ロジックをここに実装してください
GeoJSONMultiPoint point = feature!.geometry as GeoJSONMultiPoint;
LatLng latLng = LatLng(point.coordinates[0][1], point.coordinates[0][0]);
markers.add(Marker(
point: latLng,
width: 30.0,
height: 30.0,
child: getMarkerShape(feature),
));
}
}else{
debugPrint('No locations or features available');
}
return markers;
}
// Widget getMarkerShape(GeoJSONFeature i, BuildContext context) {
Widget getMarkerShape(GeoJSONFeature i) {
GeoJSONMultiPoint p = i.geometry as GeoJSONMultiPoint;
return InkWell(
onTap: () {
GeoJSONFeature? fs = indexController.getFeatureForLatLong(
p.coordinates[0][1], p.coordinates[0][0]);
if (fs != null) {
indexController.currentFeature.clear();
indexController.currentFeature.add(fs);
Destination des = destinationController.festuretoDestination(fs);
DatabaseHelper db = DatabaseHelper.instance;
db.getDestinationByLatLon(des.lat!, des.lon!).then((value) {
destinationController.shouldShowBottomSheet = false;
showModalBottomSheet(
constraints:
BoxConstraints.loose(Size(Get.width, Get.height * 0.85)),
context: context,
isScrollControlled: true,
isDismissible: true,
builder: ((context) => BottomSheetNew(
destination: des, isAlreadyCheckedIn: value.isNotEmpty)),
).whenComplete(() {
destinationController.shouldShowBottomSheet = true;
destinationController.skipGps = false;
});
});
}
},
child: Stack(
fit: StackFit.expand,
children: [
Container( // マーカー
height: 32,
width: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
border: Border.all(
color: i.properties!['buy_point'] > 0
? Colors.blue
: Colors.red,
width: 3,
style: BorderStyle.solid,
),
),
child: Stack(
alignment: Alignment.center,
children: [
const Icon(
Icons.circle,
size: 6.0,
),
i.properties!['cp'] <= 0 ? Transform.translate
(
offset: const Offset(-3, 0), //-3
child: Transform.rotate(
alignment: Alignment.centerLeft,
origin: Offset.fromDirection(1, 26),
angle: 270 * pi / 180,
child: const Icon(
Icons.play_arrow_outlined,
color: Colors.red,
size: 70,
)),
)
: Container(
color: Colors.transparent,
),
],
),
),
Transform.translate(
offset: const Offset(30, 0), // 30,0
child: Align(
alignment: Alignment.center,
child: Container (
//width: 80, // 幅を指定
//height: 60, // 40
//color: Colors.purple.withOpacity(0.2),
color: Colors.transparent,
//child: Text(' ').
//constraints: const BoxConstraints(maxWidth: 60.0), // 最大幅を設定
//constraints: BoxConstraints(maxWidth: maxWidth), // 最大幅を設定
//color: Colors.purple.withOpacity(0.2),
child: Stack(
children: <Widget>[
Text( // アウトライン
TextUtils.getDisplayTextFeture(i),
style: TextStyle(
fontSize: 16, // 16
fontWeight: FontWeight.w700,
overflow: TextOverflow.visible,
//height: 1.2,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1 // 2
..color = Colors.white,
),
maxLines: 1, // テキストを1行に制限
softWrap: false, // テキストの折り返しを無効化
),
Text( // テキスト
TextUtils.getDisplayTextFeture(i),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
overflow: TextOverflow.visible,
//fontWeight: FontWeight.bold,
//height: 1.2,
color: Colors.black,
),
maxLines: 1, // テキストを1行に制限
softWrap: false, // テキストの折り返しを無効化
),
],
),
),
),
)
],
),
);
}
List<LatLng>? getPoints() {
return indexController.routePoints.map((p) => LatLng(p.latitude, p.longitude)).toList();
List<LatLng> pts = [];
for (PointLatLng p in indexController.routePoints) {
LatLng l = LatLng(p.latitude, p.longitude);
pts.add(l);
}
return pts;
}
@override
Widget build(BuildContext context) {
final settingsController = Get.find<SettingsController>(); // これを追加
//final PopupController popupController = PopupController();
return Stack(
children: [
Obx(() => indexController.isLoading.value
? const Center(child: CircularProgressIndicator())
Obx(() => indexController.isLoading.value == true
? const Padding(
padding: EdgeInsets.only(top: 60.0),
child: CircularProgressIndicator(),
)
: FlutterMap(
mapController: indexController.mapController,
mapController: mapController,
//mapController: indexController.mapController,
options: MapOptions(
maxZoom: 18.4,
onMapReady: () {
indexController.isMapControllerReady.value = true;
_initMarkers();
//indexController.isMapControllerReady.value = true;
},
initialCenter: const LatLng(37.15319600454702, 139.58765950528198),
initialCenter:
const LatLng(37.15319600454702, 139.58765950528198),
bounds: indexController.currentBound.isNotEmpty
? indexController.currentBound[0]
: LatLngBounds.fromPoints([
@ -89,22 +387,27 @@ class MapWidget extends StatelessWidget {
const LatLng(36.642756778706904, 137.95226720406063)
]),
initialZoom: 1,
interactiveFlags: InteractiveFlag.pinchZoom | InteractiveFlag.drag,
onPositionChanged: (MapPosition pos, bool hasGesture) {
interactiveFlags:
InteractiveFlag.pinchZoom | InteractiveFlag.drag,
onPositionChanged: (MapPosition pos, hasGesture) {
if (hasGesture) {
_startIdleTimer();
_resetTimer();
}
indexController.currentBound = [pos.bounds!];
},
onMapEvent: (MapEvent mapEvent) {
//debugPrint('Map event: ${mapEvent.runtimeType}');
if (mapEvent is MapEventMove) {
destinationController.shouldShowBottomSheet = true;
}
},
//onTap: (_, __) => popupController.hideAllPopups(),
),
children: [
const BaseLayer(),
Obx(() => indexController.routePointLenght > 0
// ルートのポリライン表示
Obx(
() => indexController.routePointLenght > 0
? PolylineLayer(
polylines: [
Polyline(
@ -114,9 +417,13 @@ class MapWidget extends StatelessWidget {
),
],
)
: Container()),
: Container(),
),
// 現在位置のマーカー
CurrentLocationLayer(
positionStream: locationController.locationMarkerPositionStream,
positionStream: locationController
.locationMarkerPositionStreamController.stream,
//alignDirectionOnUpdate: AlignOnUpdate.never,
style: const LocationMarkerStyle(
marker: Stack(
children: [
@ -130,24 +437,40 @@ class MapWidget extends StatelessWidget {
markerSize: Size(27, 27),
markerDirection: MarkerDirection.heading,
),
//child: const Icon(Icons.navigation),
),
Obx(() => MarkerLayer(markers: indexController.markers)),
FutureBuilder<List<Marker>>(
future: indexController.locations.isNotEmpty ? _getMarkers() : null,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return const Center(child: Text('マーカーの読み込みに失敗しました'));
} else {
return MarkerLayer(markers: snapshot.data ?? []);
}
},
),
//MarkerLayer(markers: indexController.locations.isNotEmpty ? _getMarkers() : []),
],
)),
const Positioned(top: 0, left: 0, child: GameStateWidget()),
const Positioned(bottom: 10, right: 10, child: CurrentPosition()),
Obx(() => indexController.currentMarkerPosition.value != null
? Container() // 現在のマーカー位置が更新されたときの処理
: Container()),
StreamBuilder<LocationMarkerPosition?>(
stream: locationController.locationMarkerPositionStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
//debugPrint("====== Not display current marker");
curr_marker_display = true;
}else if(curr_marker_display){
debugPrint("====== Displayed current marker");
curr_marker_display = false;
}
return Container();
},
)
],
);
}
}
class MapResetController extends GetxController {
void Function()? resetIdleTimer;
void setResetIdleTimer(void Function() resetFunction) {
resetIdleTimer = resetFunction;
}
}

View File

@ -14,6 +14,9 @@ class _PermissionHandlerScreenState extends State<PermissionHandlerScreen> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await PermissionController.checkAndRequestPermissions();
});
}
@override

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 4.8.19+499
version: 4.8.20+500
environment:
sdk: ^3.5.0