import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:rogapp/model/auth_user.dart'; import 'package:rogapp/nrog/pages/home_page.dart'; import 'package:rogapp/provider/auth_provider.dart'; import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/widgets/c_form_text_field.dart'; import 'package:rogapp/widgets/c_password_text_filed.dart'; class AuthPage extends ConsumerStatefulWidget { const AuthPage({super.key}); @override ConsumerState createState() => _AuthPageState(); } class _AuthPageState extends ConsumerState { final _formkey = GlobalKey(); final FocusNode focusEmail = FocusNode(); final FocusNode focusPwd = FocusNode(); var _authMode = 'login'; bool _isLoginProgress = false; final TextEditingController _emailTextEditingController = TextEditingController(); final TextEditingController _passwordTextEditingController = TextEditingController(); @override void initState() { super.initState(); _emailTextEditingController.addListener(() => setState(() {})); WidgetsBinding.instance.addPostFrameCallback((_) { checkUser(); }); } void _submit() async { setState(() { _isLoginProgress = true; }); if (_formkey.currentState!.validate()) { AuthService authService = AuthService(); AuthUser? user = await authService.userLogin( _emailTextEditingController.text, _passwordTextEditingController.text); if (user != null) { setState(() { ref.read(authUserStateProvider.notifier).addLogin(user); }); } } setState(() { _isLoginProgress = false; }); } Future _submitToken(String token) async { setState(() { _isLoginProgress = true; }); AuthService authService = AuthService(); AuthUser? user = await authService.userFromToken(token); //////////////print("---user is ${user} ---"); if (user != null) { setState(() { _isLoginProgress = false; ref.read(authUserStateProvider.notifier).addLogin(user); }); } else {} } void checkUser() async { String? token = await ref.read(authUserStateProvider.notifier).tokenFromDevice(); //////////////print("--- red token is ${token} ---"); if (token != null) { await _submitToken(token); final id = ref.read(authUserStateProvider).id; if (id != null) { if (context.mounted) { Navigator.of(context) .push(MaterialPageRoute(builder: (ctx) => const HomePage())); } } } return; } @override Widget build(BuildContext context) { if (ref.read(authUserStateProvider).id != null) { Navigator.of(context) .push(MaterialPageRoute(builder: (ctx) => const HomePage())); } return Scaffold( resizeToAvoidBottomInset: true, body: KeyboardDismisser( gestures: const [ GestureType.onTap, //GestureType.onVerticalDragDown ], child: Center( child: SizedBox( child: Stack( clipBehavior: Clip.none, children: [ buildAuthCard(), buildLogo(), ], ), )), ), ); } Positioned buildLogo() { return Positioned( top: -170, left: MediaQuery.of(context).size.width / 2 - 100, child: Center( child: Container( alignment: Alignment.center, width: 200, height: 200, decoration: const BoxDecoration( shape: BoxShape.circle, image: DecorationImage( image: AssetImage('assets/images/appicon.png'), fit: BoxFit.fill), ), ), ), ); } Widget buildAuthCard() { return Card( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, children: [ Form( key: _formkey, child: Padding( padding: const EdgeInsets.only( top: 40, bottom: 10, left: 12, right: 12), child: Column( children: [ Padding( padding: const EdgeInsets.all(10.0), child: CFormTextField( cFocus: focusEmail, cController: _emailTextEditingController), ), Padding( padding: const EdgeInsets.all(10), child: CPasswordTextField( cController: _passwordTextEditingController, cFocusNode: focusPwd, ), ), const SizedBox( height: 12, ), buildControlls(), // SizedBox(height: MediaQuery.of(context).viewInsets.bottom,) ], ), ), ), ], ), ); } Widget buildControlls() { if (_isLoginProgress) { return const Center( child: CircularProgressIndicator(), ); } final usr = ref.read(authUserStateProvider); return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( padding: const EdgeInsets.only(right: 12.0), child: ElevatedButton( onPressed: _submit, child: Text(_authMode == "login" ? "Submit" : "Register", style: GoogleFonts.lato( color: Theme.of(context).colorScheme.secondary))), ) ], ), TextButton( onPressed: () { setState(() { if (_authMode == 'login') { _authMode = 'register'; } else { _authMode = 'login'; } }); }, child: Text( _authMode == "login" ? "${usr.id} Dont have account, please Register" : "Already Registered, Login", style: GoogleFonts.lato( color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline, fontSize: 16, fontWeight: FontWeight.bold), ), ), ], ); } } import 'dart:io'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:rogapp/nrog/pages/auth_page.dart'; class PermissionPage extends StatefulWidget { const PermissionPage({super.key}); @override State createState() => _PermissionPageState(); } class _PermissionPageState extends State { bool hasNavigated = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _checkPermissionStatus(); }); } _checkPermissionStatus() async { PermissionStatus status = await Permission.location.status; if (status.isGranted == false) { if (context.mounted) { showAlert(context); } } else if (status.isPermanentlyDenied) { await requestPermission(); } else { if (mounted) { Navigator.of(context) .push(MaterialPageRoute(builder: (_) => const AuthPage())); } } } @override Widget build(BuildContext context) { return const Scaffold( body: Text(""), ); } void showAlert(BuildContext context) { showDialog( context: context, builder: (_) => AlertDialog( title: const Text('ロケーション許可'), content: const SingleChildScrollView( child: ListBody( children: [ Text('このアプリでは、位置情報の収集を行います。'), Text( '岐阜ナビアプリではチェックポイントの自動チェックインの機能を可能にするために、現在地のデータが収集されます。アプリを閉じている時や、使用していないときにも収集されます。位置情報は、個人を特定できない統計的な情報として、ユーザーの個人情報とは一切結びつかない形で送信されます。お知らせの配信、位置情報の利用を許可しない場合は、この後表示されるダイアログで「許可しない」を選択してください。'), ], ), ), actions: [ ElevatedButton( child: const Text('OK'), onPressed: () { requestPermission(); }, ), ], )); } Future requestPermission() async { PermissionStatus permission = await Permission.location.status; if (permission == PermissionStatus.permanentlyDenied) { showPermanentAlert(); } else { PermissionStatus newPermission = await Permission.location.request(); if (newPermission != PermissionStatus.granted) { exit(0); } else { if (context.mounted) { Navigator.of(context) .push(MaterialPageRoute(builder: (_) => const AuthPage())); } } } } void showPermanentAlert() { showDialog( context: context, builder: (_) => AlertDialog( title: const Text('無効'), content: const SingleChildScrollView( child: ListBody( children: [ Text('位置情報が無効になっています'), Text( 'このアプリケーションへの位置情報アクセスが無効になっています。続行するには設定>プライバシーとセキュリティ>位置情報サービス>岐阜ナビ で有効にしてください。'), ], ), ), actions: [ ElevatedButton( child: const Text('OK'), onPressed: () async { await openAppSettings().then( (value) async { if (value) { if (await Permission .location.status.isPermanentlyDenied == true && await Permission.location.status.isGranted == false) { requestPermission(); /* opens app settings until permission is granted */ } } }, ); }, ), ], )); } } import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; import 'package:rogapp/provider/map_state_provider.dart'; import 'package:rogapp/widgets/base_layer_widget.dart'; class HomePage extends ConsumerStatefulWidget { const HomePage({super.key}); @override ConsumerState createState() => _HomePageState(); } class _HomePageState extends ConsumerState { StreamSubscription? subscription; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { final mapStateInstance = ref.watch(mapStateNotifierProvider); return Scaffold( //drawer: DrawerPage(), appBar: AppBar( title: const Text("Rogaining"), actions: [ IconButton( onPressed: () { //Get.toNamed(AppPages.HISTORY); }, icon: const Icon(Icons.history)), IconButton( onPressed: () { // final tk = indexController.currentUser[0]["token"]; // if (tk != null) { // destinationController.fixMapBound(tk); // } }, icon: const Icon(Icons.refresh)), InkWell( onTap: () { //Get.toNamed(AppPages.SEARCH); }, child: Container( height: 32, width: 75, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(25), ), child: const Center( child: Icon(Icons.search), ), ), ), //CatWidget(indexController: indexController,), ], ), body: Center( child: FlutterMap( mapController: mapStateInstance.mapController, options: MapOptions( maxZoom: 18.4, onMapReady: () { // indexController.is_mapController_loaded.value = true; subscription = mapStateInstance.mapController?.mapEventStream .listen((MapEvent mapEvent) { if (mapEvent is MapEventMoveStart) { //print(DateTime.now().toString() + ' [MapEventMoveStart] START'); // do something } // if (mapEvent is MapEventMoveEnd && // indexController.currentUser.isEmpty) { //print(DateTime.now().toString() + ' [MapEventMoveStart] END'); // indexController.loadLocationsBound(); //indexController.rogMapController!.move(c.center, c.zoom); // } }); }, center: LatLng(37.15319600454702, 139.58765950528198), //bounds: zoom: 18, interactiveFlags: InteractiveFlag.pinchZoom | InteractiveFlag.drag, onPositionChanged: (MapPosition pos, isvalue) { //indexController.currentBound = [pos.bounds!]; }, // onTap: (_, __) => popupController // .hideAllPopups(), // Hide popup when the map is tapped. ), children: [ const BaseLayer(), CurrentLocationLayer( followOnLocationUpdate: FollowOnLocationUpdate.once, turnOnHeadingUpdate: TurnOnHeadingUpdate.never, style: const LocationMarkerStyle( marker: DefaultLocationMarker( child: Icon( Icons.navigation, color: Colors.yellowAccent, ), ), markerSize: Size(27, 27), markerDirection: MarkerDirection.heading, ), ), const MarkerLayer(markers: []) ], ), ), ); } } import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rogapp/model/auth_user.dart'; import 'package:shared_preferences/shared_preferences.dart'; final authUserStateProvider = StateNotifierProvider((ref) { return AuthUserState(); }); class AuthUserState extends StateNotifier { AuthUserState() : super(AuthUser()); Future saveToDevice(String val) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString("user_token", val); } Future tokenFromDevice() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString("user_token"); } Future addLogin(AuthUser user) async { state = user; await saveToDevice(user.auth_token!); } void removeLogin() { state = AuthUser(); } } import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rogapp/model/map_state_instance.dart'; final mapStateNotifierProvider = StateNotifierProvider((ref) { return MapState(); }); class MapState extends StateNotifier { MapState() : super(MapStateInstance()); @override MapStateInstance get state => super.state; void startGame(MapStateInstance mi) { state = mi; } } import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rogapp/model/game_state_instance.dart'; final gameStateNotifierProvider = StateNotifierProvider((ref) { return GameStaticState(); }); class GameStaticState extends StateNotifier { GameStaticState() : super(GameInsStatetance()); @override GameInsStatetance get state => super.state; void startGame(GameInsStatetance gi) { state = gi; } void doCheckin() {} void makeGoal() {} } import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:rogapp/model/destination.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import '../model/rog.dart'; class DatabaseHelper { DatabaseHelper._privateConstructor(); static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); static Database? _database; Future get database async => _database ??= await _initDatabase(); Future _initDatabase() async { Directory documentDirectory = await getApplicationDocumentsDirectory(); String path = join(documentDirectory.path, 'rog.db'); // return await openDatabase( // path, // version: 1, // onCreate: _onCreate, // ); return openDatabase( join( await getDatabasesPath(), 'rog.db', ), version: 1, onCreate: _onCreate); } Future _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE destination( location_id INTEGER PRIMARY KEY, name TEXT, address TEXT, phone TEXT, email TEXT, webcontents TEXT, videos TEXT, category TEXT, series INTEGER, lat REAL, lon REAL, list_order INTEGER, photos TEXT, checkin_radious REAL, sub_loc_id TEXT, auto_checkin INTEGER, selected INTEGER, checkedin INTEGER, cp REAL, checkin_point REAL, buy_point REAL, hidden_location INTEGER, checkin_image TEXT, buypoint_image TEXT, forced_checkin INTEGER, recipt_times INTEGER, tags TEXT ) '''); await db.execute(''' CREATE TABLE rogaining( rog_id INTEGER PRIMARY KEY AUTOINCREMENT, course_id INTEGER, location_id INTEGER, user_id INTEGER, lat REAL, lon REAL, time_stamp INTEGER, image TEXT ) '''); await db.execute(''' CREATE TABLE rog( id INTEGER PRIMARY KEY AUTOINCREMENT, team_name TEXT, event_code TEXT, user_id INTEGER, cp_number INTEGER, checkintime INTEGER, image TEXT, rog_action_type INTEGER ) '''); } Future> allRogianing() async { Database db = await instance.database; var rog = await db.query('rog'); List roglist = rog.isNotEmpty ? rog.map((e) => Rog.fromMap(e)).toList() : []; //print("--------- $rog"); return roglist; } Future> getRogainingByLatLon(double lat, double lon) async { Database db = await instance.database; var rog = await db.query('rog', where: "lat = $lat and lon= $lon"); List roglist = rog.isNotEmpty ? rog.map((e) => Rog.fromMap(e)).toList() : []; return roglist; } Future clearSelection() async { Database db = await instance.database; Map rowClear = {"selected": false}; return await db.update("destination", rowClear); } Future toggleSelecttion(Destination dest) async { Database db = await instance.database; bool val = !dest.selected!; Map rowTarget = {"selected": val}; await clearSelection(); return await db.update("destination", rowTarget, where: 'location_id = ?', whereArgs: [dest.location_id!]); } Future deleteRogaining(int id) async { Database db = await instance.database; var rog = await db.delete('rog', where: "id = $id"); int ret = rog > 0 ? rog : -1; return ret; } Future deleteAllRogaining() async { Database db = await instance.database; await db.delete('rog'); } Future isRogAlreadyAvailable(int id) async { Database db = await instance.database; var rog = await db.query('rog', where: "id = $id"); return rog.isNotEmpty ? true : false; } Future latestGoal() async { Database db = await instance.database; return Sqflite.firstIntValue( await db.rawQuery('SELECT MAX(checkintime) FROM rog')); } Future insertRogaining(Rog rog) async { Database db = await instance.database; int? nextOrder = Sqflite.firstIntValue(await db.rawQuery('SELECT MAX(id) FROM rog')); nextOrder = nextOrder ?? 0; nextOrder = nextOrder + 1; rog.id = nextOrder; int res = await db.insert( 'rog', rog.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); //print("------ database helper insert $res-----------::::::::"); return res; } Future> getDestinations() async { Database db = await instance.database; var dest = await db.query('destination', orderBy: 'list_order'); List destList = dest.isNotEmpty ? dest.map((e) => Destination.fromMap(e)).toList() : []; //print("--------- $destList"); return destList; } Future> getDestinationById(int id) async { Database db = await instance.database; var rog = await db.query('destination', where: "location_id = $id"); List deslist = rog.isNotEmpty ? rog.map((e) => Destination.fromMap(e)).toList() : []; return deslist; } Future> getDestinationByLatLon( double lat, double lon) async { Database db = await instance.database; var dest = await db.query('destination', where: "lat = $lat and lon= $lon", orderBy: 'list_order'); List destList = dest.isNotEmpty ? dest.map((e) => Destination.fromMap(e)).toList() : []; return destList; } Future deleteDestination(int locationId) async { Database db = await instance.database; var dest = await db.delete('destination', where: "location_id = $locationId"); int ret = dest > 0 ? dest : -1; //after deleting set correct order await setOrder(); return ret; } Future setOrder() async { Database db = await instance.database; var byOrder = await db.query('destination', orderBy: 'list_order'); List desDb = byOrder.isNotEmpty ? byOrder.map((e) => Destination.fromMap(e)).toList() : []; int order = 1; for (Destination d in desDb) { Map rowTarget = {"list_order": order}; await db.update("destination", rowTarget, where: 'location_id = ?', whereArgs: [d.location_id]); order += 1; } } Future deleteAllDestinations() async { Database db = await instance.database; await db.delete('destination'); } Future isAlreadyAvailable(int locationId) async { Database db = await instance.database; var dest = await db.query('destination', where: "location_id = $locationId"); return dest.isNotEmpty ? true : false; } Future insertDestination(Destination dest) async { await deleteDestination(dest.location_id!); Database db = await instance.database; int? nextOrder = Sqflite.firstIntValue( await db.rawQuery('SELECT MAX(list_order) FROM destination')); nextOrder = nextOrder ?? 0; nextOrder = nextOrder + 1; dest.list_order = nextOrder; int res = await db.insert( 'destination', dest.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); //print("------ database helper insert ${dest.toMap()}-----------::::::::"); return res; } Future updateCancelBuyPoint(Destination destination) async { //print("---- updating puypint image in db -----"); Database db = await instance.database; Map row = {"buy_point": 0}; return await db.update("destination", row, where: 'location_id = ?', whereArgs: [destination.location_id!]); } Future updateBuyPoint(Destination destination, String imageUrl) async { //print("---- updating puypint image in db -----"); Database db = await instance.database; Map row = {"buypoint_image": imageUrl}; return await db.update("destination", row, where: 'location_id = ?', whereArgs: [destination.location_id!]); } Future updateAction(Destination destination, bool checkin) async { Database db = await instance.database; int act = checkin == false ? 0 : 1; Map row = {"checkedin": act}; return await db.update("destination", row, where: 'location_id = ?', whereArgs: [destination.location_id!]); } Future updateOrder(Destination d, int dir) async { Database db = await instance.database; var target = await db.query('destination', where: "list_order = ${d.list_order! + dir}"); var dest = await db.query('destination', where: "location_id = ${d.location_id}"); // print("--- target in db is $target"); // print("--- destine in db is $dest"); if (target.isNotEmpty) { List targetIndb = target.isNotEmpty ? target.map((e) => Destination.fromMap(e)).toList() : []; List destIndb = dest.isNotEmpty ? dest.map((e) => Destination.fromMap(e)).toList() : []; Map rowTarget = {"list_order": d.list_order}; Map rowDes = { "list_order": destIndb[0].list_order! + dir }; // print("--- target destination is ${target_indb[0].location_id}"); // print("--- destine destination is is ${dest_indb[0].location_id}"); await db.update("destination", rowTarget, where: 'location_id = ?', whereArgs: [targetIndb[0].location_id]); await db.update("destination", rowDes, where: 'location_id = ?', whereArgs: [destIndb[0].location_id]); } } // Future getPending() async{ // Database db = await instance.database; // return await Sqflite.firstIntValue(await db.rawQuery("SELECT COUNT(*) FROM incidents")); // } } import 'package:get/get.dart'; class StringValues extends Translations{ @override Map> get keys => { 'en_US': { 'drawer_title':'Rogaining participants can view checkpoints by logging in', 'app_title': '- Rogaining -', 'address':'address', 'bib':'Bib number', 'email':'Email address', 'password':'Password', 'web':'Web', 'wikipedia':'Wikipedia', 'video':'video', 'description':'Description', 'telephone':'Tel', 'how_nice': 'How nice', 'want_to_go': "Want to Go", 'schedule_point': "Schedule point", 'login': 'Login', 'logout':'Logout', 'sign_up':'Sign up', 'change_password': 'Change Password', 'profile': 'Profile', 'recommended_route': 'Recommended Route', 'point_rank': 'Point Rank', 'game_rank': 'Game Rank', 'my_route': 'My Route', 'visit_history': 'Visit History', 'rog_web': 'rog website', 'no_values': 'No Values', 'email_and_password_required': 'Email and password are required to register user', 'rogaining_user_need_tosign_up': "Rogaining participants do need to sign up.", 'add_location': 'Gifu', 'select_travel_mode':'Select your travel mode', 'walking':'Walking', 'driving': 'Driving', 'transit': 'Transit', 'are_you_sure_want_to_delete_all' : 'Are you sure want to delete all', 'all_added_destination_will_be_deleted' : 'All added destination will be deleted', 'confirm': 'Confirm', 'cancel': 'Cancel', 'all_destinations_are_deleted_successfully' : 'All destinations are deleted successfully', 'deleted': 'Deleted', 'remarks' : 'Remarks', 'old_password' : 'Old Password', 'new_password' : 'New Password', 'values_required' : 'Values Required', 'failed' : 'Failed', 'password_change_failed_please_try_again' : 'Password change failed, please try again.', 'user_registration_failed_please_try_again' : 'User registration failed, please try again.', 'all': 'All', 'sight_seeing': 'Sight seeing', 'rogaining' : 'Rogaining', 'finishing_rogaining' : 'Finishing Rogaining', 'cp_pls_take_photo' : "CP please take a photo", 'take_photo of the clock' : 'Take photo of the clock', 'finish_goal': 'finish Goal', 'goal_saved': "Goal Saved", 'goal_added_successfuly' : 'Goal added successfully', 'goal_not_added' : 'Goal not added', 'please_try_again' : 'Please try again', "Click start to start rogaining":"Click start to start rogaining", "you are at roganing point, start rogaining":"you are at roganing point, start rogaining", "Start":"Start", "Rogaining Started":"Rogaining Started", "Rogaining session started":"Rogaining session started", "Not started yet":"Not started yet", "You have not started rogaining yet.":"You have not started rogaining yet.", "Not reached the goal yet": "Not reached the goal yet", "You have not reached the goal yet.":"You have not reached the goal yet.", "delete_account": "Delete account", "delete_account_title": "Are you ok to delete your account?", "delete_account_middle": "All your account information and data history will be removed from local device and server side.", "accounted_deleted": "Account deleted", "account_deleted_message": "your account has beed successfully deleted", "privacy": "Privacy policy", "app_developed_by_gifu_dx": "This app was developed by the Gifu Prefecture DX subsidy project.", 'location_permission_title': 'Location Permission', 'location_permission_content': 'Gifu Navi app collects location data to provide better services.\nLocation data is used for automatic check-in at checkpoints and delivery of notifications.\nLocation data may be collected even when the app is closed or not in use.\nCollected location data is only used as statistical information that cannot identify individuals and is never linked to personal information.\nIf you do not allow the use of location data, please select "Do not allow" on the next screen.', 'location_disabled_title': 'Location Service Disabled', 'location_disabled_content': 'Location information is disabled.\nTo continue, please enable location services for Gifu Navi in Settings > Privacy and Security > Location Services.', 'drawer_title': 'Rogaining participants can view checkpoints by logging in', 'app_title': 'Travel Itinerary', 'want_to_go': 'Want to Go', 'schedule_point': 'Schedule Point', 'rog_web': 'Rogaining Website', 'rogaining_user_need_tosign_up': "Rogaining participants do not need to sign up.", 'add_location': 'Gifu', 'finish': 'Finish', 'my_route': 'My Route', 'visit_history': 'Visit History', 'search': 'Search', 'login': 'Login', 'password': 'Password', 'already_have_account': 'Already have an account?', 'sign_up': 'Sign Up', 'create_account': 'Create an account, it\'s free', 'confirm_password': 'Confirm Password', 'cancel_checkin': 'Cancel Check-in', 'go_here': 'Show route', 'cancel_route':'Clear route', 'start_rogaining': 'Start Rogaining', 'in_game': 'In Game', 'finish_rogaining': 'Finish Rogaining', 'checkin': 'Check-in', 'rogaining_not_started': 'Rogaining not started', 'confirm': 'Confirm', 'clear_rog_data_message': 'Starting rogaining will clear all previous rogaining data. Are you sure you want to start?', 'no': 'No', 'yes': 'Yes', 'retake': 'Retake', 'take_photo': 'Take Photo', 'take_receipt_photo': 'Please take a photo of the receipt', 'buypoint_added': 'Buypoint added.', 'no_purchase': 'No Purchase', 'complete': 'Complete', 'movement_history': 'Movement History', 'pass_history': 'Pass History', 'no_checkin_yet': 'No check-in yet', 'game_status': 'Game Status', 'take_cp_photo': 'This is a CP. Please take a photo.', 'save_goal_success': 'Goal saved successfully', 'save_goal_failed': 'Goal not added', 'please_try_again': 'Please try again', 'click_start_to_start_rogaining': 'Click start to start rogaining', 'at_rogaining_point_start': 'You are at a rogaining point, start rogaining', 'start': 'Start', 'rogaining_started': 'Rogaining Started', 'rogaining_session_started': 'Rogaining session started', 'not_started_yet': 'Not started yet', 'not_started_rogaining_yet': 'You have not started rogaining yet.', 'not_reached_goal_yet': 'Not reached the goal yet', 'not_reached_goal_yet_message': 'You have not reached the goal yet.', 'reload_qr': 'Reload QR', 'read_qr': 'Read QR', 'read_qr_code': 'Please read the QR code', 'canceled': 'Canceled', 'checkin_failed_try_again': 'Check-in failed. Please tap the checkpoint again if necessary.', 'rogaining_not_started': 'Rogaining not started', 'need_to_start_rogaining': 'You need to tap the start button to begin rogaining', 'no_destination': 'No destination', 'near_cp_not_checkin': 'Near a CP or distance-ignored CP, in-game but not checked in yet.', 'auto_checkin_case': 'Auto check-in case', 'non_auto_checkin_case': 'Non-auto check-in case', 'normal_cp_case': 'Normal CP case', 'non_normal_cp_case': 'Non-normal CP case... what case?', 'goal_clock_photo_case': 'Goal clock photo case', 'start_case_24_hours_passed': 'Start case and 24 hours have passed since the last goal', 'start_cp_24_hours_passed': 'At the start CP, and 24 hours have passed since the last goal,', 'standard_cp_not_checkin': 'Standard CP not checked in yet.', 'after_checkin_buypoint_case': 'After check-in, buypoint case.', 'goal_case': 'Goal case', 'start_case': 'Start case', 'no_match_skip_process': 'Does not match any conditions, skipping process', 'server_error_occurred': 'A server error occurred', 'could_not_communicate_with_server': 'Could not communicate with the server', 'communication_error_occurred': 'A communication error occurred', 'checked_in': 'Checked in.', 'cancel_checkin': 'Cancel Check-in', 'checkin_canceled_for': 'Check-in canceled for', 'error': 'Error', 'failed_to_cancel_checkin': 'Failed to cancel check-in.', 'buypoint_added': 'Buypoint added', 'error_occurred': 'An error occurred', 'failed_to_process_checkpoint': 'Failed to process the checkpoint.', 'start_rogaining': 'Start Rogaining', 'in_competition': 'In Competition', 'map_auto_return_message': 'If there is no map operation, it will automatically return to the current location. Please enter the timer seconds. If you check the checkbox, auto-return will not be performed.', 'no_auto_return': 'No Auto Return', 'failed_to_load_markers': 'Failed to load markers', 'screen_switching_error': 'Screen switching error', 'failed_to_switch_screen': 'Failed to switch the screen', 'timer_duration': 'Timer Duration', 'user_data_deletion': 'User Data Deletion', 'user_consent_set_for_data_deletion': 'User consent is set for data deletion. User data has been deleted from the app and server', 'go_to_gps_signal_area': 'Please go to an area with GPS signal.', 'location_service_disabled': 'Location service is disabled. Please enable location service from the settings screen. If you are unsure, please contact the engineering staff.', 'location_permission_not_granted': 'Location permission is not granted. Please allow location service for Gifu Navi from the settings screen. If you are unsure, please contact the engineering staff.', 'location_service_issue_occurred': 'An issue occurred with the location service. The location service is being restarted, please wait a moment.', 'login_failed': 'Login Failed', 'check_login_id_or_password': 'Please check your login ID or password.', 'communication_error_occurred': 'A communication error occurred', 'could_not_communicate_with_server': 'Could not communicate with the server', 'before_game': 'Before Game', 'location_permission_denied_title': 'Location Permission Denied', 'location_permission_denied_message': 'This app requires location permission to function properly. Please grant location permission to continue.', 'location_permission_permanently_denied_title': 'Location Permission Permanently Denied', 'location_permission_permanently_denied_message': 'Location permission has been permanently denied. Please open app settings to grant location permission.', 'open_settings': 'Open Settings', 'location_permission_needed_title': 'Location Permission Needed', 'location_permission_needed_main': 'Location permissions have been permanently denied. Please open app settings to enable location permissions.', 'open_settings': 'Open Settings', 'location_services_disabled_title': 'Location Services Disabled', 'location_service_disabled_main': 'Please enable location services to continue using the app.', 'location_permission_denied_title': 'Location Permission Denied', 'location_permission_denied_main': 'This app requires location permissions to function properly. Please enable location permissions in your device settings.', 'home': 'Home', 'welcome': 'Welcome to Gifu Navi', 'location_disabled_message': 'Location services are disabled. Some features may not work properly.', 'enable_location_service': 'Enable Location Service', 'start_app': 'Start App', 'location_permission_required_title': 'Location Permission Required', 'location_permission_required_message': 'This app requires access to your location. Please grant permission to continue.', 'cancel': 'Cancel', 'checkins': 'Check-ins', 'reset_button': 'Reset data', 'reset_title': 'Reset the data in this device.', 'reset_message': 'Are you ok to reset all data in this device?', 'reset_done': 'Reset Done.', 'reset_explain': 'All data has been reset. You should tap start rogaining to start game.', 'no_match': 'No match!', 'password_does_not_match':'The passwords you entered were not match.', }, 'ja_JP': { 'drawer_title':'ロゲイニング参加者はログイン するとチェックポイントが参照 できます', 'app_title': '旅行工程表', 'address':'住所', 'bib':'ゼッケン番号', 'email':'メールアドレス', 'password':'パスワード', 'web':'ウェブ', 'wikipedia':'ウィキペディア', 'video':'ビデオ', 'description':'説明', 'telephone':'電話', 'how_nice':'いいね', 'want_to_go': '行きたい', 'like': 'お気に入り', 'checkin': 'チェックイン', 'schedule_point': '予定地点', 'login': 'ログイン', 'logout':'ログアウト', 'sign_up': 'サインアップ', 'change_password': 'パスワード変更', 'profile': 'プロフィール', 'recommended_route': '推奨ルート', 'point_rank': '地点ランキング', 'game_rank': 'ゲームランキング', 'my_route': 'マイルート', 'visit_history': '訪問履歴', 'rog_web': 'ロゲイニングウェブサイト', 'no_values': '値なし', 'email_and_password_required': 'メールとパスワードの入力が必要です', 'rogaining_user_need_tosign_up': "ロゲイニング参加者はサインアップの必要はありません。", 'add_location': '岐阜', 'select_travel_mode':'移動モードを選択してください', 'walking':'歩行', 'driving': '自動車利用', 'transit': '公共交通機関利用', 'are_you_sure_want_to_delete_all' : '本当にすべて削除しますか', 'all_added_destination_will_be_deleted' : '追加したすべての宛先が削除されます', 'confirm': '確認', 'cancel': 'キャンセル', 'all_destinations_are_deleted_successfully' : 'すべての宛先が正常に削除されました', 'deleted': "削除されました", 'remarks' : '備考', 'old_password' : '以前のパスワード', 'new_password' : '新しいパスワード', 'values_required' : '必要な値', 'failed' : '失敗', 'password_change_failed_please_try_again' : 'パスワードの変更に失敗しました。もう一度お試しください', 'user_registration_failed_please_try_again' : 'ユーザー登録に失敗しました。もう一度お試しください', 'all': '全て', 'sight_seeing': '観光', 'rogaining' : 'ロゲイニング', 'finishing_rogaining' : 'ロゲイニングを終えて', 'cp_pls_take_photo' : "CPです。撮影してください。", 'take_photo of the clock' : '時計の写真を撮る', 'finish_goal': 'ゴール完了', 'goal_saved': "目標を保存しました", 'goal_added_successfuly' : '目標が正常に追加されました', 'goal_not_added' : '目標が追加されていません', 'please_try_again' : 'もう一度お試しください', "Click start to start rogaining":"開始をクリックして、ロゲイニングを開始します", "you are at roganing point, start rogaining":"あなたはロゲイニングポイントにいます、ロゲイニングを始めてください", "Start":"始める", "Rogaining Started":"ロゲイニング開始", "Rogaining session started":"ロゲイニングセッション開始", "Not started yet":"まだ開始されていません", "You have not started rogaining yet.":"あなたはまだロゲイニングを始めていません。", "Not reached the goal yet": "まだ目標に達していない", "You have not reached the goal yet.":"あなたはまだゴールに達していません。", "delete_account": "アカウントを削除します", "delete_account_title": "アカウントを削除しますがよろしいですか?", "delete_account_middle": "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます", "accounted_deleted": "アカウントが削除されました", "account_deleted_message": "あなたのアカウントは正常に削除されました", "privacy": "プライバシーポリシー", "app_developed_by_gifu_dx": "※このアプリは令和4、6年度岐阜県DX補助金事業で開発されました。", 'location_permission_title': 'ロケーション許可', 'location_permission_content': 'このアプリでは、位置情報の収集を行います。\n岐阜ナビアプリではチェックポイントの自動チェックインの機能を可能にするために、現在地のデータが収集されます。アプリを閉じている時や、使用していないときにも収集されます。位置情報は、個人を特定できない統計的な情報として、ユーザーの個人情報とは一切結びつかない形で送信されます。お知らせの配信、位置情報の利用を許可しない場合は、この後表示されるダイアログで「許可しない」を選択してください。', 'location_disabled_title': '位置情報サービスが無効です', 'location_disabled_content': '位置情報が無効になっています\nこのアプリケーションへの位置情報アクセスが無効になっています。続行するには設定>プライバシーとセキュリティ>位置情報サービス>岐阜ナビ で有効にしてください。', 'drawer_title': 'ロゲイニング参加者はログイン するとチェックポイントが参照 できます', 'app_title': '旅行工程表', 'want_to_go': '行きたい', 'schedule_point': '予定地点', 'rog_web': 'ロゲイニングウェブサイト', 'rogaining_user_need_tosign_up': "ロゲイニング参加者はサインアップの必要はありません。", 'add_location': '岐阜', 'finish': '終了する', 'my_route': 'マイルート', 'visit_history': '訪問履歴', 'search': '検索', 'login': 'ログイン', 'password': 'パスワード', 'already_have_account': 'すでにアカウントをお持ちですか?', 'sign_up': 'サインアップ', 'create_account': 'アカウントを無料で作成します', 'confirm_password': 'パスワードを認証する', 'cancel_checkin': 'チェックイン取消', 'go_here': 'ルート表示', 'cancel_route':'ルート消去', 'start_rogaining': 'ロゲ開始', 'in_game': '競技中', 'finish_rogaining': 'ロゲゴール', 'checkin': 'チェックイン', 'rogaining_not_started': 'ロゲは始まっていません', 'confirm': '確認', 'clear_rog_data_message': 'ロゲを開始すると、今までのロゲデータが全てクリアされます。本当に開始しますか?', 'no': 'いいえ', 'yes': 'はい', 'retake': '再撮影', 'take_photo': '撮影', 'take_receipt_photo': 'レシートの写真を撮ってください', 'buypoint_added': 'お買い物加点を行いました。', 'no_purchase': '買い物なし', 'complete': '完了', 'movement_history': '移動履歴', 'pass_history': '通過履歴', 'no_checkin_yet': 'チェックインはまだされてません', 'game_status': 'ゲームステータス', 'take_cp_photo': 'CPです。撮影してください。', 'save_goal_success': '目標が保存されました', 'save_goal_failed': '目標が追加されていません', 'please_try_again': 'もう一度お試しください', 'click_start_to_start_rogaining': 'ロゲを開始するには開始をクリックしてください', 'at_rogaining_point_start': 'あなたはロガニングポイントにいます、ロガニングを始めてください', 'start': '開始', 'rogaining_started': 'ロゲイニングを開始しました', 'rogaining_session_started': 'ロゲイニングセッションを開始しました', 'not_started_yet': 'まだ開始されていません', 'not_started_rogaining_yet': 'あなたはまだロゲイニングを始めていません。', 'not_reached_goal_yet': 'まだゴールに達していません', 'not_reached_goal_yet_message': 'あなたはまだゴールに達していません。', 'reload_qr': '再QR読込', 'read_qr': 'QR読込', 'read_qr_code': 'QRコードを読み取ってください', 'canceled': 'キャンセルされました', 'checkin_failed_try_again': 'チェックインしていません。必要ならもう一度チェックポイントをタップして下さい。', 'rogaining_not_started': 'ロゲが始まっていません', 'need_to_start_rogaining': 'ロゲ開始ボタンをタップして、ロゲイニングを始める必要があります', 'no_destination': '目的地がない場合', 'near_cp_not_checkin': '検知範囲または距離無視CPで、ゲーム中でまだチェックインしていない。', 'auto_checkin_case': '自動チェックインの場合', 'non_auto_checkin_case': '自動チェックイン以外の場合', 'normal_cp_case': '通常CPの場合', 'non_normal_cp_case': '通常CP以外の場合....どんな場合?', 'goal_clock_photo_case': 'ゴールで時計撮影の場合', 'start_case_24_hours_passed': 'スタートの場合で最後のゴールから24時間経過している場合', 'start_cp_24_hours_passed': '開始CPで、最後にゴールしてから24時間経過していれば、', 'standard_cp_not_checkin': '標準CP まだチェックインしていない。', 'after_checkin_buypoint_case': 'チェックイン後で買い物ポイントの場合。', 'goal_case': 'ゴールの場合', 'start_case': 'スタートの場合', 'no_match_skip_process': 'いずれにも当てはまらないので、処理スキップ', 'server_error_occurred': 'サーバーエラーがおきました', 'could_not_communicate_with_server': 'サーバーと通信できませんでした', 'communication_error_occurred': '通信エラーがおきました', 'checked_in': 'チェックインしました。', 'cancel_checkin': 'チェックイン取消', 'checkin_canceled_for': 'のチェックインは取り消されました', 'error': 'エラー', 'failed_to_cancel_checkin': 'チェックイン取り消しに失敗しました。', 'buypoint_added': 'お買い物加点を行いました', 'error_occurred': 'エラーがおきました', 'failed_to_process_checkpoint': 'チェックポイントの処理に失敗しました。', 'start_rogaining': 'ロゲ開始', 'in_competition': '競技中', 'map_auto_return_message': 'マップ操作がなければ自動的に現在地に復帰します。そのタイマー秒数を入れて下さい。チェックボックスをチェックすると、自動復帰は行われなくなります。', 'no_auto_return': '自動復帰なし', 'failed_to_load_markers': 'マーカーの読み込みに失敗しました', 'screen_switching_error': '画面切り替えでエラー', 'failed_to_switch_screen': '画面の切り替えができませんでした', 'timer_duration': 'タイマーの長さ', 'user_data_deletion': 'ユーザーデータを削除する', 'user_consent_set_for_data_deletion': 'データを削除するためにユーザーの同意が設定されています アプリとサーバーでユーザーデータが削除されました', 'go_to_gps_signal_area': 'GPSの届く場所に行って、信号を拾ってください。', 'location_service_disabled': '位置情報サービスが無効になっています。設定画面から位置情報サービスを有効にして下さい。不明な場合にはエンジニアスタッフにお問い合わせください。', 'location_permission_not_granted': '位置情報サービスが許可されていません。設定画面から岐阜ナビの位置情報サービスを許可して下さい。不明な場合にはエンジニアスタッフにお問い合わせください。', 'location_service_issue_occurred': '位置情報サービスに問題が発生しました。位置情報サービスを再起動していますので少しお待ちください。', 'login_failed': 'ログイン失敗', 'check_login_id_or_password': 'ログインIDかパスワードを確認して下さい。', 'communication_error_occurred': '通信エラーがおきました', 'could_not_communicate_with_server': 'サーバーと通信できませんでした', 'before_game': 'ゲーム前', 'location_permission_denied_title': '位置情報の許可が拒否されました', 'location_permission_denied_message': 'このアプリを適切に機能させるには、位置情報の許可が必要です。続行するには、位置情報の許可を付与してください。', 'location_permission_permanently_denied_title': '位置情報の許可が永久に拒否されました', 'location_permission_permanently_denied_message': '位置情報の許可が永久に拒否されました。位置情報の許可を付与するには、アプリ設定を開いてください。', 'open_settings': '設定を開く', 'storage_permission_needed_title': '写真ライブラリへの許可が必要です', 'storage_permission_needed_main': '岐阜ロゲでは、写真ライブラリを使用してスタート・チェックイン・ゴール等の通過照明写真の記録のために、写真ライブラリへの書き込みを行なっています。このためチェックイン時に写真をライブラリに保存する権限が必要です。設定画面で、「岐阜ナビ」に対して、ライブラリに写真の保存を許可するように設定してください。', 'location_permission_needed_title': '位置情報への許可が必要です', 'location_permission_needed_main': '岐阜ロゲでは、位置情報を使用してスタート・チェックイン・ゴール等の通過照明及び移動手段の記録のために、位置情報のトラッキングを行なっています。このためバックグラウンドでもトラッキングができるように位置情報の権限が必要です。設定画面で、「岐阜ナビ」に対して、常に位置情報を許可するように設定してください。', 'open_settings': '設定を開く', 'location_permission_denied_title': '位置情報へのアクセスが拒否されています。', 'location_permission_denied_main': 'この岐阜ナビアプリは正常に動かすには位置情報への許可が必要です。「設定」画面で位置情報の許可を指定してください。', 'location_services_disabled_title': '位置情報サービスが拒否されています', 'location_service_disabled_main': '岐阜ナビアプリを使用するには位置情報サービスを許可してください。', 'home': 'ホーム', 'welcome': '岐阜ナビへようこそ', 'location_disabled_message': '位置情報サービスが無効になっています。一部の機能が正しく動作しない可能性があります。', 'enable_location_service': '位置情報サービスを有効にする', 'start_app': 'アプリを開始する', 'location_permission_required_title': '位置情報の許可が必要です', 'location_permission_required_message': 'このアプリを使用するには、位置情報へのアクセスが必要です。続行するには許可を付与してください。', 'cancel': 'キャンセル', 'checkins': 'チェックイン', 'reset_button': 'リセット', 'reset_title': 'リセットしますがよろしいですか?', 'reset_message': 'これにより、すべてのゲーム データが削除され、すべての状態が削除されます', 'reset_done': 'リセット完了', 'reset_explain': 'すべてリセットされました。ロゲ開始から再開して下さい。', 'no_match': '不一致', 'password_does_not_match':'入力したパスワードが一致しません', }, }; } import 'package:geojson_vi/geojson_vi.dart'; import 'package:rogapp/model/destination.dart'; class TextUtils { static String getDisplayTextFeture(GeoJSONFeature f) { RegExp regex = RegExp(r'([.]*0)(?!.*\d)'); String txt = ""; if (f.properties!["sub_loc_id"] != null) { txt = "${f.properties!["sub_loc_id"]}"; } // if(f.properties!["cp"] > 0){ // //print("-- sub-- ${f.properties!["cp"]} ----"); // txt = "${f.properties!["cp"].toString().replaceAll(regex, '')}"; // } //if(f.properties!["buy_point"] != null && f.properties!["buy_point"] > 0){ //txt = "$txt${f.properties!["sub_loc_id"]}"; //} //print("Text = ${txt}"); return txt; } static String getDisplayText(Destination dp) { RegExp regex = RegExp(r'([.]*0)(?!.*\d)'); String txt = ""; if (dp.cp! > 0) { txt = dp.cp.toString().replaceAll(regex, ''); if (dp.checkin_point != null && dp.checkin_point! > 0) { txt = "$txt{${dp.checkin_point.toString().replaceAll(regex, '')}}"; } if (dp.buy_point != null && dp.buy_point! > 0) { //print("^^^^^^^^^ ${dp.sub_loc_id}^^^^^^^^^^"); txt = "#${dp.cp.toString().replaceAll(regex, '')}(${dp.checkin_point.toString().replaceAll(regex, '')}+${dp.buy_point.toString().replaceAll(regex, '')})"; } } return txt; } // static String getDisplayText(String num){ // RegExp regex = RegExp(r'([.]*0)(?!.*\d)'); // return "${num.replaceAll(regex, '')}"; // } } import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; //import 'package:rogapp/widgets/debug_widget.dart'; import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/permission/permission.dart'; // LocationControllerクラスは、GetxControllerを継承したクラスであり、位置情報の管理を担当しています。 // LocationControllerは以下の機能を提供しています。 // LocationControllerは、アプリ全体で位置情報を一元管理するための重要なコンポーネントです。 // 他のコンポーネントは、LocationControllerから位置情報を取得し、位置情報に関連する機能を実装することができます。 // // Features: // * 現在の位置情報を保持し、他のコンポーネントからアクセスできるようにします。 // * 位置情報のストリームを管理し、位置情報の更新を監視します。 // * 位置情報サービスの有効性と権限の確認を行い、適切な処理を行います。 // * 位置情報のストリームを開始、停止、再開する機能を提供します。 // * 位置マーカーの位置情報をStreamControllerを通じて他のコンポーネントに通知します。 // // Logic: // 1. startPositionStreamメソッドで、Geolocator.getPositionStreamを使用して位置情報のストリームを開始します。 // 2. ストリームから位置情報を受信すると、LocationMarkerPositionオブジェクトを作成し、locationMarkerPositionStreamControllerに追加します。 // 3. 位置情報が取得できなかった場合や、エラーが発生した場合は、適切な処理を行います。 // 4. stopPositionStreamメソッドで、位置情報のストリームを一時停止することができます。 // 5. resumePositionStreamメソッドで、一時停止中の位置情報のストリームを再開することができます。 // 6. onCloseメソッドで、コントローラーのクローズ時に位置情報のストリームをキャンセルします。 // class LocationController extends GetxController { // Reactive variable to hold the current position Rx currentPosition = Rx(null); // 現在の位置情報を保持するReactive変数です。Rx型で宣言されています。 // Subscription to the position stream StreamSubscription? positionStream; // 位置情報のストリームを保持する変数です。StreamSubscription型で宣言されています。 LatLng? lastValidLocation; DateTime lastGPSDataReceivedTime = DateTime.now(); // 最後にGPSデータを受け取った時刻 bool gpsDebugMode = true; /* // GPSシミュレーション用のメソッドを追加 void setSimulationMode(bool value) { isSimulationMode = value; } // ====== Akira , GPS信号強度をシミュレート ==== ここから // //===== Akira Added 2024-4-9 start // GPSシミュレーション用の変数を追加 ===> 本番では false にする。 bool isSimulationMode = false; // GPS信号強度をシミュレートするための変数 final Rx _simulatedSignalStrength = Rx('high'); // GPS信号強度をシミュレートするための関数 void setSimulatedSignalStrength(String strength) { if( strength!='real') { isSimulationMode = true; _simulatedSignalStrength.value = strength; latestSignalStrength.value = strength; }else{ isSimulationMode = false; _simulatedSignalStrength.value = strength; } } // シミュレートされた信号強度を取得するための関数 String getSimulatedSignalStrength() { //debugPrint("strength : ${_simulatedSignalStrength.value}"); return _simulatedSignalStrength.value; } */ // // ====== Akira , GPS信号強度をシミュレート ==== ここまで // GPS信号が弱い場合のフラグ. 本番では、false,high にする。 bool isGpsSignalWeak = false; RxString latestSignalStrength = 'high'.obs; //final _latestSignalStrength = 'low'.obs; // 初期値を設定 //String get latestSignalStrength => _latestSignalStrength.value; Stream get gpsSignalStrengthStream => latestSignalStrength.stream; bool isRunningBackgroundGPS=false; int activeEngineCount = 0; // GPS信号の強弱を判断するメソッドを追加. 10m 以内:強、30m以内:中、それ以上:弱 // String getGpsSignalStrength(Position? position) { if (isSimulationMode.value) { return getSimulatedSignalStrength(); } if (position == null) { //gpsDebugMode ? debugPrint("getGpsSignalStrength position is null.") : null; latestSignalStrength.value = "low"; isGpsSignalWeak = true; return 'low'; } final accuracy = position.accuracy; //gpsDebugMode ? debugPrint("getGpsSignalStrength : ${accuracy}") : null; /* if(isSimulationMode){ return _simulatedSignalStrength.value; // GPS信号強度シミュレーション }else { */ if (accuracy <= 10) { latestSignalStrength.value = "high"; isGpsSignalWeak = false; return 'high'; } else if (accuracy <= 50) { latestSignalStrength.value = "medium"; isGpsSignalWeak = false; return 'medium'; } else { latestSignalStrength.value = "low"; isGpsSignalWeak = true; return 'low'; } // } } // 現在位置を調整するメソッドを追加 LatLng? adjustCurrentLocation(Position? position) { if (position == null) { if( lastValidLocation!=null ) { //debugPrint("=== adjustCurrentLocation (Position:Null and using LastValidLocation ${lastValidLocation})==="); return LatLng(lastValidLocation!.latitude, lastValidLocation!.longitude); }else { print("=== adjustCurrentLocation (Position:Null and No LastValidLocation ... )==="); return null; } //return lastValidLocation ?? LatLng(0, 0); } final signalStrength = getGpsSignalStrength(position); if (signalStrength == 'high' || signalStrength == 'medium') { //debugPrint("=== adjustCurrentLocation (Position:Get and return Valid location:${position} ... )==="); lastValidLocation = LatLng(position.latitude, position.longitude); } return lastValidLocation ?? LatLng(lastValidLocation!.latitude, lastValidLocation!.longitude); } //===== Akira Added 2024-4-9 end final locationMarkerPositionStreamController = StreamController.broadcast(); // 位置マーカーの位置情報を送信するためのStreamControllerです。 // StreamController型で宣言されています。 bool isStreamPaused = false; // 位置情報のストリームが一時停止中かどうかを示すフラグです。bool型で宣言されています。 // 位置マーカーの位置情報のストリームを取得するゲッター関数です。 // locationMarkerPositionStreamController.streamを返します。 // Stream get locationMarkerPositionStream => locationMarkerPositionStreamController.stream; // コントローラーの初期化時に呼び出されるライフサイクルメソッドです。 // startPositionStreamメソッドを呼び出して、位置情報のストリームを開始します。 // @override void onInit() { super.onInit(); // Start listening to location updates when the controller is initialized startPositionStream(); } // 位置情報のストリームを開始するメソッドです。 // 位置情報サービスが有効か確認し、無効な場合はダイアログを表示します。 // 位置情報の権限を確認し、必要な権限がない場合は権限をリクエストします。 // 既存の位置情報のストリームをキャンセルします。 // Geolocator.getPositionStreamを使用して、新しい位置情報のストリームを開始します。 // ストリームから受信した位置情報をlocationMarkerPositionStreamControllerに追加します。 // エラーが発生した場合は、locationMarkerPositionStreamControllerにエラーを追加します。 // ストリームが一時停止中の場合は、ストリームを再開します。 // // 2024-4-8 Akira : See 2809 // stopPositionStreamメソッドを追加して、既存のストリームをキャンセルするようにしました。また、ストリームが完了したらnullに設定し、エラー発生時にストリームをキャンセルするようにしました。 // void startPositionStream() async { // Check for location service and permissions before starting the stream // 位置情報サービスの有効性をチェックし、無効な場合はエラーハンドリングを行います。 // await PermissionController.checkAndRequestPermissions(); /* bool isPermissionGranted = await PermissionController.checkLocationPermissions(); if (!isPermissionGranted) { PermissionController.showPermissionDeniedDialog(); return; } */ /* bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // Use GetX's context to show a dialog Get.dialog( AlertDialog( title: Text('location_services_disabled_title'.tr), content: Text('location_services_disabled_title'.tr), actions: [ TextButton( child: const Text('OK'), onPressed: () { // Close the dialog Get.back(); // Optionally, you can direct the user to the settings page // Geolocator.openLocationSettings(); }, ), ], ), barrierDismissible: false, // User must tap button to close dialog ); return; } // 位置情報の権限をチェックし、拒否されている場合はエラーハンドリングを行います。 // LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Show a dialog if permissions are still denied Get.dialog( AlertDialog( title: Text('location_permission_denied_title'.tr), content: Text('location_permission_denied_main'.tr), actions: [ TextButton( child: const Text('OK'), onPressed: () { // Close the dialog Get.back(); // Optionally, direct the user to the app settings // Geolocator.openAppSettings(); }, ), ], ), barrierDismissible: false, ); return; } } if (permission == LocationPermission.deniedForever) { // Show a dialog if permissions are permanently denied Get.dialog( AlertDialog( title: Text('location_permission_needed_title'.tr), content: Text( 'location_permission_needed_main'.tr), actions: [ TextButton( child: Text('open_settings'.tr), onPressed: () { // Close the dialog and open app settings Get.back(); Geolocator.openAppSettings(); }, ), ], ), barrierDismissible: false, ); return; } */ // 位置情報の設定を行います。z11 // Set up the location options const locationOptions = LocationSettings(accuracy: LocationAccuracy.medium, distanceFilter: 0); // 既存の位置情報のストリームをキャンセルします。 await positionStream?.cancel(); // 新しい位置情報のストリームを開始します。 // positionStream = Geolocator.getPositionStream(locationSettings: locationOptions).listen( (Position? position) { //gpsDebugMode ? debugPrint("Position = ${position}"):null; final signalStrength = getGpsSignalStrength(position); if (signalStrength == 'low') { isGpsSignalWeak = true; //gpsDebugMode ? debugPrint("LocationController.getPositionStream : isGpsSignalWeak = ${isGpsSignalWeak}"):null; } else { isGpsSignalWeak = false; //gpsDebugMode ? debugPrint("LocationController.getPositionStream : isGpsSignalWeak = ${isGpsSignalWeak}"):null; } final DestinationController destinationController = Get.find(); // ロゲ開始前、終了後、GPS=low の場合は更新しない。 // if (isGpsSignalWeak == false) { //if (destinationController.isInRog.value && isGpsSignalWeak == false) { final adjustedLocation = adjustCurrentLocation(position); if (adjustedLocation != null) { final locationMarkerPosition = LocationMarkerPosition( latitude: adjustedLocation.latitude, longitude: adjustedLocation.longitude, accuracy: position?.accuracy ?? 0, ); handleLocationUpdate(locationMarkerPosition); //locationMarkerPositionStreamController.add(locationMarkerPosition); // 位置データ送信 } else { // 位置情報が取得できなかった場合、 // locationMarkerPositionStreamControllerにnullを追加します。 locationMarkerPositionStreamController.add(null); // null 送信? //forceUpdateLocation(Position? position); } //}else{ // debugPrint("GPS処理対象外"); } }, onError: (e) { // エラーが発生した場合、locationMarkerPositionStreamControllerにエラーを追加します。 locationMarkerPositionStreamController.addError(e); }, onDone: () { positionStream = null; // ストリームが完了したらnullに設定 }, cancelOnError: true // エラー発生時にストリームをキャンセル ); // Resume stream if it was paused previously // ストリームが一時停止中の場合、ストリームを再開します。 // if (isStreamPaused) { isStreamPaused = false; positionStream!.resume(); } } // Method to stop the position stream // 位置情報のストリームを停止するメソッドです。 // positionStreamが存在する場合、ストリームを一時停止します。 // isStreamPausedフラグをtrueに設定します。 // void stopPositionStream() async { if (positionStream != null) { // updated Akira 2024-4-8 await positionStream!.cancel(); positionStream = null; //positionStream!.pause(); //isStreamPaused = true; } } // Method to resume the position stream // 位置情報のストリームを再開するメソッドです。 // positionStreamが存在し、ストリームが一時停止中の場合、ストリームを再開します。 // isStreamPausedフラグをfalseに設定します。 // void resumePositionStream() { if (positionStream != null && isStreamPaused) { positionStream!.resume(); isStreamPaused = false; } } void handleLocationUpdate(LocationMarkerPosition? position) async { //debugPrint("locationController.handleLocationUpdate"); try { if (position != null) { double currentLat = position.latitude; double currentLon = position.longitude; //debugPrint("Flutter: Received GPS signal. Latitude: $currentLat, Longitude: $currentLon"); //debugPrint("position = ${position}"); /* currentPosition.value = position; final locationMarkerPosition = LocationMarkerPosition( latitude: position.latitude, longitude: position.longitude, accuracy: position.accuracy, ); */ lastGPSDataReceivedTime = DateTime.now(); // 最後にGPS信号を受け取った時刻 locationMarkerPositionStreamController.add(position); }else{ gpsDebugMode ? debugPrint("Flutter: No GPS signal received."):null; } } catch( e ) { debugPrint("Flutter: Error in handleLocationUpdate: $e"); } } // このメソッドは、現在の位置情報を locationMarkerPositionStreamController に送信します。 // void forceUpdateLocation(Position? position) { if (position != null) { final adjustedLocation = adjustCurrentLocation(position); if (adjustedLocation != null) { final locationMarkerPosition = LocationMarkerPosition( latitude: adjustedLocation.latitude, longitude: adjustedLocation.longitude, accuracy: position.accuracy, ); locationMarkerPositionStreamController.add(locationMarkerPosition); } } } // コントローラーのクローズ時に呼び出されるライフサイクルメソッドです。 // positionStreamをキャンセルします。 // @override void onClose() { // Cancel the position stream subscription when the controller is closed positionStream?.cancel(); super.onClose(); } // シミュレーションモードのフラグ RxBool isSimulationMode = RxBool(false); // シミュレーションモードを切り替えるための関数 void setSimulationMode(bool value) { isSimulationMode.value = value; } // GPS信号強度をシミュレートするための変数 final Rx _simulatedSignalStrength = Rx('high'); // GPS信号強度をシミュレートするための関数 void setSimulatedSignalStrength(String strength) { _simulatedSignalStrength.value = strength; } // シミュレートされた信号強度を取得するための関数 String getSimulatedSignalStrength() { return _simulatedSignalStrength.value; } } import 'dart:io'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rogapp/model/gps_data.dart'; import 'package:sqflite/sqflite.dart'; class GpsDatabaseHelper { GpsDatabaseHelper._privateConstructor(); static final GpsDatabaseHelper instance = GpsDatabaseHelper._privateConstructor(); static Database? _database; Future get database async => _database ??= await _initDatabase(); Future _initDatabase() async { Directory documentDirectory = await getApplicationDocumentsDirectory(); String path = join(documentDirectory.path, 'rog.db'); // return await openDatabase( // path, // version: 1, // onCreate: _onCreate, // ); return openDatabase( join( await getDatabasesPath(), 'gps.db', ), version: 1, onCreate: _onCreate); } Future _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE gps( id INTEGER PRIMARY KEY AUTOINCREMENT, team_name TEXT, event_code TEXT, lat REAL, lon REAL, is_checkin int, created_at INTEGER, is_synced INTEGER DEFAULT 0 ) '''); } Future insertGps(GpsData gps) async { try { //print("---- try insering ${gps.toMap()}"); Database db = await instance.database; int? nextOrder = Sqflite.firstIntValue(await db.rawQuery('SELECT MAX(id) FROM gps')); nextOrder = nextOrder ?? 0; nextOrder = nextOrder + 1; gps.id = nextOrder; //print("---- insering ${gps.toMap()}"); int res = await db.insert( 'gps', gps.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); //print("------ database helper insert $res-----------::::::::"); return res; } catch (err) { print("------ error $err-----------::::::::"); return -1; } } Future> getGPSData(String team_name, String event_code) async { Database db = await instance.database; var gpss = await db.query('gps', where: "team_name = ? and event_code = ?", whereArgs: [team_name, event_code], orderBy: 'created_at'); List gpsDatas = gpss.isNotEmpty ? gpss.map((e) => GpsData.fromMap(e)).toList() : []; //print("--------- db list $gpsDatas"); return gpsDatas; } Future> getUnsyncedGPSData( String team_name, String event_code) async { Database db = await instance.database; var gpss = await db.query('gps', where: 'team_name = ? and event_code = ? and is_synced = 0', whereArgs: [team_name, event_code], orderBy: 'created_at'); return gpss.isNotEmpty ? gpss.map((e) => GpsData.fromMap(e)).toList() : []; } Future setSyncData(List data) async { Database db = await instance.database; for (var record in data) { await db.update( 'gps', {'is_synced': 1}, where: 'id = ?', whereArgs: [record.id], ); } } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; class UtilController extends GetxController{ void changeLanguage(var lang, var cnty){ var locale = Locale(lang, cnty); Get.updateLocale(locale); } } class ConstValues{ static const container_svr = "http://container.intranet.sumasen.net:8100"; static const server_uri = "https://rogaining.intranet.sumasen.net"; static const dev_server = "http://localhost:8100"; static const dev_ip_server = "http://192.168.8.100:8100"; static const dev_home_ip_server = "http://172.20.10.9:8100"; static const dev_home_ip_mserver = "http://192.168.1.10:8100"; static String currentServer(){ //return dev_ip_server; return server_uri; } } import 'package:get/get.dart'; import 'package:rogapp/spa/spa_controller.dart'; class SpaBinding extends Bindings { @override void dependencies() { Get.put(SpaController()); } }import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/spa/spa_controller.dart'; class SpaPage extends GetView { const SpaPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { throw UnimplementedError(); } } import 'package:get/get.dart'; class SpaController extends GetxController { }import 'dart:async'; import 'dart:io'; //import 'dart:convert'; //import 'dart:developer'; import 'package:rogapp/model/gps_data.dart'; //import 'package:rogapp/pages/home/home_page.dart'; import 'package:rogapp/utils/database_gps.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; //import 'package:vm_service/vm_service.dart'; //import 'package:dart_vm_info/dart_vm_info.dart'; import 'package:rogapp/pages/settings/settings_controller.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_binding.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/utils/location_controller.dart'; import 'package:rogapp/utils/string_values.dart'; import 'package:rogapp/widgets/debug_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; // import 'package:is_lock_screen/is_lock_screen.dart'; import 'package:rogapp/services/device_info_service.dart'; import 'package:rogapp/services/error_service.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; //import 'dart:async'; //import 'package:get/get.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; import 'pages/permission/permission.dart'; import 'package:rogapp/services/api_service.dart'; Map deviceInfo = {}; /* void saveGameState() async { DestinationController destinationController = Get.find(); SharedPreferences pref = await SharedPreferences.getInstance(); pref.setBool("is_in_rog", destinationController.isInRog.value); pref.setBool( "rogaining_counted", destinationController.rogainingCounted.value); pref.setBool("ready_for_goal", DestinationController.ready_for_goal); } */ // 現在のユーザーのIDも一緒に保存するようにします。 void saveGameState() async { DestinationController destinationController = Get.find(); IndexController indexController = Get.find(); SharedPreferences pref = await SharedPreferences.getInstance(); debugPrint("indexController.currentUser = ${indexController.currentUser}"); if(indexController.currentUser.isNotEmpty) { pref.setInt("user_id", indexController.currentUser[0]["user"]["id"]); }else{ debugPrint("User is empty...."); } pref.setBool("is_in_rog", destinationController.isInRog.value); pref.setBool( "rogaining_counted", destinationController.rogainingCounted.value); pref.setBool("ready_for_goal", DestinationController.ready_for_goal); } /* void restoreGame() async { SharedPreferences pref = await SharedPreferences.getInstance(); DestinationController destinationController = Get.find(); destinationController.skipGps = false; destinationController.isInRog.value = pref.getBool("is_in_rog") ?? false; destinationController.rogainingCounted.value = pref.getBool("rogaining_counted") ?? false; DestinationController.ready_for_goal = pref.getBool("ready_for_goal") ?? false; //print( // "--restored -- destinationController.isInRog.value ${pref.getBool("is_in_rog")} -- ${pref.getBool("rogaining_counted")}"); } */ void restoreGame() async { SharedPreferences pref = await SharedPreferences.getInstance(); IndexController indexController = Get.find(); int? savedUserId = pref.getInt("user_id"); //int? currUserId = indexController.currentUser[0]['user']['id']; //debugPrint("savedUserId=${savedUserId}, currentUser=${currUserId}"); if (indexController.currentUser.isNotEmpty && indexController.currentUser[0]["user"]["id"] == savedUserId) { DestinationController destinationController = Get.find(); destinationController.skipGps = false; destinationController.isInRog.value = pref.getBool("is_in_rog") ?? false; destinationController.rogainingCounted.value = pref.getBool("rogaining_counted") ?? false; DestinationController.ready_for_goal = pref.getBool("ready_for_goal") ?? false; await Get.putAsync(() => ApiService().init()); } } void main() async { WidgetsFlutterBinding.ensureInitialized(); await FlutterMapTileCaching.initialise(); final StoreDirectory instanceA = FMTC.instance('OpenStreetMap (A)'); await instanceA.manage.createAsync(); await instanceA.metadata.addAsync( key: 'sourceURL', value: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', ); await instanceA.metadata.addAsync( key: 'validDuration', value: '14', ); await instanceA.metadata.addAsync( key: 'behaviour', value: 'cacheFirst', ); deviceInfo = await DeviceInfoService.getDeviceInfo(); FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); Get.log('Flutter error: ${details.exception}'); Get.log('Stack trace: ${details.stack}'); ErrorService.reportError(details.exception, details.stack ?? StackTrace.current, deviceInfo, LogManager().operationLogs); }; //Get.put(LocationController()); //await PermissionController.checkAndRequestPermissions(); //requestLocationPermission(); // startMemoryMonitoring(); // 2024-4-8 Akira: メモリ使用量のチェックを開始 See #2810 Get.put(SettingsController()); // これを追加 /* runZonedGuarded(() { runApp(const ProviderScope(child: MyApp())); }, (error, stackTrace) { ErrorService.reportError(error, stackTrace, deviceInfo); }); */ try { // ApiServiceを初期化 //await Get.putAsync(() => ApiService().init()); await initServices(); runApp(const ProviderScope(child: MyApp())); //runApp(HomePage()); // MyApp()からHomePage()に変更 //runApp(const MyApp()); }catch(e, stackTrace){ print('Error during initialization: $e'); print('Stack trace: $stackTrace'); } } Future initServices() async { print('Starting services ...'); try { await Get.putAsync(() => ApiService().init()); print('All services started...'); }catch(e){ print('Error initializing ApiService: $e'); } try { Get.put(SettingsController()); print('SettingsController initialized successfully'); } catch (e) { print('Error initializing SettingsController: $e'); } print('All services started...'); } Future requestLocationPermission() async { try { final status = await Permission.locationAlways.request(); if (status == PermissionStatus.granted) { print('Location permission granted'); } else { print('Location permission denied'); //await showLocationPermissionDeniedDialog(); // 追加 } } catch (e) { print('Error requesting location permission: $e'); } } // メモリ使用量の解説:https://qiita.com/hukusuke1007/items/e4e987836412e9bc73b9 /* // 2024-4-8 Akira: メモリ使用量のチェックのため追加 See #2810 // startMemoryMonitoring関数が5分ごとに呼び出され、メモリ使用量をチェックします。 // メモリ使用量が閾値(ここでは500MB)を超えた場合、エラーメッセージとスタックトレースをレポートします。 // void startMemoryMonitoring() { const threshold = 500 * 1024 * 1024; // 500MB // メモリ使用量情報を取得 final memoryUsage = MemoryUsage.fromJson(DartVMInfo.getAllocationProfile()); if (memoryUsage.heapUsage > threshold) { final now = DateTime.now().toIso8601String(); final message = 'High memory usage detected at $now: ${memoryUsage.heapUsage} bytes'; print(message); reportError(message, StackTrace.current); showMemoryWarningDialog(); } Timer(const Duration(minutes: 5), startMemoryMonitoring); } class MemoryUsage { final int heapUsage; MemoryUsage({required this.heapUsage}); factory MemoryUsage.fromJson(Map json) { return MemoryUsage( heapUsage: json['heapUsage'] as int, ); } } */ // 2024-4-8 Akira: メモリ使用量のチェックのため追加 See #2810 // reportError関数でエラーレポートを送信します。具体的な実装は、利用するエラー報告サービスによって異なります。 void reportError(String message, StackTrace stackTrace) async { // エラーレポートの送信処理を実装 // 例: SentryやFirebase Crashlyticsなどのエラー報告サービスを利用 print("ReportError : ${message} . stacktrace : ${stackTrace}"); } // 2024-4-8 Akira: メモリ使用量のチェックのため追加 See #2810 // showMemoryWarningDialog関数で、メモリ使用量が高い場合にユーザーに警告ダイアログを表示します。 // void showMemoryWarningDialog() { if (Get.context != null) { showDialog( context: Get.context!, builder: (context) => AlertDialog( title: const Text('メモリ使用量の警告'), content: const Text('アプリのメモリ使用量が高くなっています。アプリを再起動することをお勧めします。'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ), ); } } StreamSubscription? positionStream; bool background=false; DateTime lastGPSCollectedTime=DateTime.now(); String team_name=""; String event_code=""; Future startBackgroundTracking() async { if (Platform.isIOS && background==false) { final IndexController indexController = Get.find(); if(indexController.currentUser.length>0) { team_name = indexController.currentUser[0]["user"]['team_name']; event_code = indexController.currentUser[0]["user"]["event_code"]; } background = true; debugPrint("バックグラウンド処理を開始しました。"); final LocationSettings locationSettings = LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 100, ); try { positionStream = Geolocator.getPositionStream(locationSettings: locationSettings) .listen((Position? position) async { if (position != null) { final lat = position.latitude; final lng = position.longitude; //final timestamp = DateTime.now(); final accuracy = position.accuracy; // GPS信号強度がlowの場合はスキップ if (accuracy > 100) { debugPrint("GPS signal strength is low. Skipping data saving."); return; } Duration difference = lastGPSCollectedTime.difference(DateTime.now()) .abs(); // 最後にGPS信号を取得した時刻から10秒以上経過、かつ10m以上経過(普通に歩くスピード) //debugPrint("時間差:${difference}"); if (difference.inSeconds >= 10 ) { debugPrint("バックグラウンドでのGPS取得時の処理(10secおき) count=${difference.inSeconds}, time=${DateTime.now()}"); // DBにGPSデータを保存 pages/destination/destination_controller.dart await addGPStoDB(lat, lng); lastGPSCollectedTime = DateTime.now(); } } }, onError: (error) { if (error is LocationServiceDisabledException) { print('Location services are disabled'); } else if (error is PermissionDeniedException) { print('Location permissions are denied'); } else { print('Location Error: $error'); } }); } catch (e) { print('Error starting background tracking: $e'); } }else if (Platform.isAndroid && background == false) { background = true; debugPrint("バックグラウンド処理を開始しました。"); try { // 位置情報の権限が許可されているかを確認 await PermissionController.checkAndRequestPermissions(); }catch(e){ print('Error starting background tracking: $e'); } } } Future addGPStoDB(double la, double ln) async { //debugPrint("in addGPStoDB ${indexController.currentUser}"); GpsDatabaseHelper db = GpsDatabaseHelper.instance; try { GpsData gps_data = GpsData( id: 0, team_name: team_name, event_code: event_code, lat: la, lon: ln, is_checkin: 0, created_at: DateTime.now().millisecondsSinceEpoch); var res = await db.insertGps(gps_data); debugPrint("バックグラウンドでのGPS保存:"); } catch (err) { print("errr ready gps ${err}"); return; } } Future stopBackgroundTracking() async { if (Platform.isIOS && background==true) { background=false; debugPrint("バックグラウンド処理:停止しました。"); await positionStream?.cancel(); positionStream = null; }else if(Platform.isAndroid && background==true){ background=false; debugPrint("バックグラウンド処理:停止しました。"); const platform = MethodChannel('location'); try { await platform.invokeMethod('stopLocationService'); } on PlatformException catch (e) { print("Failed to stop location service: '${e.message}'."); } } } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State with WidgetsBindingObserver { // This widget is the root of your application. @override void initState() { super.initState(); if (context.mounted) { restoreGame(); } WidgetsBinding.instance.addObserver(this); // ウィジェットが構築された後に権限をチェック WidgetsBinding.instance.addPostFrameCallback((_) { PermissionController.checkAndRequestPermissions(); }); debugPrint("Start MyAppState..."); } /* void showPermissionRequiredDialog() { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Text('権限が必要です'), content: Text('このアプリは機能するために位置情報の権限が必要です。設定で権限を許可してください。'), actions: [ TextButton( child: Text('設定を開く'), onPressed: () { openAppSettings(); Navigator.of(context).pop(); }, ), TextButton( child: Text('アプリを終了'), onPressed: () { // アプリを終了 Navigator.of(context).pop(); // よりクリーンな終了のために 'flutter_exit_app' のようなプラグインを使用することをお勧めします // 今回は単にすべてのルートをポップします Navigator.of(context).popUntil((route) => false); }, ), ], ); }, ); } */ @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } // void saveGameState() async { // DestinationController destinationController = Get.find(); // SharedPreferences pref = await SharedPreferences.getInstance(); // pref.setBool("is_in_rog", destinationController.is_in_rog.value); // pref.setBool("rogaining_counted", destinationController.rogaining_counted.value); // } @override void didChangeAppLifecycleState(AppLifecycleState state) { LocationController locationController = Get.find(); DestinationController destinationController = Get.find(); //DestinationController destinationController = // Get.find(); switch (state) { case AppLifecycleState.resumed: // バックグラウンド処理を停止 if (Platform.isIOS && destinationController.isRunningBackgroundGPS) { // Foreground に戻った時の処理 debugPrint(" ==(Status Changed)==> RESUMED. フォアグラウンドに戻りました"); locationController.resumePositionStream(); //print("RESUMED"); restoreGame(); stopBackgroundTracking(); destinationController.isRunningBackgroundGPS=false; destinationController.restartGPS(); } else if (Platform.isAndroid ) { if( destinationController.isRunningBackgroundGPS ){ const platform = MethodChannel('location'); platform.invokeMethod('stopLocationService'); destinationController.isRunningBackgroundGPS=false; destinationController.restartGPS(); debugPrint("stopped android location service.."); } debugPrint("==(Status Changed)==> RESUMED. android フォアグラウンドに戻りました"); locationController.resumePositionStream(); //print("RESUMED"); restoreGame(); }else{ debugPrint("==(Status Changed)==> RESUMED 不明状態"); } break; case AppLifecycleState.inactive: // アプリが非アクティブになったときに発生します。 if (Platform.isIOS && !destinationController.isRunningBackgroundGPS) { // iOSはバックグラウンドでもフロントの処理が生きている。 // これは、別のアプリやシステムのオーバーレイ(着信通話やアラームなど)によって一時的に中断された状態です。 debugPrint(" ==(Status Changed)==> INACTIVE. 非アクティブ処理。"); //locationController.resumePositionStream(); // 追加: フロントエンドのGPS信号のlistenを停止 locationController.stopPositionStream(); destinationController.isRunningBackgroundGPS=true; startBackgroundTracking(); }else if(Platform.isAndroid && !destinationController.isRunningBackgroundGPS){ debugPrint(" ==(Status Changed)==> INACTIVE. 非アクティブ処理。"); }else{ debugPrint("==(Status Changed)==> INACTIVE 不明状態"); } saveGameState(); break; case AppLifecycleState.paused: // バックグラウンドに移行したときの処理 //locationController.resumePositionStream(); debugPrint(" ==(Status Changed)==> PAUSED. バックグラウンド処理。"); if (Platform.isIOS && !destinationController.isRunningBackgroundGPS) { debugPrint("iOS already running background GPS processing when it's inactive"); } else if(Platform.isAndroid && !destinationController.isRunningBackgroundGPS) { debugPrint( " ==(Status Changed)==> PAUSED. Android バックグラウンド処理。"); locationController.stopPositionStream(); const platform = MethodChannel('location'); platform.invokeMethod('startLocationService'); //platform.invokeMethod('stopLocationService'); destinationController.isRunningBackgroundGPS = true; //startBackgroundTracking(); } saveGameState(); break; case AppLifecycleState.detached: // アプリが終了する直前に発生します。この状態では、アプリはメモリから解放される予定です。 //locationController.resumePositionStream(); debugPrint(" ==(Status Changed)==> DETACHED アプリは終了します。"); saveGameState(); break; case AppLifecycleState.hidden: // Web用の特殊な状態で、モバイルアプリでは発生しません。 //locationController.resumePositionStream(); debugPrint(" ==(Status Changed)==> Hidden アプリが隠れた"); saveGameState(); break; } } @override Widget build(BuildContext context) { return GetMaterialApp( translations: StringValues(), locale: const Locale('ja', 'JP'), //locale: const Locale('en', 'US'), fallbackLocale: const Locale('en', 'US'), title: 'ROGAINING', theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color.fromARGB(255, 36, 135, 221)), useMaterial3: true, ), debugShowCheckedModeBanner: false, defaultTransition: Transition.cupertino, opaqueRoute: Get.isOpaqueRouteDefault, popGesture: Get.isPopGestureEnable, transitionDuration: const Duration(milliseconds: 230), initialBinding: IndexBinding(), //HomeBinding(), initialRoute: AppPages.PERMISSION, getPages: AppPages.routes, enableLog: true, ); } } class GpsData { int id; String team_name; String event_code; double lat; double lon; int is_checkin; int created_at; int is_synced; GpsData({ required this.id, required this.team_name, required this.event_code, required this.lat, required this.lon, required this.created_at, this.is_checkin = 0, this.is_synced = 0, }); factory GpsData.fromMap(Map json) { return GpsData( id: json["id"], team_name: json["team_name"], event_code: json["event_code"], lat: json["lat"], lon: json["lon"], is_checkin: json["is_checkin"], created_at: json["created_at"], is_synced: json["is_synced"] ?? 0, ); } Map toMap() { return { 'id': id, 'team_name': team_name, 'event_code': event_code, 'lat': lat, 'lon': lon, 'is_checkin': is_checkin, 'created_at': created_at, 'is_synced': is_synced, }; } } // lib/models/team.dart import 'category.dart'; import 'user.dart'; class Team { final int id; final String zekkenNumber; final String teamName; final Category category; final User owner; Team({ required this.id, required this.zekkenNumber, required this.teamName, required this.category, required this.owner, }); factory Team.fromJson(Map json) { return Team( id: json['id'], zekkenNumber: json['zekken_number'], teamName: json['team_name'], category: Category.fromJson(json['category']), owner: User.fromJson(json['owner']), ); } Map toJson() { return { 'id': id, 'zekken_number': zekkenNumber, 'team_name': teamName, 'category': category.toJson(), 'owner': owner.toJson(), }; } }// プロパティの型がString?やint?などのオプショナル型になっています。 // これらのプロパティが常に値を持つことが保証されている場合は、非オプショナル型を使用することで、不要なnullチェックを回避できます。 // class AuthUser { AuthUser(); //AuthUser.from({required this.id, required this.email, required this.is_rogaining, required this.group, required this.zekken_number, required this.event_code, required this.team_name}); AuthUser.fromMap(Map map) : id = int.parse(map["id"].toString()), email = map["email"].toString(), is_rogaining = bool.parse(map["is_rogaining"].toString()), group = map["group"].toString(), zekken_number = map["zekken_number"].toString(), event_code = map["event_code"].toString(), team_name = map["team_name"].toString(), auth_token = map["token"]; int? id; String? email; bool? is_rogaining; String? group; String? zekken_number; String? event_code; String? team_name; String? auth_token; } // lib/models/user.dart class User { final int? id; final String? email; final String firstname; final String lastname; final DateTime? dateOfBirth; final bool female; final bool isActive; User({ this.id, this.email, required this.firstname, required this.lastname, this.dateOfBirth, required this.female, required this.isActive, }); factory User.fromJson(Map json) { return User( id: json['id'], email: json['email'], firstname: json['firstname'], lastname: json['lastname'], dateOfBirth: json['date_of_birth'] != null ? DateTime.parse(json['date_of_birth']) : null, female: json['female'], isActive: json['is_active'], ); } Map toJson() { return { 'id': id, 'email': email, 'firstname': firstname, 'lastname': lastname, 'date_of_birth': dateOfBirth?.toIso8601String(), 'female': female, 'is_active': isActive, }; } }// プロパティの型がString?やint?などのオプショナル型になっています。 // これらのプロパティが常に値を持つことが保証されている場合は、非オプショナル型を使用することで、不要なnullチェックを回避できます。 // class Destination { String? name; String? address; String? phone; String? email; String? webcontents; String? videos; String? category; int? series; double? lat; double? lon; String? sub_loc_id; int? location_id; int? list_order; String? photos; double? checkin_radious; int? auto_checkin; bool? selected = false; bool? checkedin = false; double? cp; double? checkin_point; double? buy_point; int? hidden_location; String? checkin_image; String? buypoint_image; bool forced_checkin = false; int recipt_times = 0; String? tags; bool use_qr_code = false; // QR code を使用するかどうか。default=false Destination( {this.name, this.address, this.phone, this.email, this.webcontents, this.videos, this.category, this.series, this.lat, this.lon, this.sub_loc_id, this.location_id, this.list_order, this.photos, this.checkin_radious, this.auto_checkin, this.selected, this.checkedin, this.cp, this.checkin_point, this.buy_point, this.hidden_location, this.checkin_image, this.buypoint_image, this.forced_checkin = false, this.recipt_times = 0, this.tags}); //, ... use_qr_code をDBに追加したらオープン // this.use_qr_code = false}); factory Destination.fromMap(Map json) { bool selec = json['selected'] == 0 ? false : true; bool checkin = json['checkedin'] == 0 ? false : true; bool forcedCheckin = json['forced_checkin'] == 0 ? false : true; bool useQrCode = json['use_qr_code'] == 1 ? true : false; //print("-----tags model----- ${json}"); return Destination( name: json['name'], address: json['address'], phone: json['phone'], email: json['email'], webcontents: json['webcontents'], videos: json['videos'], category: json['category'], series: json['series'], lat: json['lat'], lon: json['lon'], sub_loc_id: json['sub_loc_id'], location_id: json['location_id'], list_order: json['list_order'], photos: json['photos'], checkin_radious: json['checkin_radious'], auto_checkin: json['auto_checkin'], selected: selec, checkedin: checkin, cp: json['cp'], checkin_point: json['checkin_point'], buy_point: json['buy_point'], hidden_location: json['hidden_location'], checkin_image: json['checkin_image'], buypoint_image: json["buypoint_image"], forced_checkin: forcedCheckin, recipt_times: json["recipt_times"], tags: json["tags"] ); //, // use_qr_code: useQrCode); } Map toMap() { int sel = selected == false ? 0 : 1; int check = checkedin == false ? 0 : 1; int forcedCheckin = forced_checkin == false ? 0 : 1; return { 'name': name, 'address': address, 'phone': phone, 'email': email, 'webcontents': webcontents, 'videos': videos, 'category': category, 'series': series, 'lat': lat, 'lon': lon, 'sub_loc_id': sub_loc_id, 'location_id': location_id, 'list_order': list_order, 'photos': photos, 'checkin_radious': checkin_radious, 'auto_checkin': auto_checkin, 'selected': sel, 'checkedin': check, 'cp': cp, 'checkin_point': checkin_point, 'buy_point': buy_point, 'hidden_location': hidden_location, 'checkin_image': checkin_image, 'buypoint_image': buypoint_image, 'forced_checkin': forcedCheckin, 'recipt_times': recipt_times, 'tags': tags //, //'use_qr_code': use_qr_code }; } } // lib/models/category.dart class Category { final int id; final String categoryName; final int categoryNumber; final Duration duration; final int numOfMember; final bool family; final bool female; Category({ required this.id, required this.categoryName, required this.categoryNumber, required this.duration, required this.numOfMember, required this.family, required this.female, }); factory Category.fromJson(Map json) { return Category( id: json['id'], categoryName: json['category_name'], categoryNumber: json['category_number'], duration: Duration(seconds: json['duration']), numOfMember: json['num_of_member'], family: json['family'], female: json['female'], ); } Map toJson() { return { 'id': id, 'category_name': categoryName, 'category_number': categoryNumber, 'duration': duration.inSeconds, 'num_of_member': numOfMember, 'family': family, 'female': female, }; } }// lib/models/event.dart class Event { final int id; final String eventName; final DateTime startDatetime; final DateTime endDatetime; Event({ required this.id, required this.eventName, required this.startDatetime, required this.endDatetime, }); factory Event.fromJson(Map json) { return Event( id: json['id'], eventName: json['event_name'], startDatetime: DateTime.parse(json['start_datetime']), endDatetime: DateTime.parse(json['end_datetime']), ); } Map toJson() { return { 'id': id, 'event_name': eventName, 'start_datetime': startDatetime.toIso8601String(), 'end_datetime': endDatetime.toIso8601String(), }; } } class Rogaining { int? rog_id; int? course_id; int? user_id; int? location_id; double? lat; double? lon; int? time_stamp; Rogaining({ this.rog_id, this.course_id, this.user_id, this.location_id, this.lat, this.lon, this.time_stamp }); factory Rogaining.fromMap(Map json) { return Rogaining( rog_id: json['rog_id'], course_id: json['course_id'], user_id: json['user_id'], location_id: json['location_id'], lat: json['lat'], lon: json['lon'], time_stamp: json['time_stamp'] ); } Map toMap(){ return { 'rog_id':rog_id, 'course_id': course_id, 'user_id': user_id, 'location_id': location_id, 'lat': lat, 'lon': lon, 'time_stamp': time_stamp }; } } import 'package:flutter_map/flutter_map.dart'; class MapStateInstance { MapController? mapController; LatLngBounds? currentBounds; } enum LocationState { noGps, notInCheckin, withinCheckin } enum GameState { notStarted, startedNotCounted, startedCounted, nodeGoal } class GameInsStatetance { LocationState locationState = LocationState.noGps; GameState gameState = GameState.notStarted; } class Rog { int? id; String? team_name; String? event_code; int? user_id; int? cp_number; int? checkintime; String? image; int? rog_action_type; Rog({ this.id, this.team_name, this.event_code, this.user_id, this.cp_number, this.checkintime, this.image, this.rog_action_type }); factory Rog.fromMap(Map json){ return Rog( id: json['id'], team_name: json['team_name'], event_code: json['event_code'], user_id: json['user_id'], cp_number: json['cp_number'], checkintime: json['checkintime'], image: json['image'], rog_action_type: json['rog_action_type'] ); } Map toMap(){ return { 'id': id, 'team_name' : team_name, 'event_code': event_code, 'user_id': user_id, 'cp_number': cp_number, 'checkintime': checkintime, 'image': image, 'rog_action_type': rog_action_type }; } }// lib/models/entry.dart import 'event.dart'; import 'event.dart'; import 'team.dart'; import 'category.dart'; class Entry { final int id; final Team team; final Event event; final Category category; final DateTime date; final String owner; Entry({ required this.id, required this.team, required this.event, required this.category, required this.date, required this.owner, }); factory Entry.fromJson(Map json) { return Entry( id: json['id'], team: Team.fromJson(json['team']), event: Event.fromJson(json['event']), category: Category.fromJson(json['category']), date: DateTime.parse(json['date']), owner: json['owner'], ); } Map toJson() { return { 'id': id, 'team': team.toJson(), 'event': event.toJson(), 'category': category.toJson(), 'date': date.toIso8601String(), 'owner': owner, }; } }import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rogapp/nrog/pages/permission_page.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await FlutterMapTileCaching.initialise(); final StoreDirectory instanceA = FMTC.instance('OpenStreetMap (A)'); await instanceA.manage.createAsync(); await instanceA.metadata.addAsync( key: 'sourceURL', value: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', ); await instanceA.metadata.addAsync( key: 'validDuration', value: '14', ); await instanceA.metadata.addAsync( key: 'behaviour', value: 'cacheFirst', ); runApp( const ProviderScope(child: MyApp()), ); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color.fromARGB(255, 124, 156, 9)), useMaterial3: true, ), home: const PermissionPage()); } } // ignore: non_constant_identifier_names // 不要 String location_line_date = """ { "type": "FeatureCollection", "features": [ { "id": 1, "type": "Feature", "geometry": { "type": "MultiLineString", "coordinates": [ [ [ 135.495586, 34.701824 ], [ 136.491222, 35.447526 ], [ 137.184305, 35.265174 ], [ 137.50248, 35.859283 ] ] ] }, "properties": { "location_id": -1, "location_name": "fdsfsd", "category": "dsfds", "zip": "74120", "address": "567 Bridge Street Tulsa Oklahoma 74120", "prefecture": "fdfs", "area": "fsdfs", "city": "Ho", "photos": "dfs", "videos": "fsdfsd", "webcontents": "fsdfsd", "status": "sdfs", "portal": "fdsfsd", "group": "fdsfsd", "phone": "+94773051841", "fax": "fdsfds", "email": "info@initappz.com", "facility": "fdsfds", "remark": "fdfs", "tags": null, "parammeters": "fsdfsd", "created_at": "2022-03-10T01:23:44.717905+09:00", "last_updated_at": "2022-03-10T01:23:44.717922+09:00", "last_updated_user": 1 } }, { "id": 2, "type": "Feature", "geometry": { "type": "MultiLineString", "coordinates": [ [ [ 135.928001, 34.233306 ], [ 135.998211, 34.649591 ], [ 136.167984, 35.085641 ] ] ] }, "properties": { "location_id": 2, "location_name": "fdsfsd", "category": "dsfds", "zip": "32400", "address": "Akkaraipattu 02", "prefecture": "fdfs", "area": "fsdfs", "city": "Akkaraipattu", "photos": "dfs", "videos": "fsdfsd", "webcontents": "fsdfsd", "status": "sdfs", "portal": "fdsfsd", "group": "fdsfsd", "phone": "773051841", "fax": "fdsfds", "email": "admin@front.lk", "facility": "fdsfds", "remark": "fdfs", "tags": "fdsfsd", "parammeters": "fsdfsd", "created_at": "2022-03-10T01:46:58.157320+09:00", "last_updated_at": "2022-03-10T01:46:58.157336+09:00", "last_updated_user": 1 } } ] }""";// lib/pages/settings/settings_controller.dart import 'package:get/get.dart'; import 'package:rogapp/widgets/map_widget.dart'; class SettingsController extends GetxController { var timerDuration = Duration(seconds: 10).obs; var autoReturnDisabled = false.obs; final MapResetController mapResetController = Get.put(MapResetController()); void updateTimerDuration(int seconds) { timerDuration.value = Duration(seconds: seconds); } void setAutoReturnDisabled(bool value) { autoReturnDisabled.value = value; if (!value) { resetIdleTimer(); } } void resetIdleTimer() { mapResetController.resetIdleTimer!(); } } // lib/pages/settings/settings_binding.dart import 'package:get/get.dart'; import 'package:rogapp/pages/settings/settings_controller.dart'; class SettingsBinding extends Bindings { @override void dependencies() { Get.put(SettingsController()); // これを修正 //Get.lazyPut(() => SettingsController()); } } // lib/pages/settings/settings_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/settings/settings_controller.dart'; class SettingsPage extends GetView { const SettingsPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('設定'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'timer_duration'.tr, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Obx( () => controller.autoReturnDisabled.value ? Container() : Slider( value: controller.timerDuration.value.inSeconds.toDouble(), min: 5, max: 30, divisions: 5, label: '${controller.timerDuration.value.inSeconds}秒', onChanged: (value) { controller.updateTimerDuration(value.toInt()); }, ), ), const SizedBox(height: 8), const Text( 'マップ操作がなければ自動的に現在地に復帰します。そのタイマー秒数を入れて下さい。チェックボックスをチェックすると、自動復帰は行われなくなります。', style: TextStyle(fontSize: 14), ), const SizedBox(height: 16), Obx( () => CheckboxListTile( title: const Text('自動復帰なし'), value: controller.autoReturnDisabled.value, onChanged: (value) { controller.setAutoReturnDisabled(value!); }, ), ), ], ), ), ); } } import 'package:get/get.dart'; import 'package:get/get_state_manager/get_state_manager.dart'; class HomeController extends GetxController{ } import 'package:get/get.dart'; import 'package:rogapp/pages/home/home_controller.dart'; class HomeBinding extends Bindings{ @override void dependencies() { Get.put(HomeController()); } }import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/utils/string_values.dart'; class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State { bool _isLocationServiceEnabled = true; @override void initState() { super.initState(); /* WidgetsBinding.instance.addPostFrameCallback((_) { _checkLocationService(); // 非同期的に呼び出す }); */ _checkLocationService(); } Future _checkLocationService() async { final serviceEnabled = await Permission.location.serviceStatus.isEnabled; setState(() { _isLocationServiceEnabled = serviceEnabled; }); } void _showLocationDisabledDialog() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('location_disabled_title'.tr), content: Text('location_disabled_message'.tr), actions: [ TextButton( child: Text('ok'.tr), onPressed: () => Navigator.of(context).pop(), ), TextButton( child: Text('open_settings'.tr), onPressed: () { Navigator.of(context).pop(); openAppSettings(); }, ), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('home'.tr), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('welcome'.tr), SizedBox(height: 20), ElevatedButton( onPressed: _isLocationServiceEnabled ? () => Get.offNamed(AppPages.INDEX) : () => _showLocationDisabledDialog(), child: Text('start_app'.tr), ), ], ), ), ); } }import 'package:get/get.dart'; class LayerDrawerController extends GetxController { } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/widgets/debug_widget.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:rogapp/pages/WebView/WebView_page.dart'; // SafeAreaウィジェットを使用して、画面の安全領域内にメニューを表示しています。 // Columnウィジェットを使用して、メニューアイテムを縦に並べています。 // class DrawerPage extends StatelessWidget { DrawerPage({Key? key}) : super(key: key); final IndexController indexController = Get.find(); LogManager logManager = LogManager(); // 要検討:URLの起動に失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 // /* void _launchURL(url) async { if (!await launchUrl(url)) throw 'Could not launch $url'; } */ void _launchURL(BuildContext context,String urlString) async { try { logManager.addOperationLog('User clicked ${urlString} on the drawer'); Uri url = Uri.parse(urlString); if (await canLaunchUrl(url)) { await launchUrl(url); } else { // URLを開けない場合のフォールバック動作 // 例えば、WebViewを使用してアプリ内でURLを開く Navigator.push( context, MaterialPageRoute( builder: (context) => WebViewPage(url: urlString), ), ); } }catch(e){ // エラーメッセージを表示する ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('URLを開けませんでした: $e')), ); } } @override Widget build(BuildContext context) { return SafeArea( child: Drawer( child: Column( children: [ Container( height: 100, color: Colors.amber, child: Obx(() => Center( child: Padding( padding: const EdgeInsets.all(8.0), child: indexController.currentUser.isEmpty ? Flexible( child: Text( "drawer_title".tr, style: const TextStyle( color: Colors.black, fontSize: 20), )) : Text( indexController.currentUser[0]['user']['email'], style: const TextStyle( color: Colors.black, fontSize: 20), ), ), )), ), Obx(() => indexController.currentUser.isEmpty ? ListTile( leading: const Icon(Icons.login), title: Text("login".tr), onTap: () { Get.toNamed(AppPages.LOGIN); }, ) : ListTile( leading: const Icon(Icons.login), title: Text("logout".tr), onTap: () { indexController.logout(); Get.toNamed(AppPages.LOGIN); }, )), indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.password), title: Text("change_password".tr), onTap: () { Get.toNamed(AppPages.CHANGE_PASSWORD); }, ) : const SizedBox( width: 0, height: 0, ), indexController.currentUser.isEmpty ? ListTile( leading: const Icon(Icons.person), title: Text("sign_up".tr), onTap: () { Get.toNamed(AppPages.REGISTER); }, ) : const SizedBox( width: 0, height: 0, ), indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.password), title: Text('reset_button'.tr), onTap: () { logManager.addOperationLog('User clicked RESET button on the drawer'); // 要検討:リセット操作の確認メッセージをローカライズすることを検討してください。 // Get.defaultDialog( title: "reset_title".tr, middleText: "reset_message".tr, textConfirm: "confirm".tr, textCancel: "cancel".tr, onCancel: () => Get.back(), onConfirm: () async { DestinationController destinationController = Get.find(); DatabaseHelper databaseHelper = DatabaseHelper.instance; // ゲーム中のデータを削除 await databaseHelper.deleteAllRogaining(); await databaseHelper.deleteAllDestinations(); destinationController.resetRogaining(); //destinationController.resetRogaining(); //destinationController.deleteDBDestinations(); Get.back(); Get.snackbar( "reset_done".tr, "reset_explain".tr, backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 3), ); }, ); }, ) : const SizedBox( width: 0, height: 0, ), indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.delete_forever), title: Text("delete_account".tr), onTap: () { Get.defaultDialog( title: "delete_account_title".tr, middleText: "delete_account_middle".tr, textConfirm: "confirm".tr, textCancel: "cancel".tr, onCancel: () => Get.back(), onConfirm: () { logManager.addOperationLog('User clicked Confirm button on the account delete dialog'); String token = indexController.currentUser[0]['token']; AuthService.deleteUser(token).then((value) { if (value.isNotEmpty) { indexController.logout(); Get.toNamed(AppPages.TRAVEL); Get.snackbar("accounted_deleted".tr, "account_deleted_message".tr, backgroundColor: Colors.green, colorText: Colors.white ); } }); }, ); }, ) : const SizedBox( width: 0, height: 0, ), indexController.currentUser.isNotEmpty ? ListTile( leading: const Icon(Icons.featured_video), title: Text("rog_web".tr), onTap: () { _launchURL(context, "https://www.gifuai.net/?page_id=60043"); }, ) : const SizedBox( width: 0, height: 0, ), ListTile( leading: Icon(Icons.group), title: Text('チーム管理'), onTap: () { Get.back(); Get.toNamed(AppPages.TEAM_LIST); }, ), ListTile( leading: Icon(Icons.event), title: Text('エントリー管理'), onTap: () { Get.back(); Get.toNamed(AppPages.ENTRY_LIST); }, ), ListTile( leading: const Icon(Icons.privacy_tip), title: Text("privacy".tr), onTap: () { _launchURL(context, "https://rogaining.sumasen.net/api/privacy/"); }, ), ListTile( leading: const Icon(Icons.settings), title: Text('open_settings'.tr), onTap: () { Get.back(); // ドロワーを閉じる Get.toNamed(Routes.SETTINGS); }, ), ListTile( leading: const Icon(Icons.developer_mode), title: const Text('open_settings'), onTap: () { Get.back(); // ドロワーを閉じる Get.toNamed('/debug'); // デバッグ画面に遷移 }, ), // ListTile( // leading: const Icon(Icons.router), // title: Text("my_route".tr), // onTap: (){}, // ), // ListTile( // leading: const Icon(Icons.history_sharp), // title: Text("visit_history".tr), // onTap: (){}, // ), ], ), ), ); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/routes/app_pages.dart'; // 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 // ボタンのテキストをローカライズすることを検討してください。 // class LandingPage extends StatefulWidget { const LandingPage({ Key? key }) : super(key: key); @override State createState() => _LandingPageState(); } class _LandingPageState extends State { @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Container( width: double.infinity, height: MediaQuery.of(context).size.height, padding: const EdgeInsets.symmetric(horizontal: 30,vertical: 30), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( "こんにちは!", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 40), ), const SizedBox(height: 30,), Text("ログインを有効にして本人確認を行うと、サーバーが改善されます", textAlign: TextAlign.center, style: TextStyle( color: Colors.grey[700], fontSize: 15 ), ), Container( height: MediaQuery.of(context).size.height/3, decoration: const BoxDecoration( image:DecorationImage(image: AssetImage('assets/images/gradient_japanese_temple.jpg')) ), ), const SizedBox(height: 20.0,), MaterialButton( minWidth: double.infinity, height:60, onPressed: (){ Get.toNamed(AppPages.LOGIN); }, color: Colors.indigoAccent[400], shape: RoundedRectangleBorder( side: const BorderSide( color: Colors.black, ), borderRadius: BorderRadius.circular(40) ), child: const Text("ログイン",style: TextStyle( fontWeight: FontWeight.w600,fontSize: 16,color: Colors.white70 ), ), ), const SizedBox(height: 15.0,), MaterialButton( minWidth: double.infinity, height:60, onPressed: (){ Get.toNamed(AppPages.REGISTER); }, color: Colors.redAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40) ), child: const Text("サインアップ",style: TextStyle( fontWeight: FontWeight.w600,fontSize: 16, ),), ), ], ) ], ), ), ), ); } }import 'package:flutter/material.dart'; class CityPage extends StatelessWidget { const CityPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Container(); } }import 'package:flutter/material.dart'; class LoadingPage extends StatelessWidget { const LoadingPage({Key? key}) : super(key: key); // 要検討:ローディングインジケーターの値を固定値(0.8)にしていますが、実際のローディング進捗に合わせて動的に変更することを検討してください。 // @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, margin: const EdgeInsets.only(top: 20), child: const CircularProgressIndicator( value: 0.8, )); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; // 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 // エラーメッセージをローカライズすることを検討してください。 // ポップアップを閉じるボタンを追加することを検討してください。 // class LoginPopupPage extends StatelessWidget { LoginPopupPage({Key? key}) : super(key: key); final IndexController indexController = Get.find(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.white, appBar: AppBar( elevation: 0, backgroundColor: Colors.white, leading: IconButton( onPressed: () { Navigator.pop(context); }, icon: const Icon( Icons.arrow_back_ios, size: 20, color: Colors.black, )), ), body: indexController.currentUser.isEmpty ? SizedBox( width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Column( children: [ Column( children: [ Container( height: MediaQuery.of(context).size.height / 5, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage( 'assets/images/login_image.jpg'))), ), const SizedBox( height: 5, ), ], ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( children: [ makeInput( label: "email".tr, controller: emailController), makeInput( label: "password".tr, controller: passwordController, obsureText: true), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Container( padding: const EdgeInsets.only(top: 3, left: 3), decoration: BoxDecoration( borderRadius: BorderRadius.circular(40), ), child: Obx( (() => indexController.isLoading.value == true ? MaterialButton( minWidth: double.infinity, height: 60, onPressed: () {}, color: Colors.grey[400], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: const CircularProgressIndicator(), ) : Column( children: [ MaterialButton( minWidth: double.infinity, height: 60, onPressed: () { if (emailController.text.isEmpty || passwordController .text.isEmpty) { Get.snackbar( "no_values".tr, "email_and_password_required" .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( milliseconds: 800), //backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); return; } indexController.isLoading.value = true; indexController.login( emailController.text, passwordController.text, context); }, color: Colors.indigoAccent[400], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: const Text( "ログイン", style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white70), ), ), const SizedBox( height: 19.0, ), MaterialButton( minWidth: double.infinity, height: 50, onPressed: () { Get.toNamed(AppPages.REGISTER); }, color: Colors.redAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: Text( "sign_up".tr, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white70), ), ), const SizedBox( height: 19.0, ), MaterialButton( minWidth: double.infinity, height: 50, onPressed: () { Get.toNamed(AppPages.TRAVEL); }, color: Colors.grey, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: Text( "cancel".tr, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white70), ), ) ], )), ), )), const SizedBox( height: 20, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Padding( padding: const EdgeInsets.all(8.0), child: Text( "rogaining_user_need_tosign_up".tr, style: const TextStyle( overflow: TextOverflow.ellipsis, ), ), ), ), ], ) ], ), ], ), ) : TextButton( onPressed: () { indexController.currentUser.clear(); }, child: const Text("Already Logged in, Click to logout"), ), ); } } Widget makeInput( {label, required TextEditingController controller, obsureText = false}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87), ), const SizedBox( height: 5, ), TextField( controller: controller, obscureText: obsureText, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: (Colors.grey[400])!, ), ), border: OutlineInputBorder( borderSide: BorderSide(color: (Colors.grey[400])!), ), ), ), const SizedBox( height: 30.0, ) ], ); } import 'package:flutter/material.dart'; class ProgressPage extends StatelessWidget { const ProgressPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Container( color: Colors.transparent, child: const Center( child: CircularProgressIndicator(), ), ); } }import 'package:flutter/material.dart'; class CategoryPage extends StatelessWidget { const CategoryPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Container(); } }import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import 'package:rogapp/model/gps_data.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/utils/database_gps.dart'; import 'package:rogapp/widgets/base_layer_widget.dart'; class GpsPage extends StatefulWidget { const GpsPage({super.key}); @override State createState() => _GpsPageState(); } class _GpsPageState extends State { var gpsData = [].obs; MapController? mapController; StreamSubscription? subscription; final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); @override void initState() { super.initState(); loadGpsData(); } // 要検討:GPSデータの読み込みに失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 // void loadGpsData() async { final team_name = indexController.currentUser[0]["user"]['team_name']; final event_code = indexController.currentUser[0]["user"]["event_code"]; GpsDatabaseHelper db = GpsDatabaseHelper.instance; var data = await db.getGPSData(team_name, event_code); gpsData.value = data; //print("--- gps data ${data} ----"); } // 要検討:マーカーの形状を決定する際に、マジックナンバーが使用されています。定数を使用するなどして、コードの可読性を向上させることを検討してください。 // Widget getMarkerShape(GpsData i) { return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ InkWell( onTap: () {}, child: Container( height: 22, width: 22, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.transparent, border: Border.all( color: i.is_checkin == 0 ? Colors.blueAccent : Colors.green, width: i.is_checkin == 0 ? 0.4 : 2, style: BorderStyle.solid)), child: const Stack( alignment: Alignment.center, children: [ Icon( Icons.circle, size: 6.0, ), ], )), ), /* Container( color: Colors.transparent, child: i.is_checkin == 1 ? Text( DateTime.fromMicrosecondsSinceEpoch(i.created_at) .hour .toString() + ":" + DateTime.fromMicrosecondsSinceEpoch(i.created_at) .minute .toString(), // ":" + // DateTime.fromMicrosecondsSinceEpoch(i.created_at) // .second // .toString(), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black, )) : Container()), */ ], ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("movement_history".tr), ), body: Container( child: Obx( () => FlutterMap( mapController: mapController, options: MapOptions( maxZoom: 18.4, onMapReady: () {}, //center: LatLng(37.15319600454702, 139.58765950528198), bounds: indexController.currentBound.isNotEmpty ? indexController.currentBound[0] : LatLngBounds.fromPoints([ LatLng(35.03999881162295, 136.40587119778962), LatLng(36.642756778706904, 137.95226720406063) ]), zoom: 1, interactiveFlags: InteractiveFlag.pinchZoom | InteractiveFlag.drag, onPositionChanged: (MapPosition pos, bool hasGesture) { if (hasGesture) { indexController.currentBound = [pos.bounds!]; } }, onTap: (tapPos, cord) {}, // Hide popup when the map is tapped. ), children: [ const BaseLayer(), MarkerLayer( markers: gpsData.map((i) { return Marker( width: 30.0, // Fixed width height: 30.0, // Fixed height point: LatLng(i.lat, i.lon), child: getMarkerShape(i), alignment: Alignment.center); }).toList(), ), // MarkerLayer( // markers: gpsData.map((i) { // return Marker( // alignment: Alignment.center, // height: 32.0, // width: 120.0, // point: LatLng(i.lat, i.lon), // child: getMarkerShape(i)); // }).toList(), // ) ], ), )), ); } } // lib/pages/entry/entry_detail_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/entry/entry_controller.dart'; class EntryDetailPage extends GetView { @override Widget build(BuildContext context) { final Map arguments = Get.arguments ?? {}; final mode = Get.arguments['mode'] as String? ?? 'new'; final entry = Get.arguments['entry']; return Scaffold( appBar: AppBar( title: Text(mode == 'new' ? 'エントリー登録' : 'エントリー詳細'), ), body: Padding( padding: EdgeInsets.all(16.0), child: Obx(() => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( decoration: InputDecoration(labelText: 'イベント'), value: controller.selectedEvent.value, items: controller.events.map((event) => DropdownMenuItem( value: event, child: Text(event.eventName), )).toList(), onChanged: (value) => controller.updateEvent(value), ), DropdownButtonFormField( decoration: InputDecoration(labelText: 'チーム'), value: controller.selectedTeam.value, items: controller.teams.map((team) => DropdownMenuItem( value: team, child: Text(team.teamName), )).toList(), onChanged: (value) => controller.updateTeam(value), ), DropdownButtonFormField( decoration: InputDecoration(labelText: 'カテゴリ'), value: controller.selectedCategory.value, items: controller.categories.map((category) => DropdownMenuItem( value: category, child: Text(category.categoryName), )).toList(), onChanged: (value) => controller.updateCategory(value), ), // 日付選択ウィジェットを追加 if (mode == 'edit' && controller.isOwner()) ElevatedButton( child: Text('エントリーを削除'), onPressed: () => controller.deleteEntry(), ), ], ), ), ) ); } }// lib/pages/entry/entry_list_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/entry/entry_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; class EntryListPage extends GetView { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('エントリー管理'), actions: [ IconButton( icon: Icon(Icons.add), onPressed: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'new'}), ), ], ), body: Obx(() => ListView.builder( itemCount: controller.entries.length, itemBuilder: (context, index) { final entry = controller.entries[index]; return ListTile( title: Text(entry.event?.eventName ?? 'イベント未設定'), subtitle: Text('${entry.team?.teamName ?? 'チーム未設定'} - ${entry.category?.categoryName ?? 'カテゴリ未設定'}'), trailing: Text(entry.date?.toString().substring(0, 10) ?? '日付未設定'), onTap: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'edit', 'entry': entry}), ); }, )), ); } }// lib/entry/entry_controller.dart import 'package:get/get.dart'; import 'package:rogapp/model/entry.dart'; import 'package:rogapp/model/event.dart'; import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; import 'package:rogapp/services/api_service.dart'; class EntryController extends GetxController { late final ApiService _apiService; final entries = [].obs; final events = [].obs; final teams = [].obs; final categories = [].obs; final selectedEvent = Rx(null); final selectedTeam = Rx(null); final selectedCategory = Rx(null); final selectedDate = Rx(null); final currentEntry = Rx(null); @override void onInit() async{ super.onInit(); await Get.putAsync(() => ApiService().init()); _apiService = Get.find(); try { await Future.wait([ fetchEntries(), fetchEvents(), fetchTeams(), fetchCategories(), ]); if (Get.arguments != null && Get.arguments['entry'] != null) { currentEntry.value = Get.arguments['entry']; _initializeEntryData(); } Get.putAsync(() => ApiService().init()); }catch(e){ print('Error initializing data: $e'); } } void _initializeEntryData() { if (currentEntry.value != null) { selectedEvent.value = currentEntry.value!.event; selectedTeam.value = currentEntry.value!.team; selectedCategory.value = currentEntry.value!.category; selectedDate.value = currentEntry.value!.date; } } Future fetchEntries() async { try { final fetchedEntries = await _apiService.getEntries(); entries.assignAll(fetchedEntries); } catch (e) { print('Error fetching entries: $e'); } } Future fetchEvents() async { try { final fetchedEvents = await _apiService.getEvents(); events.assignAll(fetchedEvents); } catch (e) { print('Error fetching events: $e'); } } Future fetchTeams() async { try { final fetchedTeams = await _apiService.getTeams(); teams.assignAll(fetchedTeams); } catch (e) { print('Error fetching teams: $e'); } } Future fetchCategories() async { try { final fetchedCategories = await _apiService.getCategories(); categories.assignAll(fetchedCategories); } catch (e) { print('Error fetching categories: $e'); } } Future createEntry() async { if (selectedEvent.value == null || selectedTeam.value == null || selectedCategory.value == null || selectedDate.value == null) { Get.snackbar('Error', 'Please fill all fields'); return; } try { final newEntry = await _apiService.createEntry( selectedTeam.value!.id, selectedEvent.value!.id, selectedCategory.value!.id, selectedDate.value!, ); entries.add(newEntry); Get.back(); } catch (e) { print('Error creating entry: $e'); Get.snackbar('Error', 'Failed to create entry'); } } Future updateEntry() async { if (currentEntry.value == null) return; try { final updatedEntry = await _apiService.updateEntry( currentEntry.value!.id, selectedEvent.value!.id, selectedCategory.value!.id, selectedDate.value!, ); final index = entries.indexWhere((entry) => entry.id == updatedEntry.id); if (index != -1) { entries[index] = updatedEntry; } Get.back(); } catch (e) { print('Error updating entry: $e'); Get.snackbar('Error', 'Failed to update entry'); } } Future deleteEntry() async { if (currentEntry.value == null) return; try { await _apiService.deleteEntry(currentEntry.value!.id); entries.removeWhere((entry) => entry.id == currentEntry.value!.id); Get.back(); } catch (e) { print('Error deleting entry: $e'); Get.snackbar('Error', 'Failed to delete entry'); } } void updateEvent(Event? value) => selectedEvent.value = value; void updateTeam(Team? value) => selectedTeam.value = value; void updateCategory(Category? value) => selectedCategory.value = value; void updateDate(DateTime value) => selectedDate.value = value; bool isOwner() { // Implement logic to check if the current user is the owner of the entry return true; // Placeholder } }import 'package:get/get.dart'; import 'package:rogapp/pages/entry/entry_controller.dart'; import 'package:rogapp/pages/team/member_controller.dart'; import 'package:rogapp/services/api_service.dart'; class EntryBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => ApiService()); Get.lazyPut(() => EntryController()); } }import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/services/external_service.dart'; import 'package:rogapp/utils/const.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; // 関数 getTagText は、特定の条件に基づいて文字列から特定の部分を抽出し、返却するためのものです。 // 関数は2つのパラメータを受け取り、条件分岐を通じて結果を返します。 // // この関数は、タグのリスト(空白を含む文字列)を適切に解析し、条件に応じて特定のタグを抽出するために設計されています。 // 異なる種類の空白文字(半角、全角)で異なる分割を行い、与えられた条件(isRecept)に応じて適切なタグを選択して返却します。 // String getTagText(bool isRecept, String? tags) { // bool isRecept: 真偽値を受け取り、この値によって処理の分岐が行われます。 // String? tags: オプショナルな文字列(null が許容される)。空白文字を含む可能性のあるタグのリストを表します。 // 空のチェック: // tags が null または空文字列 ("") の場合、何も含まれていないことを意味し、関数はただちに空文字列を返します。 // if (tags == null || tags.isEmpty) { return ""; } // タグの分割: // tags が空ではない場合、文字列を空白文字で分割します。 // ここで2種類の空白文字(半角 " " と全角 " ")に対応するため、2回分割を行っています。 // tts: 半角スペース " " で分割した結果のリスト。 // ttt: 全角スペース " " で分割した結果のリスト。 // List tts = tags.split(" "); List ttt = tags.split(" "); // 条件分岐: // isRecept の値によって、処理が分岐します。 // if (isRecept) { // isRecept が true の場合: // 全角スペースで分割した結果 (ttt) の長さが半角スペースで分割した結果 (tts) の長さより大きく、 // かつ ttt が1つ以上の要素を持つ場合、ttt[1] (全角スペースで分割後の2番目の要素)を返します。 if (ttt.length > tts.length && ttt.length > 1) { return ttt[1]; } } if (!isRecept) { // isRecept が false の場合: // 全角スペースで分割したリストが半角スペースで分割したリストよりも長い場合、ttt[0] (全角スペースで分割後の最初の要素)を返します。 // 上記の条件に当てはまらない場合、半角スペースで分割したリストの最初の要素 tts[0] を返します。 // if (ttt.length > tts.length && ttt.length > 1) { return ttt[0]; } } if (!isRecept) { // 最終的な返却: // 上記の条件に何も該当しない場合(主に isRecept が true であり、全角スペースの条件に該当しない場合)、空文字列 "" を返します。 return tts[0]; } return ""; } // 要修正:画像の読み込みエラーが発生した場合のエラーハンドリングが不十分です。エラーメッセージを表示するなどの処理を追加してください。 // getDisplayImage は、Destination オブジェクトを受け取り、特定の条件に基づいて表示する画像を返す機能を持っています。 // Flutterの Image ウィジェットを使用して、適切な画像を表示します。 // // この関数は、提供された Destination オブジェクトに基づいて適切な画像を動的に選択し、 // その画像を表示するための Image ウィジェットを生成します。 // デフォルトの画像、完全なURL、またはサーバーURLと組み合わされた画像パスを使用して、条件に応じた画像の取得を試みます。 // また、エラー発生時にはデフォルト画像にフォールバックすることでユーザー体験を向上させます。 // Image getDisplayImage(Destination destination) { // Destination destination: これは Destination クラスのインスタンスで、 // CheckPointのデータを持っているオブジェクトです。 // このクラスには少なくとも phone と photos というプロパティが含まれている // // サーバーURLの取得: // serverUrl 変数には ConstValues.currentServer() メソッドから現在のサーバーのURLが取得されます。 // これは画像を取得する際の基本URLとして使用される可能性があります。 // String serverUrl = ConstValues.currentServer(); // デフォルト画像の設定: // img 変数にはデフォルトの画像が設定されます。 // これは、アセットから "assets/images/empty_image.png" をロードするための Image.asset コンストラクタを使用しています。 // Image img = Image.asset("assets/images/empty_image.png"); // 電話番号のチェック: // destination.phone が null の場合、関数は img(デフォルト画像)を返します。 // これは、phone プロパティが画像URLの代用として何らかの形で使用されることを示唆していますが、 // それが null であればデフォルト画像を使用するという意味です。 // if (destination.phone == null) { return img; } // 画像URLの構築と画像の返却: // destination.photos が http を含む場合、これはすでに完全なURLとして提供されていることを意味します。 // このURLを NetworkImage コンストラクタに渡し、Image ウィジェットを生成して返します。 // そうでない場合は、serverUrl と destination.photos を組み合わせたURLを生成して NetworkImage に渡し、画像を取得します。 // if (destination.photos!.contains('http')) { return Image( image: NetworkImage( destination.phone!, ), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } else { return Image( image: NetworkImage( '$serverUrl/media/compressed/${destination.photos}', ), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } } // getFinishImage は、ImageProvider 型のオブジェクトを返す関数で、Flutterアプリケーションで使用される画像を提供します。 // この関数は、DestinationController というクラスのインスタンスに依存しており、特定の状態に基づいて適切な画像を返します。 // // この関数は、アプリケーションの現在の状態に依存して動的に画像を提供します。 // DestinationController の photos リストに基づいて画像を選択し、リストが空の場合はデフォルトの画像を提供します。 // これにより、画像の動的な管理が可能になり、ユーザーインターフェースの柔軟性が向上します。 // また、ImageProvider クラスを使用することで、 // 画像の具体的な取得方法(ファイルからの読み込みやアセットからのロードなど)を抽象化し、 // Flutterの Image ウィジェットで直接使用できる形式で画像を返します。 // ImageProvider getFinishImage() { // DestinationControllerの取得: // destinationController は Get.find() を使用して取得されます。 // これは、GetXというFlutterの状態管理ライブラリの機能を使用して、 // 現在のアプリケーションコンテキストから DestinationController タイプのインスタンスを取得するものです。 // これにより、アプリケーションの他の部分で共有されている DestinationController の現在のインスタンスにアクセスします。 // DestinationController destinationController = Get.find(); // 画像の決定: // destinationController.photos リストが空でないかどうかをチェックします。 // このリストは、ファイルパスまたは画像リソースへの参照を含む可能性があります。 // if (destinationController.photos.isNotEmpty) { // リストが空でない場合、最初の要素 (destinationController.photos[0]) が使用されます。 // FileImage コンストラクタを使用して、このパスから ImageProvider を生成します。 // これは、ローカルファイルシステム上の画像ファイルを参照するためのものです。 // return FileImage(destinationController.photos[0]); } else { // destinationController.photos が空の場合、 // AssetImage を使用してアプリケーションのアセットからデフォルトの画像('assets/images/empty_image.png')を // ロードします。これはビルド時にアプリケーションに組み込まれる静的なリソースです。 // return const AssetImage('assets/images/empty_image.png'); } } // getReceiptImage は、ImageProvider 型を返す関数です。 // この関数は DestinationController オブジェクトに依存しており、条件に応じて特定の画像を返します。 // この関数は getFinishImage 関数と非常に似ており、ほぼ同じロジックを使用していますが、返されるデフォルトの画像が異なります。 // ImageProvider getReceiptImage() { DestinationController destinationController = Get.find(); if (destinationController.photos.isNotEmpty) { return FileImage(destinationController.photos[0]); } else { return const AssetImage('assets/images/money.png'); } } // CameraPageクラスは、目的地に応じて適切なカメラ機能とアクションボタンを提供します。 // 手動チェックイン、ゴール撮影、購入ポイント撮影など、様々なシナリオに対応しています。 // また、ロゲイニングが開始されていない場合は、StartRogainingウィジェットを表示して、ユーザーにロゲイニングの開始を促します。 // CameraPageクラスは、IndexControllerとDestinationControllerを使用して、 // 現在の状態や目的地の情報を取得し、適切なUIを構築します。 // また、写真の撮影や購入ポイントの処理など、様々な機能を提供しています。 // class CameraPage extends StatelessWidget { bool? manulaCheckin = false; // 手動チェックインを示すブール値(デフォルトはfalse) bool? buyPointPhoto = false; // 購入ポイントの写真を示すブール値(デフォルトはfalse) Destination destination; // 目的地オブジェクト Destination? dbDest; // データベースから取得した目的地オブジェクト(オプショナル) String? initImage; // 初期画像のパス(オプショナル) bool? buyQrCode = false; CameraPage( {Key? key, required this.destination, this.dbDest, this.manulaCheckin, this.buyPointPhoto, this.initImage}) : super(key: key); DestinationController destinationController = Get.find(); IndexController indexController = Get.find(); var settingGoal = false.obs; Timer? timer; // 現在の状態に基づいて、適切なアクションボタンを返します。 // 要修正:エラーハンドリングが不十分です。例外が発生した場合の処理を追加することをお勧めします。 // Widget getAction(BuildContext context) { if (manulaCheckin == true) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Wrap( spacing: 16.0, runSpacing: 8.0, children: [ Obx(() => ElevatedButton( onPressed: () { destinationController.openCamera(context, destination); }, style: ElevatedButton.styleFrom( shape: const CircleBorder(), padding: const EdgeInsets.all(20), backgroundColor: destinationController.photos.isEmpty ? Colors.red : Colors.grey[300], ), child: destinationController.photos.isEmpty ? const Text("撮影", style: TextStyle(color: Colors.white)) : const Text("再撮影", style: TextStyle(color: Colors.black)), )), Obx(() => destinationController.photos.isNotEmpty ? ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { await destinationController.makeCheckin(destination, true, destinationController.photos[0].path); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; Get.snackbar("チェックインしました。", "${destination.sub_loc_id} : ${destination.name}", backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); await Future.delayed(const Duration(seconds: 2)); Navigator.of(context).pop(true); }, child: const Text("チェックイン", style: TextStyle(color: Colors.white)), ) : Container()) ], ), ); } if (destinationController.isAtGoal.value && destinationController.isInRog.value && destination.cp == -1) { // isAtGoalがtrueで、isInRogがtrue、destination.cpが-1の場合は、ゴール用の撮影ボタンとゴール完了ボタンを表示します。 //goal return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( onPressed: () { if (settingGoal.value == false) { destinationController.openCamera(context, destination); } }, child: Text("take_photo of the clock".tr)), Obx(() => destinationController.photos.isNotEmpty ? settingGoal.value == false ? ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { // print( // "----- user isss ${indexController.currentUser[0]} -----"); settingGoal.value = true; try { int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"] ['team_name']; //print("--- _team : ${_team}-----"); String eventCode = indexController.currentUser[0] ["user"]["event_code"]; //print("--- _event_code : ${_event_code}-----"); String token = indexController.currentUser[0]["token"]; //print("--- _token : ${_token}-----"); DateTime now = DateTime.now(); String formattedDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(now); await ExternalService() .makeGoal( userId, token, team, destinationController.photos[0].path, formattedDate, eventCode) .then((value) { // print( // "---called ext api ${value['status']} ------"); if (value['status'] == 'OK') { Get.back(); destinationController.skipGps = false; Get.snackbar("目標が保存されました", "目標が正常に追加されました", backgroundColor: Colors.green, colorText: Colors.white ); destinationController.resetRogaining( isgoal: true); } else { //print("---- status ${value['status']} ---- "); Get.snackbar("目標が追加されていません", "please_try_again", backgroundColor: Colors.green, colorText: Colors.white ); } }); } on Exception catch (_) { settingGoal.value = false; } finally { settingGoal.value = false; } }, child: Text("finish_goal".tr)) : const Center( child: CircularProgressIndicator(), ) : Container()) ], ); } else if (destinationController.isInRog.value && dbDest?.checkedin != null && destination.cp != -1 && dbDest?.checkedin == true) { // isInRogがtrueで、dbDest?.checkedinがtrue、destination.cpが-1以外の場合は、購入ポイントの撮影ボタンと完了ボタンを表示します。 //make buypoint image return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Obx(() => ElevatedButton( onPressed: () { destinationController.openCamera(context, destination); }, child: destinationController.photos.isNotEmpty ? const Text("再撮影") : const Text("撮影"))), Obx(() => destinationController.photos.isNotEmpty ? ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { // print( // "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######"); await destinationController.makeBuyPoint( destination, destinationController.photos[0].path); Get.back(); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; Get.snackbar("お買い物加点を行いました。", "${destination.sub_loc_id} : ${destination.name}", backgroundColor: Colors.green, colorText: Colors.white ); Navigator.of(context).pop(true); // ここを修正 }, child: const Text("レシートの写真を撮ってください")) : Container()) ], ); } else if (destinationController.isInRog.value && dbDest?.checkedin != null && destination.cp != -1 && destination.use_qr_code == true && dbDest?.checkedin == true) { // isInRogがtrueで、dbDest?.checkedinがtrue、destination.cpが-1以外、qrCode == true の場合は、 // QRCode 撮影ボタンを表示 return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Obx(() => ElevatedButton( onPressed: () { destinationController.openCamera(context, destination); }, child: destinationController.photos.isNotEmpty ? const Text("再QR読込") : const Text("QR読込"))), Obx(() => destinationController.photos.isNotEmpty ? ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { // print( // "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######"); await destinationController.makeBuyPoint( destination, destinationController.photos[0].path); Get.back(); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; Get.snackbar("お買い物加点を行いました。", "${destination.sub_loc_id} : ${destination.name}", backgroundColor: Colors.green, colorText: Colors.white ); }, child: const Text("QRコードを読み取ってください")) : Container()) ], ); } else { // それ以外の場合は、撮影ボタンとチェックインボタンを表示します。 return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Obx(() => ElevatedButton( onPressed: () { destinationController.openCamera(context, destination); }, child: destinationController.photos.isNotEmpty ? const Text("再撮影") : const Text("撮影"))), Obx(() => destinationController.photos.isNotEmpty ? ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { // print( // "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######"); await destinationController.makeCheckin( indexController.currentDestinationFeature[0], true, destinationController.photos[0].path); //Get.back(); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; Get.snackbar( "チェックインしました", indexController.currentDestinationFeature[0].name ?? "", backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), // 表示時間を1秒に設定 ); // SnackBarの表示が終了するのを待ってからCameraPageを閉じる await Future.delayed(const Duration(seconds: 2)); Navigator.of(context).pop(true); // ここを修正 }, child: const Text("チェックイン")) : Container()) ], ); } } // void finishRog(){ // destinationController.addToRogaining(destinationController.current_lat, destinationController.current_lon, destination_id) // } @override Widget build(BuildContext context) { //print("---- photos ${destination.photos} ----"); if (buyPointPhoto == true) { // buyPointPhotoがtrueの場合は、BuyPointCameraウィジェットを返します。 //print("--- buy point camera ${destination.toString()}"); return BuyPointCamera(destination: destination); //}else if(destination.use_qr_code){ // return QRCodeScannerPage(destination: destination); } else if (destinationController.isInRog.value) { // isInRogがtrueの場合は、カメラページのUIを構築します。 // AppBarには、目的地の情報を表示します。 // ボディには、目的地の画像、タグ、アクションボタンを表示します。 //print("-----tags camera page----- ${destination.tags}"); //print("--- in normal camera ${destination.toString()}"); return Scaffold( appBar: destinationController.isInRog.value && destinationController.rogainingCounted.value == true ? AppBar( automaticallyImplyLeading: false, title: destination.cp == -1 ? Text("finishing_rogaining".tr) : Text("${destination.sub_loc_id} : ${destination.name}"), leading: IconButton( icon: Text("cancel".tr), onPressed: () { Navigator.of(context).pop(); destinationController.skip_10s = true; timer = Timer.periodic(const Duration(seconds: 10), (Timer t) { destinationController.skip_10s = false; }); }, ), centerTitle: true, ) : AppBar( automaticallyImplyLeading: false, title: Text("${destination.sub_loc_id} : ${destination.name}"), ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Padding( padding: const EdgeInsets.all(8.0), child: Center( child: Obx( () => Container( width: MediaQuery.of(context).size.width, height: 370, decoration: BoxDecoration( image: DecorationImage( image: destinationController.photos.isEmpty ? getDisplayImage(destination).image : getFinishImage(), fit: BoxFit.cover)), ), ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text(getTagText( false, destination.tags, )) // child: Obx(() => destinationController.photos.isEmpty == true // ? const Text("撮影してチェックインしてください。") // : const Text("チェックインをタップしてください。")), ), getAction(context), ], ), ), ); } else { // isInRogがfalseの場合は、StartRogainingウィジェットを返します。 return StartRogaining(); } } } // ロゲイニングが開始されていない場合に表示されるウィジェットです。 // "You have not started rogaining yet."というメッセージと、戻るボタンを表示します。 // class StartRogaining extends StatelessWidget { StartRogaining({Key? key}) : super(key: key); DestinationController destinationController = Get.find(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: Text( "Not started yet".tr, ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text("You have not started rogaining yet.".tr, style: const TextStyle(fontSize: 24)), const SizedBox( height: 40.0, ), ElevatedButton( onPressed: () { Get.back(); destinationController.skipGps = false; }, child: const Text("Back"), ), ], ), ), ); } } // 購入ポイントの写真撮影用のウィジェットです。 // 目的地の画像、タグ、撮影ボタン、完了ボタン、購入なしボタンを表示します。 // 撮影ボタンをタップすると、カメラが起動します。 // 完了ボタンをタップすると、購入ポイントの処理が行われます。 // 購入なしボタンをタップすると、購入ポイントがキャンセルされます。 // class BuyPointCamera extends StatelessWidget { BuyPointCamera({Key? key, required this.destination}) : super(key: key); Destination destination; DestinationController destinationController = Get.find(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: Text( "${destination.sub_loc_id} : ${destination.name}", ), ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Padding( padding: const EdgeInsets.all(8.0), child: Center( child: Obx( () => Container( width: MediaQuery.of(context).size.width, height: 370, decoration: BoxDecoration( image: DecorationImage( // 要修正:getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。 // image: getReceiptImage(), fit: BoxFit.cover)), ), ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: Text(getTagText(true, destination.tags)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Wrap( spacing: 16.0, runSpacing: 8.0, children: [ Obx(() => ElevatedButton( onPressed: () { destinationController.openCamera(context, destination); }, style: ElevatedButton.styleFrom( shape: const CircleBorder(), padding: const EdgeInsets.all(20), backgroundColor: destinationController.photos.isEmpty ? Colors.red : Colors.grey[300], ), child: destinationController.photos.isEmpty ? const Text("撮影", style: TextStyle(color: Colors.white)) : const Text("再撮影", style: TextStyle(color: Colors.black)), )), ElevatedButton( onPressed: () async { await destinationController.cancelBuyPoint(destination); Navigator.of(Get.context!).pop(); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; }, child: const Text("買い物なし")), Obx(() => destinationController.photos.isNotEmpty ? ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red), onPressed: () async { await destinationController.makeBuyPoint( destination, destinationController.photos[0].path); Get.back(); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; Get.snackbar("お買い物加点を行いました", "${destination.sub_loc_id} : ${destination.name}", backgroundColor: Colors.green, colorText: Colors.white); }, child: const Text("完了", style: TextStyle(color: Colors.white))) : Container()) ], ), ), ], ), ), ); } } /* class BuyPointCamera extends StatelessWidget { BuyPointCamera({Key? key, required this.destination}) : super(key: key); Destination destination; DestinationController destinationController = Get.find(); @override Widget build(BuildContext context) { //print("in camera purchase 1 ${destinationController.isInRog.value}"); return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: Text( "${destination.sub_loc_id} : ${destination.name}", ), ), body: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Padding( padding: const EdgeInsets.all(8.0), child: Center( child: Obx( () => Container( width: MediaQuery.of(context).size.width, height: 370, decoration: BoxDecoration( image: DecorationImage( // 要修正:getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。 // image: getReceiptImage(), fit: BoxFit.cover)), ), ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: Text(getTagText(true, destination.tags)), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.min, children: [ Obx(() => Row( mainAxisSize: MainAxisSize.min, children: [ ElevatedButton( onPressed: () { // print( // "in camera purchase 2 ${destinationController.isInRog.value}"); destinationController.openCamera( context, destination); }, child: destinationController.photos.isNotEmpty ? const Text("再撮影") : const Text("撮影")), const SizedBox( width: 10, ), ElevatedButton( onPressed: () async { await destinationController .cancelBuyPoint(destination); Navigator.of(Get.context!).pop(); //Get.back(); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; }, child: const Text("買い物なし")) ], )), Obx(() => destinationController.photos.isNotEmpty ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // ElevatedButton( // style: ElevatedButton.styleFrom( // backgroundColor: Colors.red), // onPressed: () async {}, // child: const Text("買物なし")), // const SizedBox( // width: 10, // ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red), onPressed: () async { // print( // "in camera purchase 3 ${destinationController.isInRog.value}"); await destinationController.makeBuyPoint( destination, destinationController.photos[0].path); Get.back(); // print( // "in camera purchase 4 ${destinationController.isInRog.value}"); destinationController.rogainingCounted.value = true; destinationController.skipGps = false; destinationController.isPhotoShoot.value = false; Get.snackbar("お買い物加点を行いました", "${destination.sub_loc_id} : ${destination.name}", backgroundColor: Colors.green, colorText: Colors.white ); }, child: const Text("完了")) ], ) : Container()) ], ), ], ), ); } } */ class QRCodeScannerPage extends StatefulWidget { QRCodeScannerPage({Key? key, required this.destination}) : super(key: key); Destination destination; @override _QRCodeScannerPageState createState() => _QRCodeScannerPageState(); } class _QRCodeScannerPageState extends State { final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); QRViewController? controller; @override void dispose() { controller?.dispose(); super.dispose(); } void _onQRViewCreated(QRViewController controller) { this.controller = controller; controller.scannedDataStream.listen((scanData) { // QRコードのデータを処理する debugPrint("scan data = ${scanData}"); String? qrCodeData = scanData.code; // qrCodeDataを使用してチェックポイントの処理を行う // 例えば、qrCodeDataからCPのIDと店名を取得し、加点処理を行う }); } @override Widget build(BuildContext context) { return Scaffold( body: QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, ), ); } }import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:rogapp/model/destination.dart'; class CustomCameraView extends StatefulWidget { final Function(String) onImageCaptured; final Destination? destination; const CustomCameraView({Key? key, required this.onImageCaptured, required this.destination}) : super(key: key); @override _CustomCameraViewState createState() => _CustomCameraViewState(); } class _CustomCameraViewState extends State { CameraController? _controller; late List _cameras; int _selectedCameraIndex = 0; double _currentScale = 1.0; FlashMode _currentFlashMode = FlashMode.off; Destination? destination; @override void initState() { super.initState(); _initializeCamera(); destination = widget.destination; } Future _initializeCamera() async { _cameras = await availableCameras(); _controller = CameraController(_cameras[_selectedCameraIndex], ResolutionPreset.medium); await _controller!.initialize(); setState(() {}); } @override void dispose() { _controller?.dispose(); super.dispose(); } Future _toggleCameraLens() async { final newIndex = (_selectedCameraIndex + 1) % _cameras.length; await _controller!.dispose(); setState(() { _controller = null; _selectedCameraIndex = newIndex; }); _controller = CameraController(_cameras[_selectedCameraIndex], ResolutionPreset.medium); await _controller!.initialize(); setState(() {}); } void _toggleFlashMode() { setState(() { _currentFlashMode = (_currentFlashMode == FlashMode.off) ? FlashMode.torch : FlashMode.off; }); _controller!.setFlashMode(_currentFlashMode); } void _zoomIn() { setState(() { _currentScale += 0.1; if (_currentScale > 5.0) _currentScale = 5.0; }); _controller!.setZoomLevel(_currentScale); } void _zoomOut() { setState(() { _currentScale -= 0.1; if (_currentScale < 1.0) _currentScale = 1.0; }); _controller!.setZoomLevel(_currentScale); } void _captureImage() async { if (_controller!.value.isInitialized) { final Directory appDirectory = await getApplicationDocumentsDirectory(); final String imagePath = path.join(appDirectory.path, '${DateTime.now()}.jpg'); final XFile imageFile = await _controller!.takePicture(); await imageFile.saveTo(imagePath); widget.onImageCaptured(imagePath); Navigator.pop(context); } } @override Widget build(BuildContext context) { if (_controller == null || !_controller!.value.isInitialized) { return Container(); } return Stack( children: [ Padding( padding: const EdgeInsets.only(top: 60.0), // 上部に60ピクセルのパディングを追加 child: CameraPreview(_controller!), ), Positioned( bottom: 120.0, left: 16.0, right: 16.0, child: Center( child: Text( destination?.tags ?? '', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), Positioned( bottom: 16.0, left: 16.0, right: 16.0, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( onPressed: _toggleFlashMode, icon: Icon( (_currentFlashMode == FlashMode.off) ? Icons.flash_off : Icons.flash_on, color: Colors.white, ), iconSize: 32, color: Colors.orange, ), GestureDetector( onTap: _captureImage, child: Container( height: 80, width: 80, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all(color: Colors.red, width: 4), ), child: const Icon(Icons.camera_alt, color: Colors.red, size: 40), ), ), IconButton( onPressed: _toggleCameraLens, icon: const Icon(Icons.flip_camera_ios, color: Colors.white), iconSize: 32, color: Colors.blue, ), ], ), ), Positioned( top: 16.0, right: 16.0, child: Column( children: [ IconButton( onPressed: _zoomIn, icon: const Icon(Icons.zoom_in, color: Colors.white), iconSize: 32, color: Colors.green, ), IconButton( onPressed: _zoomOut, icon: const Icon(Icons.zoom_out, color: Colors.white), iconSize: 32, color: Colors.green, ), ], ), ), ], ); } }import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; //import 'package:flutter_map_marker_popup/flutter_map_marker_popup.dart'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/utils/text_util.dart'; import 'package:rogapp/widgets/base_layer_widget.dart'; import 'package:rogapp/widgets/bottom_sheet_new.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_start.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_goal.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_normal_point.dart'; // FlutterMapウィジェットを使用して、地図を表示します。 // IndexControllerから目的地のリストを取得し、マーカーとしてマップ上に表示します。 // マーカーがタップされると、BottomSheetウィジェットを表示します。 // 現在地の表示、ルートの表示、ベースレイヤーの表示などの機能を提供します。 // 主なロジック: // FlutterMapウィジェットを使用して、地図を表示します。 // IndexControllerから目的地のリストを取得し、MarkerLayerを使用してマーカーを表示します。 // getMarkerShapeメソッドを使用して、マーカーの見た目をカスタマイズします。目的地の種類に応じて、異なるマーカーを表示します。 // マーカーがタップされると、festuretoDestinationメソッドを使用してGeoJSONFeatureをDestinationオブジェクトに変換し、showModalBottomSheetを使用してBottomSheetウィジェットを表示します。 // CurrentLocationLayerを使用して、現在地をマップ上に表示します。 // PolylineLayerを使用して、ルートをマップ上に表示します。getPointsメソッドを使用して、ルートの座標を取得します。 // BaseLayerを使用して、マップのベースレイヤーを表示します。 // class DestinationMapPage extends StatelessWidget { DestinationMapPage({Key? key}) : super(key: key); final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); StreamSubscription? subscription; //final PopupController _popupLayerController = PopupController(); List? getPoints() { //print("##### --- route point ${indexController.routePoints.length}"); List pts = []; for (PointLatLng p in indexController.routePoints) { LatLng l = LatLng(p.latitude, p.longitude); pts.add(l); } return pts; } // 要検討:マーカーのタップイベントを処理する際に、エラーハンドリングが不十分です。例外が発生した場合の処理を追加することをお勧めします。 // List? getMarkers() { List pts = []; //int index = -1; for (int i = 0; i < destinationController.destinations.length; i++) { Destination d = destinationController.destinations[i]; //print("^^^^ $d ^^^^"); Marker m = Marker( point: LatLng(d.lat!, d.lon!), alignment: Alignment.center, child: InkWell( onTap: () { //print("-- Destination is --- ${d.name} ------"); if (indexController.currentDestinationFeature.isNotEmpty) { indexController.currentDestinationFeature.clear(); } indexController.currentDestinationFeature.add(d); //indexController.getAction(); Widget bottomSheet = BottomSheetNew(destination: d); /* if (d.cp == -1 || d.cp == 0) { bottomSheet = BottomSheetStart(destination: d); } else if (d.cp == -2 || d.cp == 0) { bottomSheet = BottomSheetGoal(destination: d); } else { bottomSheet = BottomSheetNormalPoint(destination: d); } */ showModalBottomSheet( context: Get.context!, isScrollControlled: true, constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.85)), builder: ((context) => bottomSheet ), ).whenComplete(() { //print("---- set skip gps to false -----"); destinationController.skipGps = false; }); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( width: 20, height: 20, decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, border: Border.all( color: Colors.white, width: d.checkin_radious != null ? d.checkin_radious! : 1, ), ), child: Center( child: Text( (i + 1).toString(), style: const TextStyle(color: Colors.white), ), ), ), Container( color: Colors.yellow, child: Text( TextUtils.getDisplayText(d), style: const TextStyle( fontSize: 15.0, fontWeight: FontWeight.bold, overflow: TextOverflow.visible), )), ], ), )); pts.add(m); } return pts; } @override Widget build(BuildContext context) { return Obx((() => Stack( children: [ // indexController.is_rog_mapcontroller_loaded.value == false ? // Center(child: CircularProgressIndicator()) // : // Padding( // padding: const EdgeInsets.only(left:8.0), // child: BreadCrumbWidget(mapController:indexController.rogMapController), // ), Padding( padding: const EdgeInsets.only(top: 0.0), //child: TravelMap(), child: travelMap(), ), ], ))); } // 要検討:MapOptionsのboundsプロパティにハードコードされた座標が使用されています。これを動的に設定できるようにすることを検討してください。 // FlutterMap travelMap() { return FlutterMap( mapController: indexController.rogMapController, options: MapOptions( onMapReady: () { indexController.isRogMapcontrollerLoaded.value = true; subscription = indexController.rogMapController.mapEventStream .listen((MapEvent mapEvent) { if (mapEvent is MapEventMoveStart) {} if (mapEvent is MapEventMoveEnd) { //destinationController.is_gps_selected.value = true; //indexController.mapController!.move(c.center, c.zoom); LatLngBounds bounds = indexController.rogMapController.bounds!; indexController.currentBound.clear(); indexController.currentBound.add(bounds); if (indexController.currentUser.isEmpty) { indexController.loadLocationsBound(); } } }); }, bounds: indexController.currentBound.isNotEmpty ? indexController.currentBound[0] : LatLngBounds.fromPoints([ const LatLng(35.03999881162295, 136.40587119778962), const LatLng(36.642756778706904, 137.95226720406063) ]), initialZoom: 1, maxZoom: 42, interactiveFlags: InteractiveFlag.pinchZoom | InteractiveFlag.drag, ), children: [ const BaseLayer(), Obx( () => indexController.routePointLenght > 0 ? PolylineLayer( polylines: [ Polyline( points: getPoints()!, strokeWidth: 6.0, color: Colors.indigo), ], ) : Container(), ), CurrentLocationLayer(), MarkerLayer(markers: getMarkers()!), ], ); } } import 'package:get/get.dart'; import 'package:rogapp/main.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; class DestinationBinding extends Bindings { @override void dependencies() { Get.put(DestinationController()); restoreGame(); } } import 'dart:io'; import 'dart:typed_data'; import 'package:camera_camera/camera_camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:geojson_vi/geojson_vi.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:rogapp/main.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/model/gps_data.dart'; import 'package:rogapp/pages/camera/camera_page.dart'; import 'package:rogapp/pages/camera/custom_camera_view.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/DatabaseService.dart'; import 'package:rogapp/services/destination_service.dart'; import 'package:rogapp/services/external_service.dart'; import 'package:rogapp/services/location_service.dart'; import 'package:rogapp/services/maxtrix_service.dart'; import 'package:rogapp/services/perfecture_service.dart'; import 'package:rogapp/utils/database_gps.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/utils/location_controller.dart'; import 'package:rogapp/widgets/bottom_sheet_new.dart'; import 'dart:async'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:rogapp/widgets/debug_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:rogapp/utils/const.dart'; import 'package:logger/logger.dart'; import 'package:rogapp/pages/permission/permission.dart'; // 目的地に関連する状態管理とロジックを担当するクラスです。 // class DestinationController extends GetxController { late LocationSettings locationSettings; // 位置情報の設定を保持する変数です。 //Timer? _GPStimer; // GPSタイマーを保持する変数です。 var destinationCount = 0.obs; // 目的地の数を保持するReactive変数です。 List destinations = [].obs; // 目的地のリストを保持するObservable変数です。 double currentLat = 0.0; // 現在の緯度と経度を保持する変数です。 double currentLon = 0.0; double lastValidLat = 0.0; // 最後に中・強信号で拾ったGPS位置。 // ロゲ開始を屋内でやったら 0 のままなので、屋外で行うこと。 double lastValidLon = 0.0; DateTime lastGPSCollectedTime = DateTime.now(); // 最後にGPSデータが収集された時刻を保持する変数です。 bool shouldShowBottomSheet = true; // ボトムシートを表示すべきかどうかを示すフラグです。 static bool gps_push_started = false; // ゲームの状態を示す静的変数です。 static bool game_started = false; static bool ready_for_goal = false; bool skip_10s = false; // 10秒間のスキップフラグを示す変数です。 List currentSelectedDestinations = [].obs; // 現在選択されている目的地のリストを保持するObservable変数です。 var isInCheckin = false.obs; // ゲームの状態を示すReactive変数です。 var isInRog = false.obs; var isAtStart = false.obs; var isAtGoal = false.obs; var isPhotoShoot = false.obs; DateTime lastGoalAt = DateTime.now().subtract(const Duration(days: 1)); // 最後にゴールした時刻を保持する変数です。 //List rogainings = [].obs; bool checkingIn = false; // チェックイン中かどうかを示すフラグです。 var isGpsSelected = true.obs; // GPSが選択されているかどうかを示すReactive変数です。 BuildContext? context; // ビルドコンテキストを保持する変数です。 List gps = ["-- stating --"].obs; // GPSと位置情報の許可に関する情報を保持するObservable変数です。 List locationPermission = [" -- starting -- "].obs; var travelMode = 0.obs; // 移動モードを保持するReactive変数です。 bool skipGps = false; // GPSをスキップするかどうかを示すフラグです。 bool okToUseGPS = false; // 最新のGPS情報を使用して良いかを示すフラグ。 Map matrix = {}; // 行列データを保持する変数です。 final photos = [].obs; // 写真のリストを保持するReactive変数です。 final IndexController indexController = Get.find(); // IndexControllerのインスタンスを保持する変数です。 final LocationController locationController = Get.put(LocationController()); // LocationControllerのインスタンスを保持する変数です。 final DatabaseService dbService = DatabaseService(); // DatabaseServiceのインスタンスを保持する変数です。 int _start = 0; // 開始時刻を保持する変数です。 int chekcs = 0; // チェックポイントの数を保持する変数です。 var rogainingCounted = false.obs; // ロゲイニングがカウントされたかどうかを示すReactive変数です。 // destinationController.rogainingCountedは、現在のロゲイニングセッションでポイントがカウントされたかどうかを管理するフラグです。 // // このフラグは以下のような状況で使用されます: // // ロゲイニングを開始したとき、rogainingCountedはfalseに初期化されます。これは、まだポイントがカウントされていないことを示します。 // チェックポイントに到着し、チェックインが成功したとき、rogainingCountedはtrueに設定されます。これは、そのセッションでポイントがカウントされたことを示します。 // ロゲイニングを終了したとき、rogainingCountedは再びfalseに設定されます。これは、次のセッションに備えてフラグをリセットするためです。 // このフラグは、主に以下の目的で使用されます: // // ゴール地点でのロジックの制御:rogainingCountedがtrueの場合、つまりポイントがカウントされている場合にのみ、ゴール処理を実行できます。 // UI の更新:rogainingCountedの状態に基づいて、適切なメッセージやボタンを表示することができます。 bool isMapControllerReady = false; LatLng lastValidGPSLocation = LatLng(0, 0); DateTime lastGPSDataReceivedTime = DateTime.now(); DateTime lastPopupShownTime = DateTime.now().subtract(Duration(minutes: 10)); bool isPopupShown = false; bool hasReceivedGPSData = true; var isCheckingIn = false.obs; // チェックイン操作中はtrueになり、重複してポップアップが出ないようにするもの。 var isRouteShowing = false.obs; // ルートが表示されているかどうかを示すReactive変数 /* //==== Akira .. GPS信号シミュレーション用 ===== ここから、2024-4-5 // bool kDebugMode = true; // シミュレーションモードのフラグ RxBool isSimulationMode = RxBool(true); // シミュレーションモードを切り替えるための関数 void toggleSimulationMode(bool value) { isSimulationMode.value = value; } // 現在位置の取得メソッドを追加 LatLng getCurrentLocation() { return LatLng(lastValidLat, lastValidLon); } // // GPS信号の強弱を判断するメソッドを追加します。 // String getGpsSignalStrength() { // デバッグモードかつシミュレーションモードの場合は、シミュレートされた信号強度を返す print("kDebugMode : ${kDebugMode}, isSimulationMode : ${isSimulationMode.value}"); if (kDebugMode && isSimulationMode.value) { return locationController.getSimulatedSignalStrength(); } // 通常モードの場合は、実際の信号強度を返す final accuracy = locationController.currentPosition.value?.accuracy ?? double.infinity; if (accuracy <= 10) { return 'high'; } else if (accuracy <= 30) { return 'medium'; } else { return 'low'; } } // //==== Akira .. GPS信号シミュレーション用 ======= ここまで */ // ルートをクリアする関数です。 void clearRoute() { indexController.routePoints.clear(); indexController.routePointLenght.value = 0; isRouteShowing.value = false; } void showGPSDataNotReceivedPopup() { if (Get.context != null) { Get.dialog( AlertDialog( title: Text('GPS信号が受信できません'), content: Text('GPS信号が受信できる場所に移動してください。'), actions: [ TextButton( onPressed: () => Get.back(), child: Text('OK'), ), ], ), ); } else { // Get.contextがnullの場合の処理を追加 print('GPS signal not received, but context is null'); } } // 最後に有効なGPSデータを受け取ってから10分以上経過している場合にのみメッセージを表示するようにします。 // void checkGPSDataReceived() { if (!hasReceivedGPSData) { //debugPrint("GPS信号を全く受信していない。"); if (!isPopupShown) { // ポップアップしていない。 showGPSDataNotReceivedPopup(); lastPopupShownTime = DateTime.now(); isPopupShown = true; } } else { if (DateTime.now().difference(lastGPSDataReceivedTime).inSeconds >= 600) { // 前回GPS信号を受信してから10分経過。 if (!isPopupShown && DateTime.now().difference(lastPopupShownTime).inMinutes >= 3) { // 前回ポップアップしてから3分経過してなければ showGPSDataNotReceivedPopup(); lastPopupShownTime = DateTime.now(); isPopupShown = true; } } else { isPopupShown = false; } } } // 日時をフォーマットされた文字列に変換する関数です。 // String getFormatedTime(DateTime datetime) { return DateFormat('yyyy-MM-dd HH:mm:ss').format(datetime); } // 追加:Akira 2024-4-5 // GPS信号の精度が一定値以上の場合、GPS信号が弱いと判断する // bool isGpsSignalWeak() { final accuracy = locationController.currentPosition.value?.accuracy; if (accuracy == null) { return true; // 位置情報が取得できていない場合、GPS信号が弱いと見なす } return accuracy > 60; //return locationController.currentPosition.value?.accuracy ?? double.infinity > 50; } // Destination festuretoDestination(GeoJSONFeature fs) { GeoJSONMultiPoint mp = fs.geometry as GeoJSONMultiPoint; LatLng pt = LatLng(mp.coordinates[0][1], mp.coordinates[0][0]); //print("----- ${indexController.currentFeature[0].properties} -----"); return Destination( name: fs.properties!["location_name"], sub_loc_id: fs.properties!["sub_loc_id"], address: fs.properties!["address"], phone: fs.properties!["phone"], email: fs.properties!["email"], webcontents: fs.properties!["webcontents"], videos: fs.properties!["videos"], category: fs.properties!["category"], series: 1, lat: pt.latitude, lon: pt.longitude, location_id: fs.properties!["location_id"], list_order: 1, photos: fs.properties!["photos"], checkin_radious: fs.properties!["checkin_radius"], auto_checkin: fs.properties!["auto_checkin"] == true ? 1 : 0, cp: fs.properties!["cp"], checkin_point: fs.properties!["checkin_point"], buy_point: fs.properties!["buy_point"], selected: false, checkedin: false, hidden_location: fs.properties!["hidden_location"] == true ? 1 : 0, tags: fs.properties!["tags"]); } // 指定された目的地の位置情報に基づいてタイマーを開始する関数です。 // CP情報(fs)と現在位置からCPまでの距離distance を引数として渡します。 // Future startTimerLocation(GeoJSONFeature fs, double distance) async { //print("---- in startTimer ----"); // print("---- is in rog is $is_in_rog ----"); double checkinRadious = fs.properties!['checkin_radius'] ?? double.infinity; // CPのcheckin_radiusを取得し、checkinRadius に代入。値がなければinfinityとする。 if (checkinRadious >= distance) { // checkinRadious以内に入ったら、 indexController.currentFeature.clear(); // indexController.currentFeatureを空にします。 Destination d = festuretoDestination(fs); // festuretoDestination(fs)を呼び出し、GeoJSONFeatureオブジェクトfsからDestinationオブジェクトdを作成します。 // print("----- destination lenght is ${destinations.length} -----"); indexController.currentFeature.add(fs); // indexController.currentFeatureにfsを追加します。 //print("---- before calling startTimer ----"); await startTimer(d, distance); // startTimer(d, distance)を非同期で呼び出し、その完了を待機します。 return; } } // 指定された目的地に対してタイマーを開始する関数です。 // 目的地の位置情報を取得し、チェックイン半径内にいるかどうかを確認します。 // 写真撮影モードの場合は、ボトムシートを表示して写真撮影を行います。 // 目的地がデータベースに存在しない場合は、新しい目的地としてデータベースに挿入します。 // 目的地に応じて、チェックイン、ゴール、買い物ポイントの処理を行います。 // // 2024-4-8 akira: GPS信号が弱い場合でも、最後に取得した位置情報を使用してチェックインやゴールの処理を続行できるようになります。また、チェックインやゴールの処理では、GPS信号の精度チェックを緩和することで、GPS信号が弱い場合でもボタンを押せるようになります。 // // 要検討:エラーが発生した場合のエラーハンドリングを追加し、適切なメッセージを表示することを検討してください。 // // 引数:CPオブジェクトと現在地からCPまでの距離を渡す。 // Future startTimer(Destination d, double distance) async { //print("=== passed dest is ${d.location_id} ${d.checkedin} ===="); skipGps = true; //debugPrint("---- in startTimer ----"); DatabaseHelper db = DatabaseHelper.instance; List ds = await db.getDestinationByLatLon(d.lat!, d.lon!); //指定位置のオブジェクトのリストを取得。 Destination? dss; if (ds.isNotEmpty) { dss = ds.first; // 取得したリストが空でない場合、dss変数に最初の要素を代入します。 } // 変数を計算 double checkinRadious = d.checkin_radious ?? double.infinity; // 反応半径 bool autoCheckin = d.auto_checkin == 0 ? false : true; // 自動チェックイン bool buyPoint = dss != null && dss.buy_point != null && dss.buy_point! > 0 // 買い物ポイント ? true : false; bool buyPointImageAdded = // 買い物画像 dss != null && dss.buypoint_image != null ? true : false; bool buyPointCanceled = // 買い物キャンセル dss != null && dss.buy_point != null && dss.buy_point == 0 ? true : false; bool locationAlreadyCheckedIn = // チェックイン済みか ds.isNotEmpty && ds[0].checkedin == true ? true : false; bool isuserLoggedIn = indexController.currentUser.isNotEmpty ? true : false; // ログイン済みか /* // スタートとゴールは除外 debugPrint("startTimer CP=${d.cp}"); if (d.cp == -1 || d.cp == 0 || d.cp == -2) { skipGps = false; return; } */ // 初期化。GPS信号が強くても弱くても if (checkinRadious >= distance || checkinRadious == -1) { //currentSelectedDestinations.add(d); // 目的地として登録する。 //debugPrint("目的地の初期化"); indexController.currentDestinationFeature.clear(); indexController.currentDestinationFeature.add(d); // print( // "---- checked in as ${indexController.currentDestinationFeature[0].checkedin.toString()} ----"); } else { // ここには来ないのでは? debugPrint("検出範囲外..."); // GPS信号が弱い場合でも、チェックインやゴールの処理を続行する // comment out by Akira, 2024-4-5 // skipGps = false; // return; // GPS信号が弱い場合、最後に取得した高いまたは中程度の位置情報を使用 if (okToUseGPS) { double lastValidDistance = Geolocator.distanceBetween( lastValidLat, lastValidLon, d.lat!, d.lon! ); /* double lastValidDistance = distance.as( LengthUnit.Meter, LatLng(lastValidLat, lastValidLon), LatLng(d.lat!, d.lon!), ); */ if (checkinRadious >= lastValidDistance || checkinRadious == -1) { // 反応半径内か、距離無視CPなら indexController.currentDestinationFeature.clear(); indexController.currentDestinationFeature.add(d); } else { skipGps = false; return; } } else { skipGps = false; return; } } if (isPhotoShoot.value == true) { // 写真撮影するなら ... isPhotoShoot=True にしてる場所がない。 debugPrint("isPhotoShoot.value == true ... will camera popup"); photos.clear(); // まず既存の写真をクリア if (shouldShowBottomSheet) { // ボトムシートを使うべきなら shouldShowBottomSheet = false; if (d.cp == -1) return; // CPは開始点なら戻る。 // カメラページをポップアップ await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage(destination: d))) .whenComplete(() { shouldShowBottomSheet = true; skipGps = false; chekcs = 0; isInCheckin.value = false; }); } return; } // 写真撮影モードでない場合 if (ds.isEmpty) { debugPrint("* 目的地がない場合 ==> 検知半径=-1の場合"); // print("----- in location popup cp - ${d.cp}----"); if ((d.cp == -1 || d.cp==0 ) && DateTime.now().difference(lastGoalAt).inHours >= 24) { debugPrint("**1: 開始CPで、最後にゴールしてから24時間経過していれば、"); chekcs = 1; //start // print("~~~~ calling start ~~~~"); print("---- in start -----"); chekcs = 1; // スタート地点で前のゴールから24時間経過 isInCheckin.value = true; isAtStart.value = true; if (shouldShowBottomSheet) { shouldShowBottomSheet = false; // bottom_sheet を起動させない。 Widget bottomSheet = BottomSheetNew(destination: d); await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.85)), context: Get.context!, isScrollControlled: true, builder: ((context) => bottomSheet) ).whenComplete(() { shouldShowBottomSheet = true; // bottom_sheet 起動許可 skipGps = false; chekcs = 0; // ボトムシートモード=1, isAtStart.value = false; isInCheckin.value = false; }); } return; // 以下の条件分岐を追加 } else if (ds.isNotEmpty && ds[0].checkedin == true) { // 目的地がDBに存在し、すでにチェックインしている場合は自動ポップアップを表示しない debugPrint("チェックイン済み"); return; } else if (isInRog.value == true && indexController.rogMode.value == 1 && (locationAlreadyCheckedIn==null || locationAlreadyCheckedIn==false) && d.cp != -1 && d.cp != 0 && d.cp != -2) { debugPrint("**2: 標準CP まだチェックインしていない。"); // print("----- in location popup checkin cp - ${d.cp}----"); chekcs = 2; // 標準CP isInCheckin.value = true; if (shouldShowBottomSheet) { shouldShowBottomSheet = false; Widget bottomSheet = BottomSheetNew(destination: d); await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => bottomSheet) ).whenComplete(() { shouldShowBottomSheet = true; skipGps = false; chekcs = 0; isInCheckin.value = false; }); } return; } } // 以降、検知範囲にある場合。 //debugPrint("検知範囲にある場合"); // print("---- location checkin radious ${d.checkin_radious} ----"); // print("---- already checked in $locationAlreadyCheckedIn ----"); if ((checkinRadious >= distance || checkinRadious == -1) && locationAlreadyCheckedIn == false && isInRog.value == true && !isCheckingIn.value) { debugPrint("* 検知範囲または距離無視CPで、ゲーム中でまだチェックインしていない。"); if (autoCheckin) { // 自動チェックインなら if (!checkingIn) { debugPrint("** 自動チェックインの場合"); //print( // "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ make checkin ${d.sub_loc_id}@@@@@@@@@@@"); makeCheckin(d, true, ""); // チェックインして if (d.cp != -1 && d.cp != -2 && d.cp != 0 ) { rogainingCounted.value = true; // ゴール用チェックイン済み } skipGps = false; } return; // 戻る } else { // それ以外 debugPrint("* 自動チェックイン以外の場合"); // print("--- hidden loc ${d.hidden_location} ----"); // ask for checkin if (d.hidden_location != null && d.hidden_location == 0 && // 隠しCPフラグ==0 ... 通常CP isInRog.value == true && d.cp != -1 && d.cp != -2 && d.cp != 0) { // 隠しCPの場合、 debugPrint("**3 通常CPの場合"); chekcs = 3; isInCheckin.value = true; isCheckingIn.value = true; photos.clear(); // print("--- calling checkin ---"); if (shouldShowBottomSheet) { shouldShowBottomSheet = false; await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( destination: d, ))).whenComplete(() { shouldShowBottomSheet = true; skipGps = false; rogainingCounted.value = true; chekcs = 0; isInCheckin.value = false; isCheckingIn.value = false; }); } return; } else if (isInRog.value == true && (locationAlreadyCheckedIn==null || locationAlreadyCheckedIn==false) && d.cp != -1 && d.cp != -2 && d.cp != 0) { // 通常CP debugPrint("**4 通常CP以外の場合....どんな場合?"); chekcs = 4; isInCheckin.value = true; if (shouldShowBottomSheet) { shouldShowBottomSheet = false; Widget bottomSheet = BottomSheetNew(destination: d); await showMaterialModalBottomSheet( expand: true, context: Get.context!, backgroundColor: Colors.transparent, builder: (context) => bottomSheet ).whenComplete(() { shouldShowBottomSheet = true; skipGps = false; chekcs = 0; isInCheckin.value = false; }); } return; } } } else if ((checkinRadious >= distance || checkinRadious == -1) && locationAlreadyCheckedIn == true && buyPointImageAdded == false && ds.isNotEmpty && buyPoint == true && buyPointCanceled == false && isInRog.value == true) { // チェックイン後で買い物ポイントの場合。 debugPrint("**5 チェックイン後で買い物ポイントの場合"); chekcs = 5; isInCheckin.value = true; photos.clear(); //print("--- open buy point $buyPointImageAdded ${d.buypoint_image} ----"); if (shouldShowBottomSheet) { shouldShowBottomSheet = false; if (d.cp == -1 && d.cp != -2 && d.cp != 0) return; await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( destination: d, buyPointPhoto: true, dbDest: ds.first, ))).whenComplete(() { shouldShowBottomSheet = true; skipGps = false; rogainingCounted.value = true; chekcs = 0; isInCheckin.value = false; }); } return; } // print("---- cp --- ${d.cp} -----"); // print("--- at goal $is_at_goal ---"); // print("--- rog counted $rogaining_counted ---"); // print("--- loc already checked in $locationAlreadyCheckedIn ---"); // print( // "==== date diff is ${DateTime.now().difference(last_goal_at).inHours} ===="); if (isuserLoggedIn && (d.cp == -2 || d.cp == 0 || d.cp == -1 ) && // Goal CP locationAlreadyCheckedIn && skip_10s == false) { //check for rogaining if (isAtGoal.value == false && rogainingCounted.value) { //goal //print("---- in goal -----"); debugPrint("**5 ゴールで時計撮影の場合"); chekcs = 5; // Goal 時計撮影 isAtGoal.value = true; photos.clear(); if (shouldShowBottomSheet) { shouldShowBottomSheet = false; if (d.cp == -1) return; await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( destination: d, ))).whenComplete(() { shouldShowBottomSheet = true; skipGps = false; chekcs = 0; isAtGoal.value = false; }); } return; } else if (isInRog.value == false && indexController.rogMode.value == 1 && DateTime.now().difference(lastGoalAt).inHours >= 24) { //start //print("---- in start -----"); debugPrint("**5 スタートの場合で最後のゴールから24時間経過している場合"); chekcs = 6; // start point isAtStart.value = true; if (shouldShowBottomSheet) { shouldShowBottomSheet = false; if (d.cp != -1 && d.cp != 0) return; Widget bottomSheet = BottomSheetNew(destination: d); await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => bottomSheet) ).whenComplete(() { shouldShowBottomSheet = true; //print("----- finished start -------"); skipGps = false; chekcs = 0; isAtStart.value = false; }); } return; } } //print("==== _chekcs $chekcs ===="); if (chekcs == 0) { //debugPrint("いずれにも当てはまらないので、処理スキップ"); skipGps = false; } return; } // ロゲイニングをリセットする関数です。 // ゲームの状態をリセットし、データベースからデータを削除します。 // Future resetRogaining({bool isgoal = false}) async { //print("----- resetting --------"); isInCheckin.value = false; isInRog.value = false; isAtStart.value = false; isAtGoal.value = false; isGpsSelected.value = true; skipGps = false; ready_for_goal = false; _start = 0; chekcs = 0; rogainingCounted.value = false; DatabaseHelper db = DatabaseHelper.instance; if (isgoal == false) { await db.deleteAllDestinations(); await db.deleteAllRogaining(); } int? latgoal = await db.latestGoal(); if (latgoal != null) { lastGoalAt = DateTime.fromMicrosecondsSinceEpoch(latgoal); //print("===== last goal : $last_goal_at ====="); } dbService.updateDatabase(); } // すべての目的地を削除する関数です。 // void deleteAllDestinations() { DatabaseHelper db = DatabaseHelper.instance; db.deleteAllDestinations().then((value) { populateDestinations(); }); } // カメラを開いて写真を撮影する関数です。 // void openCamera(BuildContext context, Destination? destination) { photos.clear(); Navigator.push( context, MaterialPageRoute( builder: (_) => CustomCameraView( onImageCaptured: (imagePath) { photos.add(File(imagePath)); }, destination: destination, ), /* builder: (_) => CameraCamera( resolutionPreset: ResolutionPreset.medium, onFile: (file) { photos.add(file); Navigator.pop(context); //print("----image file is : $file----"); //setState(() {}); }, ) */ ), ); } // ルートポイントを取得する関数です。 // void getRoutePoints() { indexController.routePoints = []; indexController.routePointLenght.value = 0; DestinationService.getDestinationLine(destinations, matrix)?.then((value) { indexController.routePoints = value; indexController.routePointLenght.value = indexController.routePoints.length; }); } // 指定された緯度と経度に対応する目的地を取得する関数です。 // Future getDestinationForLatLong(double lat, double long) async { for (final d in destinations) { if (lat == d.lat && long == d.lon) { return d; } } return null; } // チェックインの呼び出しを行う関数です。 // 指定された目的地に対してチェックインの処理を行います。 // Future callforCheckin(Destination d) async { bool autoCheckin = d.auto_checkin == 0 ? false : true; print("---- f- checkin ${d.sub_loc_id} ----"); if (autoCheckin) { if (!checkingIn) { makeCheckin(d, true, ""); if (d.cp != -1 && d.cp != 0 && d.cp != -2) { rogainingCounted.value = true; } } } else { //print("--- hidden loc ${d.hidden_location} ----"); // ask for checkin //print("is rog ---- ${is_in_rog.value} ----"); if (d.hidden_location != null && d.hidden_location == 0 && isInRog.value == true && d.cp != -1 && d.cp != 0 && d.cp != -2) { chekcs = 3; photos.clear(); isInCheckin.value = true; final result = await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( manulaCheckin: true, destination: d, ))); if (result ?? false) { debugPrint("==> Checkin complete...."); if (d.buy_point != null && d.buy_point! > 0) { skipGps = true; photos.clear(); DatabaseHelper db = DatabaseHelper.instance; List ds = await db.getDestinationByLatLon(d.lat!, d.lon!); Destination? dss; if (ds.isNotEmpty) { dss = ds.first; } await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( buyPointPhoto: true, destination: d, dbDest: dss, ))).whenComplete(() { skipGps = false; rogainingCounted.value = true; chekcs = 0; isInCheckin.value = false; //Get.back(); }); } } else { debugPrint("キャンセルされました"); Get.snackbar( "キャンセルされました", "チェックインしていません。必要ならもう一度チェックポイントをタップして下さい。", backgroundColor: Colors.yellow, colorText: Colors.black, icon: const Icon( Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), ); } } else { Get.snackbar( "ロゲが始まっていません", "ロゲ開始ボタンをタップして、ロゲイニングを始める必要があります", backgroundColor: Colors.yellow, colorText: Colors.black, icon: const Icon( Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), ); } /* await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( manulaCheckin: true, destination: d, ))).whenComplete(() async { if (d.buy_point != null && d.buy_point! > 0) { skipGps = true; photos.clear(); DatabaseHelper db = DatabaseHelper.instance; List ds = await db.getDestinationByLatLon(d.lat!, d.lon!); Destination? dss; if (ds.isNotEmpty) { dss = ds.first; } await showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( buyPointPhoto: true, destination: d, dbDest: dss, ))).whenComplete(() { skipGps = false; rogainingCounted.value = true; chekcs = 0; isInCheckin.value = false; //Get.back(); }); } else { skipGps = false; chekcs = 0; isInCheckin.value = false; } }); } else { Get.snackbar( "ロゲが始まっていません", "ロゲ開始ボタンをタップして、ロゲイニングを始める必要があります", backgroundColor: Colors.yellow, colorText: Colors.white, icon: const Icon( Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3) // backgroundColor: Colors.yellow, ); } */ } } // GPSデータをデータベースに追加する関数です。 // Future addGPStoDB(double la, double ln, {isCheckin = 0}) async { //debugPrint("in addGPStoDB ${indexController.currentUser}"); try { GpsDatabaseHelper db = GpsDatabaseHelper.instance; if(indexController.currentUser.length>0){ final team_name = indexController.currentUser[0]["user"]['team_name']; final event_code = indexController.currentUser[0]["user"]["event_code"]; GpsData gps_data = GpsData( id: 0, team_name: team_name, event_code: event_code, lat: la, lon: ln, is_checkin: isCheckin, created_at: DateTime.now().millisecondsSinceEpoch); var res = await db.insertGps(gps_data); //debugPrint("Saved GPS data into DB...:${gps_data}"); } } catch (err) { print("errr ready gps ${err}"); return; } } // チェックインを確認する関数です。 // ゲームが開始されていない場合は、ゲームを開始します。 // 目的地のリストを走査し、現在位置がチェックイン半径内にある場合は、チェックインの処理を行います。 // GPSデータの送信を開始します。 // // 2024-4-8 Akira : See 2809 // checkForCheckinメソッドの再帰呼び出しをunawaitedで囲んで、非同期処理の結果を待たずに先に進むようにしました。また、再帰呼び出しの前に一定時間待機するようにしました。 // Future checkForCheckin() async { //print("--- Start of checkForCheckin function ---"); dbService.updateDatabase(); await Future.delayed(const Duration(milliseconds: 3000)); game_started = true; try { // ここで、エラー if( indexController.locations.length>0 ) { indexController.locations[0].features.forEach((fs) async { GeoJSONMultiPoint mp = fs!.geometry as GeoJSONMultiPoint; LatLng pt = LatLng(mp.coordinates[0][1], mp.coordinates[0][0]); double latFs = pt.latitude; double lonFs = pt.longitude; var distanceFs = const Distance(); double distFs = distanceFs.as(LengthUnit.Meter, LatLng(latFs, lonFs), LatLng(currentLat, currentLon)); Destination des = festuretoDestination(fs); if (distFs <= des.checkin_radious! && skipGps == false //&& des.isCheckedIn == false && des.cp!=0 && des.cp!=-1 && des.cp!=-2) { 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()); } } //print("--- 123 ---- $skip_gps----"); } catch (e) { print("An error occurred: $e"); // await checkForCheckin(); } finally { await Future.delayed(const Duration(seconds: 5)); // 一定時間待機してから再帰呼び出し //print("--- End of checkForCheckin function, calling recursively ---"); unawaited( checkForCheckin() ); } } // GPSデータをサーバーにプッシュする関数です。 // Future pushGPStoServer() async { // print( // "^^^^^^^^ ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}"); try { gps_push_started = true; ExternalService().pushGPS(); } catch (e) { //print("An error occurred: $e"); //await pushGPStoServer(); } finally { //print("--- End of pushGPStoServer function, calling recursively ---"); await Future.delayed(const Duration(seconds: 5 * 60)); await pushGPStoServer(); } } // ロゲイニングにデータを追加する関数です。 // void addToRogaining(double lat, double lon, int destinationId) async { DatabaseHelper db = DatabaseHelper.instance; List d = await db.getDestinationById(destinationId); if (d.isEmpty) { Destination df = festuretoDestination(indexController.currentFeature[0]); //print("--- made checkin ${df.location_id} ----"); makeCheckin(df, true, ""); } isInRog.value = true; saveGameState(); } // 買い物ポイントをキャンセルする関数です。 // Future cancelBuyPoint(Destination destination) async { DatabaseHelper db = DatabaseHelper.instance; await db.updateCancelBuyPoint(destination); populateDestinations(); } // 指定されたパスの画像をギャラリーに保存する関数です。 // _saveImageFromPath(String imagePath) async { try { // Read the image file from the given path File imageFile = File(imagePath); Uint8List imageBytes = await imageFile.readAsBytes(); // Save the image to the gallery final result = await ImageGallerySaver.saveImage(imageBytes); //print("--- save result --- ${result}"); } catch(e, stackTrace){ print('エラーが発生しました: $e'); print('スタックトレース: $stackTrace'); } } Future _saveImageToGallery(String imagePath) async { final status = await PermissionController.checkStoragePermission(); if(!status){ await PermissionController.requestStoragePermission(); } /* final status = await Permission.storage.status; if (!status.isGranted) { final result = await Permission.storage.request(); if (!result.isGranted) { // ユーザーがストレージの権限を拒否した場合の処理 showDialog( context: Get.context!, builder: (BuildContext context) { return AlertDialog( title: Text('ストレージの権限が必要です'), content: Text( '画像をギャラリーに保存するには、ストレージの権限が必要です。アプリの設定画面で権限を許可してください。'), actions: [ TextButton( child: Text('キャンセル'), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( child: Text('設定'), onPressed: () { Navigator.of(context).pop(); openAppSettings(); // アプリの設定画面を開く }, ), ], ); } ); return; } } */ try { final result = await ImageGallerySaver.saveFile(imagePath); print('Image saved to gallery: $result'); } catch (e) { print('Failed to save image to gallery: $e'); } } // 買い物ポイントを作成する関数です。 指定された目的地に対して買い物ポイントの処理を行います。 // // 買い物ポイントの作成に失敗した場合のエラーハンドリングを追加することを検討してください。 // Future makeBuyPoint(Destination destination, String imageurl) async { DatabaseHelper db = DatabaseHelper.instance; await db.updateBuyPoint(destination, imageurl); populateDestinations(); //await _saveImageFromPath(imageurl); await _saveImageToGallery(imageurl); if (indexController.currentUser.isNotEmpty) { double cpNum = destination.cp!; int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; //print("--- _team : ${_team}-----"); String eventCode = indexController.currentUser[0]["user"]["event_code"]; //print("--- _event_code : ${_event_code}-----"); String token = indexController.currentUser[0]["token"]; //print("--- _token : ${_token}-----"); DateTime now = DateTime.now(); String formattedDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(now); //print("------ checkin event $eventCode ------"); ExternalService() .makeCheckpoint(userId, token, formattedDate, team, cpNum.round(), eventCode, imageurl) .then((value) { //print("------Ext service check point $value ------"); }); } } // チェックインを行う関数です。 指定された目的地に対してチェックインの処理を行います。 // // 要検討:チェックインのリクエストが失敗した場合のエラーハンドリングを追加することをお勧めします。 // Future makeCheckin( Destination destination, bool action, String imageurl) async { try { // print("~~~~ calling checkin function ~~~~"); // print( // "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ressssss ${destination.sub_loc_id}@@@@@@@@@@@"); DatabaseHelper db = DatabaseHelper.instance; List ddd = await db.getDestinationByLatLon(destination.lat!, destination.lon!); if (ddd.isEmpty) { destination.checkedin = true; destination.checkin_image = imageurl; await db.insertDestination(destination); // print("~~~~ inserted into db ~~~~"); } if (imageurl == null || imageurl.isEmpty) { if (photos.isNotEmpty) { // imageurlが空の場合は、destinationのcheckin_imageプロパティを使用する debugPrint("photos = ${photos}"); imageurl = photos[0].path; } debugPrint("imageurl = ${imageurl}"); //await _saveImageFromPath(imageurl!); } if (imageurl.isNotEmpty) { await _saveImageToGallery(imageurl); } populateDestinations(); /// post to NATNAT if (indexController.currentUser.isNotEmpty) { double cpNum = destination.cp!; int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; //print("--- _team : ${_team}-----"); String eventCode = indexController.currentUser[0]["user"]["event_code"]; //print("--- _event_code : ${_event_code}-----"); String token = indexController.currentUser[0]["token"]; //print("--- _token : ${_token}-----"); DateTime now = DateTime.now(); String formattedDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(now); await addGPStoDB(currentLat, currentLon, isCheckin: 1); // print("------ checkin event $eventCode ------"); ExternalService() .makeCheckpoint( userId, token, formattedDate, team, cpNum.round(), eventCode, imageurl) .then((value) { // print("------Ext service check point $value ------"); }); } // dbService.updateDatabase(); }catch(e, stacktrace){ print("エラー:${e}"); //print("stack : ${stacktrace}"); }finally{ dbService.updateDatabase(); } } // チェックインを削除する関数です。 // Future removeCheckin(int cp) { dbService.updateDatabase(); return ExternalService().removeCheckin(cp); } // ゲームを開始する関数です。 // Future startGame() async { debugPrint("------ starting game ------"); if (game_started == false) { await checkForCheckin(); } } Timer? gpsCheckTimer; // 一定間隔でGPSデータの受信状態をチェックするタイマー void startGPSCheckTimer() { gpsCheckTimer = Timer.periodic(Duration(seconds: 5), (timer) { checkGPSDataReceived(); }); } // コントローラーの初期化時に呼び出されるライフサイクルメソッドです。 // bool inError=false; bool isRunningBackgroundGPS=false; int activeEngineCount = 0; @override void onInit() async { super.onInit(); /* WidgetsBinding.instance.addPostFrameCallback((_) async { await PermissionController.checkAndRequestPermissions(); }); */ startGPSCheckTimer(); // MapControllerの初期化完了を待機するフラグを設定 WidgetsBinding.instance.addPostFrameCallback((_) { //checkGPSDataReceived(); removed 2024-5-4 isMapControllerReady = true; }); // 要検討:エラーメッセージを表示するなどの適切な処理を追加することを検討してください。 // // locationController からデバイスの受け取るGPS情報を取得し、 // handleLocationUpdate を呼び出している。 // locationController.locationMarkerPositionStream.listen( (locationMarkerPosition) { //if (locationMarkerPosition != null) { handleLocationUpdate(locationMarkerPosition); //} }, onError: (err) { if(inError==false){ inError = true; debugPrint("Location Error: $err"); // エラーが発生した場合、locationMarkerPositionStreamControllerにエラーを追加します。 locationController.locationMarkerPositionStreamController.addError(err); // ここにエラー発生時の処理を追加します。 if (err is LocationServiceDisabledException) { // 位置情報サービスが無効になっている場合の処理 print('Location services are disabled'); Get.snackbar( 'エラー', '位置情報サービスが無効になっています。設定画面から位置情報サービスを有効にして下さい。不明な場合にはエンジニアスタッフにお問い合わせください。', backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 3), ); inError = false; } else if (err is PermissionDeniedException) { // 位置情報の権限がない場合の処理 print('Location permissions are denied'); Get.snackbar( 'エラー', '位置情報サービスが許可されていません。設定画面から岐阜ナビの位置情報サービスを許可して下さい。不明な場合にはエンジニアスタッフにお問い合わせください。', backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 3), ); inError = false; } else { // その他のエラーの場合の処理 print('Location Error: $err'); Get.snackbar( 'エラー', '位置情報サービスに問題が発生しました。位置情報サービスを再起動していますので少しお待ちください。', backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 3), ); // GPSデータのListenを再開する処理を追加 if( isRunningBackgroundGPS==false && inError ) { restartGPS(); } } } //print("Location Error: $err"); }); startGame(); //checkGPSDataReceived(); } void restartGPS(){ // GPSデータのListenを再開する処理を追加 Future.delayed(Duration(seconds: 5), () { locationController.startPositionStream(); inError=false; }); } // コントローラーのクローズ時に呼び出されるライフサイクルメソッドです。 // @override void onClose() { gpsCheckTimer?.cancel(); locationController.stopPositionStream(); super.onClose(); } // 位置情報の更新を処理する関数です。 // 現在位置とスタート地点との距離を計算します。 // 現在位置と前回の位置情報との距離と時間差を確認し、一定の条件を満たす場合はGPSデータをデータベースに追加します。 // // 要検討:GPSデータの追加に失敗した場合のエラーハンドリングを追加することをお勧めします。 // double prevLat = 0.0; // 直前の位置 double prevLon = 0.0; bool gpsDebugMode=false; void handleLocationUpdate(LocationMarkerPosition? position) async { //debugPrint("DestinationController.handleLocationUpdate"); try { //final DestinationController destinationController = Get.find(); //final signalStrength = locationController.getGpsSignalStrength(); okToUseGPS = false; if (position != null) { currentLat = position.latitude; currentLon = position.longitude; if( prevLat==0.0 ){ prevLat = currentLat; prevLon = currentLon; } lastValidGPSLocation = LatLng(currentLat, currentLon); lastValidLat = currentLat; lastValidLon = currentLon; okToUseGPS = true; lastGPSDataReceivedTime = DateTime.now(); hasReceivedGPSData = true; } else { debugPrint("....position is null...."); checkGPSDataReceived(); // 信号強度が低い場合、最後に取得した高いまたは中程度の位置情報を使用 // 但し、最初から高精度のものがない場合、どうするか? // // GPSデータが受信できない場合、最後に有効なGPSデータを使用 position = LocationMarkerPosition( latitude: lastValidGPSLocation.latitude, longitude: lastValidGPSLocation.longitude, accuracy: 0, ); currentLat = position.latitude; currentLon = position.longitude; okToUseGPS = false; /* if (lastValidLat != 0.0 && lastValidLon != 0.0) { currentLat = lastValidLat; currentLon = lastValidLon; okToUseGPS = true; } else { // GPSの届く場所に行って、信号を拾ってください。とメッセージを出す。 position = null; print("GPSの届く場所に行って、信号を拾ってください。"); Get.snackbar( "GPS信号を正確に拾えていません", "空が大きく見えるところへ行ってGPS信号を拾ってください。", icon: const Icon( Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), backgroundColor: Colors.yellow, ); } */ } if (okToUseGPS && position != null) { // スタート位置から150m離れたら、ready_for_goal if (distanceToStart() >= 150) { ready_for_goal = true; } var distance = const Distance(); double distanceToDest = distance.as( LengthUnit.Meter, LatLng(position.latitude, position.longitude), LatLng(prevLat, prevLon) ); Duration difference = lastGPSCollectedTime.difference(DateTime.now()) .abs(); // 最後にGPS信号を取得した時刻から10秒以上経過、かつ10m以上経過(普通に歩くスピード) //debugPrint("時間差:${difference.inSeconds}, 距離差:${distanceToDest}"); if (difference.inSeconds >= 10 || distanceToDest >= 30) { // print( // "^^^^^^^^ GPS data collected ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}, ^^^ ${position.latitude}, ${position.longitude}"); LogManager().addLog( "GPS : $currentLat, $currentLon - ${DateTime .now() .hour}:${DateTime .now() .minute}:${DateTime .now() .second}:${DateTime .now() .microsecond}"); if (isInRog.value) { await addGPStoDB(position.latitude, position.longitude); lastGPSCollectedTime = DateTime.now(); prevLat = position.latitude; prevLon = position.longitude; gpsDebugMode ? debugPrint("フロントエンドでのGPS保存(時間差:${difference.inSeconds}, 距離差:${distanceToDest}) : Time=${lastGPSCollectedTime}"):null; } } } } catch(e) { debugPrint("handleLocationUpdate Error: ${e}"); } finally { /* Akira , 2024-4-5 if (position != null && (position.latitude != 0 && position.longitude != 0)) { currentLat = position.latitude; currentLon = position.longitude; } */ if (okToUseGPS) { // 位置情報が取得できた場合、精度に関わらず最後の位置情報を更新 //currentLat = position.latitude; //currentLon = position.longitude; } } } // スタート地点までの距離を計算する関数です。 // double distanceToStart() { if (indexController.locations.isEmpty) { return 1000000000; } //print("=== gfs len == ${indexController.locations[0].collection.length}"); double distanceToDest = double.infinity; if (indexController.locations[0].features.isEmpty) { return distanceToDest; } GeoJSONFeature? gfs = indexController.locations[0].features.firstWhere( (element) => festuretoDestination(element!).cp == -1, orElse: () => null, // Provide a null value if no element is found ); //print("gfs : ${gfs}"); if (gfs == null) { return distanceToDest; } //final currentLocation = getCurrentLocation(); // GPS信号中以上での現在位置 Destination des = festuretoDestination(gfs); //print("=== gfs == ${des.toMap()}"); var distance = const Distance(); distanceToDest = distance.as(LengthUnit.Meter, LatLng(currentLat,currentLon), LatLng(des.lat!, des.lon!)); // LatLng(currentLat, currentLon), LatLng(des.lat!, des.lon!)); //print("==== dist==${distanceToDest}"); return distanceToDest; } // 強制チェックイン距離を取得する関数です。 // int getForcedChckinDistance(Destination dest) { if (dest.checkin_radious == -1) { return 10000000000000000; } int _retValue = 100; if (dest.cp == -1) { return 500; } Destination? ds; GeoJSONFeature? gfs = indexController.locations[0].features.firstWhere( (element) => festuretoDestination(element!).cp == -1, orElse: () => null, // Provide a null value if no element is found ); if (gfs == null) { return _retValue; } ds = festuretoDestination(gfs); var distance = const Distance(); double distanceToDest = distance.as(LengthUnit.Meter, LatLng(dest.lat!, dest.lon!), LatLng(ds.lat!, ds.lon!)); if (distanceToDest <= 500) { return 500; } //print("==== forced dist ==${distanceToDest}"); return _retValue; } // ユーザートークンを読み取る関数です。 // readUserToken() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); indexController.userToken = prefs.getString("user_token"); } // コントローラーの準備完了時に呼び出されるライフサイクルメソッドです。 // @override void onReady() async { await readUserToken(); final token = indexController.userToken; if (token != null && token.isNotEmpty) { await indexController.loadUserDetailsForToken(token); fixMapBound(token); }else { Get.toNamed(AppPages.LOGIN)!.then((value) { if (indexController.currentUser.isNotEmpty) { final tk = indexController.currentUser[0]["token"]; fixMapBound(tk); } else { Get.toNamed(AppPages.TRAVEL); PerfectureService.getSubExt("9").then((value) { if (value != null) { LatLngBounds bnds = LatLngBounds( LatLng(value[1], value[0]), LatLng(value[3], value[2])); indexController.mapController .fitBounds(bnds); //.centerZoomFitBounds(bnds); } }); } }); } // 地図のイベントリスナーを設定 indexController.mapController.mapEventStream.listen((MapEvent mapEvent) { if (mapEvent is MapEventMoveEnd) { indexController.loadLocationsBound(); } }); super.onReady(); } // 地図の境界を修正する関数です。 // void fixMapBound(String token) { //String _token = indexController.currentUser[0]["token"]; indexController.switchPage(AppPages.INDEX); if (isMapControllerReady) { LocationService.getLocationsExt(token).then((value) { if (value != null) { //print("--- loc ext is - $value ----"); LatLngBounds bnds = LatLngBounds( LatLng(value[1], value[0]), LatLng(value[3], value[2])); //print("--- bnds is - $bnds ----"); indexController.mapController.fitBounds( bnds, ); indexController.currentBound.clear(); indexController.currentBound.add(bnds); indexController.loadLocationsBound(); centerMapToCurrentLocation(); } }); } else { // MapControllerの初期化が完了していない場合は、遅延して再試行 Future.delayed(Duration(milliseconds: 100), () { fixMapBound(token); }); } } /* void fixMapBound(String token) { indexController.switchPage(AppPages.INDEX); LocationService.getLocationsExt(token).then((value) { if (value != null) { LatLngBounds bnds = LatLngBounds( LatLng(value[1], value[0]), LatLng(value[3], value[2]), ); if (indexController.isMapControllerReady.value) { indexController.mapController.fitBounds( bnds, ); indexController.currentBound.clear(); indexController.currentBound.add(bnds); indexController.loadLocationsBound(); centerMapToCurrentLocation(); } else { // MapControllerが初期化されるまで待機し、その後fitBoundsを実行 WidgetsBinding.instance.addPostFrameCallback((_) { indexController.mapController.fitBounds( bnds, ); indexController.currentBound.clear(); indexController.currentBound.add(bnds); indexController.loadLocationsBound(); centerMapToCurrentLocation(); }); } } }); } */ // 地図を現在位置に中央揃えする関数です。 // void centerMapToCurrentLocation() { //print("center is ${currentLat}, ${currentLon}"); // Akira ... 状況によって呼ぶか呼ばないか if (currentLat != 0 || currentLon != 0) { indexController.mapController.move(LatLng(currentLat, currentLon), 17.0); } } // 接続状態が変更されたときに呼び出される関数です。 // void connectionChanged(String val) { //print('----- %%%%%%%%%%%%%%%%%%%%% ----- $val'); Map res = {}; if (val == "wifi" || val == "mobile") { String token = indexController.currentUser[0]["token"]; DatabaseHelper db = DatabaseHelper.instance; db.allRogianing().then((value) { value.forEach((e) async { if (e.rog_action_type == 0) { res = await ExternalService().startRogaining(); } else if (e.rog_action_type == 1) { var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!); res = await ExternalService().makeCheckpoint( e.user_id!, token, getFormatedTime(datetime), e.team_name!, e.cp_number!, e.event_code!, e.image!); } else if (e.rog_action_type == 2) { var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!); res = await ExternalService().makeGoal( e.user_id!, token, e.team_name!, e.image!, getFormatedTime(datetime), e.event_code!); } if (res.isNotEmpty) { db.deleteRogaining(e.id!); } }); }); } } /* // 位置情報の許可を確認する関数です。 // void checkPermission() async { LocationPermission permission = await Geolocator.checkPermission(); if (permission != LocationPermission.whileInUse || permission != LocationPermission.always) { locationPermission.clear(); locationPermission.add(permission.name); permission = await Geolocator.requestPermission(); } } */ // IDに基づいて目的地を取得する関数です。 // Destination? destinationById(int id) { Destination? d; //print("--- target des - $id ----"); for (Destination ss in destinations) { //print("--- des - ${ss.location_id} ----"); if (ss.location_id == id) { d = ss; break; } } return d; } // 目的地を削除する関数です。 // void deleteDestination(Destination d) { //int id = destinations[index].location_id!; //print("---- index ${destinations[index].location_id!}-----"); for (Destination ss in destinations) { if (ss.location_id == d.location_id) { destinations.remove(ss); break; } } DatabaseHelper db = DatabaseHelper.instance; db.deleteDestination(d.location_id!).then((value) { populateDestinations(); }); dbService.updateDatabase(); } // データベースからすべての目的地を削除する関数です。 // void deleteDBDestinations() { DatabaseHelper db = DatabaseHelper.instance; db.deleteAllDestinations().then((value) { populateDestinations(); }); dbService.updateDatabase(); } // ---------- database ------------------/// // 目的地を追加する関数です。 // void addDestinations(Destination dest) { DatabaseHelper db = DatabaseHelper.instance; db.getDestinationByLatLon(dest.lat!, dest.lon!).then((value) { if (value.isNotEmpty) { db.deleteDestination(value[0].location_id!).then((value) { db.insertDestination(dest).then((value) { //print( // "----- destination controller deleted and inserted destination id $value ---- :::::"); populateDestinations(); }); }); } else { db.insertDestination(dest).then((value) { //print("----- destination controller added as new $value--- :::::"); populateDestinations(); }); } }); dbService.updateDatabase(); } // 目的地の選択状態を切り替える関数です。 // void toggleSelection(Destination dest) async { try { DatabaseHelper db = DatabaseHelper.instance; await db.toggleSelecttion(dest); destinations.clear(); db.getDestinations().then((value) { destinationCount.value = 0; currentSelectedDestinations.clear(); for (Destination d in value) { //print("------ destination controller populating destination-------- ${d.checkedin}-------- :::::"); //print("-----populated----- ${d.toMap()}"); if (d.selected!) { currentSelectedDestinations.add(d); } destinations.add(d); } destinationCount.value = destinations.length; }); } catch( e ){ print('Error in toggleSelection: $e'); Get.snackbar( "画面切り替えでエラー", "画面の切り替えができませんでした", 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), //backgroundColor: Colors.yellow, ); } } // ダイアログを表示する関数です。 // buildShowDialog(BuildContext context) { return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return const Center( child: CircularProgressIndicator(), ); }); } // 現在地点からの目的地の行列を計算する関数です。 // void destinationMatrixFromCurrentPoint(List points) { //buildShowDialog(Get.context!); MatrixService.getDestinations(points).then((mat) { //print(" matrix is ------- $mat"); matrix = mat; try { indexController.routePoints = []; indexController.routePointLenght.value = 0; DestinationService.getDestinationLine(points, matrix)?.then((value) { indexController.routePoints = value; indexController.routePointLenght.value = indexController.routePoints.length; //Get.toNamed(AppPages.TRAVEL); }); destinationCount.value = destinations.length; } catch (_) { skipGps = false; return; } finally { //Get.back(); isRouteShowing.value = true; } }); } // 目的地のリストを取得してObservable変数を更新する関数です。 // void populateDestinations() { DatabaseHelper db = DatabaseHelper.instance; destinations.clear(); db.getDestinations().then((value) { destinationCount.value = 0; for (Destination d in value) { destinations.add(d); } if (destinations.isEmpty) { rogainingCounted.value = false; } }); } // 目的地の順序を変更する関数です。 // void makeOrder(Destination d, int dir) { DatabaseHelper db = DatabaseHelper.instance; db.updateOrder(d, dir).then((value) { populateDestinations(); }); } } import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class WebViewPage extends StatelessWidget { final String url; const WebViewPage({Key? key, required this.url}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('WebView'), ), body: WebView( initialUrl: url, javascriptMode: JavascriptMode.unrestricted, ), ); } }import 'package:geojson_vi/geojson_vi.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; class SearchBarController extends GetxController { List searchResults = [].obs; @override void onInit() { IndexController indexController = Get.find(); if (indexController.locations.isNotEmpty) { for (int i = 0; i <= indexController.locations[0].features.length - 1; i++) { GeoJSONFeature p = indexController.locations[0].features[i]!; searchResults.add(p); } } super.onInit(); } } import 'package:get/get.dart'; import 'package:rogapp/pages/search/search_controller.dart'; class SearchBinding extends Bindings { @override void dependencies() { Get.put(SearchBarController()); } } import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:geojson_vi/geojson_vi.dart'; import 'package:get/get.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/search/search_controller.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_start.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_goal.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_normal_point.dart'; import 'package:rogapp/widgets/bottom_sheet_new.dart'; class SearchPage extends StatelessWidget { SearchPage({Key? key}) : super(key: key); SearchBarController searchController = Get.find(); IndexController indexController = Get.find(); Image getImage(int index) { if (searchController.searchResults[index].properties!["photos"] == null || searchController.searchResults[index].properties!["photos"] == "") { return const Image(image: AssetImage('assets/images/empty_image.png')); } else { return Image( image: NetworkImage( searchController.searchResults[index].properties!["photos"]), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, backgroundColor: Colors.white, leading: IconButton( onPressed: () { Get.back(); }, icon: const Icon( Icons.arrow_back_ios_new, color: Colors.black, )), centerTitle: true, //title: const CupertinoSearchTextField(), ), body: SingleChildScrollView( child: TypeAheadField( // textFieldConfiguration: TextFieldConfiguration( // autofocus: true, // style: DefaultTextStyle.of(context).style.copyWith( // fontStyle: FontStyle.normal, // fontSize: 15.0, // ), // decoration: InputDecoration( // border: const OutlineInputBorder(), // hintText: "検索", // prefixIcon: const Icon(Icons.search), // suffixIcon: IconButton( // icon: const Icon(Icons.clear), // onPressed: () { // // clear the text field // }, // ), // ), // ), onSelected: (GeoJSONFeature suggestion) { indexController.currentFeature.clear(); indexController.currentFeature.add(suggestion); DestinationController destinationController = Get.find(); Destination des = destinationController.festuretoDestination(suggestion); Get.back(); Widget bottomSheet = BottomSheetNew(destination: des); /* if (des.cp == -1 || des.cp == 0) { bottomSheet = BottomSheetStart(destination: des); } else if (des.cp == -2 || des.cp == 0) { bottomSheet = BottomSheetGoal(destination: des); } else { bottomSheet = BottomSheetNormalPoint(destination: des); } */ showModalBottomSheet( constraints: BoxConstraints.loose(Size(Get.width, Get.height * 0.75)), isScrollControlled: true, context: context, builder: ((context) => bottomSheet) ); }, suggestionsCallback: (pattern) async { return searchController.searchResults .where((GeoJSONFeature element) => element .properties!["location_name"] .toString() .contains(pattern)) .toList(); //return await }, itemBuilder: (context, GeoJSONFeature suggestion) { return ListTile( title: Text(suggestion.properties!["location_name"]), subtitle: suggestion.properties!["category"] != null ? Text(suggestion.properties!["category"]) : const Text(""), //leading: getImage(index), ); }, ), ), ); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; class RegisterPage extends StatelessWidget { final IndexController indexController = Get.find(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); TextEditingController confirmPasswordController = TextEditingController(); RegisterPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, backgroundColor: Colors.white, appBar: AppBar( elevation: 0, backgroundColor: Colors.white, leading: IconButton( onPressed: () { Navigator.pop(context); }, icon: const Icon( Icons.arrow_back_ios, size: 20, color: Colors.black, )), ), body: SafeArea( child: SingleChildScrollView( child: SizedBox( height: MediaQuery.of(context).size.height, width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( children: [ Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( "sign_up".tr, // "サインアップ" style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, ), ), const SizedBox( height: 20, ), Text( "create_account".tr, // アカウントを無料で作成します。 style: TextStyle( fontSize: 15, color: Colors.grey[700], ), ), const SizedBox( height: 30, ) ], ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( children: [ makeInput(label: "email".tr, controller: emailController), // メールアドレス makeInput( label: "password".tr, controller: passwordController, obsureText: true), // パスワード makeInput( label: "confirm_password".tr, controller: confirmPasswordController, obsureText: true) // パスワード再確認 ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Container( padding: const EdgeInsets.only(top: 3, left: 3), decoration: BoxDecoration( borderRadius: BorderRadius.circular(40), border: const Border( bottom: BorderSide(color: Colors.black), top: BorderSide(color: Colors.black), right: BorderSide(color: Colors.black), left: BorderSide(color: Colors.black))), child: MaterialButton( minWidth: double.infinity, height: 60, onPressed: () { if (passwordController.text != confirmPasswordController.text) { Get.snackbar( "no_match".tr, "password_does_not_match".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), // backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); } if (emailController.text.isEmpty || passwordController.text.isEmpty) { Get.snackbar( "no_values".tr, "email_and_password_required".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), //backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); return; } indexController.isLoading.value = true; indexController.register(emailController.text, passwordController.text, context); }, color: Colors.redAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: Text( "sign_up".tr, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, ), ), ), ), ), const SizedBox( height: 20, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible(child: Text("already_have_account".tr)), TextButton( onPressed: () { Get.toNamed(AppPages.LOGIN); }, child: Text( "login".tr, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18), ), ), ], ) ], ), ], ), ), ), ), ); } } Widget makeInput( {label, required TextEditingController controller, obsureText = false}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87), ), const SizedBox( height: 5, ), TextField( controller: controller, obscureText: obsureText, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: (Colors.grey[400])!, ), ), border: OutlineInputBorder( borderSide: BorderSide(color: (Colors.grey[400])!), ), ), ), const SizedBox( height: 30, ) ], ); } import 'package:get/get.dart'; import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/services/api_service.dart'; class TeamBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => ApiService()); Get.lazyPut(() => TeamController()); } }// lib/pages/team/team_list_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/api_service.dart'; class TeamListPage extends GetView { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('チーム管理'), actions: [ IconButton( icon: Icon(Icons.add), onPressed: () => Get.toNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'new'}), ), ], ), body: FutureBuilder( future: Get.putAsync(() => ApiService().init()), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return Obx(() => ListView.builder( itemCount: controller.teams.length, itemBuilder: (context, index) { final team = controller.teams[index]; return ListTile( title: Text(team.teamName), subtitle: Text('${team.category.categoryName} - ${team.zekkenNumber}'), //trailing: Text('メンバー: ${team.memberCount}'), trailing: Text('メンバー: (メンバー数表示予定)'), onTap: () => Get.toNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'edit', 'team': team}), ); }, )); } else { return Center(child: CircularProgressIndicator()); } }, ), ); } }import 'package:get/get.dart'; import 'package:rogapp/pages/team/member_controller.dart'; import 'package:rogapp/services/api_service.dart'; class MemberBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => ApiService()); Get.lazyPut(() => MemberController()); } }// lib/controllers/team_controller.dart import 'package:get/get.dart'; import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; import 'package:rogapp/model/user.dart'; import 'package:rogapp/services/api_service.dart'; class TeamController extends GetxController { late final ApiService _apiService; final teams = [].obs; final categories = [].obs; final teamMembers = [].obs; final selectedCategory = Rx(null); final currentUser = Rx(null); final isLoading = true.obs; final error = RxString(''); @override void onInit() async{ super.onInit(); try { //await Get.putAsync(() => ApiService().init()); _apiService = Get.find(); await Future.wait([ fetchTeams(), fetchCategories(), getCurrentUser(), ]); }catch(e){ print("Team Controller error: $e"); }finally{ isLoading.value = false; } } Future fetchTeams() async { try { final fetchedTeams = await _apiService.getTeams(); teams.assignAll(fetchedTeams); } catch (e) { print('Error fetching teams: $e'); } } Future fetchCategories() async { try { final fetchedCategories = await _apiService.getCategories(); categories.assignAll(fetchedCategories); } catch (e) { print('Error fetching categories: $e'); } } Future getCurrentUser() async { try { final user = await _apiService.getCurrentUser(); currentUser.value = user; } catch (e) { print('Error getting current user: $e'); } } Future createTeam(String teamName, int categoryId) async { try { final newTeam = await _apiService.createTeam(teamName, categoryId); teams.add(newTeam); } catch (e) { print('Error creating team: $e'); } } Future updateTeam(int teamId, String teamName, int categoryId) async { try { final updatedTeam = await _apiService.updateTeam(teamId, teamName, categoryId); final index = teams.indexWhere((team) => team.id == teamId); if (index != -1) { teams[index] = updatedTeam; } } catch (e) { print('Error updating team: $e'); } } Future deleteTeam(int teamId) async { try { await _apiService.deleteTeam(teamId); teams.removeWhere((team) => team.id == teamId); } catch (e) { print('Error deleting team: $e'); } } Future fetchTeamMembers(int teamId) async { try { final members = await _apiService.getTeamMembers(teamId); teamMembers.assignAll(members); } catch (e) { print('Error fetching team members: $e'); } } void updateTeamName(String value) { // Update local state } void updateCategory(Category? value) { selectedCategory.value = value; } }// lib/pages/team/team_detail_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/api_service.dart'; class TeamDetailPage extends GetView { @override Widget build(BuildContext context) { final mode = Get.arguments['mode']; final team = Get.arguments['team']; return Scaffold( appBar: AppBar( title: Text(mode == 'new' ? '新規チーム作成' : 'チーム詳細'), actions: [ if (mode == 'edit') IconButton( icon: Icon(Icons.add), onPressed: () => Get.toNamed(AppPages.MEMBER_DETAIL, arguments: {'mode': 'new', 'teamId': team.id}), ), ], ), body: Padding( padding: EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( decoration: InputDecoration(labelText: 'チーム名'), onChanged: (value) => controller.updateTeamName(value), ), DropdownButtonFormField( decoration: InputDecoration(labelText: 'カテゴリ'), value: controller.selectedCategory.value, items: controller.categories.map((category) => DropdownMenuItem( value: category, child: Text(category.categoryName), )).toList(), onChanged: (value) => controller.updateCategory(value), ), if (mode == 'edit') Text('ゼッケン番号: ${team.zekkenNumber}'), Obx(() { final currentUser = controller.currentUser.value; return Text('所有者: ${currentUser?.email ?? "未設定"}'); }), SizedBox(height: 20), Text('メンバーリスト', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), Expanded( child: Obx(() => ListView.builder( itemCount: controller.teamMembers.length, itemBuilder: (context, index) { final member = controller.teamMembers[index]; return ListTile( title: Text('${member.lastname}, ${member.firstname}'), onTap: () => Get.toNamed(AppPages.MEMBER_DETAIL, arguments: {'mode': 'edit', 'member': member}), ); }, )), ), ], ), ), ); } }// lib/pages/team/member_detail_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/team/member_controller.dart'; class MemberDetailPage extends GetView { @override Widget build(BuildContext context) { final mode = Get.arguments['mode']; final member = Get.arguments['member']; return Scaffold( appBar: AppBar( title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'), ), body: Padding( padding: EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (mode == 'edit' && member.email != null) Text('Email: ${member.email}'), if (mode == 'new' || (mode == 'edit' && member.email == null)) TextField( decoration: InputDecoration(labelText: 'Email'), onChanged: (value) => controller.updateEmail(value), ), TextField( decoration: InputDecoration(labelText: '姓'), onChanged: (value) => controller.updateLastname(value), ), TextField( decoration: InputDecoration(labelText: '名'), onChanged: (value) => controller.updateFirstname(value), ), // 誕生日選択ウィジェットを追加 if (mode == 'edit') Text('ステータス: ${controller.getMemberStatus()}'), if (mode == 'edit' && controller.getMemberStatus() == '招待中') ElevatedButton( child: Text('招待メールを再送信'), onPressed: () => controller.resendInvitation(), ), if (mode == 'edit') ElevatedButton( child: Text('メンバーから除外'), onPressed: () => controller.deleteMember(), ), ], ), ), ); } }// lib/controllers/member_controller.dart import 'package:get/get.dart'; import 'package:rogapp/model/user.dart'; import 'package:rogapp/services/api_service.dart'; class MemberController extends GetxController { late final ApiService _apiService; final int teamId = 0; final member = Rx(null); final email = ''.obs; final firstname = ''.obs; final lastname = ''.obs; final dateOfBirth = Rx(null); @override void onInit() async{ super.onInit(); await Get.putAsync(() => ApiService().init()); _apiService = Get.find(); if (Get.arguments != null && Get.arguments['member'] != null) { member.value = Get.arguments['member']; _initializeMemberData(); } } void _initializeMemberData() { if (member.value != null) { email.value = member.value!.email ?? ''; firstname.value = member.value!.firstname ?? ''; lastname.value = member.value!.lastname ?? ''; dateOfBirth.value = member.value!.dateOfBirth; } } Future createMember(int teamId) async { try { final newMember = await _apiService.createTeamMember( teamId, email.value, firstname.value, lastname.value, dateOfBirth.value, ); member.value = newMember; } catch (e) { print('Error creating member: $e'); } } Future updateMember() async { if (member.value == null) return; int? memberId = member.value?.id; try { final updatedMember = await _apiService.updateTeamMember( teamId, memberId!, firstname.value, lastname.value, dateOfBirth.value, ); member.value = updatedMember; } catch (e) { print('Error updating member: $e'); } } Future deleteMember() async { if (member.value == null) return; int? memberId = member.value?.id; try { await _apiService.deleteTeamMember(teamId,memberId!); member.value = null; Get.back(); } catch (e) { print('Error deleting member: $e'); } } Future resendInvitation() async { if (member.value == null || member.value!.email == null) return; int? memberId = member.value?.id; try { await _apiService.resendMemberInvitation(memberId!); Get.snackbar('Success', 'Invitation resent successfully'); } catch (e) { print('Error resending invitation: $e'); Get.snackbar('Error', 'Failed to resend invitation'); } } void updateEmail(String value) => email.value = value; void updateFirstname(String value) => firstname.value = value; void updateLastname(String value) => lastname.value = value; void updateDateOfBirth(DateTime value) => dateOfBirth.value = value; String getMemberStatus() { if (member.value == null) return ''; if (member.value!.email == null) return '未登録'; if (member.value!.isActive) return '承認済'; return '招待中'; } }import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/widgets/debug_widget.dart'; class ChangePasswordPage extends StatelessWidget { ChangePasswordPage({Key? key}) : super(key: key); LogManager logManager = LogManager(); IndexController indexController = Get.find(); TextEditingController oldPasswordController = TextEditingController(); TextEditingController newPasswordController = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.white, appBar: AppBar( elevation: 0, backgroundColor: Colors.white, leading: IconButton( onPressed: () { logManager.addOperationLog('User clicked cancel button on the drawer'); Navigator.pop(context); }, icon: const Icon( Icons.arrow_back_ios, size: 20, color: Colors.black, )), ), body: SizedBox( width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( children: [ Column( children: [ Text( "change_password".tr, style: const TextStyle(fontSize: 24.0), ), const SizedBox( height: 30, ), ], ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( children: [ makeInput( label: "old_password".tr, controller: oldPasswordController), makeInput( label: "new_password".tr, controller: newPasswordController, obsureText: true), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Container( padding: const EdgeInsets.only(top: 3, left: 3), decoration: BoxDecoration( borderRadius: BorderRadius.circular(40), ), child: Obx( (() => indexController.isLoading.value == true ? MaterialButton( minWidth: double.infinity, height: 60, onPressed: () {}, color: Colors.grey[400], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: const CircularProgressIndicator(), ) : Column( children: [ MaterialButton( minWidth: double.infinity, height: 60, onPressed: () { if (oldPasswordController .text.isEmpty || newPasswordController .text.isEmpty) { logManager.addOperationLog('User tried to login with blank old password ${oldPasswordController.text} or new password ${newPasswordController.text}.'); Get.snackbar( "no_values".tr, "values_required".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( milliseconds: 800), //backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); return; } indexController.isLoading.value = true; indexController.changePassword( oldPasswordController.text, newPasswordController.text, context); //indexController.login(oldPasswordController.text, newPasswordController.text, context); }, color: Colors.indigoAccent[400], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: Text( "login".tr, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white70), ), ), const SizedBox( height: 10.0, ), ], )), ), )), const SizedBox( height: 20, ), const Row( mainAxisAlignment: MainAxisAlignment.center, children: [], ) ], ), ], ), )); } Widget makeInput( {label, required TextEditingController controller, obsureText = false}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87), ), const SizedBox( height: 5, ), TextField( controller: controller, obscureText: obsureText, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: (Colors.grey[400])!, ), ), border: OutlineInputBorder( borderSide: BorderSide(color: (Colors.grey[400])!), ), ), ), const SizedBox( height: 30.0, ) ], ); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; class SubPerfPage extends StatelessWidget { SubPerfPage({Key? key}) : super(key: key); IndexController indexController = Get.find(); @override Widget build(BuildContext context) { debugPrint("SubPerfPage ---->"); return Scaffold( appBar: AppBar( title: const Text("Select Sub Perfecture"), ), body: ListView.builder( itemCount: indexController.subPerfs.length, itemBuilder: (context, index){ return ListTile( onTap: (){ indexController.dropdownValue = indexController.perfectures[index][0]["id"].toString(); //indexController.populateForPerf(indexController.dropdownValue, indexController.mapController!); Get.back(); }, title: Text(indexController.perfectures[index][0]["adm1_ja"].toString()), ); }, ), ); } }import 'dart:io'; import 'package:flutter/material.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'package:get/get.dart'; class HistoryPage extends StatefulWidget { const HistoryPage({super.key}); @override State createState() => _HistoryPageState(); } class _HistoryPageState extends State { DatabaseHelper db = DatabaseHelper.instance; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("pass_history".tr), ), body: SingleChildScrollView( child: Column( children: [ FutureBuilder( // 要検討:スナップショットのエラーハンドリングが行われていますが、具体的なエラーメッセージを表示するようにすることをお勧めします。 // future: db.getDestinations(), builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { return Center( child: Text( '${snapshot.error} occurred', style: const TextStyle(fontSize: 18), ), ); } else if (snapshot.hasData) { final dests = snapshot.data; if (dests!.isNotEmpty) { debugPrint("----- 通過履歴表示 -----"); return SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: ListView.builder( itemCount: dests.length, itemBuilder: (ctx, index) { //print("--- photo ${dests[index].checkin_image!} ----"); return Padding( padding: const EdgeInsets.all(8.0), child: CustomWidget( // 要検討:画像のサイズがハードコードされています。画像のサイズを動的に設定できるようにすることを検討してください。 title: dests[index].name!, subtitle: "${dests[index].sub_loc_id} : ${dests[index].name}", image1: dests[index].checkin_image != null ? Image.file( File(dests[index].checkin_image!)) : null, image2: dests[index].buypoint_image != null ? Image.file(File( dests[index].buypoint_image!)) : null, ), ); })); } else { return Center(child: Text("no_checkin_yet".tr)); } } } else if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator(), ); } return Container(); }), ], ), ), ); } } class CustomWidget extends StatelessWidget { final Image? image1; final Image? image2; final String title; final String subtitle; const CustomWidget({ super.key, this.image1, this.image2, required this.title, required this.subtitle, }); @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 104, // 50 (width of each image) + 2 (space between images) + 2*1 (padding on both sides) child: Row( children: [ if (image1 != null) SizedBox( width: 50, height: 100, child: image1, ), if (image1 != null && image2 != null) const SizedBox(width: 2), if (image2 != null) SizedBox( width: 50, height: 100, child: image2, ), ], ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), maxLines: null, // Allows the text to wrap onto an unlimited number of lines ), Text( subtitle, style: const TextStyle(fontSize: 16), maxLines: null, // Allows the text to wrap onto an unlimited number of lines ), ], ), ), ], ); } } // import 'package:flutter/material.dart'; // import 'package:get/get.dart'; // import 'package:rogapp/pages/index/index_controller.dart'; // class MainPerfPage extends StatelessWidget { // MainPerfPage({Key? key}) : super(key: key); // IndexController indexController = Get.find(); // @override // Widget build(BuildContext context) { // return Scaffold( // appBar: AppBar( // title: const Text("Select Main Perfecture"), // ), // body: ListView.builder( // itemCount: indexController.perfectures.length, // itemBuilder: (context, index){ // return ListTile( // onTap: (){ // indexController.dropdownValue = indexController.perfectures[index][0]["id"].toString(); // indexController.populateForPerf(indexController.dropdownValue, indexController.mapController); // Get.back(); // }, // title: Text(indexController.perfectures[index][0]["adm1_ja"].toString()), // ); // }, // ), // ); // } // }import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/drawer/drawer_page.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/widgets/list_widget.dart'; import 'package:rogapp/widgets/map_widget.dart'; import 'package:rogapp/utils/location_controller.dart'; // index_page.dartファイルの主な内容です。 // このファイルは、アプリのメインページのUIを構築し、各機能へのナビゲーションを提供しています。 // また、IndexControllerとDestinationControllerを使用して、状態管理と各種機能の実装を行っています。 // // MapWidgetとListWidgetは、それぞれ別のファイルで定義されているウィジェットであり、マップモードとリストモードの表示を担当しています。 // // 全体的に、index_page.dartはアプリのメインページの構造を定義し、他のコンポーネントやページへの橋渡しを行っているファイルです。 // // 要検討:GPSデータの表示アイコンをタップした際のエラーハンドリングを追加することをお勧めします。 // MapWidgetとListWidgetの切り替えにObxを使用していますが、パフォーマンスを考慮して、必要な場合にのみウィジェットを再構築するようにしてください。 // DestinationControllerのisSimulationModeを使用してGPS信号の強弱をシミュレーションしていますが、本番環境では適切に実際のGPS信号を使用するようにしてください。 // IndexPageクラスは、GetViewを継承したStatelessWidgetです。このクラスは、アプリのメインページを表すウィジェットです。 // class IndexPage extends GetView { IndexPage({Key? key}) : super(key: key); // IndexControllerとDestinationControllerのインスタンスを取得しています。 // final LocationController locationController = Get.find(); final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); // buildメソッドは、ウィジェットのUIを構築するメソッドです。 // ここでは、WillPopScopeウィジェットを使用して、端末の戻るボタンが押された際の動作を制御しています。 // @override Widget build(BuildContext context) { return PopScope( canPop: false, child: Scaffold( // // Scaffoldウィジェットを使用して、アプリのメインページのレイアウトを構築しています。 // drawer: DrawerPage(), appBar: AppBar( 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 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)), IconButton( onPressed: () { Get.toNamed(AppPages.HISTORY); }, icon: const Icon(Icons.history)), IconButton( onPressed: () { final tk = indexController.currentUser[0]["token"]; if (tk != null) { destinationController.fixMapBound(tk); } }, icon: const Icon(Icons.refresh)), InkWell( onTap: () { Get.toNamed(AppPages.SEARCH); }, child: Container( height: 32, width: 75, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(25), ), child: const Center( child: Icon(Icons.search), ), ), ), //CatWidget(indexController: indexController,), // // デバッグ時のみリロードボタンの横にGPS信号レベルの設定ボタンを設置し、 // タップすることでGPS信号の強弱をシミュレーションできるようにする // Akira 2024-4-5 // /* Obx(() { if (locationController.isSimulationMode) { return DropdownButton( value: locationController.getSimulatedSignalStrength(), onChanged: (value) { //debugPrint("DropDown changed!"); locationController.setSimulatedSignalStrength(value!); }, items: ['low', 'medium', 'high', 'real'] .map>((String value) { return DropdownMenuItem( value: value, child: Text(value), ); }).toList(), ); } else { return Container(); } }), */ ], ), // bottomNavigationBar: BottomAppBar( // child: Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ // 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); // }, // ), // ))), // ), // ], // ), // ), // // マップモードとリストモードを切り替えるためのボタンです。 // floatingActionButton: FloatingActionButton( onPressed: () { indexController.toggleMode(); }, elevation: 1.0, // // Obxウィジェットを使用して、indexController.mode.valueの値に基づいて、MapWidgetまたはListWidgetを表示しています。 // child: Obx( () => indexController.mode.value == 0 ? const Image(image: AssetImage('assets/images/list2.png')) : const Image(image: AssetImage('assets/images/map.png')), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, // // bodyには、SafeAreaウィジェットを使用して、画面の安全な領域内にUIを構築しています。 // body: SafeArea( child: Column( children: [ Expanded( child: Obx( () => indexController.mode.value == 0 ? MapWidget() : const ListWidget(), )) ], ), ), ), ); } } import 'package:get/get.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/utils/location_controller.dart'; class IndexBinding extends Bindings { @override void dependencies() { Get.put(IndexController()); Get.put(LocationController()); Get.put(DestinationController()); } } import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; 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:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/services/location_service.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/widgets/debug_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:rogapp/services/api_service.dart'; import '../../main.dart'; class IndexController extends GetxController with WidgetsBindingObserver { List locations = [].obs; List currentFeature = [].obs; List currentDestinationFeature = [].obs; List perfectures = [].obs; List currentBound = [].obs; List subPerfs = [].obs; List areas = [].obs; List customAreas = [].obs; List cats = [].obs; List currentCat = [].obs; List> currentUser = >[].obs; List currentAction = [].obs; List routePoints = [].obs; var routePointLenght = 0.obs; double currentLat = 0.0, currentLon = 0.0; var isLoading = false.obs; var isRogMapcontrollerLoaded = false.obs; var isCustomAreaSelected = false.obs; RxBool isMapControllerReady = RxBool(false); // MapControllerの初期化状態を管理するフラグ //final mapControllerReadyStream = Stream.value(false); // MapControllerの初期化状態を通知するためのストリーム MapController mapController = MapController(); MapController rogMapController = MapController(); LogManager logManager = LogManager(); String? userToken; // mode = 0 is map mode, mode = 1 list mode var mode = 0.obs; // master mode, rog or selection var rogMode = 1.obs; var desinationMode = 1.obs; bool showPopup = true; String dropdownValue = "9"; String subDropdownValue = "-1"; String areaDropdownValue = "-1"; String cateogory = "-all-"; ConnectivityResult connectionStatus = ConnectivityResult.none; var connectionStatusName = "".obs; final Connectivity _connectivity = Connectivity(); late StreamSubscription _connectivitySubscription; void toggleMode() { if (mode.value == 0) { mode += 1; } else { mode -= 1; } } void toggleDestinationMode() { if (desinationMode.value == 0) { desinationMode.value += 1; } else { desinationMode.value -= 1; } } void switchPage(String page) { ////print("######## ${currentUser[0]["user"]["id"]}"); switch (page) { case AppPages.INDEX: { rogMode.value = 0; //print("-- rog mode is ctrl is ${rog_mode.value}"); Get.toNamed(page); } break; case AppPages.TRAVEL: { rogMode.value = 1; //Get.back(); //Get.off(DestnationPage(), binding: DestinationBinding()); } break; case AppPages.LOGIN: { rogMode.value = 2; Get.toNamed(page); } break; default: { rogMode.value = 1; Get.toNamed(AppPages.INDEX); } } } Future _checkLocationPermission() async { if (Get.context == null) { debugPrint('Get.context is null in _checkLocationPermission'); return; } LocationPermission permission = await Geolocator.checkPermission(); //permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { debugPrint('GPS : Denied'); await showLocationPermissionDeniedDialog(); } else if (permission == LocationPermission.deniedForever) { debugPrint('GPS : Denied forever'); await showLocationPermissionDeniedDialog(); }else if (permission == LocationPermission.whileInUse){ debugPrint('GPS : While-In-Use'); await showLocationPermissionDeniedDialog(); }else{ debugPrint("Permission is no problem...."); } } // 追加 Future showLocationPermissionDeniedDialog() async { if (Get.context != null) { print('Showing location permission denied dialog'); await showDialog( context: Get.context!, barrierDismissible: false, builder: (BuildContext context) { return WillPopScope( onWillPop: () async => false, child: AlertDialog( title: Text('location_permission_needed_title'.tr), content: Text('location_permission_needed_main'.tr), actions: [ TextButton( onPressed: () { logManager.addOperationLog("User tapped confirm button for location permission required."); Navigator.of(context).pop(); }, child: Text('confirm'.tr), ), ], ), ); }, ); } else { print('Get.context is null in showLocationPermissionDeniedDialog'); // Get.contextがnullの場合の処理 print('Location permission denied, but context is null'); } } @override void onInit() { _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); super.onInit(); WidgetsBinding.instance?.addObserver(this); _startLocationService(); // アプリ起動時にLocationServiceを開始する initializeApiService(); print('IndexController onInit called'); // デバッグ用の出力を追加 } Future initializeApiService() async { if (currentUser.isNotEmpty) { // 既にログインしている場合 await Get.putAsync(() => ApiService().init()); // 必要に応じて追加の初期化処理 } } /* void checkPermission() { debugPrint("MapControllerの初期化が完了したら、位置情報の許可をチェックする"); _checkLocationPermission(); } */ @override void onClose() { _connectivitySubscription.cancel(); WidgetsBinding.instance?.removeObserver(this); _stopLocationService(); // アプリ終了時にLocationServiceを停止する super.onClose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { if (!_isLocationServiceRunning()) { _startLocationService(); } } else if (state == AppLifecycleState.paused) { _stopLocationService(); } } bool _isLocationServiceRunning() { // LocationServiceが実行中かどうかを確認する処理を実装する // 例えば、SharedPreferencesにサービスの状態を保存するなど // ここでは簡単のために、常にfalseを返すようにしています return false; } void _startLocationService() async { const platform = MethodChannel('location'); try { logManager.addOperationLog("Called start location service."); await platform.invokeMethod('startLocationService'); } on PlatformException catch (e) { print("Failed to start location service: '${e.message}'."); } } void _stopLocationService() async { const platform = MethodChannel('location'); try { logManager.addOperationLog("Called stop location service."); await platform.invokeMethod('stopLocationService'); } on PlatformException catch (e) { print("Failed to stop location service: '${e.message}'."); } } /* @override void onReady() async { await readUserToken(); final token = userToken; if (token != null && token.isNotEmpty) { await loadUserDetailsForToken(token); fixMapBound(token); } else { // ユーザートークンが存在しない場合はログイン画面にリダイレクト Get.offAllNamed(AppPages.LOGIN); } // 地図のイベントリスナーを設定 indexController.mapController.mapEventStream.listen((MapEvent mapEvent) { if (mapEvent is MapEventMoveEnd) { indexController.loadLocationsBound(); } }); super.onReady(); } */ Future _updateConnectionStatus(ConnectivityResult result) async { connectionStatus = result; connectionStatusName.value = result.name; } Future initConnectivity() async { late ConnectivityResult result; // Platform messages may fail, so we use a try/catch PlatformException. try { result = await _connectivity.checkConnectivity(); } on PlatformException catch (_) { //print('Couldn\'t check connectivity status --- $e'); return; } return _updateConnectionStatus(result); } LatLngBounds boundsFromLatLngList(List list) { double? x0, x1, y0, y1; for (LatLng latLng in list) { if (x0 == null || x1 == null || y0 == null || y1 == null) { x0 = x1 = latLng.latitude; y0 = y1 = latLng.longitude; } else { if (latLng.latitude > x1) x1 = latLng.latitude; if (latLng.latitude < x0) x0 = latLng.latitude; if (latLng.longitude > y1) y1 = latLng.longitude; if (latLng.longitude < y0) y0 = latLng.longitude; } } logManager.addOperationLog("Called boundsFromLatLngList (${x1!},${y1!})-(${x0!},${y0!})."); return LatLngBounds(LatLng(x1!, y1!), LatLng(x0!, y0!)); } // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // void login(String email, String password, BuildContext context) async{ AuthService.login(email, password).then((value) async { print("------- logged in user details ######## $value ###### --------"); if (value.isNotEmpty) { logManager.addOperationLog("User logged in : ${value}."); // Navigator.pop(context); print("--------- user details login ----- $value"); changeUser(value); // ログイン成功後、api_serviceを初期化 await Get.putAsync(() => ApiService().init()); } else { logManager.addOperationLog("User failed login : ${email} , ${password}."); isLoading.value = false; Get.snackbar( "login_failed".tr, "check_login_id_or_password".tr, backgroundColor: Colors.red, colorText: Colors.white, icon: const Icon(Icons.error, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), //backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); } }); } // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // void changePassword( String oldpassword, String newpassword, BuildContext context) { String token = currentUser[0]['token']; ////print("------- change password ######## ${currentUser[0]['token']} ###### --------"); AuthService.changePassword(oldpassword, newpassword, token).then((value) { ////print("------- change password ######## $value ###### --------"); if (value.isNotEmpty) { logManager.addOperationLog("User successed to change password : ${oldpassword} , ${newpassword}."); isLoading.value = false; Navigator.pop(context); if (rogMode.value == 1) { switchPage(AppPages.TRAVEL); } else { switchPage(AppPages.INDEX); } } else { logManager.addOperationLog("User failed to change password : ${oldpassword} , ${newpassword}."); Get.snackbar( 'failed'.tr, 'password_change_failed_please_try_again'.tr, backgroundColor: Colors.red, colorText: Colors.white, icon: const Icon(Icons.error, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(milliseconds: 800), //backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); } }); isLoading.value = false; } /* void logout() async { locations.clear(); DatabaseHelper db = DatabaseHelper.instance; db.deleteAllDestinations().then((value) { DestinationController destinationController = Get.find(); destinationController.populateDestinations(); }); currentUser.clear(); cats.clear(); // ユーザートークンをデバイスから削除 final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.remove("user_token"); } */ void logout() async { logManager.addOperationLog("User logout : ${currentUser} ."); saveGameState(); locations.clear(); DatabaseHelper db = DatabaseHelper.instance; db.deleteAllDestinations().then((value) { DestinationController destinationController = Get.find(); destinationController.populateDestinations(); }); currentUser.clear(); cats.clear(); final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.remove("user_token"); } // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // void register(String email, String password, BuildContext context) { AuthService.register(email, password).then((value) { if (value.isNotEmpty) { debugPrint("ユーザー登録成功:${email}, ${password}"); logManager.addOperationLog("User tried to register new account : ${email} , ${password} ."); currentUser.clear(); currentUser.add(value); isLoading.value = false; Navigator.pop(context); Get.toNamed(AppPages.INDEX); } else { debugPrint("ユーザー登録失敗:${email}, ${password}"); logManager.addOperationLog("User failed to register new account : ${email} , ${password} ."); isLoading.value = false; Get.snackbar( 'failed'.tr, // 失敗 'user_registration_failed_please_try_again'.tr, // ユーザー登録に失敗しました。 backgroundColor: Colors.red, colorText: Colors.white, icon: const Icon(Icons.error, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), //backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); } }); } void saveToDevice(String val) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString("user_token", val); print("saveToDevice: ${val}"); } /* void changeUser(Map value, {bool replace = true}) { print("---- change user to $value -----"); currentUser.clear(); currentUser.add(value); if (replace) { saveToDevice(currentUser[0]["token"]); } isLoading.value = false; loadLocationsBound(); if (currentUser.isNotEmpty) { rogMode.value = 0; } else { rogMode.value = 1; } print('--- c rog mode --- ${rogMode.value}'); Get.toNamed(AppPages.INDEX); } */ void changeUser(Map value, {bool replace = true}) { currentUser.clear(); currentUser.add(value); if (replace) { saveToDevice(currentUser[0]["token"]); } isLoading.value = false; loadLocationsBound(); if (currentUser.isNotEmpty) { rogMode.value = 0; restoreGame(); } else { rogMode.value = 1; } Get.toNamed(AppPages.INDEX); } loadUserDetailsForToken(String token) async { AuthService.userForToken(token).then((value) { print("----token val-- $value ------"); if (value![0]["user"].isEmpty) { Get.toNamed(AppPages.LOGIN); return; } changeUser(value[0], replace: false); }); } /* Old code void loadLocationsBound() { if (isCustomAreaSelected.value == true) { return; } locations.clear(); String cat = currentCat.isNotEmpty ? currentCat[0] : ""; if (currentCat.isNotEmpty && currentCat[0] == "-all-") { cat = ""; } LatLngBounds bounds = mapController.bounds!; currentBound.clear(); currentBound.add(bounds); ////print(currentCat); LocationService.loadLocationsBound( bounds.southWest.latitude, bounds.southWest.longitude, bounds.northWest.latitude, bounds.northWest.longitude, bounds.northEast.latitude, bounds.northEast.longitude, bounds.southEast.latitude, bounds.southEast.longitude, cat) .then((value) { ////print("---value length ------ ${value!.collection.length}"); if (value == null) { return; } if (value.features.isEmpty) { if (showPopup == false) { return; } Get.snackbar( "Too many Points", "please zoom in", icon: const Icon(Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 2), backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); showPopup = false; //Get.showSnackbar(GetSnackBar(message: "Too many points, please zoom in",)); } if (value.features.isNotEmpty) { ////print("---- added---"); locations.add(value); } }); } */ // 2024-04-03 Akira .. Update the code . See ticket 2800. // // 2024-4-8 Akira : See 2809 // IndexControllerクラスでは、Future.delayedの呼び出しをunawaitedで囲んで、 // 非同期処理の結果を待たずに先に進むようにしました。これにより、メモリリークを防ぐことができます // // 要検討:Future.delayedを使用して非同期処理を待たずに先に進むようにしていますが、 // これによってメモリリークが発生する可能性があります。非同期処理の結果を適切に処理することを検討してください。 // void loadLocationsBound() async { if (isCustomAreaSelected.value == true) { return; } // MapControllerの初期化が完了するまで待機 await waitForMapControllerReady(); locations.clear(); String cat = currentCat.isNotEmpty ? currentCat[0] : ""; if (currentCat.isNotEmpty && currentCat[0] == "-all-") { cat = ""; } /* // Akira Add 2024-4-6 if( mapController.controller == null ) { print("操作が完了する前にMapControllerまたはウィジェットが破棄されました。"); isLoading.value = true; // ローディング状態をtrueに設定 return; } // */ LatLngBounds bounds = mapController.bounds!; if (bounds == null) { // MapControllerの初期化が完了していない場合は処理を行わない return; } currentBound.clear(); currentBound.add(bounds); isLoading.value = true; // ローディング状態をtrueに設定 //print("bounds --- (${bounds.southWest.latitude},${bounds.southWest.longitude}),(${bounds.northWest.latitude},${bounds.northWest.longitude}),(${bounds.northEast.latitude},${bounds.northEast.longitude}),(${bounds.southEast.latitude},${bounds.southEast.longitude})"); // 要検討:APIからのレスポンスがnullの場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 try { final value = await LocationService.loadLocationsBound( bounds.southWest.latitude, bounds.southWest.longitude, bounds.northWest.latitude, bounds.northWest.longitude, bounds.northEast.latitude, bounds.northEast.longitude, bounds.southEast.latitude, bounds.southEast.longitude, cat ); /* if (value == null) { // APIからのレスポンスがnullの場合 print("LocationService.loadLocationsBound からの回答がnullのため、マップをリロード"); DestinationController destinationController = Get.find(); // 追加 final tk = currentUser[0]["token"]; // 追加 if (tk != null) { // 追加 destinationController.fixMapBound(tk); // 追加 } // 追加 return; } */ isLoading.value = false; // ローディング状態をfalseに設定 if (value == null) { // APIからのレスポンスがnullの場合 print("LocationService.loadLocationsBound からの回答がnullです"); } else { if (value.features.isEmpty) { if (showPopup == false) { return; } Get.snackbar( "Too many Points", "please zoom in", backgroundColor: Colors.yellow, colorText: Colors.white, icon: const Icon(Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), ); showPopup = false; } if (value.features.isNotEmpty) { locations.add(value); } } /* if (value != null && value.features.isEmpty) { if (showPopup == false) { return; } Get.snackbar( "Too many Points", "please zoom in", backgroundColor: Colors.yellow, colorText: Colors.white, icon: const Icon( Icons.assistant_photo_outlined, size: 40.0, color: Colors.blue), snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), //backgroundColor: Colors.yellow, ); showPopup = false; } if (value != null && value.features.isNotEmpty) { locations.add(value); } */ } catch ( e) { print("Error in loadLocationsBound: $e"); // エラーが発生した場合のリトライ処理や適切なエラーメッセージの表示を行う // 例えば、一定時間後に再度loadLocationsBound()を呼び出すなど } } //===Akira 追加:2024-4-6 #2800 // 要検討:MapControllerの初期化が完了するまで待機していますが、タイムアウトを設定することを検討してください。 // 初期化に時間がかかりすぎる場合、ユーザーにわかりやすいメッセージを表示するようにしてください。 // Future waitForMapControllerReady() async { if (!isMapControllerReady.value) { await Future.doWhile(() async { await Future.delayed(const Duration(milliseconds: 100)); return !isMapControllerReady.value; }); } } //===Akira 追加:2024-4-6 #2800 void setBound(LatLngBounds bounds) { currentBound.clear(); currentBound.add(bounds); } GeoJSONFeature? getFeatureForLatLong(double lat, double long) { if (locations.isNotEmpty) { GeoJSONFeature? foundFeature; locations[0].features.forEach((i) { GeoJSONMultiPoint p = i!.geometry as GeoJSONMultiPoint; if (p.coordinates[0][1] == lat && p.coordinates[0][0] == long) { foundFeature = i; } }); return foundFeature; } return null; } } import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:rogapp/services/location_service.dart'; import 'dart:developer' as developer; import 'dart:async'; class PermissionController { static bool _isRequestingPermission = false; static Completer? _permissionCompleter; static Future checkLocationPermissions() async { final locationPermission = await Permission.location.status; final whenInUsePermission = await Permission.locationWhenInUse.status; final alwaysPermission = await Permission.locationAlways.status; return locationPermission == PermissionStatus.granted && (whenInUsePermission == PermissionStatus.granted || alwaysPermission == PermissionStatus.granted); } static Future checkAndRequestPermissions() async { if (_isRequestingPermission) { return _permissionCompleter!.future; } _isRequestingPermission = true; _permissionCompleter = Completer(); bool hasPermissions = await checkLocationPermissions(); if (!hasPermissions) { bool userAgreed = await showLocationDisclosure(); if (userAgreed) { try { await requestAllLocationPermissions(); hasPermissions = await checkLocationPermissions(); } catch (e) { print('Error requesting location permissions: $e'); hasPermissions = false; } } else { print('User did not agree to location usage'); hasPermissions = false; // アプリを終了 SystemNavigator.pop(); } } _isRequestingPermission = false; _permissionCompleter!.complete(hasPermissions); return _permissionCompleter!.future; } static Future requestAllLocationPermissions() async { await Permission.location.request(); await Permission.locationWhenInUse.request(); await Permission.locationAlways.request(); if (await Permission.locationAlways.isGranted) { const platform = MethodChannel('location'); try { await platform.invokeMethod('startLocationService'); } on PlatformException catch (e) { debugPrint("Failed to start location service: '${e.message}'."); } } } static Future showLocationDisclosure() async { return await Get.dialog( AlertDialog( title: Text('位置情報の使用について'), content: const SingleChildScrollView( child: ListBody( children: [ Text('このアプリでは、以下の目的で位置情報を使用します:'), Text('• チェックポイントの自動チェックイン(アプリが閉じているときも含む)'), Text('• 移動履歴の記録(バックグラウンドでも継続)'), Text('• 現在地周辺の情報表示'), Text('\nバックグラウンドでも位置情報を継続的に取得します。'), Text('これにより、バッテリーの消費が増加する可能性があります。'), Text('同意しない場合には、アプリは終了します。'), ], ), ), actions: [ TextButton( child: const Text('同意しない'), onPressed: () => Get.back(result: false), ), TextButton( child: const Text('同意する'), onPressed: () => Get.back(result: true), ), ], ), barrierDismissible: false, ) ?? false; } static void showPermissionDeniedDialog(String title,String message) { Get.dialog( AlertDialog( //title: Text('location_permission_needed_title'.tr), title: Text(title.tr), // 位置情報への許可が必要です //content: Text('location_permission_needed_main'.tr), content: Text(message.tr), // 岐阜ロゲでは、位置情報を使用してスタート・チェックイン・ゴール等の通過照明及び移動手段の記録のために、位置情報のトラッキングを行なっています。このためバックグラウンドでもトラッキングができるように位置情報の権限が必要です。 // 設定画面で、「岐阜ナビ」に対して、常に位置情報を許可するように設定してください。 actions: [ TextButton( child: Text('キャンセル'), onPressed: () => Get.back(), ), TextButton( child: Text('設定'), onPressed: () { Get.back(); openAppSettings(); }, ), ], ), ); } /* static Future requestLocationPermissions(BuildContext context) async { if (_isRequestingPermission) { // If a request is already in progress, wait for it to complete return _permissionCompleter!.future; } _isRequestingPermission = true; _permissionCompleter = Completer(); bool userAgreed = await showLocationDisclosure(context); if (userAgreed) { try { final locationStatus = await Permission.location.request(); final whenInUseStatus = await Permission.locationWhenInUse.request(); final alwaysStatus = await Permission.locationAlways.request(); if (locationStatus == PermissionStatus.granted && (whenInUseStatus == PermissionStatus.granted || alwaysStatus == PermissionStatus.granted)) { _permissionCompleter!.complete(true); } else { showPermissionDeniedDialog('location_permission_needed_title', 'location_permission_needed_main'); _permissionCompleter!.complete(false); } } catch (e) { print('Error requesting location permission: $e'); _permissionCompleter!.complete(false); } } else { print('User did not agree to location usage'); _permissionCompleter!.complete(false); // Exit the app SystemNavigator.pop(); } _isRequestingPermission = false; return _permissionCompleter!.future; } */ static Future checkStoragePermission() async { //debugPrint("(gifunavi)== checkStoragePermission =="); final storagePermission = await Permission.storage.status; return storagePermission == PermissionStatus.granted; } static Future requestStoragePermission() async { //debugPrint("(gifunavi)== requestStoragePermission =="); final storagePermission = await Permission.storage.request(); if (storagePermission == PermissionStatus.granted) { return; } if (storagePermission == PermissionStatus.permanentlyDenied) { // リクエストが完了するまで待機 await Future.delayed(Duration(milliseconds: 500)); showPermissionDeniedDialog('storage_permission_needed_title','storage_permission_needed_main'); } } /* static Future checkLocationBasicPermission() async { //debugPrint("(gifunavi)== checkLocationBasicPermission =="); final locationPermission = await Permission.location.status; return locationPermission == PermissionStatus.granted; } static Future checkLocationWhenInUsePermission() async { //debugPrint("(gifunavi)== checkLocationWhenInUsePermission =="); final whenInUsePermission = await Permission.locationWhenInUse.status; return whenInUsePermission == PermissionStatus.granted; } static Future checkLocationAlwaysPermission() async { //debugPrint("(gifunavi)== checkLocationAlwaysPermission =="); final alwaysPermission = await Permission.locationAlways.status; return alwaysPermission == PermissionStatus.granted; } static bool isBasicPermission=false; static Future requestLocationBasicPermissions() async { //debugPrint("(gifunavi)== requestLocationBasicPermissions =="); try{ if(!isBasicPermission){ isBasicPermission=true; final locationStatus = await Permission.location.request(); if (locationStatus != PermissionStatus.granted) { showPermissionDeniedDialog('location_permission_needed_title','location_permission_needed_main'); } } }catch (e, stackTrace){ print('Exception: $e'); print('Stack trace: $stackTrace'); debugPrintStack(label: 'Exception occurred', stackTrace: stackTrace); } } static bool isLocationServiceRunning = false; static bool isRequestedWhenInUsePermission = false; static Future requestLocationWhenInUsePermissions() async { //debugPrint("(gifunavi)== requestLocationWhenInUsePermissions =="); try{ if(!isRequestedWhenInUsePermission){ isRequestedWhenInUsePermission=true; final whenInUseStatus = await Permission.locationWhenInUse.request(); if (whenInUseStatus != PermissionStatus.granted) { showPermissionDeniedDialog('location_permission_needed_title','location_permission_needed_main'); }else{ if( !isLocationServiceRunning ){ isLocationServiceRunning=true; const platform = MethodChannel('location'); try { await platform.invokeMethod('startLocationService'); // Location Service を開始する。 } on PlatformException catch (e) { debugPrint("Failed to start location service: '${e.message}'."); } } } } }catch (e, stackTrace){ debugPrint('Exception: $e'); debugPrint('Stack trace: $stackTrace'); debugPrintStack(label: 'Exception occurred', stackTrace: stackTrace); } } static bool isRequestedAlwaysPermission = false; static Future requestLocationAlwaysPermissions() async { //debugPrint("(gifunavi)== requestLocationAlwaysPermissions =="); try { if( !isRequestedAlwaysPermission ){ isRequestedAlwaysPermission=true; final alwaysStatus = await Permission.locationAlways.request(); if (alwaysStatus != PermissionStatus.granted) { showPermissionDeniedDialog('location_permission_needed_title','location_permission_needed_main'); } } }catch (e, stackTrace){ print('Exception: $e'); print('Stack trace: $stackTrace'); debugPrintStack(label: 'Exception occurred', stackTrace: stackTrace); } } static Future checkAndRequestPermissions() async { final hasPermissions = await checkLocationBasicPermission(); if (!hasPermissions) { await requestLocationBasicPermissions(); } final hasWIUPermissions = await checkLocationWhenInUsePermission(); if (!hasWIUPermissions) { await requestLocationWhenInUsePermissions(); } final hasAlwaysPermissions = await checkLocationAlwaysPermission(); if (!hasAlwaysPermissions) { await requestLocationAlwaysPermissions(); } } */ }import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; // 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 // エラーメッセージをローカライズすることを検討してください。 // ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。 // class LoginPage extends StatelessWidget { final IndexController indexController = Get.find(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); LoginPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.white, appBar: AppBar( elevation: 0, backgroundColor: Colors.white, automaticallyImplyLeading: false, ), body: indexController.currentUser.isEmpty ? SizedBox( width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Column( children: [ Column( children: [ Container( height: MediaQuery.of(context).size.height / 6, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage( 'assets/images/login_image.jpg'))), ), const SizedBox( height: 5, ), ], ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( children: [ makeInput( label: "email".tr, controller: emailController), makeInput( label: "password".tr, controller: passwordController, obsureText: true), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Container( padding: const EdgeInsets.only(top: 3, left: 3), decoration: BoxDecoration( borderRadius: BorderRadius.circular(40), ), child: Obx( (() => indexController.isLoading.value == true ? MaterialButton( minWidth: double.infinity, height: 60, onPressed: () {}, color: Colors.grey[400], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: const CircularProgressIndicator(), ) : Column( children: [ MaterialButton( minWidth: double.infinity, height: 40, onPressed: () async { if (emailController.text.isEmpty || passwordController .text.isEmpty) { Get.snackbar( "no_values".tr, "email_and_password_required" .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), // backgroundColor: Colors.yellow, //icon:Image(image:AssetImage("assets/images/dora.png")) ); return; } indexController.isLoading.value = true; indexController.login( emailController.text, passwordController.text, context); }, color: Colors.indigoAccent[400], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: Text( "login".tr, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white70), ), ), const SizedBox( height: 5.0, ), MaterialButton( minWidth: double.infinity, height: 36, onPressed: () { Get.toNamed(AppPages.REGISTER); }, color: Colors.redAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: Text( "sign_up".tr, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white70), ), ), const SizedBox( height: 2.0, ), MaterialButton( minWidth: double.infinity, height: 36, onPressed: () { Get.back(); }, color: Colors.grey, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40)), child: Text( "cancel".tr, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white70), ), ), ], )), ), )), const SizedBox( height: 3, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Padding( padding: const EdgeInsets.all(8.0), child: Text( "rogaining_user_need_tosign_up".tr, style: const TextStyle( overflow: TextOverflow.ellipsis, fontSize: 10.0 ), ), ), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Padding( padding: const EdgeInsets.all(8.0), child: Text( "app_developed_by_gifu_dx".tr, style: const TextStyle( overflow: TextOverflow.ellipsis, fontSize: 10.0), ), ), ), ], ), const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Padding( padding: EdgeInsets.all(8.0), child: Text( "※第8回と第9回は、岐阜県の令和5年度「清流の国ぎふ」SDGs推進ネットワーク連携促進補助金を受けています", style: TextStyle( //overflow: TextOverflow.ellipsis, fontSize: 10.0, // Consider adjusting the font size if the text is too small. // Removed overflow: TextOverflow.ellipsis to allow text wrapping. ), ), ), ), ], ), ], ), ], ), ) : TextButton( onPressed: () { indexController.currentUser.clear(); }, child: const Text("Already Logged in, Click to logout"), ), ); } } Widget makeInput( {label, required TextEditingController controller, obsureText = false}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87), ), const SizedBox( height: 5, ), TextField( controller: controller, obscureText: obsureText, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: (Colors.grey[400])!, ), ), border: OutlineInputBorder( borderSide: BorderSide(color: (Colors.grey[400])!), ), ), ), const SizedBox( height: 30.0, ) ], ); } import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:rogapp/utils/location_controller.dart'; class DebugController extends GetxController { final LocationController locationController = Get.find(); final gpsSignalStrength = 'high'.obs; final currentPosition = Rx(null); final isSimulationMode = false.obs; @override void onInit() { super.onInit(); // 位置情報の更新を監視 locationController.locationMarkerPositionStream.listen((position) { if (position != null) { currentPosition.value = Position( latitude: position.latitude, longitude: position.longitude, accuracy: position.accuracy, altitudeAccuracy: 30, headingAccuracy: 30, heading: 0, altitude: 0, speed: 0, speedAccuracy: 0, timestamp: DateTime.now(), ); } }); } void setGpsSignalStrength(String value) { gpsSignalStrength.value = value; locationController.setSimulatedSignalStrength(value); } void toggleSimulationMode() { isSimulationMode.value = !isSimulationMode.value; locationController.setSimulationMode(isSimulationMode.value); if (!isSimulationMode.value) { // 標準モードに切り替えた場合は、シミュレートされた信号強度をリセット locationController.setSimulatedSignalStrength('low'); gpsSignalStrength.value = 'low'; } } }import 'package:get/get.dart'; import 'package:rogapp/pages/debug/debug_controller.dart'; class DebugBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => DebugController()); } }import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/debug/debug_controller.dart'; class DebugPage extends GetView { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('デバッグモード'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('GPS信号強度'), const SizedBox(height: 20), Obx( () => DropdownButton( value: controller.gpsSignalStrength.value, onChanged: (value) { controller.setGpsSignalStrength(value!); }, items: ['high', 'medium', 'low'] .map>((String value) { return DropdownMenuItem( value: value, child: Text(value), ); }).toList(), ), ), const SizedBox(height: 20), const Text('現在のGPS座標'), const SizedBox(height: 10), Obx( () => Text( '緯度: ${controller.currentPosition.value?.latitude ?? '-'}, 経度: ${controller.currentPosition.value?.longitude ?? '-'}', ), ), const SizedBox(height: 20), const Text('現在のGPS精度'), const SizedBox(height: 10), Obx( () => Text( '精度: ${controller.currentPosition.value?.accuracy.toStringAsFixed(2) ?? '-'} m', ), ), const SizedBox(height: 20), Obx( () => ElevatedButton( onPressed: controller.toggleSimulationMode, child: Text(controller.isSimulationMode.value ? 'シミュレーションモード' : '標準モード'), ), ), ], ), ), ); } }import 'package:get/get.dart'; import 'package:get/get_navigation/src/routes/get_route.dart'; import 'package:rogapp/pages/category/category_page.dart'; import 'package:rogapp/pages/changepassword/change_password_page.dart'; import 'package:rogapp/pages/city/city_page.dart'; import 'package:rogapp/pages/destination/destination_binding.dart'; import 'package:rogapp/pages/gps/gps_page.dart'; import 'package:rogapp/pages/history/history_page.dart'; import 'package:rogapp/pages/home/home_binding.dart'; import 'package:rogapp/pages/home/home_page.dart'; import 'package:rogapp/pages/index/index_page.dart'; import 'package:rogapp/pages/landing/landing_page.dart'; import 'package:rogapp/pages/loading/loading_page.dart'; import 'package:rogapp/pages/login/login_page.dart'; import 'package:rogapp/pages/mainperf/mainperf_page.dart'; import 'package:rogapp/pages/permission/permission.dart'; import 'package:rogapp/pages/progress/progress.dart'; import 'package:rogapp/pages/register/register_page.dart'; import 'package:rogapp/pages/search/search_binding.dart'; import 'package:rogapp/pages/search/search_page.dart'; import 'package:rogapp/pages/settings/settings_page.dart'; import 'package:rogapp/pages/settings/settings_binding.dart'; import 'package:rogapp/pages/debug/debug_page.dart'; import 'package:rogapp/pages/debug/debug_binding.dart'; import 'package:rogapp/pages/subperf/subperf_page.dart'; import 'package:rogapp/spa/spa_binding.dart'; import 'package:rogapp/spa/spa_page.dart'; import 'package:rogapp/widgets/permission_handler_screen.dart'; import 'package:rogapp/pages/team/team_binding.dart'; import 'package:rogapp/pages/team/team_list_page.dart'; import 'package:rogapp/pages/team/team_detail_page.dart'; import 'package:rogapp/pages/team/member_binding.dart'; import 'package:rogapp/pages/team/member_detail_page.dart'; import 'package:rogapp/pages/entry/entry_list_page.dart'; import 'package:rogapp/pages/entry/entry_detail_page.dart'; import 'package:rogapp/pages/entry/entry_binding.dart'; part 'app_routes.dart'; class AppPages { // ignore: constant_identifier_names static const INDEX = Routes.INDEX; // ignore: constant_identifier_names static const SPA = Routes.SPA; static const LANDING = Routes.LANDING; static const LOGIN = Routes.LOGIN; static const REGISTER = Routes.REGISTER; static const TRAVEL = Routes.TRAVEL; static const LOADING = Routes.LOADING; static const DESTINATION_MAP = Routes.DESTINATION_MAP; static const HOME = Routes.HOME; static const PERMISSION = Routes.PERMISSION; //static const PERMISSION = Routes.HOME; static const SEARCH = Routes.SEARCH; static const MAINPERF = Routes.MAINPERF; static const SUBPERF = Routes.SUBPERF; static const CITY = Routes.CITY; static const CATEGORY = Routes.CATEOGORY; static const CHANGE_PASSWORD = Routes.CHANGE_PASSWORD; static const CAMERA_PAGE = Routes.CAMERA_PAGE; static const PROGRESS = Routes.PROGRESS; static const HISTORY = Routes.HISTORY; static const GPS = Routes.GPS; static const SETTINGS = Routes.SETTINGS; static const DEBUG = Routes.DEBUG; static const TEAM_LIST = Routes.TEAM_LIST; static const TEAM_DETAIL = Routes.TEAM_DETAIL; static const MEMBER_DETAIL = Routes.MEMBER_DETAIL; static const ENTRY_LIST = Routes.ENTRY_LIST; static const ENTRY_DETAIL = Routes.ENTRY_DETAIL; static final routes = [ GetPage( name: Routes.INDEX, page: () => IndexPage(), //binding: IndexBinding(), ), GetPage( name: Routes.LANDING, page: () => const LandingPage(), //binding: SpaBinding(), ), GetPage( name: Routes.LOGIN, page: () => LoginPage(), //binding: SpaBinding(), ), GetPage( name: Routes.REGISTER, page: () => RegisterPage(), //binding: SpaBinding(), ), GetPage( name: Routes.LOADING, page: () => const LoadingPage(), //binding: DestinationBinding(), ), GetPage( name: Routes.HOME, page: () => HomePage(), binding: HomeBinding(), ), GetPage( name: Routes.PERMISSION, page: () => PermissionHandlerScreen(), ), GetPage( name: Routes.SEARCH, page: () => SearchPage(), binding: SearchBinding(), ), GetPage( name: Routes.CITY, page: () => const CityPage(), ), GetPage( name: Routes.CHANGE_PASSWORD, page: () => ChangePasswordPage(), ), GetPage( name: Routes.PROGRESS, page: () => const ProgressPage(), ), GetPage( name: Routes.HISTORY, page: () => const HistoryPage(), ), GetPage( name: Routes.GPS, page: () => const GpsPage(), ), GetPage( name: Routes.SETTINGS, page: () => const SettingsPage(), binding: SettingsBinding(), ), GetPage( name: Routes.DEBUG, page: () => DebugPage(), binding: DebugBinding(), ), GetPage( name: Routes.TEAM_LIST, page: () => TeamListPage(), binding: TeamBinding(), ), GetPage( name: Routes.TEAM_DETAIL, page: () => TeamDetailPage(), binding: TeamBinding(), ), GetPage( name: Routes.MEMBER_DETAIL, page: () => MemberDetailPage(), binding: MemberBinding(), ), GetPage( name: Routes.ENTRY_LIST, page: () => EntryListPage(), binding: EntryBinding(), ), GetPage( name: Routes.ENTRY_DETAIL, page: () => EntryDetailPage(), binding: EntryBinding(), ), ]; } part of 'app_pages.dart'; abstract class Routes { // Main Menu Route // static const HOME = '/'; // static const MAP = '/map'; // ignore: constant_identifier_names static const INDEX = '/'; // ignore: constant_identifier_names static const SPA = '/spa'; static const LANDING = '/landing'; static const LOGIN = '/login'; static const REGISTER = '/register'; static const TRAVEL = '/travel'; static const LOADING = '/loading'; static const DESTINATION_MAP = '/destination_map'; static const HOME = '/home'; static const PERMISSION = '/permission'; static const SEARCH = '/search'; static const MAINPERF = '/mainperf'; static const SUBPERF = '/subperf'; static const CITY = '/city'; static const CATEOGORY = '/category'; static const CHANGE_PASSWORD = '/change_password'; static const CAMERA_PAGE = '/camera_page'; static const PROGRESS = '/progress'; static const HISTORY = '/history'; static const GPS = '/gp'; static const SETTINGS = '/settings'; static const DEBUG = '/debug'; static const TEAM_LIST = '/team-list'; static const TEAM_DETAIL = '/team-detail'; static const MEMBER_DETAIL = '/member-detail'; static const ENTRY_LIST = '/entry-list'; static const ENTRY_DETAIL = '/entry-detail'; } import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:rogapp/utils/const.dart'; class ActionService { static Future> makeAction(int userId, int locationId, bool wanttogo, bool like, bool checkin) async { //print("----- action is ---- $like-- $wanttogo-- $checkin"); Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = "$serverUrl/api/makeaction/?user_id=$userId&location_id=$locationId&wanttogo=$wanttogo&like=$like&checkin=$checkin"; //String url = "http://localhost:8100/api/makeaction/?user_id=${user_id}&location_id=${location_id}&wanttogo=${wanttogo}&like=${like}&checkin=${checkin}"; //print('++++++++$url'); //print("url is ------ $url"); final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } static Future?> userAction(int userId, int locationId) async { List cats = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/useraction/?user_id=$userId&location_id=$locationId'; //print('++++++++$url'); //String url = 'http://localhost:8100/api/useraction/?user_id=${user_id}&location_id=${location_id}'; final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } } // import 'package:geojson/geojson.dart'; // import 'package:http/http.dart' as http; // import '../utils/const.dart'; // class LocationLineService { // static Future loadLocationLines() async { // final geo = GeoJson(); // GeoJsonFeature? fs; // String serverUrl = ConstValues.currentServer(); // String url = '$serverUrl/api/location_line/'; // //print('++++++++$url'); // final response = await http.get( // Uri.parse(url), // headers: { // 'Content-Type': 'application/json; charset=UTF-8', // }, // ); // if (response.statusCode == 200) { // geo.processedFeatures.listen((fst) { // fs = fst; // }); // await geo.parse(response.body, verbose: true); // return fs; // } else { // throw Exception('Failed to create album.'); // } // } // } import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:geojson_vi/geojson_vi.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/utils/const.dart'; class LocationService { static Future loadLocationsFor( String perfecture, String cat) async { final IndexController indexController = Get.find(); String url = ""; String serverUrl = ConstValues.currentServer(); if (cat.isNotEmpty) { if (indexController.currentUser.isNotEmpty) { bool rog = indexController.currentUser[0]['user']['is_rogaining']; String r = rog == true ? 'True' : 'False'; var grp = indexController.currentUser[0]['user']['event_code']; url = '$serverUrl/api/inperf/?rog=$r&perf=$perfecture&cat=$cat'; } else { url = '$serverUrl/api/inperf/?perf=$perfecture&cat=$cat'; } } else { if (indexController.currentUser.isNotEmpty) { bool rog = indexController.currentUser[0]['user']['is_rogaining']; String r = rog == true ? 'True' : 'False'; var grp = indexController.currentUser[0]['user']['event_code']; url = '$serverUrl/api/inperf/?rog=$r&perf=$perfecture'; } else { url = '$serverUrl/api/inperf/?perf=$perfecture'; } } //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { GeoJSONFeatureCollection cc = GeoJSONFeatureCollection.fromJSON(utf8.decode(response.bodyBytes)); //print(cc); return cc; //featuresFromGeoJson(utf8.decode(response.bodyBytes)); } return null; } static Future?> getLocationsExt(String token) async { List ext = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/locsext/'; //print('++++++++$url'); final response = await http.post(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Token $token' }, body: jsonEncode({ 'token': token, })); if (response.statusCode == 200) { ext = json.decode(utf8.decode(response.bodyBytes)); } return ext; } static Future loadLocationsBound( double lat1, double lon1, double lat2, double lon2, double lat3, double lon3, double lat4, double lon4, String cat) async { //print("-------- in location for bound -------------"); final IndexController indexController = Get.find(); String url = ""; String serverUrl = ConstValues.currentServer(); if (cat.isNotEmpty) { if (indexController.currentUser.isNotEmpty) { bool rog = indexController.currentUser[0]['user']['is_rogaining']; String r = rog == true ? 'True' : 'False'; var grp = indexController.currentUser[0]['user']['event_code']; url = '$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat'; } else { url = '$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat'; } } else { if (indexController.currentUser.isNotEmpty) { bool rog = indexController.currentUser[0]['user']['is_rogaining']; String r = rog == true ? 'True' : 'False'; var grp = indexController.currentUser[0]['user']['event_code']; //print("-------- requested user group $grp -------------"); url = '$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; } else { url = '$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; } } //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 500) { return null; //featuresFromGeoJson(utf8.decode(response.bodyBytes)); } if (response.statusCode == 200) { DestinationController destinationController = Get.find(); GeoJSONFeatureCollection cc = GeoJSONFeatureCollection.fromJSON(utf8.decode(response.bodyBytes)); if (cc.features.isEmpty) { return null; } else { //print("---- feature got from server is ${cc.collection[0].properties} ------"); return cc; } } return null; } static Future loadCustomLocations( String name, String cat) async { final IndexController indexController = Get.find(); String url = ""; if (cat == "-all-") { cat = ""; } String serverUrl = ConstValues.currentServer(); //print("loadCustomLocations url is ----- $cat"); if (cat.isNotEmpty) { if (indexController.currentUser.isNotEmpty) { bool rog = indexController.currentUser[0]['user']['is_rogaining']; String r = rog == true ? 'True' : 'False'; var grp = indexController.currentUser[0]['user']['event_code']; url = '$serverUrl/api/custom_area/?rog=$r&&cat=$cat'; } else { url = '$serverUrl/api/custom_area/?&cat=$cat'; } } else { if (indexController.currentUser.isNotEmpty) { bool rog = indexController.currentUser[0]['user']['is_rogaining']; String r = rog == true ? 'True' : 'False'; var grp = indexController.currentUser[0]['user']['event_code']; url = '$serverUrl/api/customarea?rog=$r&name=$name'; } else { url = '$serverUrl/api/customarea?name=$name'; } } //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 500) { return null; //featuresFromGeoJson(utf8.decode(response.bodyBytes)); } if (response.statusCode == 200) { GeoJSONFeatureCollection cc = GeoJSONFeatureCollection.fromJSON(utf8.decode(response.bodyBytes)); if (cc.features.isEmpty) { return null; } else { return cc; } } return null; } static const platform = MethodChannel('location'); static Future isLocationServiceRunning() async { try { final bool isRunning = await platform.invokeMethod('isLocationServiceRunning'); return isRunning; } catch (e) { print("Failed to check if location service is running: $e"); return false; } } } import 'dart:convert'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; class MatrixService { static Future> getDestinations( List destinations) async { final DestinationController destinationController = Get.find(); String locs = ""; String origin = ""; String destination = ""; int i = 0; for (Destination d in destinations) { //print("---- getting matrix for $d ------------"); if (i == 0) { origin = "${d.lat}, ${d.lon}"; i++; continue; } if (i == (destinations.length - 1)) { destination = "${d.lat}, ${d.lon}"; i++; continue; } //print("lat is ${d.lat}, long is ${d.lon}"); locs += "${d.lat}, ${d.lon}|"; i++; } locs = "optimize:false|$locs"; String mode = "walking"; switch (destinationController.travelMode.value) { case 1: mode = "driving"; break; case 2: mode = "transit"; break; default: mode = "walking"; break; } Map cats = {}; String url = "https://maps.googleapis.com/maps/api/directions/json?destination=$destination&mode=$mode&waypoints=$locs&origin=$origin&key=AIzaSyCN2xFsqFyadWwpjiFxymrxzS6G1tNzraI"; //print('++++++++$url'); final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } } import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; class ErrorService { static Future reportError(dynamic error, StackTrace stackTrace, Map deviceInfo, List operationLogs) async { try { final String errorMessage = error.toString(); final String stackTraceString = stackTrace.toString(); final String estimatedCause = _estimateErrorCause(errorMessage); //final String deviceInfo = await _getDeviceInfo(); final Uri url = Uri.parse('https://rogaining.sumasen.net/report-error'); final response = await http.post( url, body: { 'error_message': errorMessage, 'stack_trace': stackTraceString, 'estimated_cause': estimatedCause, 'device_info': deviceInfo, 'operation_logs': operationLogs.join('\n'), // オペレーションログを改行で結合して送信 }, ); if (response.statusCode == 200) { // エラー報告が成功した場合の処理(必要に応じて) debugPrint("===== エラーログ送信成功しました。 ===="); } else { // エラー報告が失敗した場合の処理(必要に応じて) debugPrint("===== エラーログ送信失敗しました。 ===="); } } catch (e) { // エラー報告中にエラーが発生した場合の処理(必要に応じて) debugPrint("===== エラーログ送信中にエラーになりました。 ===="); } } static String _estimateErrorCause(String errorMessage) { // エラーメッセージに基づいてエラーの原因を推定するロジックを追加する if (errorMessage.contains('NetworkException')) { return 'ネットワーク接続エラー'; } else if (errorMessage.contains('DatabaseException')) { return 'データベースエラー'; } else if (errorMessage.contains('AuthenticationException')) { return '認証エラー'; } else { return '不明なエラー'; } } /* // 2024-4-8 Akira: メモリ使用量のチェックのため追加 See #2810 // static void reportMemoryError(String message, StackTrace stackTrace) async { final errorDetails = FlutterErrorDetails( exception: Exception(message), stack: stackTrace, ); await reportError(errorDetails.exception, errorDetails.stack ?? StackTrace.current, deviceInfo); } */ } import 'dart:convert'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:get/get.dart'; //import 'package:google_maps_webservice/directions.dart'; import 'package:http/http.dart' as http; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import '../utils/const.dart'; class DestinationService { static Future> getDestinations(int userId) async { List cats = []; String serverUrl = ConstValues.currentServer(); String url = "$serverUrl/api/destinations/?user_id=$userId"; //print('++++++++$url'); final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } static Future> deleteDestination(int destId) async { Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = "$serverUrl/api/delete_destination/?dest_id=$destId"; //print('++++++++$url'); final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } //print("####### ---- $cats"); return cats; } static Future updateOrder(int actionId, int order, String dir) async { int cats = 0; String serverUrl = ConstValues.currentServer(); String url = "$serverUrl/api/updateorder/?user_action_id=$actionId&order=$order&dir=$dir"; //print('++++++++$url'); final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } static Future>? getDestinationLine( List destinations, Map mtx) async { PolylinePoints polylinePoints = PolylinePoints(); if (destinations.isEmpty) { return []; } //print("##### @@@@@ ${destinations[0].lat}"); PointLatLng origin = PointLatLng(destinations[0].lat!, destinations[0].lon!); PointLatLng dest = PointLatLng(destinations[destinations.length - 1].lat!, destinations[destinations.length - 1].lon!); List wayPoints = []; int i = 0; for (dynamic d in destinations) { if (i == 0 || i == (destinations.length - 1)) { i += 1; continue; } double la = d.lat; double ln = d.lon; int j = 0; PolylineWayPoint pwp = PolylineWayPoint(location: "$la,$ln", stopOver: false); //print("----- UUUUUU ${pwp}"); //PointLatLng wp = PointLatLng(d["Location"]["geometry"][0][1], d["Location"]["geometry"][0][0]); wayPoints.add(pwp); i += 1; j += 4; } final DestinationController destinationController = Get.find(); // int travMode = destinationController.travelMode.value; // String mode = "WALKING"; // if (travMode == 1) { // //_mode = TravelMode.driving; // mode = "DRIVING"; // } else if (travMode == 2) { // //_mode = TravelMode.transit; // mode = "TRANSIT"; // } //PolylineResult result = await polylinePoints.getRouteBetweenCoordinates("AIzaSyAUBI1ablMKuJwGj2-kSuEhvYxvB1A-mOE", PointLatLng(35.389282, 136.498027), PointLatLng(36.285848, 137.575186)); Map pl = destinationController.matrix["routes"][0]["overview_polyline"]; List result = polylinePoints.decodePolyline(pl["points"]); //List result = polylinePoints.decodePolyline("qkyvEq`z`Yp@DBMr@XL@Td@Eb@PREd@IFe@rKIzCY|GEvCBzCHvS@xC?HnBHtBHlBFnBFhGRtDVW~BE`@ICHLk@dE_ClPgAtHu@bFsAhPg@~Ge@bFaEtg@kEpi@oCd\\w@nIw@hGe@fBy@nBqAjC{@zBgBtFOd@M@Wv@i@`BQf@ITKCuE`@yDZqBRCHa@DKG_AHwBRiBR_Fp@y@LYBY]M@KJo@v@M@cAGoGN_Cx@}Cf@}@@mM~@qF`@gCLwBj@sBrAeAhAsAtCoF|MmAbD{@fBwAdBw@p@_Ax@BFOHAl@?`@MAQCEAOIQSaBx@{Ah@eATsAHSB?d@A`D?f@IdWy@AS??V?|BCJ}@?cA?k@Au@wBqDuKQaACg@z@gELg@GK~@uEp@{A@q@y@CHwFHcG?KDqCDK^ABABEH{AE{B{@_Ho@uFIaBFQhBaC@SQSg@k@g@q@Yw@qA{De@}B]uDCsAMEWDqAFu@@^A@TDjA@vDA`CETK|AEtAIFY@o@ALpBZ~HBlCBn@EDGPu@pASJO`@Qf@?ROr@K?qDLHnEUTsDNkENYB{Ab@I^?zA}CrCkBfBw@t@@LwA`Bo@r@eGvD}BrAGGuAj@[?i@rBVi@P}@T?F?^MxDuBhDsBzAcAn@s@zCgDAI~A{A|CsC?{A?UHItA_@DCXC~J_@TUIoEvDKTm@?Y^iALIb@k@f@aAE}AA_BC{@\\Cv@CxAEj@ExCwDDc@CYFANCh@WHEIIRQhB}B|C_E\\w@Hq@JE?a@O}CGkAIwEGmDEmDAKLA^?A}@C{@?e@E_DFQNi@LcB\\eBPsADGKOEWBOH[GCPs@Pq@\\cANs@^q@jAu@fCqAf@]HCXoCV_BVmAZmBVcDBeCCgDAaB?s@RE?aCCaEAyHAoDd@EJiD@_@AaAj@A\\A?Gp@@r@oBXm@LQ?IEy@Fy@tA[n@Gj@Tz@[~ACdAAx@Lp@Kr@]hAa@HAQoCMwCSwGSiGK_CCCKaBCgCOoCOgECwGB_OB{JHkBEmC?yCDyFF{QFue@BsYByE?oAEgAByLBiL?gLBuGXsEd@cCNA?OHa@jAuCn@eAtCyDh@k@v@EvBKr@EEkACUKaC?G~@gAlCeDFBT[jFeGZAfBEh@UpBM`AEMaFjFYIhE?hEPpCJzAPt@Fj@GNUFu@N[FyBbAuB`@_@LEIOB}@HUBQFk@FcAACGQA}@Bi@F@F[Dc@D[FQHELGhBMtDGR?D"); //PolylineResult result = await polylinePoints.getRouteBetweenCoordinates("AIzaSyAUBI1ablMKuJwGj2-kSuEhvYxvB1A-mOE", origin,dest, travelMode: _mode, wayPoints: wayPoints); //print("#####@@@@@ ${result.points}"); return result; } } import 'dart:convert'; import 'package:http/http.dart' as http; import '../utils/const.dart'; class CatService { static Future?> loadCats(double lat1, double lon1, double lat2, double lon2, double lat3, double lon3, double lat4, double lon4) async { List cats = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/cats/?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } static Future?> loadCatByCity(String cityname) async { List cats = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/catbycity/?$cityname'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } } import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:rogapp/model/rog.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/utils/database_gps.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'dart:convert'; import '../model/gps_data.dart'; import '../utils/const.dart'; // // Rog type 0- start 1- checkin 2- goal // class ExternalService { static final ExternalService _instance = ExternalService._internal(); factory ExternalService() { return _instance; } ExternalService._internal(); String getFormatedTime(DateTime datetime) { return DateFormat('yyyy-MM-dd HH:mm:ss').format(datetime); } Future> startRogaining() async { final IndexController indexController = Get.find(); debugPrint("== startRogaining =="); Map res = {}; int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; //print("--- _team : ${_team}-----"); String eventCode = indexController.currentUser[0]["user"]["event_code"]; if (indexController.connectionStatusName.value != "wifi" && indexController.connectionStatusName.value != "mobile") { debugPrint("== No network =="); DatabaseHelper db = DatabaseHelper.instance; Rog rog = Rog( id: 1, team_name: team, event_code: eventCode, user_id: userId, cp_number: -1, checkintime: DateTime.now().toUtc().microsecondsSinceEpoch, image: null, rog_action_type: 0); db.insertRogaining(rog); } else { debugPrint("== startRogaining processing=="); String url = 'https://rogaining.sumasen.net/gifuroge/start_from_rogapp'; //print('++++++++$url'); final http.Response response = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode( {'team_name': team, 'event_code': eventCode}), ); //print("---- start rogianing api status ---- ${response.statusCode}"); if (response.statusCode == 200) { res = json.decode(utf8.decode(response.bodyBytes)); //print('----_res : $res ----'); } } return res; } Future> makeCheckpoint( int userId, String token, String checkinTime, String teamname, int cp, String eventcode, String imageurl) async { // print("~~~~ in API call function ~~~~"); // print("~~~~ cp is $cp ~~~~"); //print("--cpcp-- ${cp}"); Map res = {}; String url = 'https://rogaining.sumasen.net/gifuroge/checkin_from_rogapp'; //print('++++++++$url'); final IndexController indexController = Get.find(); if (imageurl != null) { if (indexController.connectionStatusName.value != "wifi" && indexController.connectionStatusName.value != "mobile") { debugPrint("== checkin without network =="); DatabaseHelper db = DatabaseHelper.instance; Rog rog = Rog( id: 1, team_name: teamname, event_code: eventcode, user_id: userId, cp_number: cp, checkintime: DateTime.now().toUtc().microsecondsSinceEpoch, image: imageurl, rog_action_type: 1, ); db.insertRogaining(rog); } else { debugPrint("== Normal Check in ==="); String serverUrl = ConstValues.currentServer(); String url1 = "$serverUrl/api/checkinimage/"; final im1Bytes = File(imageurl).readAsBytesSync(); String im1_64 = base64Encode(im1Bytes); //print("~~~~ before calling api 1 ~~~~"); try { final http.Response response = await http.post( Uri.parse(url1), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Token $token' }, // 'id', 'user', 'goalimage', 'goaltime', 'team_name', 'event_code','cp_number' body: jsonEncode({ 'user': userId.toString(), 'team_name': teamname, 'event_code': eventcode, 'checkinimage': im1_64, 'checkintime': checkinTime, 'cp_number': cp.toString() }), ); res = json.decode(utf8.decode(response.bodyBytes)); //print("~~~~ api1 result $res ~~~~"); //print("-----@@@@@ checkin $_res -----"); if (response.statusCode == 201) { //print("~~~~ image from api1 ${res["checkinimage"].toString()} ~~~~"); //print('---- toekn is ${token} -----'); //print("~~~~ token is $token ~~~~"); //print("~~~~ before callling api2 ~~~~"); final http.Response response2 = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({ 'team_name': teamname, 'cp_number': cp.toString(), 'event_code': eventcode, 'image': res["checkinimage"].toString().replaceAll( 'http://localhost:8100', 'http://rogaining.sumasen.net') }), ); var vv = jsonEncode({ 'team_name': teamname, 'cp_number': cp.toString(), 'event_code': eventcode, 'image': res["checkinimage"].toString().replaceAll( 'http://localhost:8100', 'http://rogaining.sumasen.net') }); //print("~~~~ api 2 values $vv ~~~~"); //print("--json-- $vv"); //print("--- checnin response ${response2.statusCode}----"); if (response2.statusCode == 200) { res = json.decode(utf8.decode(response2.bodyBytes)); //print('----checkin res _res : $res ----'); if (res["status"] == "ERROR" && cp>0 ) { // スタート・ゴールはエラー除外。 Get.snackbar("エラーがおきました", res["detail"], backgroundColor: Colors.red, colorText: Colors.white ); } } } else { Get.snackbar("サーバーエラーがおきました", "サーバーと通信できませんでした", backgroundColor: Colors.red, colorText: Colors.white ); } } catch( e ) { print('Error in makeCheckpoint: $e'); Get.snackbar("通信エラーがおきました", "サーバーと通信できませんでした", backgroundColor: Colors.red, colorText: Colors.white ); } } } else { if (indexController.connectionStatusName.value != "wifi" || indexController.connectionStatusName.value != "mobile") { DatabaseHelper db = DatabaseHelper.instance; Rog rog = Rog( id: 1, team_name: teamname, event_code: eventcode, user_id: userId, cp_number: cp, checkintime: DateTime.now().toUtc().microsecondsSinceEpoch, image: null, rog_action_type: 1, ); db.insertRogaining(rog); } else { final http.Response response3 = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({ 'team_name': teamname, 'cp_number': cp.toString(), 'event_code': eventcode, 'image': "" }), ); // var vvv = jsonEncode({ // 'team_name': teamname, // 'cp_number': cp.toString(), // 'event_code': eventcode, // 'image': res["checkinimage"].toString().replaceAll( // 'http://localhost:8100', 'http://rogaining.sumasen.net') // }); // print("--json-- $vvv"); // print("--- checnin response ${response3.statusCode}----"); if (response3.statusCode == 200) { res = json.decode(utf8.decode(response3.bodyBytes)); //print('----checkin res _res : $res ----'); } } } //print("~~~~ done checkin ~~~~"); return res; } Future> makeGoal(int userId, String token, String teamname, String image, String goalTime, String eventcode) async { Map res2 = {}; final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); debugPrint("== goal Rogaining =="); //if(indexController.connectionStatusName != "wifi" && indexController.connectionStatusName != "mobile"){ DatabaseHelper db = DatabaseHelper.instance; Rog rog = Rog( id: 1, team_name: teamname, event_code: eventcode, user_id: userId, cp_number: -1, checkintime: DateTime.now().toUtc().microsecondsSinceEpoch, image: image, rog_action_type: 1, ); db.insertRogaining(rog); // } // else{ String serverUrl = ConstValues.currentServer(); String url1 = "$serverUrl/api/goalimage/"; final im1Bytes = File(image).readAsBytesSync(); String im1_64 = base64Encode(im1Bytes); final http.Response response = await http.post( Uri.parse(url1), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Token $token' }, // 'id', 'user', 'goalimage', 'goaltime', 'team_name', 'event_code','cp_number' body: jsonEncode({ 'user': userId.toString(), 'team_name': teamname, 'event_code': eventcode, 'goaltime': goalTime, 'goalimage': im1_64, 'cp_number': "-1" }), ); String url = 'https://rogaining.sumasen.net/gifuroge/goal_from_rogapp'; //print('++++++++$url'); if (response.statusCode == 201) { Map res = json.decode(utf8.decode(response.bodyBytes)); // print('----_res : $res ----'); // print('---- image url ${res["goalimage"]} ----'); final http.Response response2 = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({ 'team_name': teamname, 'event_code': eventcode, 'goal_time': goalTime, 'image': res["goalimage"].toString().replaceAll( 'http://localhost:8100', 'http://rogaining.sumasen.net') }), ); String rec = jsonEncode({ 'team_name': teamname, 'event_code': eventcode, 'goal_time': goalTime, 'image': res["goalimage"] .toString() .replaceAll('http://localhost:8100', 'http://rogaining.sumasen.net') }); //print("-- json -- $rec"); //print('----- response2 is $response2 --------'); if (response2.statusCode == 200) { res2 = json.decode(utf8.decode(response2.bodyBytes)); } } //} destinationController.resetRogaining(isgoal: true); return res2; } Future removeCheckin(int cp) async { final IndexController indexController = Get.find(); //int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; //print("--- _team : ${_team}-----"); String eventCode = indexController.currentUser[0]["user"]["event_code"]; if (indexController.connectionStatusName.value != "wifi" && indexController.connectionStatusName.value != "mobile") { return Future.value(false); } else { String url = 'https://rogaining.sumasen.net/gifuroge/remove_checkin_from_rogapp'; //print('++++++++$url'); final http.Response response = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({ 'event_code': eventCode, 'team_name': team, 'cp_number': cp.toString() }), ); //print("---- remove checkin ---- ${response.statusCode}"); if (response.statusCode == 200) { return Future.value(true); //print('----_res : $res ----'); } } return Future.value(false); } String timestampToTimeString(int timestamp) { // Convert timestamp to DateTime and format it as needed var dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); //print("^^^^ time ${dateTime}"); // Format dateTime to a time string (e.g., '12:00:00') // Adjust the format as needed return "${dateTime.hour}:${dateTime.minute}:${dateTime.second}"; } Future pushGPS() async { //print("^^^^ pushed ^^^"); final IndexController indexController = Get.find(); //int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; //print("--- _team : ${_team}-----"); String eventCode = indexController.currentUser[0]["user"]["event_code"]; List gpsDataList = []; if (indexController.connectionStatusName.value != "wifi" && indexController.connectionStatusName.value != "mobile") { return Future.value(false); } else { // Step 1: Fetch data from the local database gpsDataList = await GpsDatabaseHelper.instance.getUnsyncedGPSData(team, eventCode); // Step 2: Transform data into the required format var payload = { 'team_name': team, 'event_code': eventCode, 'waypoints': gpsDataList.map((gpsData) { return { 'latitude': gpsData.lat.toString(), 'longitude': gpsData.lon.toString(), // Convert the timestamp to a formatted time string 'time': timestampToTimeString(gpsData.created_at), }; }).toList(), }; //print("calling push gps step 2 ${payload}"); String urlS = 'https://rogaining.sumasen.net/gifuroge/get_waypoint_datas_from_rogapp'; //print('++++++++$url'); var url = Uri.parse(urlS); // Replace with your server URL var response = await http.post( url, headers: {"Content-Type": "application/json"}, body: json.encode(payload), ); //print("GPS Data res ${response.statusCode}"); if (response.statusCode == 200) { // Handle success // make local data as synced await GpsDatabaseHelper.instance.setSyncData(gpsDataList); //print("GPS Data sent successfully"); } else { // Handle error //print("Failed to send data"); } } return Future.value(false); } static Future> usersEventCode( String teamcode, String password) async { Map res = {}; String url = "https://rogaining.sumasen.net/gifuroge/check_event_code?zekken_number=$teamcode&password=$password"; //print('++++++++$url'); final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }); if (response.statusCode == 200) { res = json.decode(utf8.decode(response.bodyBytes)); } return res; } } import 'dart:async'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/utils/database_helper.dart'; class DatabaseService { // Private constructor DatabaseService._privateConstructor(); // Static instance static final DatabaseService _instance = DatabaseService._privateConstructor(); // Factory constructor to return the instance factory DatabaseService() { return _instance; } // StreamController for updates final StreamController> _dbUpdateController = StreamController>.broadcast(); // Getter for the stream Stream> get destinationUpdatesStream => _dbUpdateController.stream; // Method to fetch destinations Future> fetchDestinations() async { // Your database fetch logic here List destinations = await _fetchFromDb(); _dbUpdateController.add(destinations); return destinations; } // Method to update the database and emit an update through the stream Future updateDatabase() async { // Your update logic here // After updating, fetch the updated list and add it to the stream fetchDestinations(); } // Method to fetch data from the database (placeholder) Future> _fetchFromDb() async { // Simulate fetching data with a delay DatabaseHelper db = DatabaseHelper.instance; List destinations = await db.getDestinations(); return destinations; // Replace with your actual fetch operation } // Dispose method to close the stream controller void dispose() { _dbUpdateController.close(); } } import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:rogapp/model/auth_user.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import '../utils/const.dart'; class AuthService { Future userLogin(String email, String password) async { final serverUrl = ConstValues.currentServer(); final url = '$serverUrl/api/login/'; try { final http.Response response = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({'email': email, 'password': password}), ); switch (response.statusCode) { case 200: final data = json.decode(utf8.decode(response.bodyBytes)); AuthUser user = AuthUser.fromMap(data["user"]); final String token = data["token"]; user.auth_token = token; return user; default: return null; //throw Exception(response.reasonPhrase); } } on Exception catch (_) { rethrow; } } Future userFromToken(String token) async { final serverUrl = ConstValues.currentServer(); final url = '$serverUrl/api/user/'; try { final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Token $token' }); switch (response.statusCode) { case 200: final data = json.decode(utf8.decode(response.bodyBytes)); AuthUser user = AuthUser.fromMap(data); user.auth_token = token; return user; default: return null; //throw Exception(response.reasonPhrase); } } on Exception catch (_) { rethrow; } } static Future> changePassword( String oldpassword, String newpassword, String token) async { Map changePassword = {}; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/change-password/'; //print('++++++++$url'); final http.Response response = await http.put( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Token $token' }, body: jsonEncode({ 'old_password': oldpassword, 'new_password': newpassword }), ); if (response.statusCode == 200) { changePassword = json.decode(utf8.decode(response.bodyBytes)); } return changePassword; } static Future> login( String email, String password) async { //print("------- in logged email $email pwd $password ###### --------"); Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/login/'; //print('++++++++$url'); //String url = 'http://localhost:8100/api/login/'; try { final http.Response response = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode( {'email': email, 'password': password}), ); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } else { print('Login failed with status code: ${response.statusCode}'); cats = {}; } } catch( e ){ print('Error in login: $e'); Get.snackbar("通信エラーがおきました", "サーバーと通信できませんでした", backgroundColor: Colors.red, colorText: Colors.white); Get.snackbar( "通信エラーがおきました", "サーバーと通信できませんでした", 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), //backgroundColor: Colors.yellow, ); cats = {}; } return cats; } // ユーザー登録 // static Future> register( String email, String password) async { Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/register/'; debugPrint('++++++++${url}'); final http.Response response = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({'email': email, 'password': password}), ); //print(response.body); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } static Future> deleteUser(String token) async { Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/delete-account/'; //print('++++++++$url'); final http.Response response = await http.get(Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Token $token' }); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } static Future?> userDetails(int userid) async { List cats = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/userdetials?user_id=$userid'; //print('++++++++$url'); //print("---- UserDetails url is $url"); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } static Future?> userForToken(String token) async { Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/user/'; //print('++++++++$url'); //print("---- UserDetails url is $url"); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Token $token' }, ); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); //print("--- eeeeee $cats ----"); } return [ {"user": cats, "token": token} ]; } } import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:rogapp/utils/const.dart'; class TrackingService { static Future> addTrack( String userId, double lat, double lon) async { Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/track/'; //print('++++++++$url'); final geom = '{"type": "MULTIPOINT", "coordinates": [[$lon, $lat]]}'; final http.Response response = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode({'user_id': userId, 'geom': geom}), ); if (response.statusCode == 200) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; } } import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:rogapp/utils/const.dart'; class PerfectureService { static Future?> loadPerfectures() async { List perfs = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/perf_main/'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { perfs = json.decode(utf8.decode(response.bodyBytes)); } return perfs; } static Future?> loadSubPerfectures(String area) async { List perfs = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/subperfinmain/?area=$area'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { perfs = json.decode(utf8.decode(response.bodyBytes)); } return perfs; } static Future?> getMainPerfExt(String id) async { List perfs = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/mainperfext/?perf=$id'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { perfs = json.decode(utf8.decode(response.bodyBytes)); } return perfs; } static Future?> loadGifuAreas(String perf) async { List perfs = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/allgifuareas/?perf=$perf'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { perfs = json.decode(utf8.decode(response.bodyBytes)); } return perfs; } static Future?> loadCustomAreas() async { List perfs = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/customareanames'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { perfs = json.decode(utf8.decode(response.bodyBytes)); } return perfs; } static Future?> getSubExt(String id) async { List perfs = []; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/perfext/?sub_perf=$id'; //print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { 'Content-Type': 'application/json; charset=UTF-8', }, ); if (response.statusCode == 200) { perfs = json.decode(utf8.decode(response.bodyBytes)); } return perfs; } } import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; class DeviceInfoService { static Future> getDeviceInfo() async { final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); final PackageInfo packageInfo = await PackageInfo.fromPlatform(); if (Platform.isAndroid) { final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; return { 'os': 'Android', 'os_version': androidInfo.version.release, 'device_model': androidInfo.model, 'app_version': packageInfo.version, 'app_build_number': packageInfo.buildNumber, }; } else if (Platform.isIOS) { final IosDeviceInfo iosInfo = await deviceInfo.iosInfo; return { 'os': 'iOS', 'os_version': iosInfo.systemVersion, 'device_model': iosInfo.model, 'app_version': packageInfo.version, 'app_build_number': packageInfo.buildNumber, }; } else { return { 'os': Platform.operatingSystem, 'os_version': Platform.operatingSystemVersion, 'app_version': packageInfo.version, 'app_build_number': packageInfo.buildNumber, }; } } } // lib/services/api_service.dart import 'package:get/get.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:rogapp/model/entry.dart'; import 'package:rogapp/model/event.dart'; import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; import 'package:rogapp/model/user.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import '../utils/const.dart'; class ApiService extends GetxService{ static ApiService get to => Get.find(); String serverUrl = ''; String baseUrl = ''; String token = 'your-auth-token'; // これが使用されている。 Future init() async { try { // ここで必要な初期化処理を行う serverUrl = ConstValues.currentServer(); baseUrl = '$serverUrl/api'; //await Future.delayed(Duration(seconds: 2)); // 仮の遅延(実際の初期化処理に置き換えてください) print('ApiService initialized successfully'); } catch(e) { print('Error in ApiService initialization: $e'); rethrow; // エラーを再スローして、呼び出し元で処理できるようにする } } String getToken() { // IndexControllerの初期化を待つ final indexController = Get.find(); if (indexController.currentUser.isNotEmpty) { token = indexController.currentUser[0]['token'] ?? ''; print("Get token = $token"); }else{ token = ""; } return token; } Future> getTeams() async { init(); getToken(); final response = await http.get( Uri.parse('$baseUrl/teams/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode == 200) { List teamsJson = json.decode(response.body); return teamsJson.map((json) => Team.fromJson(json)).toList(); } else { throw Exception('Failed to load teams'); } } Future> getCategories() async { init(); getToken(); final response = await http.get( Uri.parse('$baseUrl/categories/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode == 200) { List categoriesJson = json.decode(response.body); return categoriesJson.map((json) => Category.fromJson(json)).toList(); } else { throw Exception('Failed to load categories'); } } Future getCurrentUser() async { init(); getToken(); final response = await http.get( Uri.parse('$baseUrl/user/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode == 200) { return User.fromJson(json.decode(response.body)); } else { throw Exception('Failed to get current user'); } } Future createTeam(String teamName, int categoryId) async { init(); getToken(); final response = await http.post( Uri.parse('$baseUrl/teams/'), headers: { 'Authorization': 'Token $token', 'Content-Type': 'application/json', }, body: json.encode({ 'team_name': teamName, 'category': categoryId, }), ); if (response.statusCode == 201) { return Team.fromJson(json.decode(response.body)); } else { throw Exception('Failed to create team'); } } Future updateTeam(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', }, body: json.encode({ 'team_name': teamName, 'category': categoryId, }), ); if (response.statusCode == 200) { return Team.fromJson(json.decode(response.body)); } else { throw Exception('Failed to update team'); } } Future deleteTeam(int teamId) async { init(); getToken(); final response = await http.delete( Uri.parse('$baseUrl/teams/$teamId/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode != 204) { throw Exception('Failed to delete team'); } } Future> getTeamMembers(int teamId) async { init(); getToken(); final response = await http.get( Uri.parse('$baseUrl/teams/$teamId/members/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode == 200) { List membersJson = json.decode(response.body); return membersJson.map((json) => User.fromJson(json)).toList(); } else { throw Exception('Failed to load team members'); } } Future createTeamMember(int teamId, String email, String firstname, String lastname, DateTime? dateOfBirth) async { init(); getToken(); final response = await http.post( Uri.parse('$baseUrl/teams/$teamId/members/'), headers: { 'Authorization': 'Token $token', 'Content-Type': 'application/json', }, body: json.encode({ 'email': email, 'firstname': firstname, 'lastname': lastname, 'date_of_birth': dateOfBirth?.toIso8601String(), }), ); if (response.statusCode == 201) { return User.fromJson(json.decode(response.body)); } else { throw Exception('Failed to create team member'); } } Future updateTeamMember(int teamId,int memberId, String firstname, String lastname, DateTime? dateOfBirth) async { init(); getToken(); final response = await http.put( Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'), headers: { 'Authorization': 'Token $token', 'Content-Type': 'application/json', }, body: json.encode({ 'firstname': firstname, 'lastname': lastname, 'date_of_birth': dateOfBirth?.toIso8601String(), }), ); if (response.statusCode == 200) { return User.fromJson(json.decode(response.body)); } else { throw Exception('Failed to update team member'); } } Future deleteTeamMember(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 resendMemberInvitation(int memberId) async { init(); getToken(); final response = await http.post( Uri.parse('$baseUrl/members/$memberId/resend-invitation/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode != 200) { throw Exception('Failed to resend invitation'); } } Future> getEntries() async { init(); getToken(); final response = await http.get( Uri.parse('$baseUrl/entries/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode == 200) { List entriesJson = json.decode(response.body); return entriesJson.map((json) => Entry.fromJson(json)).toList(); } else { throw Exception('Failed to load entries'); } } Future> getEvents() async { init(); getToken(); final response = await http.get( Uri.parse('$baseUrl/new-events/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode == 200) { List eventsJson = json.decode(response.body); return eventsJson.map((json) => Event.fromJson(json)).toList(); } else { throw Exception('Failed to load events'); } } Future createEntry(int teamId, int eventId, int categoryId, DateTime date) async { init(); getToken(); final response = await http.post( Uri.parse('$baseUrl/entry/'), headers: { 'Authorization': 'Token $token', 'Content-Type': 'application/json', }, body: json.encode({ 'team': teamId, 'event': eventId, 'category': categoryId, 'date': date.toIso8601String(), }), ); if (response.statusCode == 201) { return Entry.fromJson(json.decode(response.body)); } else { throw Exception('Failed to create entry'); } } Future updateEntry(int entryId, int eventId, int categoryId, DateTime date) async { init(); getToken(); final response = await http.put( Uri.parse('$baseUrl/entry/$entryId/'), headers: { 'Authorization': 'Token $token', 'Content-Type': 'application/json', }, body: json.encode({ 'event': eventId, 'category': categoryId, 'date': date.toIso8601String(), }), ); if (response.statusCode == 200) { return Entry.fromJson(json.decode(response.body)); } else { throw Exception('Failed to update entry'); } } Future deleteEntry(int entryId) async { init(); getToken(); final response = await http.delete( Uri.parse('$baseUrl/entry/$entryId/'), headers: {'Authorization': 'Token $token'}, ); if (response.statusCode != 204) { throw Exception('Failed to delete entry'); } } }// import 'package:geojson/geojson.dart'; // import 'package:http/http.dart' as http; // import '../utils/const.dart'; // class LocationPolygonervice { // static Future loadLocationLines() async { // final geo = GeoJson(); // GeoJsonFeature? fs; // String serverUrl = ConstValues.currentServer(); // String url = '$serverUrl/api/location_polygon/'; // //print('++++++++$url'); // final response = await http.get( // Uri.parse(url), // headers: { // 'Content-Type': 'application/json; charset=UTF-8', // }, // ); // if (response.statusCode == 200) { // geo.processedFeatures.listen((fst) { // fs = fst; // }); // await geo.parse(response.body, verbose: true); // return fs; // } else { // throw Exception('Failed to create album.'); // } // } // } import 'package:flutter/material.dart'; class CustomIcons { static const _fontFamily = 'CustomIcons'; static const IconData gps_signal_low = IconData(0xe900, fontFamily: _fontFamily); static const IconData gps_signal_middle = IconData(0xe913, fontFamily: _fontFamily); static const IconData gps_signal_high = IconData(0xe91d, fontFamily: _fontFamily); } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/utils/location_controller.dart'; enum GPSStatus { high, middle, low } class GpsSignalStrengthIndicator extends StatelessWidget { LocationController locationController; final bool minimized; // コンストラクタにminimizedパラメータを追加し、デフォルト値をfalseに設定 GpsSignalStrengthIndicator({ super.key, required this.locationController, this.minimized = false, // ここでデフォルト値を指定 }); @override Widget build(BuildContext context) { // final LocationController locationController = Get.find(); return Obx(() { String signalStrength = locationController.latestSignalStrength.value; //debugPrint("GpsSignalStrengthIndicator : signalStrength=${signalStrength}"); IconData iconData; Color backgroundColor; String text; // signalStrengthに応じて、アイコン、背景色、テキストを設定 switch (signalStrength) { case 'high': backgroundColor = Colors.green; iconData = Icons.signal_cellular_alt; // iconData = CustomIcons.gps_signal_high; text = 'GPS 強'; break; case 'medium': backgroundColor = Colors.orange; iconData = Icons.signal_cellular_alt_2_bar; // iconData = CustomIcons.gps_signal_middle; text = 'GPS 中'; break; default: backgroundColor = Colors.grey; // Fallback color iconData = Icons.signal_cellular_connected_no_internet_4_bar; // iconData = CustomIcons.gps_signal_low; text = 'GPS 弱'; } // コンテナの設定をminimizedの値に応じて調整 return Container( height: minimized ? 40 : null, width: minimized ? 40 : null, padding: minimized ? null : const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: backgroundColor, shape: minimized ? BoxShape.circle : BoxShape.rectangle, borderRadius: minimized ? null : BorderRadius.circular(10), ), child: minimized ? Center( child: Icon(iconData, color: Colors.white, size: 24), ) : Row( mainAxisSize: MainAxisSize.min, children: [ Icon(iconData, color: Colors.white), const SizedBox(width: 8), Text( text, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ], ), ); }); } } import 'dart:ui' as ui; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:flutter/material.dart'; import 'package:geojson_vi/geojson_vi.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:latlong2/latlong.dart'; import 'package:rogapp/main.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/camera/camera_page.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/services/external_service.dart'; import 'package:rogapp/utils/const.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/utils/text_util.dart'; import 'package:rogapp/widgets/bottom_sheet_controller.dart'; import 'package:rogapp/widgets/debug_widget.dart'; import 'package:url_launcher/url_launcher.dart'; // BottomSheetNewは、StatelessWidgetを継承したクラスで、目的地の詳細情報を表示するボトムシートのUIを構築します。 // コンストラクタでは、destination(目的地オブジェクト)とisAlreadyCheckedIn(すでにチェックイン済みかどうかのフラグ)を受け取ります。 // buildメソッドでは、detailsSheetメソッドを呼び出して、目的地の詳細情報を表示します。 // class BottomSheetNew extends GetView { BottomSheetNew( {this.isAlreadyCheckedIn = false, Key? key, required this.destination}) : super(key: key); final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); final Destination destination; // 目的地オブジェクト final bool isAlreadyCheckedIn; // すでにチェックイン済みかどうかのフラグ final RxBool isButtonDisabled = false.obs; // 目的地の画像を取得するためのメソッドです。 // indexController.rogModeの値に基づいて、適切な画像を返します。画像が見つからない場合は、デフォルトの画像を返します。 // Image getImage() { String serverUrl = ConstValues.currentServer(); if (indexController.rogMode == 1) { if (indexController.currentDestinationFeature.isEmpty || indexController.currentDestinationFeature[0].photos! == "") { return const Image(image: AssetImage('assets/images/empty_image.png')); } else { //print("@@@@@@@@@@@@@ rog mode -------------------- ${indexController.currentDestinationFeature[0].photos} @@@@@@@@@@@"); String photo = indexController.currentDestinationFeature[0].photos!; if (photo.contains('http')) { return Image( image: NetworkImage( indexController.currentDestinationFeature[0].photos!, ), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } else { return Image( image: NetworkImage( '$serverUrl/media/compressed/${indexController.currentDestinationFeature[0].photos!}', ), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } } } else { GeoJSONFeature gf = indexController.currentFeature[0]; //print("=== photo sss ${gf.properties!["photos"]}"); if (gf.properties!["photos"] == null || gf.properties!["photos"] == "") { return const Image(image: AssetImage('assets/images/empty_image.png')); } else { String photo = gf.properties!["photos"]; if (photo.contains('http')) { return Image( image: NetworkImage( gf.properties!["photos"], ), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } else { String imageUrl = Uri.encodeFull( '$serverUrl/media/compressed/${gf.properties!["photos"]}'); return Image( image: NetworkImage( imageUrl, ), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } } } } // URLを開くためのメソッドです。 // url_launcherパッケージを使用して、指定されたURLを開きます。 // void _launchURL(url) async { if (!await launchUrl(url)) throw 'Could not launch $url'; } // 指定されたlocationidが目的地リストに含まれているかどうかを確認するメソッドです。 // destinationController.destinationsリストを走査し、locationidが一致する目的地があるかどうかを返します。 // bool isInDestination(String locationid) { int lid = int.parse(locationid); if (destinationController.destinations .where((element) => element.location_id == lid) .isNotEmpty) { return true; } else { return false; } } Future saveTemporaryImage(Destination destination) async { final serverUrl = ConstValues.currentServer(); final imagePath = '${serverUrl}/media/compressed/${destination.photos}'; final tempDir = await getTemporaryDirectory(); final tempFile = await File('${tempDir.path}/temp_image.jpg').create(recursive: true); final response = await http.get(Uri.parse(imagePath)); await tempFile.writeAsBytes(response.bodyBytes); destinationController.photos.clear(); destinationController.photos.add(tempFile); } // アクションボタン(チェックイン、ゴールなど)を表示するためのメソッドです。 // 現在の状態に基づいて、適切なボタンを返します。 // ボタンがタップされたときの処理も含まれています。 // Widget getActionButton(BuildContext context, Destination destination) { /* debugPrint("getActionButton ${destinationController.rogainingCounted.value}"); debugPrint("getActionButton ${destinationController.distanceToStart()}"); debugPrint("getActionButton ${destination.cp}"); debugPrint("getActionButton ${DestinationController.ready_for_goal}"); // ...2024-04-03 Akira デバッグモードのみ出力するようにした。 */ // bool isInRog=false; Destination cdest = destinationController .festuretoDestination(indexController.currentFeature[0]); var distance = const Distance(); double distanceToDest = distance.as( LengthUnit.Meter, LatLng( destinationController.currentLat, destinationController.currentLon), LatLng(cdest.lat!, cdest.lon!)); // Check conditions to show confirmation dialog if (destinationController.isInRog.value == false && (destinationController.distanceToStart() <= 100 || destinationController.isGpsSignalWeak() ) && //追加 Akira 2024-4-5 (destination.cp == -1 || destination.cp == 0 ) && destinationController.rogainingCounted.value == false) { // ゲームが始まってなければ // ロゲ開始している && (開始地点から100m以内 又は 電波が弱い) && CP番号が 1 or 0 && rogainingCounted==false(どこにもチェックインしていない) なら return Obx(() { final isInRog = destinationController.isInRog.value; return ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.secondary, ), onPressed: destinationController.isInRog.value ? null : () async { destinationController.isInRog.value = true; // Show confirmation dialog Get.dialog( AlertDialog( title: Text("confirm".tr), //confirm content: Text( "clear_rog_data_message".tr), //are you sure actions: [ TextButton( child: Text("no".tr), //no onPressed: () { // ダイアログをキャンセルした場合はボタンを再度有効化 destinationController.isInRog.value = false; Get.back(); // Close the dialog Get.back(); // Close the bottom sheet }, ), TextButton( child: Text("yes".tr), //yes onPressed: () async { destinationController.isInRog.value = true; await saveTemporaryImage(destination); // Clear data and start game logic here destinationController.resetRogaining(); destinationController.addToRogaining( destinationController.currentLat, destinationController.currentLon, destination.location_id!, ); saveGameState(); await ExternalService().startRogaining(); Get.back(); Get.back();// Close the dialog and potentially navigate away }, ), ], ), barrierDismissible: false, // User must tap a button to close the dialog ); }, child: Text( isInRog ? 'in_game'.tr : 'start_rogaining'.tr, style: TextStyle(color: Colors.white), ), ); }); //print("counted ${destinationController.rogainingCounted.value}"); }else if (destinationController.rogainingCounted.value == true && // destinationController.distanceToStart() <= 500 && ... GPS信号が弱い時でもOKとする。 (destinationController.distanceToStart() <= 500 || destinationController.isGpsSignalWeak() ) && (destination.cp == 0 || destination.cp == -2 || destination.cp == -1) && // (destination.cp == 0 || destination.cp == -2 ) && DestinationController.ready_for_goal == true) { // ready_for_goal && (開始地点から500m以内 又は 電波が弱い) && CP番号が -1 or -2 or 0 && rogainingCounted==true なら // Goal ボタン //goal return ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: destinationController.rogainingCounted.value == true && destinationController.distanceToStart() <= 500 && (destination.cp == 0 || destination.cp == -2|| destination.cp == -1) && DestinationController.ready_for_goal == true ? () async { destinationController.isAtGoal.value = true; destinationController.photos.clear(); await showModalBottomSheet( constraints: BoxConstraints.loose( ui.Size(Get.width, Get.height * 0.75)), context: Get.context!, isScrollControlled: true, builder: ((context) => CameraPage( destination: destination, ))).whenComplete(() { destinationController.skipGps = false; destinationController.chekcs = 0; destinationController.isAtGoal.value = false; }); } : null, child: Text( "finish_rogaining".tr, style: TextStyle(color: Colors.white), )); } else if (distanceToDest <= destinationController.getForcedChckinDistance(destination)) { // cpごとの強制チェックイン以内にいれば //start return ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.secondary, ), onPressed: isAlreadyCheckedIn == true ? null : () async { try{ destinationController.isCheckingIn.value = true; // ここを追加 Get.back(); Get.back(); await Future.delayed(Duration(milliseconds: 500)); await destinationController.callforCheckin(destination); destinationController.isCheckingIn.value = false; } catch (e) { // エラーハンドリング Get.snackbar( 'Error', 'An error occurred while processing check-in.', backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 3), ); // 必要に応じてエラーログを記録 print('Error processing check-in: $e'); } }, child: Text( destination.cp == -1 && destinationController.isInRog.value == false && destinationController.rogainingCounted.value == false ? "ロゲ開始" : destinationController.isInRog.value == true && destination.cp == -1 ? "in_game".tr : isAlreadyCheckedIn == true ? "in_game".tr : destinationController.isInRog.value == true ? "checkin".tr : "rogaining_not_started".tr, style: TextStyle(color: Theme.of(context).colorScheme.onSecondary), ), ); } return Container(); } // 継承元のbuild をオーバーライドし、detailsSheetメソッドを呼び出して、目的地の詳細情報を表示します。 @override Widget build(BuildContext context) { //print("to start ${destinationController.distanceToStart()}"); destinationController.skipGps = true; // print('--- c use --- ${indexController.currentUser[0].values}'); // print('---- rog_mode ----- ${indexController.rogMode.value} -----'); // return indexController.rogMode.value == 0 // ? detailsSheet(context) // : destinationSheet(context); return Obx(() { if (!destinationController.isCheckingIn.value) { return detailsSheet(context); } else { return Container(); // チェックイン操作中は空のコンテナを返す } }); } // 指定された目的地がすでにチェックイン済みかどうかを確認するメソッドです。 // DatabaseHelperを使用して、目的地の位置情報に基づいてデータベースを検索し、結果を返します。 // Future isDestinationCheckedIn(Destination d) async { DatabaseHelper db = DatabaseHelper.instance; List ds = await db.getDestinationByLatLon(d.lat!, d.lon!); return ds.isNotEmpty; } // show add location details // 目的地の詳細情報を表示するためのUIを構築するメソッドです。 // 目的地の画像、名前、住所、電話番号、Webサイト、備考などの情報を表示します。 // また、アクションボタンや「ここへ行く」ボタンも表示されます。 // SingleChildScrollView detailsSheet(BuildContext context) { Destination cdest = destinationController .festuretoDestination(indexController.currentFeature[0]); var distance = const Distance(); double distanceToDest = distance.as( LengthUnit.Meter, LatLng( destinationController.currentLat, destinationController.currentLon), LatLng(cdest.lat!, cdest.lon!)); debugPrint("Distance from current point : $distanceToDest"); debugPrint( "forced distance for point : ${destinationController.getForcedChckinDistance(destination)}"); debugPrint( "current point : ${destinationController.currentLat}, ${destinationController.currentLon} - ${DateTime.now().hour}:${DateTime.now().minute}:${DateTime.now().second}:${DateTime.now().microsecond}"); debugPrint("Checkin radius : ${destination.checkin_radious}"); debugPrint("--${destination.cp}--"); return SingleChildScrollView( child: Column( children: [ Padding( // 1行目 padding: const EdgeInsets.all(8.0), child: Row( children: [ MaterialButton( // キャンセルボタン onPressed: () { Get.back(); }, color: Colors.blue, textColor: Colors.white, padding: const EdgeInsets.all(16), shape: const CircleBorder(), child: const Icon( Icons.arrow_back_ios, size: 14, ), ), Expanded( // チェックポイント番号+ポイント名 child: Container( alignment: Alignment.centerLeft, child: Obx(() => Text( "${TextUtils.getDisplayTextFeture(indexController.currentFeature[0])} : ${indexController.currentFeature[0].properties!["location_name"]}", style: const TextStyle( fontSize: 15.0, fontWeight: FontWeight.bold, ), )), ), ), ], ), ), Row( // 2行目 チェックポイント写真 children: [ Expanded( child: SizedBox( height: 260.0, child: Obx(() => getImage()), )), ], ), Obx(() => Padding( // 3行め ボタン類 padding: const EdgeInsets.all(8.0), child: Column( children: [ Row( // 開始・ゴールボタン mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ // Finish or Goal (destination.cp == -1 || destination.cp == 0 || destination.cp == -2) ? getActionButton(context, destination) : Container(), // 一般CPは空 ], ), Row( // 2列目 チェックインボタン mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ //checkin or remove checkin destinationController.isInRog.value == true && (distanceToDest <= destinationController.getForcedChckinDistance(destination) || destination.checkin_radious==-1 ) && destination.cp != 0 && destination.cp != -1 && destination.cp != -2 ? (isAlreadyCheckedIn == false ? ElevatedButton( // まだチェックインしていなければ、チェックインボタンを表示 style: ElevatedButton.styleFrom( backgroundColor: Colors.red), onPressed: () async { try { Get.back(); await destinationController.callforCheckin(destination); } catch (e) { // エラーハンドリング Get.snackbar( 'Error', 'An error occurred while processing check-in.', backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 3), ); // 必要に応じてエラーログを記録 print('Error processing check-in: $e'); } }, child: Text( "checkin".tr, style: TextStyle(color: Colors.white), ) ) : ElevatedButton( // チェックインしていれば、チェックイン取消ボタン style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[300]), onPressed: () async { try { await destinationController.removeCheckin(destination.cp!.toInt()); destinationController.deleteDestination(destination); Get.back(); Get.snackbar( 'チェックイン取り消し', '${destination.name}のチェックインは取り消されました', backgroundColor: Colors.green, colorText: Colors.white, duration: Duration(seconds: 3), ); } catch (e) { // エラーハンドリング Get.snackbar( 'Error', 'An error occurred while canceling check-in.', backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 3), ); // 必要に応じてエラーログを記録 print('Error canceling check-in: $e'); } }, child: Text( "cancel_checkin".tr, style: TextStyle(color: Colors.black), ) ) ) : Container(), // 近くにいなければ空 // go here or cancel route Obx(() => ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context) .colorScheme .onPrimaryContainer), onPressed: () async { if (destinationController.isRouteShowing.value) { destinationController.clearRoute(); Get.back(); } else { Get.back(); Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.bestForNavigation, forceAndroidLocationManager: true); Destination ds = Destination( lat: position.latitude, lon: position.longitude); Destination tp = Destination( lat: destination.lat, lon: destination.lon); destinationController .destinationMatrixFromCurrentPoint([ds, tp]); } }, child: Text( //ルート表示 or ルート消去 destinationController.isRouteShowing.value ? "cancel_route".tr : "go_here".tr, style: TextStyle( color: Theme.of(context).colorScheme.onPrimary), ) ) ), ], ), Row( children: [ Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ indexController.currentDestinationFeature .isNotEmpty && destinationController.isInCheckin.value == true ? Container() : FutureBuilder( future: wantToGo(context), builder: (context, snapshot) { return Container( child: snapshot.data, ); }, ), indexController.currentFeature[0] .properties!["location_name"] != null && (indexController.currentFeature[0] .properties!["location_name"] as String) .isNotEmpty ? Flexible( child: Text(indexController .currentFeature[0] .properties!["location_name"])) : const SizedBox( width: 0.0, height: 0, ), ], ), ), ], ), const SizedBox( height: 8.0, ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ const Icon(Icons.roundabout_left), const SizedBox( width: 8.0, ), indexController.currentFeature[0] .properties!["address"] != null && (indexController.currentFeature[0] .properties!["address"] as String) .isNotEmpty ? getDetails( context, "address".tr, indexController.currentFeature[0] .properties!["address"] ?? '') : const SizedBox( width: 0.0, height: 0, ), ], ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ const Icon(Icons.phone), const SizedBox( width: 8.0, ), indexController.currentFeature[0] .properties!["phone"] != null && (indexController.currentFeature[0] .properties!["phone"] as String) .isNotEmpty ? getDetails( context, "telephone".tr, indexController.currentFeature[0] .properties!["phone"] ?? '') : const SizedBox( width: 0.0, height: 0, ), ], ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ const Icon(Icons.email), const SizedBox( width: 8.0, ), indexController.currentFeature[0] .properties!["email"] != null && (indexController.currentFeature[0] .properties!["email"] as String) .isNotEmpty ? getDetails( context, "email".tr, indexController.currentFeature[0] .properties!["email"] ?? '') : const SizedBox( width: 0.0, height: 0, ), ], ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ const Icon(Icons.language), const SizedBox( width: 8.0, ), indexController.currentFeature[0] .properties!["webcontents"] != null && (indexController.currentFeature[0] .properties!["webcontents"] as String) .isNotEmpty ? getDetails( context, "web".tr, indexController.currentFeature[0] .properties!["webcontents"] ?? '', isurl: true) : const SizedBox( width: 0.0, height: 0, ), ], ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ const SizedBox( width: 8.0, ), indexController.currentFeature[0] .properties!["remark"] != null && (indexController.currentFeature[0] .properties!["remark"] as String) .isNotEmpty ? getDetails( context, "remarks".tr, indexController.currentFeature[0] .properties!["remark"] ?? '', isurl: false) : const SizedBox( width: 0.0, height: 0, ), ], ), ), ], ), )), const SizedBox( height: 60.0, ) ], ), ); } // 「行きたい」ボタンを表示するためのUIを構築するメソッドです。 // 目的地が選択されているかどうかに基づいて、適切なアイコンとテキストを表示します。 // ボタンがタップされたときの処理も含まれています。 // Future wantToGo(BuildContext context) async { bool selected = false; // print( // '---target-- ${indexController.currentFeature[0].properties!["location_id"]}----'); for (Destination d in destinationController.destinations) { //print('---- ${d.location_id.toString()} ----'); if (d.location_id == indexController.currentFeature[0].properties!["location_id"]) { selected = true; break; } } DatabaseHelper db = DatabaseHelper.instance; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // indexController.rog_mode == 0 ? // // IconButton( // // icon: Icon(Icons.pin_drop_sharp, size: 32, color: _selected == true ? Colors.amber : Colors.blue,), // // onPressed: (){ // // if(_selected){ // // // show remove from destination // // Get.defaultDialog( // // title: "本当にこのポイントを通過順から外しますか?", // // middleText: "場所は目的地リストから削除されます", // // backgroundColor: Colors.blue.shade300, // // titleStyle: TextStyle(color: Colors.white), // // middleTextStyle: TextStyle(color: Colors.white), // // textConfirm: "はい", // // textCancel: "いいえ", // // cancelTextColor: Colors.white, // // confirmTextColor: Colors.blue, // // buttonColor: Colors.white, // // barrierDismissible: false, // // radius: 10, // // content: Column( // // children: [ // // ], // // ), // // onConfirm: (){ // // int _id = indexController.currentFeature[0].properties!["location_id"]; // // Destination? d = destinationController.destinationById(_id); // // print('--- des id is : ${d} -----'); // // if(d != null) { // // //print('--- des id is : ${d.location_id} -----'); // // destinationController.deleteDestination(d); // // Get.back(); // // Get.back(); // // Get.snackbar("追加した", "場所が削除されました"); // // } // // } // // ); // // return; // // } // // // show add to destination // // Get.defaultDialog( // // title: "この場所を登録してもよろしいですか", // // middleText: "ロケーションがロガニング リストに追加されます", // // backgroundColor: Colors.blue.shade300, // // titleStyle: TextStyle(color: Colors.white), // // middleTextStyle: TextStyle(color: Colors.white), // // textConfirm: "はい", // // textCancel: "いいえ", // // cancelTextColor: Colors.white, // // confirmTextColor: Colors.blue, // // buttonColor: Colors.white, // // barrierDismissible: false, // // radius: 10, // // content: Column( // // children: [ // // ], // // ), // // onConfirm: (){ // // GeoJsonMultiPoint mp = indexController.currentFeature[0].geometry as GeoJsonMultiPoint; // // LatLng pt = LatLng(mp.geoSerie!.geoPoints[0].latitude, mp.geoSerie!.geoPoints[0].longitude); // // print("----- want to go sub location is ---- ${indexController.currentFeature[0].properties!["sub_loc_id"]} -----"); // // Destination dest = Destination( // // name: indexController.currentFeature[0].properties!["location_name"], // // address: indexController.currentFeature[0].properties!["address"], // // phone: indexController.currentFeature[0].properties!["phone"], // // email: indexController.currentFeature[0].properties!["email"], // // webcontents: indexController.currentFeature[0].properties!["webcontents"], // // videos: indexController.currentFeature[0].properties!["videos"], // // category: indexController.currentFeature[0].properties!["category"], // // series: 1, // // lat: pt.latitude, // // lon: pt.longitude, // // sub_loc_id: indexController.currentFeature[0].properties!["sub_loc_id"], // // location_id: indexController.currentFeature[0].properties!["location_id"], // // list_order: 1, // // photos: indexController.currentFeature[0].properties!["photos"], // // checkin_radious: indexController.currentFeature[0].properties!["checkin_radius"], // // auto_checkin: indexController.currentFeature[0].properties!["auto_checkin"] == true ? 1 : 0, // // cp: indexController.currentFeature[0].properties!["cp"], // // checkin_point: indexController.currentFeature[0].properties!["checkin_point"], // // buy_point: indexController.currentFeature[0].properties!["buy_point"], // // selected: false, // // checkedin: false, // // hidden_location: indexController.currentFeature[0].properties!["hidden_location"] == true ?1 : 0 // // ); // // destinationController.addDestinations(dest); // // Get.back(); // // Get.back(); // // Get.snackbar("追加した", "場所が追加されました"); // // } // // ); // // }, // // ): // // Container(), const SizedBox( width: 8.0, ), Obx((() => indexController.rogMode.value == 1 ? ElevatedButton( onPressed: () async { Destination dest = indexController.currentDestinationFeature[0]; //print("~~~~ before checking button ~~~~"); //print("------ curent destination is ${dest!.checkedIn}-------"); destinationController.makeCheckin( dest, !dest.checkedin!, ""); }, child: indexController .currentDestinationFeature[0].checkedin == false ? const Text("チェックイン") : const Text("チェックアウト")) : Container())), ], ), ], ); } Widget getCheckin(BuildContext context) { //print("------ currentAction ----- ${indexController.currentAction}-----"); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ indexController.currentAction[0][0]["checkin"] == false ? Column( children: [ Row( mainAxisSize: MainAxisSize.max, children: [ ElevatedButton( child: const Text("Image"), onPressed: () { final ImagePicker picker = ImagePicker(); picker .pickImage(source: ImageSource.camera) .then((value) { //print("----- image---- ${value!.path}"); }); }, ) ], ), ElevatedButton( onPressed: () { if (indexController.currentAction.isNotEmpty) { //print(indexController.currentAction[0]); indexController.currentAction[0][0]["checkin"] = true; Map temp = Map.from( indexController.currentAction[0][0]); indexController.currentAction.clear(); //print("---temp---${temp}"); indexController.currentAction.add([temp]); } }, child: Text("checkin".tr)) ], ) : ElevatedButton( onPressed: () { if (indexController.currentAction.isNotEmpty) { //print(indexController.currentAction[0]); indexController.currentAction[0][0]["checkin"] = false; Map temp = Map.from( indexController.currentAction[0][0]); indexController.currentAction.clear(); //print("---temp---${temp}"); indexController.currentAction.add([temp]); } }, child: const Icon(Icons.favorite, color: Colors.red), ) ], ) ], ); } // 目的地の詳細情報(住所、電話番号、Webサイトなど)を表示するためのUIを構築するメソッドです。 // ラベルとテキストを受け取り、適切なアイコンとともに表示します。 // Widget getDetails(BuildContext context, String label, String text, {bool isurl = false}) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(label), const SizedBox( width: 10.0, ), InkWell( onTap: () { if (isurl) { if (indexController.rogMode.value == 0) { _launchURL(indexController .currentFeature[0].properties!["webcontents"]); } else { indexController.currentDestinationFeature[0].webcontents; } } }, child: SizedBox( width: MediaQuery.of(context).size.width - (MediaQuery.of(context).size.width * 0.35), child: Text( text, textAlign: TextAlign.justify, style: TextStyle( color: isurl ? Colors.blue : Colors.black, ), ), ), ), ], ); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/utils/const.dart'; import 'package:rogapp/utils/database_helper.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_start.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_goal.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_normal_point.dart'; import 'package:rogapp/widgets/bottom_sheet_new.dart'; import 'package:timeline_tile/timeline_tile.dart'; class DestinationWidget extends StatelessWidget { DestinationWidget({Key? key}) : super(key: key); final DestinationController destinationController = Get.find(); final IndexController indexController = Get.find(); final List _items = List.generate(50, (int index) => index); Image getImage(int index) { if (destinationController.destinations[index].photos == null || destinationController.destinations[index].photos == "") { return const Image(image: AssetImage('assets/images/empty_image.png')); } else { // print( // "------- image is ${destinationController.destinations[index].photos!}------"); String photo = destinationController.destinations[index].photos!; if (photo.contains('http')) { return Image( image: NetworkImage(destinationController.destinations[index].photos!), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } else { String serverUrl = ConstValues.currentServer(); //print("==== photo is ${server_url + '/media/compressed/' + destinationController.destinations[index].photos!} ==="); return Image( image: NetworkImage( '$serverUrl/media/compressed/${destinationController.destinations[index].photos!}'), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } } } // bool getSelection(int index){ // bool ret = false; // destinationController.destination_index_data.forEach((element) { // if(index == element["index"]){ // if(element["selected"] == true){ // ret = true; // return; // } // } // }); // return ret; // } void doDelete() { for (var element in destinationController.currentSelectedDestinations) { destinationController.deleteDestination(element); destinationController.resetRogaining(); } // destinationController.destination_index_data.forEach((element) { // //print(element["index"]); // destinationController.deleteDestination(element["index"]); // }); // destinationController.destination_index_data.clear(); } void moveUp() { Destination? d; for (Destination ad in destinationController.destinations) { if (ad.selected == true) { d = ad; break; } } if (d != null) { //print("--- selected destination is ${d.list_order}"); destinationController.makeOrder(d, -1); } } void moveDown() { Destination? d; for (Destination ad in destinationController.destinations) { if (ad.selected == true) { d = ad; break; } } if (d != null) { //print("--- selected destination is ${d.list_order}"); destinationController.makeOrder(d, 1); } } void clearall() { Get.defaultDialog( title: "are_you_sure_want_to_delete_all".tr, middleText: "all_added_destination_will_be_deleted".tr, backgroundColor: Colors.blue.shade300, titleStyle: const TextStyle(color: Colors.white), middleTextStyle: const TextStyle(color: Colors.white), textConfirm: "confirm".tr, textCancel: "cancel".tr, cancelTextColor: Colors.white, confirmTextColor: Colors.blue, buttonColor: Colors.white, barrierDismissible: false, radius: 10, content: const Column( children: [], ), onConfirm: () { destinationController.deleteAllDestinations(); Get.back(); Get.snackbar( "deleted".tr, "all_destinations_are_deleted_successfully".tr, backgroundColor: Colors.green, colorText: Colors.white); }); } void interChange() { // int first_index = -1; // destinationController.destination_index_data.forEach((element) { // //print(element["index"]); // int action_id = destinationController.destinations[element["index"]]["id"] as int; // destinationController.makeOrder(action_id, (element["index"] as int) + 1, "up"); // }); } Future getIsLocationAvilable(int locationId) async { DatabaseHelper db = DatabaseHelper.instance; return await db.isAlreadyAvailable(locationId); } @override Widget build(BuildContext context) { print( "------ destination widget------ ${destinationController.destinationCount.value} ----------"); return Obx(() => Stack( children: [ Padding( padding: const EdgeInsets.only(top: 45.0), child: ListView.builder( itemCount: destinationController.destinationCount.value, itemBuilder: (BuildContext context, int index) { return TimelineTile( alignment: TimelineAlign.manual, lineXY: 0.2, isFirst: index == 0 ? true : false, indicatorStyle: IndicatorStyle( indicator: CircleAvatar( backgroundColor: Colors.red, child: Text( destinationController.destinations[index].list_order .toString(), style: const TextStyle(color: Colors.white), ), ), ), key: Key(index.toString()), endChild: Card( child: Container( constraints: const BoxConstraints( minHeight: 80, ), child: ListTile( onTap: () async { { Destination? fs = destinationController.destinations[index]; //print("----fsf-----$index"); if (indexController .currentDestinationFeature.isNotEmpty) { indexController.currentDestinationFeature .clear(); } indexController.currentDestinationFeature .add(fs); // print( // "--- ndexController.currentDestinationFeature ----- ${indexController.currentDestinationFeature[0].name} ----"); //indexController.getAction(); Widget bottomSheet = BottomSheetNew(destination: fs); /* if (fs.cp == -1 || fs.cp == 0) { bottomSheet = BottomSheetStart(destination: fs); } else if (fs.cp == -2 || fs.cp == 0) { bottomSheet = BottomSheetGoal(destination: fs); } else { bottomSheet = BottomSheetNormalPoint(destination: fs); } */ showModalBottomSheet( constraints: BoxConstraints.loose( Size(Get.width, Get.height * 0.85)), context: context, isScrollControlled: true, //builder:((context) => BottomSheetWidget()) builder: ((context) => bottomSheet) ); } }, onLongPress: () { destinationController.toggleSelection( destinationController.destinations[index]); }, selectedTileColor: Colors.amberAccent, selected: destinationController .destinations[index].selected!, leading: getImage(index), title: Text(destinationController .destinations[index].name!), subtitle: Text(destinationController .destinations[index].category!), ), ), ), startChild: index > 0 && destinationController.matrix["routes"][0] ["legs"] != null ? Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text(destinationController.matrix["routes"][0] ["legs"][index - 1]["distance"] != null ? destinationController.matrix["routes"][0] ["legs"][index - 1]["distance"] ["text"] .toString() : ''), Text(destinationController.matrix["routes"][0] ["legs"][index - 1]["duration"] != null ? destinationController.matrix["routes"][0] ["legs"][index - 1]["duration"] ["text"] .toString() : '') ], ) : Container(), ); }), ), Container( decoration: BoxDecoration( boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.3), spreadRadius: 5, blurRadius: 3, offset: const Offset(0, 7), // changes position of shadow ), ], ), height: 44.0, width: MediaQuery.of(context).size.width, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.delete_forever), //onPressed: (){doDelete();}, onPressed: clearall, ), IconButton( icon: const Icon(Icons.cancel), //onPressed: (){doDelete();}, onPressed: destinationController .currentSelectedDestinations.isNotEmpty ? doDelete : null, ), IconButton( icon: const Icon(Icons.move_up), onPressed: destinationController .currentSelectedDestinations.isNotEmpty ? moveUp : null, ), IconButton( icon: const Icon(Icons.move_down), onPressed: destinationController .currentSelectedDestinations.isNotEmpty ? moveDown : null, ), // IconButton( // icon:Icon(Icons.sync), // onPressed: destinationController.destination_index_data.length == 2 ? interChange : null, // ), ], ), ) ], )); } } import 'package:flutter/material.dart'; class CFormTextField extends StatelessWidget { const CFormTextField({ super.key, required this.cFocus, required TextEditingController cController, }) : cTextEditingController = cController; final FocusNode cFocus; final TextEditingController cTextEditingController; @override Widget build(BuildContext context) { return TextFormField( autocorrect: false, autofocus: true, focusNode: cFocus, controller: cTextEditingController, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.done, validator: (value) { if (value == null || value.isEmpty) { return "Need a valied email address"; } return null; }, decoration: InputDecoration( //filled: true, //fillColor: Theme.of(context).colorScheme.primaryContainer, hintText: "Enter email address", labelText: "Email", labelStyle: TextStyle( color: Theme.of(context).colorScheme.onPrimaryContainer, fontSize: 16), prefixIcon: const Icon(Icons.email_outlined), suffixIcon: cTextEditingController.text.isNotEmpty ? IconButton( onPressed: () { cTextEditingController.clear(); }, icon: const Icon(Icons.clear)) : Container( width: 0, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( width: 1, color: Theme.of(context).colorScheme.secondary)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( width: 2, color: Theme.of(context).colorScheme.primary))), ); } } import 'dart:async'; import 'package:flutter/material.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:rogapp/pages/permission/permission.dart'; import 'package:rogapp/pages/settings/settings_binding.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/utils/location_controller.dart'; import 'package:rogapp/utils/text_util.dart'; import 'package:rogapp/widgets/base_layer_widget.dart'; import 'package:rogapp/widgets/bottom_sheet_new.dart'; import 'package:rogapp/widgets/current_position_widget.dart'; import 'package:rogapp/widgets/game_state_view.dart'; import 'package:rogapp/pages/settings/settings_controller.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; class MapResetController { void Function()? resetIdleTimer; } class MapWidget extends StatefulWidget { const MapWidget({Key? key}) : super(key: key); @override State createState() => _MapWidgetState(); } class _MapWidgetState extends State with WidgetsBindingObserver { //class _MapWidgetState extends State { final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); final LocationController locationController = Get.find(); final SettingsController settingsController = Get.find(); late MapController mapController; final Completer mapControllerCompleter = Completer(); StreamSubscription? subscription; Timer? _timer; bool curr_marker_display = false; Map _markerCache = {}; List _markers = []; @override void initState() { super.initState(); SettingsBinding().dependencies(); // これを追加 _startIdleTimer(); mapController = MapController(); indexController.mapController = mapController; // added by Akira WidgetsBinding.instance.addObserver(this); _startIdleTimer(); // マップの操作イベントをリッスンして、_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); // indexController.mapController = MapController(initCompleter: mapControllerCompleter); } void _resetIdleTimer() { debugPrint("_resetIdleTimer..."); _timer?.cancel(); _startIdleTimer(); } @override void dispose() { 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秒間でコール void _startIdleTimer() { //debugPrint("_startIdleTimer ...."); final settingsController = Get.find(); 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 _initMarkers() async { List markers = await _getMarkers(); setState(() { _markers = markers; }); } Future> _getMarkers() async { List 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), )); } } 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: [ 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? getPoints() { List 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(); // これを追加 //final PopupController popupController = PopupController(); return Stack( children: [ Obx(() => indexController.isLoading.value == true ? const Padding( padding: EdgeInsets.only(top: 60.0), child: CircularProgressIndicator(), ) : FlutterMap( mapController: mapController, //mapController: indexController.mapController, options: MapOptions( maxZoom: 18.4, onMapReady: () { _initMarkers(); //indexController.isMapControllerReady.value = true; }, initialCenter: const LatLng(37.15319600454702, 139.58765950528198), bounds: indexController.currentBound.isNotEmpty ? indexController.currentBound[0] : LatLngBounds.fromPoints([ const LatLng(35.03999881162295, 136.40587119778962), const LatLng(36.642756778706904, 137.95226720406063) ]), initialZoom: 1, interactiveFlags: InteractiveFlag.pinchZoom | InteractiveFlag.drag, onPositionChanged: (MapPosition pos, hasGesture) { if (hasGesture) { _resetTimer(); } indexController.currentBound = [pos.bounds!]; }, onMapEvent: (MapEvent mapEvent) { if (mapEvent is MapEventMove) { destinationController.shouldShowBottomSheet = true; } }, //onTap: (_, __) => popupController.hideAllPopups(), ), children: [ const BaseLayer(), Obx( () => indexController.routePointLenght > 0 ? PolylineLayer( polylines: [ Polyline( points: getPoints()!, strokeWidth: 6.0, color: Colors.indigo, ), ], ) : Container(), ), CurrentLocationLayer( positionStream: locationController .locationMarkerPositionStreamController.stream, //alignDirectionOnUpdate: AlignOnUpdate.never, style: const LocationMarkerStyle( marker: Stack( children: [ CircleAvatar( radius: 13.5, backgroundColor: Colors.blue, child: Icon(Icons.navigation, color: Colors.white), ), ], ), markerSize: Size(27, 27), markerDirection: MarkerDirection.heading, ), //child: const Icon(Icons.navigation), ), FutureBuilder>( 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()), StreamBuilder( 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(); }, ) ], ); } } import 'package:flutter/material.dart'; import 'package:rogapp/widgets/GameState/Colors.dart'; enum ConnectionStatus { none, mobile, wifi } class ConnectionStatusIndicator extends StatelessWidget { final ConnectionStatus connectionStatus; final bool minimized; const ConnectionStatusIndicator({ Key? key, required this.connectionStatus, this.minimized = false, }) : super(key: key); @override Widget build(BuildContext context) { Color backgroundColor; IconData iconData; String text; switch (connectionStatus) { case ConnectionStatus.none: backgroundColor = JapaneseColors.ume; iconData = Icons.signal_cellular_off; text = 'No Connection'; break; case ConnectionStatus.mobile: backgroundColor = JapaneseColors.take; iconData = Icons.signal_cellular_alt; text = 'Mobile Data'; break; case ConnectionStatus.wifi: backgroundColor = JapaneseColors.sora; iconData = Icons.wifi; text = 'Wi-Fi'; break; default: backgroundColor = Colors.grey; // Fallback color iconData = Icons.device_unknown; text = 'Unknown'; } return Container( height: minimized ? 40 : null, width: minimized ? 40 : null, padding: minimized ? null : EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: backgroundColor, shape: minimized ? BoxShape.circle : BoxShape.rectangle, borderRadius: minimized ? null : BorderRadius.circular(10), ), child: minimized ? Center( child: Icon(iconData, color: Colors.white, size: 24), ) : Row( mainAxisSize: MainAxisSize.min, children: [ Icon(iconData, color: Colors.white), SizedBox(width: 8), Text( text, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ], ), ); } } import 'package:flutter/material.dart'; import 'package:rogapp/widgets/GameState/Colors.dart'; class LocationVisitedWidget extends StatelessWidget { final int count; final bool minimized; const LocationVisitedWidget( {Key? key, required this.count, this.minimized = false}) : super(key: key); @override Widget build(BuildContext context) { if (minimized) { return Container( height: 40, width: 40, decoration: BoxDecoration( color: JapaneseColors.mizu, shape: BoxShape.circle, ), child: Center( child: Text( '$count', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ); } else { return Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8), decoration: BoxDecoration( color: JapaneseColors.matcha, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle_outline, color: Colors.white, size: 24), SizedBox(width: 8), Text( '$count チェックイン', // "X Check-ins" in Japanese style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16), ), ], ), ); } } } import 'package:flutter/material.dart'; class JapaneseColors { static const Color mizu = Color(0xFFA4DDED); // Mizu (light blue) static const Color matcha = Color(0xFFC5E1A5); static const Color ume = Color(0xFFE1A8A8); // Ume (plum) static const Color take = Color(0xFF7B8D42); // Take (bamboo) static const Color sora = Color(0xFFA1CAF1); static const Color indigo = Color(0xFF264653); // Aizome static const Color sakuraPink = Color(0xFFFAD2E1); // Sakura-iro /// Matcha (green tea) } import 'package:flutter/material.dart'; import 'package:rogapp/widgets/GameState/CheckinState.dart'; import 'package:rogapp/widgets/GameState/game_on_off.dart'; class DashboardWidget extends StatelessWidget { final bool gameStarted; final int locationsVisited; final bool isMinimized; const DashboardWidget({ Key? key, required this.gameStarted, required this.locationsVisited, this.isMinimized = false, }) : super(key: key); @override Widget build(BuildContext context) { List widgets = [ GameStatusIndicator(gameStarted: gameStarted, minimized: isMinimized), SizedBox( height: isMinimized ? 0 : 8, width: isMinimized ? 8 : 0), // Spacing LocationVisitedWidget(count: locationsVisited, minimized: isMinimized), ]; return Container( padding: EdgeInsets.all(isMinimized ? 8 : 16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 7, offset: Offset(0, 3), ), ], borderRadius: BorderRadius.circular(10), ), child: isMinimized ? Row( mainAxisSize: MainAxisSize.min, children: widgets, ) : Column( mainAxisSize: MainAxisSize.min, children: widgets, ), ); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/widgets/GameState/Colors.dart'; class GameStatusIndicator extends StatelessWidget { final bool gameStarted; final bool minimized; const GameStatusIndicator( {Key? key, required this.gameStarted, this.minimized = true}) : super(key: key); @override Widget build(BuildContext context) { // Icons to show based on the game status IconData iconData = gameStarted ? Icons.stop_circle : Icons.play_circle_filled; // Text to show based on the game status String text = gameStarted ? 'in_game'.tr : 'start_game'.tr; // Layout for minimized view if (minimized) { return Container( height: 40, // Square size width: 40, // Square size decoration: BoxDecoration( color: gameStarted ? JapaneseColors.indigo : JapaneseColors.sakuraPink, shape: BoxShape .circle, // Making it circle when minimized for a more distinct look ), child: Icon(iconData, color: Colors.white), ); } // Layout for expanded view return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: gameStarted ? JapaneseColors.indigo : JapaneseColors.sakuraPink, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(iconData, color: Colors.white), SizedBox(width: 8), Text( text, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ], ), ); } } import 'package:flutter/material.dart'; import 'package:rogapp/pages/search/search_page.dart'; class FakeSearch extends StatelessWidget { const FakeSearch({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: (){ Navigator.push(context, MaterialPageRoute(builder: (context) => SearchPage())); }, child: Container( height: 35, decoration: BoxDecoration( border: Border.all(color:Colors.blue, width: 1.4), borderRadius: BorderRadius.circular(25) ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Icon(Icons.search, color: Colors.grey,), ), const Text("What are you looking for", style: TextStyle(fontSize: 16, color: Colors.grey),), Container( height: 32, width: 75, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(25), ), child: const Center(child: Text("Search", style: TextStyle(fontSize: 16, color: Colors.white),)), ) ], ), ), ); } }import 'package:flutter/material.dart'; import 'package:geojson_vi/geojson_vi.dart'; import 'package:get/get.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/services/maxtrix_service.dart'; import 'package:rogapp/utils/const.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_start.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_goal.dart'; //import 'package:rogapp/widgets/bottom_sheets/bottom_sheet_normal_point.dart'; import 'package:rogapp/widgets/bottom_sheet_new.dart'; class ListWidget extends StatefulWidget { const ListWidget({Key? key}) : super(key: key); @override State createState() => _ListWidgetState(); } // IndexControllerから目的地のリストを取得し、ListView.builderを使用してリストを表示します。 // 各リストアイテムは、目的地の画像、名前、カテゴリ、サブロケーションID、現在地からの距離を表示します。 // リストアイテムがタップされると、changeCurrentFeatureメソッドを呼び出して現在の目的地を更新し、 BottomSheetウィジェットを表示します。 // 主なロジック: // IndexControllerから目的地のリストを取得し、ListView.builderを使用してリストを構築します。 // getImageメソッドを使用して、目的地の画像を取得し表示します。画像が存在しない場合は、デフォルトの画像を表示します。 // matrixDistanceメソッドを使用して、現在地から目的地までの距離を計算し表示します。 // リストアイテムがタップされると、changeCurrentFeatureメソッドを呼び出して現在の目的地を更新し、showModalBottomSheetを使用してBottomSheetウィジェットを表示します。 // class _ListWidgetState extends State { final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); Image getImage(int index) { if (indexController.locations[0].features[index]!.properties!["photos"] == null || indexController.locations[0].features[index]!.properties!["photos"] == "") { return const Image(image: AssetImage('assets/images/empty_image.png')); } else { //print("==== photo index is $index ==="); String serverUrl = ConstValues.currentServer(); GeoJSONFeature gf = indexController.locations[0].features[index]!; String photo = gf.properties!["photos"]; return Image( image: NetworkImage('$serverUrl/media/compressed/$photo'), errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Image.asset("assets/images/empty_image.png"); }, ); } } // 未使用? void changeCurrentFeature(GeoJSONFeature fs) { if (indexController.currentFeature.isNotEmpty) { indexController.currentFeature.clear(); } indexController.currentFeature.add(fs); } @override void initState() { super.initState(); } Destination createDestination(GeoJSONFeature feature) { final props = feature.properties; GeoJSONMultiPoint pt = feature.geometry as GeoJSONMultiPoint; return Destination( cp: props!['cp'], lat: pt.coordinates[0][1], lon: pt.coordinates[0][0], ); } Future matrixDistance(int i) async { // Create two destinations directly from indexController.locations[0].collection Destination desCurr = Destination( lat: indexController.currentLat, lon: indexController.currentLon); //Destination dest1 = createDestination(indexController.locations[0].collection[0]); Destination dest2 = createDestination(indexController.locations[0].features[i]!); // Get the distance between these two destinations final res = await MatrixService.getDestinations([desCurr, dest2]); return res["routes"][0]["legs"][0]["distance"]["text"]; //print("matrix result is ${i} : ${res["routes"][0]["legs"][0]["distance"]["text"]} "); } Future _pullRefresh() async { //print("pull to refesh"); indexController.locations[0].features.sort((a, b) => (a!.properties!['cp'] as Comparable) .compareTo(b!.properties!['cp'] as Comparable)); setState(() {}); } @override Widget build(BuildContext context) { debugPrint("_ListWidgetState"); return Obx( () => indexController.locations.isNotEmpty ? RefreshIndicator( onRefresh: _pullRefresh, child: ListView.builder( itemCount: indexController.locations[0].features.length, shrinkWrap: true, itemBuilder: (_, index) { bool isFound = false; for (Destination d in destinationController.destinations) { if (indexController.locations[0].features[index]! .properties!['location_id'] == d.location_id) { isFound = true; break; } } return Card( child: ListTile( selected: isFound, selectedTileColor: Colors.yellow.shade200, onTap: () { GeoJSONFeature gf = indexController.locations[0].features[index]!; Destination des = destinationController.festuretoDestination(gf); changeCurrentFeature(gf); Widget bottomSheet = BottomSheetNew(destination: des); /* if (des.cp == -1 || des.cp == 0) { bottomSheet = BottomSheetStart(destination: des); } else if (des.cp == -2 || des.cp == 0) { bottomSheet = BottomSheetGoal(destination: des); } else { bottomSheet = BottomSheetNormalPoint(destination: des); } */ showModalBottomSheet( constraints: BoxConstraints.loose( Size(Get.width, Get.height * 0.85)), isScrollControlled: true, context: context, builder: ((context) => bottomSheet ), ); }, leading: getImage(index), title: indexController.locations[0].features[index]! .properties!['location_name'] != null ? Text(indexController.locations[0].features[index]! .properties!['location_name'] .toString()) : const Text(""), subtitle: indexController.locations[0].features[index]! .properties!['category'] != null ? Text(indexController.locations[0].features[index]! .properties!['category']) : const Text(""), trailing: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ indexController.locations[0].features[index]! .properties!['sub_loc_id'] != null ? Text(indexController.locations[0] .features[index]!.properties!['sub_loc_id']) : const Text(""), SizedBox( width: 100, child: FutureBuilder( future: matrixDistance(index), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator(), ); } if (snapshot.hasError) { return const Text("-"); } else { return Text( snapshot.data ?? '', style: const TextStyle( color: Colors.red, fontWeight: FontWeight.bold), ); } }, ), ) ], )), ); }, ), ) : const SizedBox( width: 0, height: 0, ), ); } } import 'package:geojson_vi/geojson_vi.dart'; import 'package:get/get_state_manager/get_state_manager.dart'; class BottomSheetController extends GetxController { List? currentFeature = []; BottomSheetController({this.currentFeature}); } //import 'dart:ffi'; //import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/services/DatabaseService.dart'; //import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/widgets/GameState/CheckinState.dart'; import 'package:rogapp/widgets/GameState/ConnectionStatus.dart'; //import 'package:rogapp/widgets/GameState/DashboardWidget.dart'; import 'package:rogapp/widgets/GameState/game_on_off.dart'; //import 'package:rogapp/widgets/GameState/Colors.dart'; import 'package:rogapp/widgets/gps_status.dart'; import 'package:rogapp/utils/location_controller.dart'; class GameStateManager { static final GameStateManager _instance = GameStateManager._internal(); factory GameStateManager() { return _instance; } GameStateManager._internal(); final List _logs = []; final List _listeners = []; List get logs => _logs; void addLog(String log) { _logs.add(log); _notifyListeners(); // Notify all listeners } void clearLogs() { _logs.clear(); _notifyListeners(); // Notify all listeners } void addListener(VoidCallback listener) { _listeners.add(listener); } void removeListener(VoidCallback listener) { _listeners.remove(listener); } void _notifyListeners() { for (var listener in _listeners) { listener(); } } } class GameStateWidget extends StatefulWidget { const GameStateWidget({super.key}); @override State createState() => _GameStateWidgetState(); } class _GameStateWidgetState extends State { final GameStateManager gameStateManager = GameStateManager(); final IndexController indexController = Get.find(); final DestinationController destinationController = Get.find(); @override void initState() { super.initState(); gameStateManager.addListener(_updateLogs); } @override void dispose() { gameStateManager.removeListener(_updateLogs); super.dispose(); } void _updateLogs() { Future.delayed(Duration.zero, () { if (mounted) { setState(() {}); } }); } void toggleExpanded() { setState(() { isExpanded = !isExpanded; }); } void clearLogs() { gameStateManager.clearLogs(); } bool isExpanded = false; @override Widget build(BuildContext context) { final DatabaseService dbService = DatabaseService(); //final LocationController locationController = Get.find(); return Container( width: MediaQuery.of(context).size.width, decoration: const BoxDecoration(color: Colors.black12), child: GestureDetector( onTap: toggleExpanded, child: AnimatedContainer( duration: const Duration(milliseconds: 200), color: isExpanded ? Colors.black54 : Colors.black12, height: isExpanded ? 160.0 : 48.0, // Adjust sizes as needed child: Column( children: [ // Top bar with clear button if (isExpanded) Container( padding: const EdgeInsets.symmetric(horizontal: 10.0), color: Colors.blueGrey, // Adjust color as needed child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('game_status'.tr, style: TextStyle(color: Colors.white)), IconButton( icon: const Icon(Icons.clear, color: Colors.white), onPressed: toggleExpanded, ), ], ), ), // Log messages Expanded( child: SingleChildScrollView( child: Wrap( alignment: WrapAlignment.spaceEvenly, children: [ Obx(() => Padding( padding: const EdgeInsets.all(4.0), child: GameStatusIndicator( gameStarted: destinationController.isInRog.value, minimized: !isExpanded, ), )), Padding( padding: const EdgeInsets.all(4.0), child: StreamBuilder>( stream: dbService.destinationUpdatesStream, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } else if (snapshot.hasError) { return LocationVisitedWidget( count: 0, minimized: !isExpanded, ); } else if (snapshot.hasData) { return LocationVisitedWidget( count: snapshot.data!.length, minimized: !isExpanded, ); } else { return LocationVisitedWidget( count: 0, minimized: !isExpanded, ); } }, ), // child: LocationVisitedWidget( // count: // minimized: !isExpanded, // ), ), Padding( padding: const EdgeInsets.all(4.0), child: Obx(() => ConnectionStatusIndicator( connectionStatus: (indexController .connectionStatusName.value == "wifi" || indexController .connectionStatusName.value == "mobile") ? indexController.connectionStatusName.value == "wifi" ? ConnectionStatus.wifi : ConnectionStatus.mobile : ConnectionStatus.none, minimized: !isExpanded)), ), // Expanded view Padding( padding: const EdgeInsets.all(4.0), child:GpsSignalStrengthIndicator( locationController: Get.find(), minimized: !isExpanded, // isExpanded はあなたのロジックに依存した変数), ) ), ], ), // child: Obx( // () => DashboardWidget( // gameStarted: destinationController.isInRog.value, // locationsVisited: 3, // isMinimized: false, // ), ), ), ], ), ), ), ); } } import 'package:flutter/material.dart'; class CPasswordTextField extends StatefulWidget { const CPasswordTextField( {super.key, required this.cFocusNode, required this.cController}); final FocusNode cFocusNode; final TextEditingController cController; @override State createState() => _CPasswordTextFieldState(); } class _CPasswordTextFieldState extends State { var _isVisible = false; @override Widget build(BuildContext context) { return TextFormField( controller: widget.cController, textInputAction: TextInputAction.go, obscureText: !_isVisible, validator: (value) { if (value == null || value.isEmpty || value.length < 5) { return "Need a valied password with more than 4 charectors"; } return null; }, decoration: InputDecoration( //filled: true, //fillColor: Theme.of(context).colorScheme.tertiaryContainer, hintText: "Enter password", labelText: "Password", labelStyle: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 16), suffixIcon: IconButton( onPressed: () { setState(() { _isVisible = !_isVisible; }); }, icon: _isVisible ? const Icon(Icons.visibility) : const Icon(Icons.visibility_off)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( width: 1, color: Theme.of(context).colorScheme.secondary)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( width: 2, color: Theme.of(context).colorScheme.primary)))); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; // これを追加 class CurrentPosition extends StatefulWidget { const CurrentPosition({super.key}); @override State createState() => _CurrentPositionState(); } class _CurrentPositionState extends State { final DestinationController destinationController = Get.find(); void _onLongPress() async { PermissionStatus status = await Permission.location.status; if (!status.isGranted) { await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('位置情報の許可が必要です'), content: Text('現在位置を表示するには、位置情報の許可が必要です。「設定」からアプリの権限を許可してください。'), actions: [ TextButton( child: Text('キャンセル'), onPressed: () => Navigator.of(context).pop(), ), TextButton( child: Text('設定'), onPressed: () { Navigator.of(context).pop(); openAppSettings(); }, ), ], ); }, ); } else { Get.toNamed(AppPages.SETTINGS); } } @override Widget build(BuildContext context) { return GestureDetector( // GestureDetectorを追加 onLongPress: _onLongPress, // 長押しイベントを追加 child: Container ( // return Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(20.0)), child: IconButton( onPressed: () { destinationController.centerMapToCurrentLocation(); }, icon: const Icon( Icons.location_searching, color: Colors.white, ), ), ), ); } } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/permission/permission.dart'; class PermissionHandlerScreen extends StatefulWidget { @override _PermissionHandlerScreenState createState() => _PermissionHandlerScreenState(); } class _PermissionHandlerScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { await PermissionController.checkAndRequestPermissions(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('権限の確認'), ), body: Center( child: Text('権限の確認中...'), ), ); } }import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; class BaseLayer extends StatelessWidget { const BaseLayer({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return TileLayer( urlTemplate: "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png", tileProvider: FMTC.instance('OpenStreetMap (A)').getTileProvider( settings: FMTCTileProviderSettings( behavior: CacheBehavior.values.byName('cacheFirst'), cachedValidDuration: const Duration(days: 14), ), ), ); } } // var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { // attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' // });import 'package:flutter/material.dart'; class LogManager { static final LogManager _instance = LogManager._internal(); factory LogManager() { return _instance; } LogManager._internal(); List _logs = []; List _listeners = []; List _operationLogs = []; List get operationLogs => _operationLogs; List get logs => _logs; void addLog(String log) { _logs.add(log); _notifyListeners(); // Notify all listeners } void clearLogs() { _logs.clear(); _notifyListeners(); // Notify all listeners } void addOperationLog(String log) { _operationLogs.add(log); _notifyListeners(); } void clearOperationLogs() { _operationLogs.clear(); _notifyListeners(); } void addListener(VoidCallback listener) { _listeners.add(listener); } void removeListener(VoidCallback listener) { _listeners.remove(listener); } void _notifyListeners() { for (var listener in _listeners) { listener(); } } } class DebugWidget extends StatefulWidget { const DebugWidget({super.key}); @override State createState() => _DebugWidgetState(); } class _DebugWidgetState extends State { final LogManager logManager = LogManager(); @override void initState() { super.initState(); logManager.addListener(_updateLogs); } @override void dispose() { logManager.removeListener(_updateLogs); super.dispose(); } void _updateLogs() { Future.delayed(Duration.zero, () { if (mounted) { setState(() {}); } }); } void toggleExpanded() { setState(() { isExpanded = !isExpanded; }); } void clearLogs() { logManager.clearLogs(); } bool isExpanded = false; @override Widget build(BuildContext context) { return Positioned( left: 0, right: 0, bottom: 0, child: GestureDetector( onTap: toggleExpanded, child: AnimatedContainer( duration: Duration(milliseconds: 200), color: Colors.black54, height: isExpanded ? 450.0 : 50.0, // Adjust sizes as needed child: Column( children: [ // Top bar with clear button if (isExpanded) Container( padding: EdgeInsets.symmetric(horizontal: 10.0), color: Colors.blueGrey, // Adjust color as needed child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Debug Logs', style: TextStyle(color: Colors.white)), IconButton( icon: Icon(Icons.clear, color: Colors.white), onPressed: clearLogs, ), ], ), ), // Log messages Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: logManager.logs.reversed .map((log) => Text( "${DateTime.now().hour}:${DateTime.now().minute}:${DateTime.now().second}:${DateTime.now().microsecond} - $log", style: const TextStyle(color: Colors.white))) .toList(), ), ), ), ], ), ), ), ); } }