From 66ade1fe09ef890916007d6fc8bbf129971cf34f Mon Sep 17 00:00:00 2001 From: Akira Date: Mon, 5 Aug 2024 03:08:12 +0900 Subject: [PATCH] release 4.8.9 --- all_combined.txt | 17257 ---------------- assets/images/QR_certificate.png | Bin 0 -> 85858 bytes assets/images/QR_gifuroge_stage1.png | Bin 0 -> 518 bytes ios/Runner.xcodeproj/project.pbxproj | 26 +- lib/model/category.dart | 17 +- lib/model/entry.dart | 4 + lib/model/team.dart | 8 +- lib/pages/camera/camera_page.dart | 321 +- .../destination/destination_controller.dart | 21 +- lib/pages/drawer/drawer_page.dart | 3 + lib/pages/entry/entry_controller.dart | 25 + lib/pages/entry/entry_list_page.dart | 57 + lib/pages/entry/event_entries_controller.dart | 70 +- lib/pages/entry/event_entries_page.dart | 83 +- lib/pages/index/index_controller.dart | 63 +- lib/pages/index/index_page.dart | 3 +- lib/pages/login/login_page.dart | 480 +- lib/pages/login/login_page.dart_backup | 381 + lib/pages/login_popup/login_popup_page.dart | 3 +- lib/pages/register/register_page.dart | 60 +- lib/pages/team/team_detail_page.dart | 5 - lib/pages/team/team_list_page.dart | 2 +- lib/services/api_service.dart | 61 +- lib/services/auth_service.dart | 7 +- lib/services/external_service.dart | 57 +- lib/utils/const.dart | 8 +- lib/utils/string_values.dart | 5 + lib/widgets/bottom_sheet_new.dart | 1 + login_page.dart | 381 + pubspec.yaml | 2 +- 30 files changed, 1765 insertions(+), 17646 deletions(-) delete mode 100644 all_combined.txt create mode 100644 assets/images/QR_certificate.png create mode 100644 assets/images/QR_gifuroge_stage1.png create mode 100644 lib/pages/login/login_page.dart_backup create mode 100644 login_page.dart diff --git a/all_combined.txt b/all_combined.txt deleted file mode 100644 index 961bf14..0000000 --- a/all_combined.txt +++ /dev/null @@ -1,17257 +0,0 @@ -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 container_svr = "http://container.sumasen.net:8100"; - //static const server_uri = "https://rogaining.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 'dart:convert'; -import 'category.dart'; -import 'user.dart'; - -class Team { - final int id; - final String zekkenNumber; - final String teamName; - final NewCategory 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'] ?? 0, - zekkenNumber: json['zekken_number'] ?? 'Unknown', - teamName: json['team_name'] ?? 'Unknown Team', - category: json['category'] != null - ? NewCategory.fromJson(json['category']) - : NewCategory(id: 0, categoryName: 'Unknown', categoryNumber: 0, duration: Duration.zero, numOfMember: 1, family: false, female: false), - owner: json['owner'] != null - ? User.fromJson(json['owner']) - : User(id: 0, email: 'unknown@example.com', firstname: 'Unknown', lastname: 'User', dateOfBirth: null, female: false, isActive: false), - ); - } - - 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'] ?? 'Unknown', - lastname: json['lastname'] ?? 'Unknown', - dateOfBirth: json['date_of_birth'] != null - ? DateTime.tryParse(json['date_of_birth']) - : null, - female: json['female'] ?? false, - isActive: json['is_active'] ?? false, - ); - } - - 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 NewCategory { - final int id; - final String categoryName; - final int categoryNumber; - final Duration duration; - final int numOfMember; - final bool family; - final bool female; - - NewCategory({ - required this.id, - required this.categoryName, - required this.categoryNumber, - required this.duration, - required this.numOfMember, - required this.family, - required this.female, - }); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is NewCategory && - runtimeType == other.runtimeType && - id == other.id; - - @override - int get hashCode => id.hashCode; - - factory NewCategory.fromJson(Map json) { - return NewCategory( - id: json['id'] ?? 0, - categoryName: json['category_name'] ?? 'Unknown Category', - categoryNumber: json['category_number'] ?? 0, - duration: Duration(seconds: json['duration'] ?? 0), - numOfMember: json['num_of_member'] ?? 1, - family: json['family'] ?? false, - female: json['female'] ?? false, - ); - } - - 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 NewCategory 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: NewCategory.fromJson(json['category']), - date: json['date'] != null - ? DateTime.tryParse(json['date']) - : null, - owner: json['owner'] is Map ? json['owner']['name'] ?? '' : 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), - ), - ), - )), - ), - - 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: Icon(Icons.event), - title: Text('イベント参加'), - onTap: () { - Get.back(); // ドロワーを閉じる - Get.toNamed(AppPages.EVENT_ENTRY); - }, - ), - ListTile( - leading: const Icon(Icons.person), - title: Text("個人情報の修正"), - onTap: () { - Get.back(); // Close the drawer - Get.toNamed(AppPages.USER_DETAILS_EDIT); - }, - ), - - 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: 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(), - // ) - ], - ), - )), - ); - } -} -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:rogapp/pages/entry/event_entries_controller.dart'; - -class EventEntriesPage extends GetView { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('イベント参加')), - 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.category.categoryName} - ${entry.date}'), - onTap: () => controller.joinEvent(entry), - ); - }, - )), - ); - } -}// 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'; -import 'package:rogapp/model/event.dart'; -import 'package:rogapp/model/category.dart'; -import 'package:rogapp/model/team.dart'; -import 'package:intl/intl.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']; - - if (mode == 'edit' && entry != null) { - controller.initializeEditMode(entry); - } - - return Scaffold( - appBar: AppBar( - title: Text(mode == 'new' ? 'エントリー登録' : 'エントリー詳細'), - ), - body: Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } - return Padding( - padding: EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDropdown( - label: 'イベント', - items: controller.events, - selectedId: controller.selectedEvent.value?.id, - onChanged: (eventId) => controller.updateEvent( - controller.events.firstWhere((e) => e.id == eventId) - ), - getDisplayName: (event) => event.eventName, - getId: (event) => event.id, - ), - SizedBox(height: 16), - _buildDropdown( - label: 'チーム', - items: controller.teams, - selectedId: controller.selectedTeam.value?.id, - onChanged: (teamId) => controller.updateTeam( - controller.teams.firstWhere((t) => t.id == teamId) - ), - getDisplayName: (team) => team.teamName, - getId: (team) => team.id, - ), - SizedBox(height: 16), - _buildDropdown( - label: 'カテゴリ', - items: controller.categories, - selectedId: controller.selectedCategory.value?.id, - onChanged: (categoryId) => controller.updateCategory( - controller.categories.firstWhere((c) => c.id == categoryId) - ), - getDisplayName: (category) => category.categoryName, - getId: (category) => category.id, - ), - SizedBox(height: 16), - ListTile( - title: Text('日付'), - subtitle: Text( - controller.selectedDate.value != null - ? DateFormat('yyyy-MM-dd').format(controller.selectedDate.value!) - : '日付を選択してください', - ), - onTap: () async { - if (controller.selectedEvent.value == null) { - Get.snackbar('Error', 'Please select an event first'); - return; - } - final DateTime? picked = await showDatePicker( - context: context, - initialDate: controller.selectedDate.value ?? controller.selectedEvent.value!.startDatetime, - firstDate: controller.selectedEvent.value!.startDatetime, - lastDate: controller.selectedEvent.value!.endDatetime, - ); - if (picked != null) { - controller.updateDate(picked); - } - }, - ), - SizedBox(height: 32), - if (mode == 'new') - ElevatedButton( - child: Text('エントリーを作成'), - onPressed: () => controller.createEntry(), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - minimumSize: Size(double.infinity, 50), - ), - ) - else - Row( - children: [ - Expanded( - child: ElevatedButton( - child: Text('エントリーを削除'), - onPressed: () => controller.deleteEntry(), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - minimumSize: Size(0, 50), - ), - ), - ), - SizedBox(width: 16), - Expanded( - child: ElevatedButton( - child: Text('エントリーを更新'), - onPressed: () => controller.updateEntry(), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.lightBlue, - minimumSize: Size(0, 50), - ), - ), - ), - ], - ), - ], - ), - ), - ); - }), - ); - } - - Widget _buildDropdown({ - required String label, - required List items, - required int? selectedId, - required void Function(int?) onChanged, - required String Function(T) getDisplayName, - required int Function(T) getId, - }) { - return DropdownButtonFormField( - decoration: InputDecoration(labelText: label), - value: selectedId, - items: items.map((item) => DropdownMenuItem( - value: getId(item), - child: Text(getDisplayName(item)), - )).toList(), - onChanged: onChanged, - ); - } -}// 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((){ - // エントリーを日付昇順にソート - final sortedEntries = controller.entries.toList() - ..sort((a, b) => (a.date ?? DateTime(0)).compareTo(b.date ?? DateTime(0))); - - return ListView.builder( - itemCount: sortedEntries.length, - itemBuilder: (context, index) { - final entry = sortedEntries[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:intl/intl.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 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); - final isLoading = true.obs; - - @override - void onInit() async { - super.onInit(); - await initializeApiService(); - await loadInitialData(); - } - - Future initializeApiService() async { - try { - _apiService = await Get.putAsync(() => ApiService().init()); - } catch (e) { - print('Error initializing ApiService: $e'); - Get.snackbar('Error', 'Failed to initialize API service'); - } - } - - Future loadInitialData() async { - try { - isLoading.value = true; - await Future.wait([ - fetchEntries(), - fetchEvents(), - fetchTeams(), - fetchCategories(), - ]); - if (Get.arguments != null && Get.arguments['entry'] != null) { - currentEntry.value = Get.arguments['entry']; - initializeEditMode(currentEntry.value!); - } else { - // 新規作成モードの場合、最初のイベントを選択 - if (events.isNotEmpty) { - selectedEvent.value = events.first; - selectedDate.value = events.first.startDatetime; - } - } - } catch(e) { - print('Error initializing data: $e'); - Get.snackbar('Error', 'Failed to load initial data'); - } finally { - isLoading.value = false; - } - } - - - void initializeEditMode(Entry entry) { - currentEntry.value = entry; - selectedEvent.value = entry.event; - selectedTeam.value = entry.team; - selectedCategory.value = entry.category; - selectedDate.value = entry.date; - } - - void updateEvent(Event? value) { - selectedEvent.value = value; - if (value != null) { - // イベント変更時に日付を調整 - if (selectedDate.value == null || - selectedDate.value!.isBefore(value.startDatetime) || - selectedDate.value!.isAfter(value.endDatetime)) { - selectedDate.value = value.startDatetime; - } - } - } - - void updateTeam(Team? value) => selectedTeam.value = value; - void updateCategory(NewCategory? value) => selectedCategory.value = value; - void updateDate(DateTime value) => selectedDate.value = value; - - /* - void updateDate(DateTime value){ - selectedDate.value = DateFormat('yyyy-MM-dd').format(value!) as DateTime?; - } - - */ - - 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'); - Get.snackbar('Error', 'Failed to fetch entries'); - } - } - - Future fetchEvents() async { - try { - final fetchedEvents = await _apiService.getEvents(); - events.assignAll(fetchedEvents); - } catch (e) { - print('Error fetching events: $e'); - Get.snackbar('Error', 'Failed to fetch events'); - } - } - - Future fetchTeams() async { - try { - final fetchedTeams = await _apiService.getTeams(); - teams.assignAll(fetchedTeams); - } catch (e) { - print('Error fetching teams: $e'); - Get.snackbar('Error', 'Failed to fetch team'); - } - } - - Future fetchCategories() async { - try { - final fetchedCategories = await _apiService.getCategories(); - categories.assignAll(fetchedCategories); - } catch (e) { - print('Error fetching categories: $e'); - Get.snackbar('Error', 'Failed to fetch categories'); - } - } - - 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) { - Get.snackbar('Error', 'No entry selected for update'); - return; - } - try { - final updatedEntry = await _apiService.updateEntry( - currentEntry.value!.id, - currentEntry.value!.team.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) { - Get.snackbar('Error', 'No entry selected for deletion'); - 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'); - } - } - - - 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/model/entry.dart'; -import 'package:rogapp/pages/index/index_controller.dart'; -import 'package:rogapp/services/api_service.dart'; - -class EventEntriesController extends GetxController { - final ApiService _apiService = Get.find(); - final IndexController _indexController = Get.find(); - - final entries = [].obs; - - @override - void onInit() { - super.onInit(); - fetchEntries(); - } - - Future fetchEntries() async { - try { - final fetchedEntries = await _apiService.getEntries(); - entries.assignAll(fetchedEntries); - } catch (e) { - print('Error fetching entries: $e'); - // エラー処理を追加 - } - } - - Future joinEvent(Entry entry) async { - try { - - final userid = _indexController.currentUser[0]["user"]["id"]; - - await _apiService.updateUserInfo(userid,entry); - - _indexController.currentUser[0]["user"]["event_code"] = entry.event.eventName; - _indexController.currentUser[0]["user"]["team_name"] = entry.team.teamName; - _indexController.currentUser[0]["user"]["group"] = entry.team.category.categoryName; - _indexController.currentUser[0]["user"]["zekken_number"] = entry.team.zekkenNumber; - - Get.back(); // エントリー一覧ページを閉じる - //_indexController.isLoading.value = true; - _indexController.reloadMap(entry.event.eventName); // マップをリロード - } catch (e) { - print('Error joining event: $e'); - // エラー処理を追加 - } - } -}import 'package:get/get.dart'; -import 'package:rogapp/pages/entry/event_entries_controller.dart'; - -class EventEntriesBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => EventEntriesController()); - } -} -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(indexController.currentUser[0]["user"]["event_code"]); - } - } - }); - }, - 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: 1)); // 一定時間待機してから再帰呼び出し - //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(indexController.currentUser[0]["user"]["event_code"]); - } - }); - - 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(indexController.currentUser[0]["user"]["event_code"]); - 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'; -import 'package:rogapp/widgets/helper_dialog.dart'; - -class RegisterPage extends StatefulWidget { - @override - _RegisterPageState createState() => _RegisterPageState(); -} - -class _RegisterPageState extends State { - final IndexController indexController = Get.find(); - - final TextEditingController emailController = TextEditingController(); - final TextEditingController passwordController = TextEditingController(); - final TextEditingController confirmPasswordController = TextEditingController(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - showHelperDialog( - '登録メールにアクティベーションメールが送信されます。メールにあるリンクをタップすると正式登録になります。', - 'register_page' - ); - }); - } - - @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: Container( - height: MediaQuery.of(context).size.height, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - 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), - makeInput(label: "email".tr, controller: emailController), - makeInput(label: "password".tr, controller: passwordController, obsureText: true), - makeInput(label: "confirm_password".tr, controller: confirmPasswordController, obsureText: true), - const SizedBox(height: 20), - ElevatedButton( - onPressed: _handleRegister, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(40)), - minimumSize: const Size(double.infinity, 60), - ), - 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: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18)), - ), - ], - ) - ], - ), - ), - ), - ), - ); - } - - void _handleRegister() { - if (passwordController.text != confirmPasswordController.text) { - _showErrorSnackbar("no_match".tr, "password_does_not_match".tr); - return; - } - if (emailController.text.isEmpty || passwordController.text.isEmpty) { - _showErrorSnackbar("no_values".tr, "email_and_password_required".tr); - return; - } - - indexController.isLoading.value = true; - try { - indexController.register( - emailController.text, - passwordController.text, - context - ); - // 登録が成功したと仮定し、ログインページに遷移 - Get.offNamed(AppPages.LOGIN); - } catch (error) { - _showErrorSnackbar("registration_error".tr, error.toString()); - } finally { - indexController.isLoading.value = false; - } - } - - void _showErrorSnackbar(String title, String message) { - Get.snackbar( - title, - message, - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error_outline, size: 40.0, color: Colors.white), - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - ); - } -} - -Widget makeInput({required String label, required TextEditingController controller, bool 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: 20), - ], - ); -} - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:rogapp/model/user.dart'; -import 'package:rogapp/routes/app_pages.dart'; -import 'package:rogapp/services/api_service.dart'; -import 'package:rogapp/pages/index/index_controller.dart'; - -class UserDetailsEditPage extends StatefulWidget { - @override - _UserDetailsEditPageState createState() => _UserDetailsEditPageState(); -} - -class _UserDetailsEditPageState extends State { - final _formKey = GlobalKey(); - final IndexController indexController = Get.find(); - late User _user; - final TextEditingController _firstnameController = TextEditingController(); - final TextEditingController _lastnameController = TextEditingController(); - final TextEditingController _dateOfBirthController = TextEditingController(); - late bool _female; - - @override - void initState() { - super.initState(); - _user = User.fromJson(indexController.currentUser[0]['user']); - _firstnameController.text = _user.firstname; - _lastnameController.text = _user.lastname; - _dateOfBirthController.text = _user.dateOfBirth != null - ? '${_user.dateOfBirth!.year}/${_user.dateOfBirth!.month.toString().padLeft(2, '0')}/${_user.dateOfBirth!.day.toString().padLeft(2, '0')}' - : ''; - _female = _user.female; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('個人情報の修正'), - automaticallyImplyLeading: false, - ), - body: Form( - key: _formKey, - child: ListView( - padding: EdgeInsets.all(16.0), - children: [ - TextFormField( - controller: _lastnameController, - decoration: InputDecoration( - labelText: '姓', - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return '姓を入力してください'; - } - return null; - }, - ), - - SizedBox(height: 16), - TextFormField( - controller: _firstnameController, - decoration: InputDecoration( - labelText: '名', - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return '名を入力してください'; - } - return null; - }, - ), - SizedBox(height: 16), - TextFormField( - controller: _dateOfBirthController, - decoration: InputDecoration( - labelText: '生年月日 (YYYY/MM/DD)', - border: OutlineInputBorder(), - hintText: 'YYYY/MM/DD', - ), - keyboardType: TextInputType.datetime, - validator: (value) { - if (value == null || value.isEmpty) { - return '生年月日を入力してください'; - } - if (!RegExp(r'^\d{4}/\d{2}/\d{2}$').hasMatch(value)) { - return '正しい形式で入力してください (YYYY/MM/DD)'; - } - final date = DateTime.tryParse(value.replaceAll('/', '-')); - if (date == null) { - return '有効な日付を入力してください'; - } - if (date.isAfter(DateTime.now())) { - return '未来の日付は入力できません'; - } - return null; - }, - ), - SizedBox(height: 16), - SwitchListTile( - title: Text('性別'), - subtitle: Text(_female ? '女性' : '男性'), - value: _female, - onChanged: (bool value) { - setState(() { - _female = value; - }); - }, - ), - SizedBox(height: 16), - TextFormField( - initialValue: _user.email, - decoration: InputDecoration( - labelText: 'メールアドレス', - border: OutlineInputBorder(), - ), - enabled: false, - ), - SizedBox(height: 16), - SwitchListTile( - title: Text('アクティブ状態'), - value: _user.isActive, - onChanged: null, - ), - SizedBox(height: 32), - ElevatedButton( - child: Text('更新'), - onPressed: _updateUserDetails, - ), - ], - ), - ), - ); - } - - void _updateUserDetails() async { - if (_formKey.currentState!.validate()) { - final dateOfBirth = DateTime.tryParse(_dateOfBirthController.text.replaceAll('/', '-')); - if (dateOfBirth == null || dateOfBirth.isAfter(DateTime.now())) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('生年月日が無効です', style: TextStyle(color: Colors.red))), - ); - return; - } - - User updatedUser = User( - id: _user.id, - email: _user.email, - firstname: _firstnameController.text, - lastname: _lastnameController.text, - dateOfBirth: dateOfBirth, - female: _female, - isActive: _user.isActive, - ); - - try { - bool success = await ApiService.updateUserDetail(updatedUser, indexController.currentUser[0]['token']); - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('個人情報が更新されました')), - ); - indexController.updateCurrentUser(updatedUser); - Get.offAllNamed(AppPages.INDEX); - //Get.back(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('更新に失敗しました', style: TextStyle(color: Colors.red))), - ); - } - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('エラーが発生しました: $e', style: TextStyle(color: Colors.red))), - ); - } - } - } -} - - - - -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 GetWidget { - @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: Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } else if (controller.error.value.isNotEmpty) { - return Center(child: Text(controller.error.value)); - } else if (controller.teams.isEmpty) { - return Center(child: Text('チームがありません')); - } else { - return RefreshIndicator( - onRefresh: controller.fetchTeams, - child: 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}'), - onTap: () async { - await Get.toNamed( - AppPages.TEAM_DETAIL, - arguments: {'mode': 'edit', 'team': team}, - ); - controller.fetchTeams(); - }, - ); - }, - ), - ); - } - }), - ); - } -} - - -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 'dart:convert'; -import 'package:flutter/material.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 selectedTeam = Rx(null); - final currentUser = Rx(null); - - final teamName = ''.obs; - final isLoading = false.obs; - final error = RxString(''); - - @override - void onInit() async{ - super.onInit(); - try { - _apiService = Get.find(); - isLoading.value = true; - - await fetchCategories(); - await Future.wait([ - fetchTeams(), - getCurrentUser(), - ]); - - print("selectedCategory=$selectedCategory.value"); - // カテゴリが取得できたら、最初のカテゴリを選択状態にする - if (categories.isNotEmpty && selectedCategory.value == null) { - selectedCategory.value = categories.first; - } - - }catch(e){ - print("Team Controller error: $e"); - error.value = e.toString(); - }finally{ - isLoading.value = false; - } - - } - - void setSelectedTeam(Team team) { - selectedTeam.value = team; - teamName.value = team.teamName; - if (categories.isNotEmpty) { - selectedCategory.value = categories.firstWhere( - (category) => category.id == team.category.id, - orElse: () => categories.first, - ); - } else { - // カテゴリリストが空の場合、teamのカテゴリをそのまま使用 - selectedCategory.value = team.category; - } - fetchTeamMembers(team.id); - } - - void resetForm() { - selectedTeam.value = null; - teamName.value = ''; - if (categories.isNotEmpty) { - selectedCategory.value = categories.first; - } else { - selectedCategory.value = null; - // カテゴリが空の場合、エラーメッセージをセット - error.value = 'カテゴリデータが取得できませんでした。'; - } teamMembers.clear(); - } - - void cleanupForNavigation() { - selectedTeam.value = null; - teamName.value = ''; - selectedCategory.value = categories.isNotEmpty ? categories.first : null; - teamMembers.clear(); - //teamMembersはクリアしない - // 必要に応じて他のクリーンアップ処理を追加 - } - - Future fetchTeams() async { - try { - isLoading.value = true; - final fetchedTeams = await _apiService.getTeams(); - teams.assignAll(fetchedTeams); - } catch (e) { - error.value = 'チームの取得に失敗しました: $e'; - print('Error fetching teams: $e'); - } finally { - isLoading.value = false; - } - } - - bool checkIfUserHasEntryData(){ - if (teams.isEmpty) { - return false; - }else { - return true; - } - } - - Future fetchCategories() async { - try { - final fetchedCategories = await _apiService.getCategories(); - categories.assignAll(fetchedCategories); - print("Fetched categories: ${categories.length}"); // デバッグ用 - - } 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 { - final newTeam = await _apiService.createTeam(teamName, categoryId); - - // 自分自身をメンバーとして自動登録 - await _apiService.createTeamMember( - newTeam.id, - currentUser.value?.email, - currentUser.value!.firstname, - currentUser.value!.lastname, - currentUser.value?.dateOfBirth, - currentUser.value?.female, - ); - - return newTeam; - } - - Future updateTeam(int teamId, String teamName, int categoryId) async { - // APIサービスを使用してチームを更新 - final updatedTeam = await _apiService.updateTeam(teamId, teamName, categoryId); - return updatedTeam; - } - - Future deleteTeam(int teamId) async { - bool confirmDelete = await Get.dialog( - AlertDialog( - title: Text('チーム削除の確認'), - content: Text('このチームとそのすべてのメンバーを削除しますか?この操作は取り消せません。'), - actions: [ - TextButton( - child: Text('キャンセル'), - onPressed: () => Get.back(result: false), - ), - TextButton( - child: Text('削除'), - onPressed: () => Get.back(result: true), - ), - ], - ), - ) ?? false; - - if (confirmDelete) { - try { - // まず、チームのメンバーを全て削除 - await _apiService.deleteAllTeamMembers(teamId); - - // その後、チームを削除 - await _apiService.deleteTeam(teamId); - - // ローカルのチームリストを更新 - teams.removeWhere((team) => team.id == teamId); - - /* - Get.snackbar( - '成功', - 'チームとそのメンバーが削除されました', - backgroundColor: Colors.green, - colorText: Colors.white, - snackPosition: SnackPosition.BOTTOM, - ); - */ - - // チームリスト画面に戻る - Get.back(); - - } catch (e) { - print('Error deleting team and members: $e'); - Get.snackbar('エラー', 'チームとメンバーの削除に失敗しました'); - } - } - } - - Future fetchTeamMembers(int teamId) async { - try { - isLoading.value = true; - final members = await _apiService.getTeamMembers(teamId); - teamMembers.assignAll(members); - } catch (e) { - error.value = 'メンバーの取得に失敗しました: $e'; - print('Error fetching team members: $e'); - } finally { - isLoading.value = false; - } - } - - Future updateMember(teamId, User member) async { - try { - isLoading.value = true; - await _apiService.updateTeamMember(teamId,member.id, member.firstname, member.lastname, member.dateOfBirth, member.female); - await fetchTeamMembers(selectedTeam.value!.id); - } catch (e) { - error.value = 'メンバーの更新に失敗しました: $e'; - print('Error updating member: $e'); - } finally { - isLoading.value = false; - } - } - - Future deleteMember(int memberId, int teamId) async { - try { - isLoading.value = true; - await _apiService.deleteTeamMember(teamId,memberId); - await fetchTeamMembers(teamId); - } catch (e) { - error.value = 'メンバーの削除に失敗しました: $e'; - print('Error deleting member: $e'); - } finally { - isLoading.value = false; - } - } - - - // Future addMember(User member, int teamId) async { - // try { - // isLoading.value = true; - // await _apiService.createTeamMember(teamId, member.email, member.firstname, member.lastname, member.dateOfBirth); - // await fetchTeamMembers(teamId); - // } catch (e) { - // error.value = 'メンバーの追加に失敗しました: $e'; - // print('Error adding member: $e'); - // } finally { - // isLoading.value = false; - // } - // } - - - void updateTeamName(String value) { - teamName.value = value; - } - - void updateCategory(NewCategory? value) { - if (value != null) { - selectedCategory.value = value; - } - } - //void updateCategory(NewCategory? value) { - // if (value != null) { - // selectedCategory.value = categories.firstWhere( - // (category) => category.id == value.id, - // orElse: () => value, - // ); - // } - //} - - Future saveTeam() async { - try { - isLoading.value = true; - if (selectedCategory.value == null) { - throw Exception('カテゴリを選択してください'); - } - - if (selectedTeam.value == null) { - await createTeam(teamName.value, selectedCategory.value!.id); - } else { - await updateTeam(selectedTeam.value!.id, teamName.value, selectedCategory.value!.id); - } - - // サーバーから最新のデータを再取得 - await fetchTeams(); - update(); // UIを強制的に更新 - } catch (e) { - error.value = 'チームの保存に失敗しました: $e'; - - } finally { - isLoading.value = false; - } - } - - - Future deleteSelectedTeam() async { - if (selectedTeam.value != null) { - await deleteTeam(selectedTeam.value!.id); - selectedTeam.value = null; - } - } - - List getFilteredCategories() { - //List teamMembers = getCurrentTeamMembers(); - return categories.where((category) { - return isCategoryValid(category, teamMembers); - }).toList(); - } - - bool isCategoryValid(NewCategory category, List teamMembers) { - int maleCount = teamMembers.where((member) => !member.female).length; - int femaleCount = teamMembers.where((member) => member.female).length; - int totalCount = teamMembers.length; - - bool isValidGender = category.female ? (femaleCount == totalCount) : true; - bool isValidMemberCount = totalCount == category.numOfMember; - bool isValidFamily = category.family ? areAllMembersFamily(teamMembers) : true; - - return isValidGender && isValidMemberCount && isValidFamily; - } - - bool areAllMembersFamily(List teamMembers) { - // 家族かどうかを判断するロジック(例: 同じ姓を持つメンバーが2人以上いる場合は家族とみなす) - Set familyNames = teamMembers.map((member) => member.lastname).toSet(); - return familyNames.length < teamMembers.length; - } - - -}// 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/model/team.dart'; -import 'package:rogapp/model/category.dart'; - -class TeamDetailPage extends StatefulWidget { - @override - _TeamDetailPageState createState() => _TeamDetailPageState(); -} - -class _TeamDetailPageState extends State { - late TeamController controller; - late TextEditingController _teamNameController = TextEditingController(); - final RxString mode = ''.obs; - final Rx team = Rx(null); - Worker? _teamNameWorker; - - @override - void initState() { - super.initState(); - controller = Get.find(); - _teamNameController = TextEditingController(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _initializeData(); - }); - } - - void _initializeData() { - final args = Get.arguments; - if (args != null && args is Map) { - mode.value = args['mode'] as String? ?? ''; - team.value = args['team'] as Team?; - - if (mode.value == 'edit' && team.value != null) { - controller.setSelectedTeam(team.value!); - } else { - controller.resetForm(); - } - } else { - // 引数がない場合は新規作成モードとして扱う - mode.value = 'new'; - controller.resetForm(); - } - - _teamNameController.text = controller.teamName.value; - - // Use ever instead of direct listener - _teamNameWorker = ever(controller.teamName, (String value) { - if (_teamNameController.text != value) { - _teamNameController.text = value; - } - }); - } - - @override - void dispose() { - _teamNameWorker?.dispose(); - _teamNameController.dispose(); - //controller.resetForm(); // Reset the form when leaving the page - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(Get.arguments['mode'] == 'new' ? '新規チーム作成' : 'チーム詳細'), - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { - controller.cleanupForNavigation(); - Get.back(); - }, - ), - actions: [ - IconButton( - icon: Icon(Icons.save), - onPressed: () async { - try { - await controller.saveTeam(); - Get.back(); - } catch (e) { - Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM); - } - }, - ), - Obx(() { - if (mode.value == 'edit') { - return IconButton( - icon: Icon(Icons.delete), - onPressed: () async { - try { - await controller.deleteSelectedTeam(); - Get.back(); - } catch (e) { - Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM); - } - }, - ); - } else { - return SizedBox.shrink(); - } - }), - ], - ), - body: Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } - - return SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - decoration: InputDecoration(labelText: 'チーム名'), - controller: _teamNameController, - onChanged: (value) => controller.updateTeamName(value), - ), - - SizedBox(height: 16), - Obx(() { - if (controller.categories.isEmpty) { - return Text('カテゴリデータを読み込めませんでした。', style: TextStyle(color: Colors.red)); - } - return DropdownButtonFormField( - decoration: InputDecoration(labelText: 'カテゴリ'), - value: controller.selectedCategory.value, - items: controller.categories.map((category) => DropdownMenuItem( - value: category, - child: Text(category.categoryName), - )).toList(), - /* - items: controller.getFilteredCategories().map((category) => DropdownMenuItem( - value: category, - child: Text(category.categoryName), - )).toList(), - */ - onChanged: (value) => controller.updateCategory(value), - ); - }), - if (mode.value == 'edit') - Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Text('ゼッケン番号: ${controller.selectedTeam.value?.zekkenNumber ?? ""}'), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'), - ), - - if (mode.value == 'edit') ...[ - SizedBox(height: 24), - Text('メンバーリスト', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - SizedBox(height: 8), - ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: controller.teamMembers.length, - itemBuilder: (context, index) { - final member = controller.teamMembers[index]; - final isDummyEmail = member.email?.startsWith('dummy_') ?? false; - final displayName = isDummyEmail - ? '${member.lastname} ${member.firstname}' - : member.isActive - ? '${member.lastname} ${member.firstname}' - : '${member.email?.split('@')[0] ?? ''}'; //(未承認)'; - return ListTile( - title: Text(displayName), - subtitle: isDummyEmail ? Text('Email未設定') : null, - onTap: () async { - final result = await Get.toNamed( - AppPages.MEMBER_DETAIL, - arguments: {'mode': 'edit', 'member': member, 'teamId': controller.selectedTeam.value?.id}, - ); - if (result == true) { - await controller.fetchTeamMembers(controller.selectedTeam.value!.id); - } - }, - ); - }, - ), - SizedBox(height: 16), - ElevatedButton( - child: Text('新しいメンバーを追加'), - onPressed: () async { - await Get.toNamed( - AppPages.MEMBER_DETAIL, - arguments: {'mode': 'new', 'teamId': controller.selectedTeam.value?.id}, - ); - if (controller.selectedTeam.value != null) { - controller.fetchTeamMembers(controller.selectedTeam.value!.id); - } - }, - ), - ], - ], - ), - ) - ); - }), - ); - - } - - -} - - - - - - -// 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'; -import 'package:intl/intl.dart'; // この行を追加 -import 'package:flutter_localizations/flutter_localizations.dart'; // 追加 -import 'package:flutter/cupertino.dart'; - -class MemberDetailPage extends StatefulWidget { - @override - _MemberDetailPageState createState() => _MemberDetailPageState(); -} - -class _MemberDetailPageState extends State { - final MemberController controller = Get.find(); - late TextEditingController _firstNameController; - late TextEditingController _lastNameController; - late TextEditingController _emailController; - - - @override - void initState() { - super.initState(); - _initializeControllers(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - final mode = Get.arguments['mode']; - final member = Get.arguments['member']; - if (mode == 'edit' && member != null) { - controller.setSelectedMember(member); - } - }); - } - - void _initializeControllers() { - _firstNameController = TextEditingController(text: controller.firstname.value); - _lastNameController = TextEditingController(text: controller.lastname.value); - _emailController = TextEditingController(text: controller.email.value); - - controller.firstname.listen((value) { - if (_firstNameController.text != value) { - _firstNameController.value = TextEditingValue( - text: value, - selection: TextSelection.fromPosition(TextPosition(offset: value.length)), - ); - } - }); - - controller.lastname.listen((value) { - if (_lastNameController.text != value) { - _lastNameController.value = TextEditingValue( - text: value, - selection: TextSelection.fromPosition(TextPosition(offset: value.length)), - ); - } - }); - - controller.email.listen((value) { - if (_emailController.text != value) { - _emailController.value = TextEditingValue( - text: value, - selection: TextSelection.fromPosition(TextPosition(offset: value.length)), - ); - } - }); - } - - @override - Widget build(BuildContext context) { - final mode = Get.arguments['mode'] as String; - //final member = Get.arguments['member']; - final teamId = Get.arguments['teamId'] as int; - - /* - return MaterialApp( // MaterialApp をここに追加 - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('ja', 'JP'), - ], - home:Scaffold( - */ - return Scaffold( - appBar: AppBar( - title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'), - ), - body: Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } - // TextEditingControllerをObxの中で作成 - final emailController = TextEditingController(text: controller.email.value); - // カーソル位置を保持 - emailController.selection = TextSelection.fromPosition( - TextPosition(offset: controller.email.value.length), - ); - - return Column( - children: [ - Expanded( - child: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (mode == 'new') - TextFormField( - decoration: InputDecoration(labelText: 'メールアドレス'), - //onChanged: (value) => controller.email.value = value, - onChanged: (value) { - controller.email.value = value; - // カーソル位置を更新 - emailController.selection = TextSelection.fromPosition( - TextPosition(offset: value.length), - ); - }, - controller: emailController, - //controller: TextEditingController(text: controller.email.value), - keyboardType: TextInputType.emailAddress, // メールアドレス用のキーボードを表示 - autocorrect: false, // 自動修正を無効化 - enableSuggestions: false, - ) - else if (controller.isDummyEmail) - Text('メールアドレス: (メアド無し)') - else - Text('メールアドレス: ${controller.email.value}'), - - if (controller.email.value.isEmpty || mode == 'edit') ...[ - TextFormField( - decoration: InputDecoration(labelText: '姓'), - onChanged: (value) => controller.lastname.value = value, - controller: TextEditingController(text: controller.lastname.value), - ), - TextFormField( - decoration: InputDecoration(labelText: '名'), - onChanged: (value) => controller.firstname.value = value, - controller: TextEditingController(text: controller.firstname.value), - ), - // 生年月日 - if (controller.isDummyEmail || !controller.isOver18()) - ListTile( - title: Text('生年月日'), - subtitle: Text(controller.dateOfBirth.value != null - ? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})' - : '未設定'), - onTap: () async { - final date = await showDatePicker( - context: context, - initialDate: controller.dateOfBirth.value ?? DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime.now(), - //locale: const Locale('ja', 'JP'), - ); - if (date != null) controller.dateOfBirth.value = date; - }, - ) - else - Text('18歳以上'), - - SwitchListTile( - title: Text('性別'), - subtitle: Text(controller.female.value ? '女性' : '男性'), - value: controller.female.value, - onChanged: (value) => controller.female.value = value, - ), - ], - ]), - ), - ), - Padding( - padding: EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - child: Text('削除'), - onPressed: () async { - final confirmed = await Get.dialog( - AlertDialog( - title: Text('確認'), - content: Text('このメンバーを削除してもよろしいですか?'), - actions: [ - TextButton( - child: Text('キャンセル'), - onPressed: () => Get.back(result: false), - ), - TextButton( - child: Text('削除'), - onPressed: () => Get.back(result: true), - ), - ], - ), - ); - if (confirmed == true) { - await controller.deleteMember(); - Get.back(result: true); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - ), - if (!controller.isDummyEmail && !controller.isApproved) - ElevatedButton( - child: Text('招待再送信'), - onPressed: () => controller.resendInvitation(), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), - ), - ElevatedButton( - child: Text('保存・招待'), - onPressed: () async { - await controller.saveMember(); - Get.back(result: true); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ), - ], - ), - ), - ], - ); - }), - ); - } - - @override - void dispose() { - _firstNameController.dispose(); - _lastNameController.dispose(); - _emailController.dispose(); - super.dispose(); - } -}// lib/controllers/member_controller.dart - -import 'package:get/get.dart'; -import 'package:rogapp/model/user.dart'; -import 'package:rogapp/pages/team/team_controller.dart'; -import 'package:rogapp/services/api_service.dart'; - -class MemberController extends GetxController { - late final ApiService _apiService; - - final selectedMember = Rx(null); - int teamId = 0; - final member = Rx(null); - final email = ''.obs; - final firstname = ''.obs; - final lastname = ''.obs; - final female = false.obs; - final dateOfBirth = Rx(null); - final isLoading = false.obs; // isLoadingプロパティを追加 - final isActive = false.obs; - - //MemberController(this._apiService); - - @override - void onInit() { - super.onInit(); - _apiService = Get.find(); - ever(member, (_) => _initializeMemberData()); - loadInitialData(); - } - - bool get isDummyEmail => email.value.startsWith('dummy_'); - bool get isApproved => !email.value.startsWith('dummy_') && member.value?.isActive == true; - - Future loadInitialData() async { - try { - isLoading.value = true; - if (Get.arguments != null) { - if (Get.arguments['member'] != null) { - member.value = Get.arguments['member']; - } - if (Get.arguments['teamId'] != null) { - teamId = Get.arguments['teamId']; - } - } - // 他の必要な初期データの取得をここで行う - } catch (e) { - print('Error loading initial data: $e'); - } finally { - isLoading.value = false; - } - } - - - 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; - } - } - - - void setSelectedMember(User member) { - this.member.value = member; - email.value = member.email ?? ''; - firstname.value = member.firstname ?? ''; - lastname.value = member.lastname ?? ''; - dateOfBirth.value = member.dateOfBirth; - female.value = member.female ?? false; - isActive.value = member.isActive ?? false; - } - - bool _validateInputs() { - if (email.value.isNotEmpty && !isDummyEmail) { - return true; // Emailのみの場合は有効 - } - if (firstname.value.isEmpty || lastname.value.isEmpty || dateOfBirth.value == null || female.value == null) { - Get.snackbar('エラー', 'Emailが空の場合、姓名と生年月日及び性別は必須です', snackPosition: SnackPosition.BOTTOM); - return false; - } - return true; - } - - void updateFirstName(String value) { - firstname.value = value; - } - - void updateLastName(String value) { - lastname.value = value; - } - - Future saveMember() async { - if (!_validateInputs()) return false; - - try { - isLoading.value = true; - User updatedMember; - if (member.value == null || member.value!.id == null) { - // 新規メンバー作成 - updatedMember = await _apiService.createTeamMember( - teamId, - isDummyEmail ? null : email.value, // dummy_メールの場合はnullを送信 - firstname.value, - lastname.value, - dateOfBirth.value, - female.value, - ); - } else { - // 既存メンバー更新 - updatedMember = await _apiService.updateTeamMember( - teamId, - member.value!.id!, - firstname.value, - lastname.value, - dateOfBirth.value, - female.value, - ); - } - member.value = updatedMember; - Get.snackbar('成功', 'メンバーが保存されました', snackPosition: SnackPosition.BOTTOM); - return true; - } catch (e) { - print('Error saving member: $e'); - Get.snackbar('エラー', 'メンバーの保存に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM); - return false; - } finally { - isLoading.value = false; - } - } - - - String getDisplayName() { - if (!isActive.value && !isDummyEmail) { - final displayName = email.value.split('@')[0]; - return '$displayName(未承認)'; - } - return '${lastname.value} ${firstname.value}'.trim(); - } - - 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, - female.value, - ); - member.value = updatedMember; - } catch (e) { - print('Error updating member: $e'); - } - } - - Future deleteMember() async { - if (member.value == null || member.value!.id == null) { - Get.snackbar('エラー', 'メンバー情報が不正です', snackPosition: SnackPosition.BOTTOM); - return; - } - - try { - isLoading.value = true; - await _apiService.deleteTeamMember(teamId, member.value!.id!); - Get.snackbar('成功', 'メンバーが削除されました', snackPosition: SnackPosition.BOTTOM); - member.value = null; - } catch (e) { - print('Error deleting member: $e'); - Get.snackbar('エラー', 'メンバーの削除に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM); - } finally { - isLoading.value = false; - } - } - -/* - 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 (isDummyEmail) { - Get.snackbar('エラー', 'ダミーメールアドレスには招待メールを送信できません', snackPosition: SnackPosition.BOTTOM); - return; - } - - 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 '招待中'; - } - - int calculateAge() { - if (dateOfBirth.value == null) return 0; - final today = DateTime.now(); - int age = today.year - dateOfBirth.value!.year; - if (today.month < dateOfBirth.value!.month || - (today.month == dateOfBirth.value!.month && today.day < dateOfBirth.value!.day)) { - age--; - } - return age; - } - - String calculateGrade() { - if (dateOfBirth.value == null) return '不明'; - - final today = DateTime.now(); - final birthDate = dateOfBirth.value!; - - // 今年の4月1日 - final thisYearSchoolStart = DateTime(today.year, 4, 1); - - // 生まれた年の翌年の4月1日(学齢期の始まり) - final schoolStartDate = DateTime(birthDate.year + 1, 4, 1); - - // 学齢期の開始からの年数 - int yearsFromSchoolStart = today.year - schoolStartDate.year; - - // 今年の4月1日より前なら1年引く - if (today.isBefore(thisYearSchoolStart)) { - yearsFromSchoolStart--; - } - - if (yearsFromSchoolStart < 0) return '未就学'; - if (yearsFromSchoolStart < 6) return '小${yearsFromSchoolStart + 1}'; - if (yearsFromSchoolStart < 9) return '中${yearsFromSchoolStart - 5}'; - if (yearsFromSchoolStart < 12) return '高${yearsFromSchoolStart - 8}'; - return '成人'; - } - - String getAgeAndGrade() { - final age = calculateAge(); - final grade = calculateGrade(); - return '$age歳/$grade'; - } - - bool isOver18() { - return calculateAge() >= 18; - } -}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です。このクラスは、アプリのメインページを表すウィジェットです。 -// -import 'package:rogapp/widgets/helper_dialog.dart'; - -class IndexPage extends StatefulWidget { - @override - _IndexPageState createState() => _IndexPageState(); -} - -class _IndexPageState extends State { - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - //checkEntryAndShowHelper(); - }); - } - - /* - void checkEntryAndShowHelper() async { - final hasEntry = await checkIfUserHasEntry(); // この関数は実装する必要があります - if (!hasEntry) { - showHelperDialog( - context, - 'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。', - 'entry_helper', - showDoNotShowAgain: true, - ); - } - } - - */ - -// 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/model/entry.dart'; -import 'package:rogapp/pages/destination/destination_controller.dart'; -import 'package:rogapp/pages/team/team_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 'package:rogapp/model/user.dart'; - - -import 'package:rogapp/main.dart'; - -import 'package:rogapp/widgets/helper_dialog.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; - - //late final ApiService _apiService; - final ApiService _apiService = Get.find(); - - // 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; - - final Rx lastUserUpdateTime = DateTime.now().obs; - - /* - void updateUserInfo(Map newUserInfo) { - currentUser.clear(); - currentUser.add(newUserInfo); - lastUserUpdateTime.value = DateTime.now(); - } - */ - - 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()); - //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()); - - // ユーザー情報の完全性をチェック - if (await checkUserInfoComplete()) { - Get.offAllNamed(AppPages.INDEX); - } else { - Get.offAllNamed(AppPages.USER_DETAILS_EDIT); - } - - } 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")) - ); - } - }); - } - - Future checkUserInfoComplete() async { - final user = await ApiService.to.getCurrentUser(); - return user.firstname.isNotEmpty && - user.lastname.isNotEmpty && - user.dateOfBirth != null && - user.female != null; - } - - // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 - // - 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( currentUser[0]["user"]["event_code"]); - 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(String eventCode) 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 eventCode = currentUser[0]["user"]["event_code"]; - 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, - eventCode - ); - /* - 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; - } - - - void reloadMap( String eventCode ) { - // マップをリロードするロジックを実装 - // 例: 現在の位置情報を再取得し、マップを更新する - loadLocationsBound( eventCode ); - } - - Future checkEntryData() async { - // エントリーデータの有無をチェックするロジック - final teamController = TeamController(); - bool hasEntryData = teamController.checkIfUserHasEntryData(); - if (!hasEntryData) { - await showHelperDialog( - 'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。', - 'entry_check' - ); - // ドロワーを表示するロジック - Get.toNamed('/drawer'); - } - } - - void updateCurrentUser(User updatedUser) { - currentUser[0]['user'] = updatedUser.toJson(); - update(); - } -} -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'; -import 'package:rogapp/widgets/helper_dialog.dart'; - -// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 -// エラーメッセージをローカライズすることを検討してください。 -// ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。 -// -class LoginPage extends StatefulWidget { - @override - _LoginPageState createState() => _LoginPageState(); -} - -class _LoginPageState extends State { -//class LoginPage extends StatelessWidget { - final IndexController indexController = Get.find(); - - TextEditingController emailController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - showHelperDialog( - '参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。', - 'login_page' - ); - }); - } - - //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'; - -import 'package:rogapp/pages/entry/event_entries_page.dart'; -import 'package:rogapp/pages/entry/event_entries_binding.dart'; -import 'package:rogapp/pages/register/user_detail_page.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 const EVENT_ENTRY = Routes.EVENT_ENTRIES; - static const USER_DETAILS_EDIT = Routes.USER_DETAILS_EDIT; - - - 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(), - ), - GetPage( - name: Routes.EVENT_ENTRIES, - page: () => EventEntriesPage(), - binding: EventEntriesBinding(), - ), - GetPage( - name: Routes.USER_DETAILS_EDIT, - page: () => UserDetailsEditPage(), - ), - ]; - -} -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'; - - static const EVENT_ENTRIES = '/event-entries'; - static const USER_DETAILS_EDIT = '/user-details-edit'; - -} -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, - String event_code) async { - //print("-------- in location for bound -------------"); - final IndexController indexController = Get.find(); - final updateTime = indexController.lastUserUpdateTime.value; - - // ユーザー情報の更新を最大5秒間待つ - - try { - /* - // ユーザー情報の更新を最大5秒間待つ - final newUpdateTime = await indexController.lastUserUpdateTime.stream - .firstWhere( - (time) => time.isAfter(updateTime), - orElse: () => updateTime, - ).timeout(Duration(seconds: 5)); - - if (newUpdateTime == updateTime) { - print('ユーザー情報の更新がタイムアウトしました'); - // タイムアウト時の処理(例:エラー表示やリトライ) - return null; - } - */ - - /* - await indexController.lastUserUpdateTime.stream.firstWhere( - (time) => time.isAfter(updateTime), - orElse: () => updateTime, - ).timeout(Duration(seconds: 2), onTimeout: () => updateTime); - */ - - 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 = event_code; //indexController.currentUser[0]['user']['event_code']; - print("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&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; - } - } - } - } catch(e) { - print("Error: $e"); - } - - 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'; -//import 'package:rogapp/services/team_service.dart'; -//import 'package:rogapp/services/member_service.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; - } - - // ユーザー登録 - // - /* - Future registerUser(String email, String password, bool isFemale) async { - final user = await register(email, password); - if (user != null) { - final _teamController = TeamController(); - _teamController.createTeam(String teamName, int categoryId) ; - final teamService = TeamService(); - final memberService = MemberService(); - - final team = await teamService.createSoloTeam(user.id, isFemale); - await memberService.addMember(team.id, user.id); - } - } - - */ - - 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:flutter/foundation.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/model/user.dart'; -import 'package:rogapp/pages/index/index_controller.dart'; -import '../utils/const.dart'; -import 'package:intl/intl.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'); - return this; - } catch(e) { - print('Error in ApiService initialization: $e'); - rethrow; // エラーを再スローして、呼び出し元で処理できるようにする - //return this; - } - } - - /* - このメソッドは以下のように動作します: - - まず、渡された type パラメータに基づいて、どのクラスのフィールドを扱っているかを判断します。 - 次に、クラス内で fieldName に対応する期待される型を返します。 - クラスや フィールド名が予期されていないものである場合、'Unknown' または 'Unknown Type' を返します。 - - このメソッドを ApiService クラスに追加することで、_printDataComparison メソッドは各フィールドの期待される型を正確に表示できるようになります。 - さらに、このメソッドを使用することで、API レスポンスのデータ型が期待と異なる場合に簡単に検出できるようになります。例えば、Category クラスの duration フィールドが整数型(秒数)で期待されているのに対し、API が文字列を返した場合、すぐに問題を特定できます。 - 注意点として、API のレスポンス形式が変更された場合や、新しいフィールドが追加された場合は、このメソッドも更新する必要があります。そのため、API の変更とクライアントサイドのコードの同期を保つことが重要です。 - */ - - 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(); - - try { - final response = await http.get( - Uri.parse('$baseUrl/teams/'), - headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, - ); - - if (response.statusCode == 200) { - // UTF-8でデコード - final decodedResponse = utf8.decode(response.bodyBytes); - //print('User Response body: $decodedResponse'); - List teamsJson = json.decode(decodedResponse); - - List teams = []; - for (var teamJson in teamsJson) { - //print('\nTeam Data:'); - //_printDataComparison(teamJson, Team); - teams.add(Team.fromJson(teamJson)); - } - return teams; - } else { - throw Exception('Failed to load teams. Status code: ${response.statusCode}'); - } - } catch (e, stackTrace) { - print('Error in getTeams: $e'); - print('Stack trace: $stackTrace'); - rethrow; - } - } - - - - Future> getCategories() async { - init(); - getToken(); - - try { - final response = await http.get( - Uri.parse('$baseUrl/categories/'), - headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - print('User Response body: $decodedResponse'); - List categoriesJson = json.decode(decodedResponse); - - List categories = []; - for (var categoryJson in categoriesJson) { - //print('\nCategory Data:'); - //_printDataComparison(categoryJson, NewCategory); - categories.add(NewCategory.fromJson(categoryJson)); - } - - return categories; - } else { - throw Exception( - 'Failed to load categories. Status code: ${response.statusCode}'); - } - }catch(e, stackTrace){ - print('Error in getCategories: $e'); - print('Stack trace: $stackTrace'); - rethrow; - } - } - - Future getCurrentUser() async { - init(); - getToken(); - - try { - final response = await http.get( - Uri.parse('$baseUrl/user/'), - headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - //print('User Response body: $decodedResponse'); - final jsonData = json.decode(decodedResponse); - - //print('\nUser Data Comparison:'); - //_printDataComparison(jsonData, User); - - return User.fromJson(jsonData); - } else { - throw Exception('Failed to get current user. Status code: ${response.statusCode}'); - } - } catch (e, stackTrace) { - print('Error in getCurrentUser: $e'); - print('Stack trace: $stackTrace'); - rethrow; - } - } - - void _printDataComparison(Map data, Type expectedType) { - print('Field\t\t| Expected Type\t| Actual Type\t| Actual Value'); - print('------------------------------------------------------------'); - data.forEach((key, value) { - String expectedFieldType = _getExpectedFieldType(expectedType, key); - _printComparison(key, expectedFieldType, value); - }); - } - - String _getExpectedFieldType(Type type, String fieldName) { - // This method should return the expected type for each field based on the class definition - // You might need to implement this based on your class structures - - switch (type) { - case NewCategory: - switch (fieldName) { - case 'id': return 'int'; - case 'category_name': return 'String'; - case 'category_number': return 'int'; - case 'duration': return 'int (seconds)'; - case 'num_of_member': return 'int'; - case 'family': return 'bool'; - case 'female': return 'bool'; - default: return 'Unknown'; - } - case Team: - switch (fieldName) { - case 'id': return 'int'; - case 'zekken_number': return 'String'; - case 'team_name': return 'String'; - case 'category': return 'NewCategory (Object)'; - case 'owner': return 'User (Object)'; - default: return 'Unknown'; - } - case User: - switch (fieldName) { - case 'id': return 'int'; - case 'email': return 'String'; - case 'firstname': return 'String'; - case 'lastname': return 'String'; - case 'date_of_birth': return 'String (ISO8601)'; - case 'female': return 'bool'; - case 'is_active': return 'bool'; - default: return 'Unknown'; - } - default: - return 'Unknown Type'; - } - } - - void _printComparison(String fieldName, String expectedType, dynamic actualValue) { - String actualType = actualValue?.runtimeType.toString() ?? 'null'; - String displayValue = actualValue.toString(); - if (displayValue.length > 50) { - displayValue = '${displayValue.substring(0, 47)}...'; - } - print('$fieldName\t\t| $expectedType\t\t| $actualType\t\t| $displayValue'); - } - - 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; charset=UTF-8", - }, - body: json.encode({ - 'team_name': teamName, - 'category': categoryId, - }), - ); - - if (response.statusCode == 201) { - final decodedResponse = utf8.decode(response.bodyBytes); - return Team.fromJson(json.decode(decodedResponse)); - } else { - throw Exception('Failed to create team'); - } - } - - Future 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; charset=UTF-8', - }, - body: json.encode({ - 'team_name': teamName, - 'category': categoryId, - }), - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - - return Team.fromJson(json.decode(decodedResponse)); - } else { - throw Exception('Failed to update team'); - } - } - - Future deleteTeam(int teamId) async { - init(); - getToken(); - - final response = await http.delete( - Uri.parse('$baseUrl/teams/$teamId/'), - headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'}, - ); - - if( response.statusCode == 400) { - final decodedResponse = utf8.decode(response.bodyBytes); - print('User Response body: $decodedResponse'); - throw Exception('まだメンバーが残っているので、チームを削除できません。'); - }else if (response.statusCode != 204) { - throw Exception('Failed to delete team'); - } - } - - Future> getTeamMembers(int teamId) async { - init(); - getToken(); - - final response = await http.get( - Uri.parse('$baseUrl/teams/$teamId/members/'), - headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'}, - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - print('User Response body: $decodedResponse'); - List membersJson = json.decode(decodedResponse); - - 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,bool? female) async { - init(); - getToken(); - - // emailが値を持っている場合の処理 - if (email != null && email.isNotEmpty) { - firstname ??= "dummy"; - lastname ??= "dummy"; - dateOfBirth ??= DateTime.now(); - female ??= false; - } - - String? formattedDateOfBirth; - if (dateOfBirth != null) { - formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth); - } - - final response = await http.post( - Uri.parse('$baseUrl/teams/$teamId/members/'), - headers: { - 'Authorization': 'Token $token', - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: json.encode({ - 'email': email, - 'firstname': firstname, - 'lastname': lastname, - 'date_of_birth': formattedDateOfBirth, - 'female': female, - }), - ); - - if (response.statusCode == 201) { - final decodedResponse = utf8.decode(response.bodyBytes); - return User.fromJson(json.decode(decodedResponse)); - } else { - throw Exception('Failed to create team member'); - } - } - - Future updateTeamMember(int teamId,int? memberId, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async { - init(); - getToken(); - - String? formattedDateOfBirth; - if (dateOfBirth != null) { - formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth); - } - - final response = await http.put( - Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'), - headers: { - 'Authorization': 'Token $token', - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: json.encode({ - 'firstname': firstname, - 'lastname': lastname, - 'date_of_birth': formattedDateOfBirth, - 'female': female, - }), - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - return User.fromJson(json.decode(decodedResponse)); - } else { - throw Exception('Failed to update team member'); - } - } - - Future 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 deleteAllTeamMembers(int teamId) async { - final response = await http.delete( - Uri.parse('$baseUrl/teams/$teamId/members/destroy_all/?confirm=true'), - headers: {'Authorization': 'Token $token'}, - ); - - if (response.statusCode != 200) { - throw Exception('Failed to delete team members'); - } - } - - Future resendMemberInvitation(int memberId) async { - init(); - getToken(); - - final response = await http.post( - Uri.parse('$baseUrl/members/$memberId/resend-invitation/'), - headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8', - }, - ); - - if (response.statusCode != 200) { - throw Exception('Failed to resend invitation'); - } - } - - Future> getEntries() async { - init(); - getToken(); - - final response = await http.get( - Uri.parse('$baseUrl/entry/'), - headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8', - }, - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - List entriesJson = json.decode(decodedResponse); - 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', 'Content-Type': 'application/json; charset=UTF-8', - }, - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - print('Response body: $decodedResponse'); - List eventsJson = json.decode(decodedResponse); - - 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(); - - String? formattedDate; - if (date != null) { - formattedDate = DateFormat('yyyy-MM-dd').format(date); - } - - final response = await http.post( - Uri.parse('$baseUrl/entry/'), - headers: { - 'Authorization': 'Token $token', - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: json.encode({ - 'team': teamId, - 'event': eventId, - 'category': categoryId, - 'date': formattedDate, - }), - ); - - if (response.statusCode == 201) { - final decodedResponse = utf8.decode(response.bodyBytes); - - return Entry.fromJson(json.decode(decodedResponse)); - } else { - throw Exception('Failed to create entry'); - } - } - - Future updateUserInfo(int userId, Entry entry) async { - init(); - getToken(); - - final entryId = entry.id; - - DateTime? date = entry.date; - String? formattedDate; - if (date != null) { - formattedDate = DateFormat('yyyy-MM-dd').format(date); - } - - final response = await http.put( - Uri.parse('$baseUrl/userinfo/$userId/'), - headers: { - 'Authorization': 'Token $token', - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: json.encode({ - 'zekken_number': entry.team.zekkenNumber, - 'event_code': entry.event.eventName, - 'group': entry.team.category.categoryName, - 'team_name': entry.team.teamName, - 'date': formattedDate, - }), - ); - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - final updatedUserInfo = json.decode(decodedResponse); - //Get.find().updateUserInfo(updatedUserInfo); - - } else { - throw Exception('Failed to update entry'); - } - } - - - Future updateEntry(int entryId, int teamId, int eventId, int categoryId, DateTime date) async { - init(); - getToken(); - - String? formattedDate; - if (date != null) { - formattedDate = DateFormat('yyyy-MM-dd').format(date); - } - - final response = await http.put( - Uri.parse('$baseUrl/entry/$entryId/'), - headers: { - 'Authorization': 'Token $token', - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: json.encode({ - 'team': teamId, - 'event': eventId, - 'category': categoryId, - 'date': formattedDate, - }), - ); - - if (response.statusCode == 200) { - final decodedResponse = utf8.decode(response.bodyBytes); - - return Entry.fromJson(json.decode(decodedResponse)); - } 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'); - } - } - - static Future updateUserDetail(User user, String token) async { - String serverUrl = ConstValues.currentServer(); - int? userid = user.id; - String url = '$serverUrl/api/userdetail/$userid/'; - - try { - String? formattedDate; - if (user.dateOfBirth != null) { - formattedDate = DateFormat('yyyy-MM-dd').format(user.dateOfBirth!); - } - final http.Response response = await http.put( - Uri.parse(url), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'Authorization': 'Token $token' - }, - body: jsonEncode({ - 'firstname': user.firstname, - 'lastname': user.lastname, - 'date_of_birth': formattedDate, - 'female': user.female, - }), - ); - - if (response.statusCode == 200) { - return true; - } else { - print('Update failed with status code: ${response.statusCode}'); - return false; - } - } catch (e) { - print('Error in updateUserDetail: $e'); - return false; - } - } - -}// 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(), - ), - ), - ), - ], - ), - ), - ), - ); - } -} -// lib/widgets/helper_dialog.dart -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class HelperDialog extends StatefulWidget { - final String message; - final String screenKey; - - const HelperDialog({Key? key, required this.message, required this.screenKey}) : super(key: key); - - @override - _HelperDialogState createState() => _HelperDialogState(); -} - -class _HelperDialogState extends State { - bool _doNotShowAgain = false; - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.help_outline, color: Colors.blue), - SizedBox(width: 10), - Text('ヘルプ'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.message), - SizedBox(height: 20), - Row( - children: [ - Checkbox( - value: _doNotShowAgain, - onChanged: (value) { - setState(() { - _doNotShowAgain = value!; - }); - }, - ), - Text('この画面を二度と表示しない'), - ], - ), - ], - ), - actions: [ - TextButton( - child: Text('OK'), - onPressed: () async { - if (_doNotShowAgain) { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('helper_${widget.screenKey}', false); - } - Get.back(); - }, - ), - ], - ); - } -} - -// ヘルパー画面を表示する関数 -Future showHelperDialog(String message, String screenKey) async { - final prefs = await SharedPreferences.getInstance(); - final showHelper = prefs.getBool('helper_$screenKey') ?? true; - if (showHelper) { - Get.dialog(HelperDialog(message: message, screenKey: screenKey)); - } -} \ No newline at end of file diff --git a/assets/images/QR_certificate.png b/assets/images/QR_certificate.png new file mode 100644 index 0000000000000000000000000000000000000000..2097f1794a659ca7367e5bfdcb7434c107baccdf GIT binary patch literal 85858 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 A00000 literal 0 HcmV?d00001 diff --git a/assets/images/QR_gifuroge_stage1.png b/assets/images/QR_gifuroge_stage1.png new file mode 100644 index 0000000000000000000000000000000000000000..91dcdcce5753665930c75155e791783f0d386934 GIT binary patch literal 518 zcmV+h0{Q)kP)^Ym~%(quR!R1-XMhqeg&q~Ag}DT`5_2wfN4lCfA5@2;0?&I z&=^A7%elZkm`Pf`#O>$A6}SU24WaO_d-PKN7uWy;W&14g56+z~um>VJaR|}*18W}; z_#TAV*I*Qx8sE$071#ldBD3U?uk(y4umfh@hxE+b$%_Q`z){t1Yys$~rMkc^IGv@& zHLx$vE^q^~)bXOj)w9D2?189x6h66>-vqWm=4ri*4|G*L1c5yeV^NrURlND4wZIP` zMdccL=T^Z2KY-OTD^ZQgFO5{-9vtu`q^mKLpT{GC8}K-$H}C_E6x(wX5V!?P_%fqx z2g^Vza0jN~yh~qN(pKOn(6OcuEsit^E^rTqf7{do%=0Nr0GVc(Nrc&Wo&W#<07*qo IM6N<$f<<2B+5i9m literal 0 HcmV?d00001 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 35c588b..0553e89 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -398,11 +398,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 486; + CURRENT_PROJECT_VERSION = 488; DEVELOPMENT_TEAM = UMNEWT25JR; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 4.8.6; - FLUTTER_BUILD_NUMBER = 486; + FLUTTER_BUILD_NAME = 4.8.8; + FLUTTER_BUILD_NUMBER = 488; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; @@ -411,7 +411,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.8.6; + MARKETING_VERSION = 4.8.8; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -539,11 +539,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 486; + CURRENT_PROJECT_VERSION = 488; DEVELOPMENT_TEAM = UMNEWT25JR; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 4.8.6; - FLUTTER_BUILD_NUMBER = 486; + FLUTTER_BUILD_NAME = 4.8.8; + FLUTTER_BUILD_NUMBER = 488; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; @@ -552,7 +552,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.8.6; + MARKETING_VERSION = 4.8.8; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -571,11 +571,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 486; + CURRENT_PROJECT_VERSION = 488; DEVELOPMENT_TEAM = UMNEWT25JR; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 4.8.6; - FLUTTER_BUILD_NUMBER = 486; + FLUTTER_BUILD_NAME = 4.8.8; + FLUTTER_BUILD_NUMBER = 488; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; @@ -584,7 +584,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.8.6; + MARKETING_VERSION = 4.8.8; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/lib/model/category.dart b/lib/model/category.dart index ad58c63..8bc3caa 100644 --- a/lib/model/category.dart +++ b/lib/model/category.dart @@ -34,13 +34,28 @@ class NewCategory { id: json['id'] ?? 0, categoryName: json['category_name'] ?? 'Unknown Category', categoryNumber: json['category_number'] ?? 0, - duration: Duration(seconds: json['duration'] ?? 0), + duration: parseDuration(json['duration']), numOfMember: json['num_of_member'] ?? 1, family: json['family'] ?? false, female: json['female'] ?? false, ); } + static Duration parseDuration(String s) { + int hours = 0; + int minutes = 0; + int micros; + List parts = s.split(':'); + if (parts.length > 2) { + hours = int.parse(parts[parts.length - 3]); + } + if (parts.length > 1) { + minutes = int.parse(parts[parts.length - 2]); + } + micros = (double.parse(parts[parts.length - 1]) * 1000000).round(); + return Duration(hours: hours, minutes: minutes, microseconds: micros); + } + Map toJson() { return { 'id': id, diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 8fa81a4..c58c869 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -10,6 +10,7 @@ class Entry { final Event event; final NewCategory category; final DateTime? date; + final int zekkenNumber; // 新しく追加 final String owner; Entry({ @@ -18,6 +19,7 @@ class Entry { required this.event, required this.category, required this.date, + required this.zekkenNumber, required this.owner, }); @@ -30,6 +32,7 @@ class Entry { date: json['date'] != null ? DateTime.tryParse(json['date']) : null, + zekkenNumber: json['zekken_number'], // 新しく追加 owner: json['owner'] is Map ? json['owner']['name'] ?? '' : json['owner'] ?? '', ); } @@ -41,6 +44,7 @@ class Entry { 'event': event.toJson(), 'category': category.toJson(), 'date': date?.toIso8601String(), + 'zekken_number': zekkenNumber, // 新しく追加 'owner': owner, }; } diff --git a/lib/model/team.dart b/lib/model/team.dart index 53024a4..eb2830c 100644 --- a/lib/model/team.dart +++ b/lib/model/team.dart @@ -6,7 +6,7 @@ import 'user.dart'; class Team { final int id; - final String zekkenNumber; +// final String zekkenNumber; final String teamName; final NewCategory category; final User owner; @@ -14,7 +14,7 @@ class Team { Team({ required this.id, - required this.zekkenNumber, +// required this.zekkenNumber, required this.teamName, required this.category, required this.owner, @@ -23,7 +23,7 @@ class Team { factory Team.fromJson(Map json) { return Team( id: json['id'] ?? 0, - zekkenNumber: json['zekken_number'] ?? 'Unknown', + //zekkenNumber: json['zekken_number'] ?? 'Unknown', teamName: json['team_name'] ?? 'Unknown Team', category: json['category'] != null ? NewCategory.fromJson(json['category']) @@ -37,7 +37,7 @@ class Team { Map toJson() { return { 'id': id, - 'zekken_number': zekkenNumber, + //'zekken_number': zekkenNumber, 'team_name': teamName, 'category': category.toJson(), 'owner': owner.toJson(), diff --git a/lib/pages/camera/camera_page.dart b/lib/pages/camera/camera_page.dart index 205f107..bd07db0 100644 --- a/lib/pages/camera/camera_page.dart +++ b/lib/pages/camera/camera_page.dart @@ -1,9 +1,12 @@ import 'dart:async'; import 'dart:convert'; // この行を追加または確認 +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; @@ -493,7 +496,10 @@ class CameraPage extends StatelessWidget { if (buyPointPhoto == true) { // buyPointPhotoがtrueの場合は、BuyPointCameraウィジェットを返します。 //print("--- buy point camera ${destination.toString()}"); - return BuyPointCamera(destination: destination); + //return BuyPointCamera(destination: destination); + + return SwitchableBuyPointCamera(destination: destination); + //}else if(destination.use_qr_code){ // return QRCodeScannerPage(destination: destination); } else if (destinationController.isInRog.value) { @@ -617,105 +623,189 @@ class StartRogaining extends StatelessWidget { // 完了ボタンをタップすると、購入ポイントの処理が行われます。 // 購入なしボタンをタップすると、購入ポイントがキャンセルされます。 // +class SwitchableBuyPointCamera extends StatefulWidget { + final Destination destination; + + const SwitchableBuyPointCamera({Key? key, required this.destination}) : super(key: key); + + @override + _SwitchableBuyPointCameraState createState() => _SwitchableBuyPointCameraState(); +} + +class _SwitchableBuyPointCameraState extends State { + bool isQRMode = true; + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final qrViewWidth = screenWidth * 2 / 3; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text("${widget.destination.sub_loc_id} : ${widget.destination.name}"), + ), + body: SafeArea( + child: Stack( + children: [ + if (isQRMode) + Column( + children: [ + SizedBox(height: screenHeight * 0.1), + Center( + child: SizedBox( + width: qrViewWidth, + height: qrViewWidth, + child: BuyPointCamera_QR(destination: widget.destination), + ), + ), + Expanded( + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + "岐阜ロゲQRコードにかざしてください。", + style: TextStyle(fontSize: 16), + ), + ), + ), + ), + ], + ) + else + Positioned.fill( + child: BuyPointCamera(destination: widget.destination), + ), + Positioned( + right: 16, + bottom: 16, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.7), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(isQRMode ? "カメラへ" : "QRへ"), + Switch( + value: isQRMode, + onChanged: (value) { + setState(() { + isQRMode = value; + }); + }, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + + 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], + return 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)), ), - 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()) - ], ), ), - ], - ), + ), + + 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()) + ], + ), + ), + ], ), ); } @@ -743,28 +833,9 @@ class _BuyPointCamera_QRState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - title: Text("${widget.destination.sub_loc_id} : ${widget.destination.name}"), - ), - body: Column( - children: [ - Expanded( - flex: 4, - child: QRView( - key: qrKey, - onQRViewCreated: _onQRViewCreated, - ), - ), - Expanded( - flex: 1, - child: Center( - child: Text('QRコードをスキャンしてください'), - ), - ), - ], - ), + return QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, ); } @@ -773,11 +844,39 @@ class _BuyPointCamera_QRState extends State { controller.scannedDataStream.listen((scanData) { if (!isQRScanned && scanData.code != null && scanData.code!.startsWith('https://rogaining.sumasen.net/api/activate_buy_point/')) { isQRScanned = true; - _activateBuyPoint(scanData.code!); + _processBuyPoint(); + //_activateBuyPoint(scanData.code!); } }); } + Future getImageFilePathFromAssets(String assetPath) async { + final byteData = await rootBundle.load(assetPath); + final buffer = byteData.buffer; + Directory tempDir = await getTemporaryDirectory(); + String tempPath = tempDir.path; + var filePath = '$tempPath/temp_qr_receipt.png'; + return (await File(filePath).writeAsBytes( + buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes) + )).path; + } + + + void _processBuyPoint() async { + // アセットの画像をテンポラリファイルにコピー + String predefinedImagePath = await getImageFilePathFromAssets('assets/images/QR_certificate.png'); + + try { + await destinationController.makeBuyPoint(widget.destination, predefinedImagePath); + Get.snackbar('成功', 'お買い物ポイントが有効化されました'); + Navigator.of(context).pop(); + } catch (e) { + Get.snackbar('エラー', 'お買い物ポイントの有効化に失敗しました'); + } finally { + isQRScanned = false; + } + } + void _activateBuyPoint(String qrCode) async { final IndexController indexController = Get.find(); diff --git a/lib/pages/destination/destination_controller.dart b/lib/pages/destination/destination_controller.dart index e2a25d7..76f2317 100644 --- a/lib/pages/destination/destination_controller.dart +++ b/lib/pages/destination/destination_controller.dart @@ -16,6 +16,7 @@ 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/pages/team/team_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/DatabaseService.dart'; import 'package:rogapp/services/destination_service.dart'; @@ -44,6 +45,7 @@ import 'package:rogapp/pages/permission/permission.dart'; class DestinationController extends GetxController { late LocationSettings locationSettings; // 位置情報の設定を保持する変数です。 + //late TeamController teamController = TeamController(); //Timer? _GPStimer; // GPSタイマーを保持する変数です。 var destinationCount = 0.obs; // 目的地の数を保持するReactive変数です。 @@ -161,11 +163,11 @@ class DestinationController extends GetxController { } } - // //==== Akira .. GPS信号シミュレーション用 ======= ここまで */ + // ルートをクリアする関数です。 void clearRoute() { indexController.routePoints.clear(); @@ -1156,9 +1158,13 @@ class DestinationController extends GetxController { //await _saveImageFromPath(imageurl); await _saveImageToGallery(imageurl); + + if (indexController.currentUser.isNotEmpty) { double cpNum = destination.cp!; + //int teamId = indexController.teamId.value; // teamIdを使用 + int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; @@ -1222,6 +1228,11 @@ class DestinationController extends GetxController { if (indexController.currentUser.isNotEmpty) { double cpNum = destination.cp!; + //int teamId = indexController.teamId.value; // teamIdを使用 + //Team team0 = teamController.teams[0]; + //print("team={team0}"); + + int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; @@ -1238,7 +1249,7 @@ class DestinationController extends GetxController { // print("------ checkin event $eventCode ------"); ExternalService() .makeCheckpoint( - userId, + userId, // teamIdを使用 token, formattedDate, team, @@ -1716,6 +1727,8 @@ class DestinationController extends GetxController { //print('----- %%%%%%%%%%%%%%%%%%%%% ----- $val'); Map res = {}; if (val == "wifi" || val == "mobile") { + //int teamId = indexController.teamId.value; // teamIdを使用 + String token = indexController.currentUser[0]["token"]; DatabaseHelper db = DatabaseHelper.instance; db.allRogianing().then((value) { @@ -1725,7 +1738,7 @@ class DestinationController extends GetxController { } else if (e.rog_action_type == 1) { var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!); res = await ExternalService().makeCheckpoint( - e.user_id!, + e.user_id!, // teamId??? token, getFormatedTime(datetime), e.team_name!, @@ -1735,7 +1748,7 @@ class DestinationController extends GetxController { } else if (e.rog_action_type == 2) { var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!); res = await ExternalService().makeGoal( - e.user_id!, + e.user_id!, // // teamId??? token, e.team_name!, e.image!, diff --git a/lib/pages/drawer/drawer_page.dart b/lib/pages/drawer/drawer_page.dart index 8ae212a..8a89fe2 100644 --- a/lib/pages/drawer/drawer_page.dart +++ b/lib/pages/drawer/drawer_page.dart @@ -257,6 +257,7 @@ class DrawerPage extends StatelessWidget { }, ), + /* ListTile( leading: const Icon(Icons.developer_mode), title: const Text('open_settings'), @@ -265,6 +266,8 @@ class DrawerPage extends StatelessWidget { Get.toNamed('/debug'); // デバッグ画面に遷移 }, ), + */ + // ListTile( // leading: const Icon(Icons.router), // title: Text("my_route".tr), diff --git a/lib/pages/entry/entry_controller.dart b/lib/pages/entry/entry_controller.dart index 7ef94f8..003d78d 100644 --- a/lib/pages/entry/entry_controller.dart +++ b/lib/pages/entry/entry_controller.dart @@ -8,6 +8,8 @@ import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; import 'package:rogapp/services/api_service.dart'; +import '../index/index_controller.dart'; + class EntryController extends GetxController { late ApiService _apiService; @@ -155,26 +157,44 @@ class EntryController extends GetxController { return; } try { + isLoading.value = true; + // Get zekken number + final updatedCategory = await _apiService.getZekkenNumber(selectedCategory.value!.id); + final zekkenNumber = updatedCategory.categoryNumber.toString(); + final newEntry = await _apiService.createEntry( selectedTeam.value!.id, selectedEvent.value!.id, selectedCategory.value!.id, selectedDate.value!, + zekkenNumber, ); entries.add(newEntry); Get.back(); } catch (e) { print('Error creating entry: $e'); Get.snackbar('Error', 'Failed to create entry'); + } finally { + isLoading.value = false; } } + Future updateEntryAndRefreshMap() async { + await updateEntry(); + + // エントリーが正常に更新された後、マップをリフレッシュ + final indexController = Get.find(); + final eventCode = currentEntry.value?.event.eventName ?? ''; + indexController.reloadMap(eventCode); + } + Future updateEntry() async { if (currentEntry.value == null) { Get.snackbar('Error', 'No entry selected for update'); return; } try { + isLoading.value = true; final updatedEntry = await _apiService.updateEntry( currentEntry.value!.id, currentEntry.value!.team.id, @@ -190,6 +210,8 @@ class EntryController extends GetxController { } catch (e) { print('Error updating entry: $e'); Get.snackbar('Error', 'Failed to update entry'); + } finally { + isLoading.value = false; } } @@ -199,12 +221,15 @@ class EntryController extends GetxController { return; } try { + isLoading.value = true; 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'); + } finally { + isLoading.value = false; } } diff --git a/lib/pages/entry/entry_list_page.dart b/lib/pages/entry/entry_list_page.dart index 9da59ba..c70c6e7 100644 --- a/lib/pages/entry/entry_list_page.dart +++ b/lib/pages/entry/entry_list_page.dart @@ -2,10 +2,63 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.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('エントリー管理'), + ), + body: Obx(() { + if (controller.entries.isEmpty) { + return Center( + child: Text('表示するエントリーがありません。'), + ); + } + return ListView.builder( + itemCount: controller.entries.length, + itemBuilder: (context, index) { + final entry = controller.entries[index]; + return ListTile( + title: Row( + children: [ + Expanded( + child: Text('${_formatDate(entry.date)}: ${entry.event.eventName}'), + ), + Text(entry.team.teamName, style: TextStyle(fontWeight: FontWeight.bold)), + ], + ), + subtitle: Row( + children: [ + Expanded( + child: Text('カテゴリー: ${entry.category.categoryName}'), + ), + Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'), + ], + ), + onTap: () => + Get.toNamed(AppPages.ENTRY_DETAIL, + arguments: {'mode': 'edit', 'entry': entry}), + ); + }, + ); + }), + ); + } + + String _formatDate(DateTime? date) { + if (date == null) { + return '日時未設定'; + } + return DateFormat('yyyy-MM-dd').format(date); + } +} + +class EntryListPage_old extends GetView { @override Widget build(BuildContext context) { return Scaffold( @@ -19,6 +72,10 @@ class EntryListPage extends GetView { ], ), body: Obx((){ + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + // エントリーを日付昇順にソート final sortedEntries = controller.entries.toList() ..sort((a, b) => (a.date ?? DateTime(0)).compareTo(b.date ?? DateTime(0))); diff --git a/lib/pages/entry/event_entries_controller.dart b/lib/pages/entry/event_entries_controller.dart index 78df60e..24fc76c 100644 --- a/lib/pages/entry/event_entries_controller.dart +++ b/lib/pages/entry/event_entries_controller.dart @@ -1,17 +1,26 @@ import 'package:get/get.dart'; import 'package:rogapp/model/entry.dart'; import 'package:rogapp/pages/index/index_controller.dart'; +import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/services/api_service.dart'; +import 'package:flutter/material.dart'; class EventEntriesController extends GetxController { final ApiService _apiService = Get.find(); final IndexController _indexController = Get.find(); + late final DestinationController _destinationController; final entries = [].obs; + final filteredEntries = [].obs; + final showTodayEntries = true.obs; @override void onInit() { super.onInit(); + // DestinationControllerが登録されていない場合に備えて、lazyPutを使用 + Get.lazyPut(() => DestinationController(), fenix: true); + _destinationController = Get.find(); + fetchEntries(); } @@ -19,14 +28,51 @@ class EventEntriesController extends GetxController { try { final fetchedEntries = await _apiService.getEntries(); entries.assignAll(fetchedEntries); + filterEntries(); } catch (e) { print('Error fetching entries: $e'); // エラー処理を追加 } } + void filterEntries() { + if (showTodayEntries.value) { + filterEntriesForToday(); + } else { + filteredEntries.assignAll(entries); + } + } + + void filterEntriesForToday() { + final now = DateTime.now(); + filteredEntries.assignAll(entries.where((entry) => + entry.date?.year == now.year && + entry.date?.month == now.month && + entry.date?.day == now.day + )); + } + + void toggleShowTodayEntries() { + showTodayEntries.toggle(); + filterEntries(); + } + + void refreshMap() { + final tk = _indexController.currentUser[0]["token"]; + if (tk != null) { + + _destinationController.fixMapBound(tk); + } + } + Future joinEvent(Entry entry) async { - try { + final now = DateTime.now(); + bool isToday = entry.date?.year == now.year && + entry.date?.month == now.month && + entry.date?.day == now.day; + + _indexController.setReferenceMode(!isToday); + _indexController.setSelectedEventName(entry.event.eventName); final userid = _indexController.currentUser[0]["user"]["id"]; @@ -35,14 +81,24 @@ class EventEntriesController extends GetxController { _indexController.currentUser[0]["user"]["event_code"] = entry.event.eventName; _indexController.currentUser[0]["user"]["team_name"] = entry.team.teamName; _indexController.currentUser[0]["user"]["group"] = entry.team.category.categoryName; - _indexController.currentUser[0]["user"]["zekken_number"] = entry.team.zekkenNumber; + _indexController.currentUser[0]["user"]["zekken_number"] = entry.zekkenNumber; Get.back(); // エントリー一覧ページを閉じる //_indexController.isLoading.value = true; - _indexController.reloadMap(entry.event.eventName); // マップをリロード - } catch (e) { - print('Error joining event: $e'); - // エラー処理を追加 - } + _indexController.reloadMap(entry.event.eventName); + + refreshMap(); + + if (isToday) { + Get.snackbar('成功', 'イベントに参加しました。', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white); + } else { + Get.snackbar('参照モード', '過去または未来のイベントを参照しています。ロゲの開始やチェックインはできません。', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white); + } } } \ No newline at end of file diff --git a/lib/pages/entry/event_entries_page.dart b/lib/pages/entry/event_entries_page.dart index c302ef4..e4aaa91 100644 --- a/lib/pages/entry/event_entries_page.dart +++ b/lib/pages/entry/event_entries_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:rogapp/pages/entry/event_entries_controller.dart'; -class EventEntriesPage extends GetView { +class EventEntriesPage_old extends GetView { @override Widget build(BuildContext context) { return Scaffold( @@ -20,4 +21,82 @@ class EventEntriesPage extends GetView { )), ); } -} \ No newline at end of file +} + +class EventEntriesPage extends GetView { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx(() => Text(controller.showTodayEntries.value ? 'イベント参加' : 'イベント参照')), + ), + body: Column( + children: [ + Padding( + padding: EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx(() => Text( + controller.showTodayEntries.value ? '本日のエントリー' : 'すべてのエントリー', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + )), + Obx(() => Switch( + value: !controller.showTodayEntries.value, + onChanged: (value) { + controller.toggleShowTodayEntries(); + }, + activeColor: Colors.blue, + )), + ], + ), + ), + Expanded( + child: Obx(() { + if (controller.filteredEntries.isEmpty) { + return Center( + child: Text('表示するエントリーがありません。'), + ); + } + return ListView.builder( + itemCount: controller.filteredEntries.length, + itemBuilder: (context, index) { + final entry = controller.filteredEntries[index]; + return ListTile( + title: Row( + children: [ + Expanded( + child: Text('${_formatDate(entry.date)}: ${entry.event.eventName}'), + ), + Text(entry.team.teamName, style: TextStyle(fontWeight: FontWeight.bold)), + ], + ), + subtitle: Row( + children: [ + Expanded( + child: Text('カテゴリー: ${entry.category.categoryName}'), + ), + Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'), + ], + ), + onTap: () async { + await controller.joinEvent(entry); + }, + ); + }, + ); + }), + ), + ], + ), + ); + } + + String _formatDate(DateTime? date) { + if (date == null) { + return '日時未設定'; + } + return DateFormat('yyyy-MM-dd').format(date); + } +} + diff --git a/lib/pages/index/index_controller.dart b/lib/pages/index/index_controller.dart index 30d6f24..a8a5a42 100644 --- a/lib/pages/index/index_controller.dart +++ b/lib/pages/index/index_controller.dart @@ -82,6 +82,12 @@ class IndexController extends GetxController with WidgetsBindingObserver { String areaDropdownValue = "-1"; String cateogory = "-all-"; + final selectedEventName = 'add_location'.tr.obs; + + void setSelectedEventName(String eventName) { + selectedEventName.value = eventName; + } + ConnectivityResult connectionStatus = ConnectivityResult.none; var connectionStatusName = "".obs; final Connectivity _connectivity = Connectivity(); @@ -89,6 +95,9 @@ class IndexController extends GetxController with WidgetsBindingObserver { final Rx lastUserUpdateTime = DateTime.now().obs; + RxInt teamId = RxInt(-1); // チームIDを保存するための変数 + + //late TeamController teamController = TeamController(); /* void updateUserInfo(Map newUserInfo) { currentUser.clear(); @@ -97,6 +106,20 @@ class IndexController extends GetxController with WidgetsBindingObserver { } */ + final isReferenceMode = false.obs; + + void setReferenceMode(bool value) { + isReferenceMode.value = value; + } + + bool canStartRoge() { + return !isReferenceMode.value; + } + + bool canCheckin() { + return !isReferenceMode.value; + } + void toggleMode() { if (mode.value == 0) { mode += 1; @@ -214,6 +237,8 @@ class IndexController extends GetxController with WidgetsBindingObserver { print('IndexController onInit called'); // デバッグ用の出力を追加 + //teamController = Get.find(); + } Future initializeApiService() async { @@ -462,16 +487,27 @@ class IndexController extends GetxController with WidgetsBindingObserver { // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // - void register(String email, String password, BuildContext context) { - AuthService.register(email, password).then((value) { + void register(String email, String password, String password2, BuildContext context) { + AuthService.register(email, password,password2).then((value) { if (value.isNotEmpty) { debugPrint("ユーザー登録成功:${email}, ${password}"); logManager.addOperationLog("User tried to register new account : ${email} , ${password} ."); currentUser.clear(); - currentUser.add(value); + //currentUser.add(value); isLoading.value = false; - Navigator.pop(context); + + // ユーザー登録成功メッセージを表示 + Get.snackbar( + 'success'.tr, + 'user_registration_successful'.tr, + backgroundColor: Colors.green, + colorText: Colors.white, + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + ); + + //Navigator.pop(context); Get.toNamed(AppPages.INDEX); } else { debugPrint("ユーザー登録失敗:${email}, ${password}"); @@ -518,7 +554,7 @@ class IndexController extends GetxController with WidgetsBindingObserver { } */ - void changeUser(Map value, {bool replace = true}) { + void changeUser(Map value, {bool replace = true}) async{ currentUser.clear(); currentUser.add(value); if (replace) { @@ -529,12 +565,29 @@ class IndexController extends GetxController with WidgetsBindingObserver { if (currentUser.isNotEmpty) { rogMode.value = 0; restoreGame(); + + // チームデータを取得 + await fetchTeamData(); } else { rogMode.value = 1; } Get.toNamed(AppPages.INDEX); } + Future fetchTeamData() async { + try { + Get.put(TeamController()); + // \"TeamController\" not found. You need to call \"Get.put(TeamController())\" or \"Get.lazyPut(()=>TeamController())\" + final teamController = Get.find(); + await teamController.fetchTeams(); + if (teamController.teams.isNotEmpty) { + teamId.value = teamController.teams.first.id; + } + } catch (e) { + print("Error fetching team data: $e"); + } + } + loadUserDetailsForToken(String token) async { AuthService.userForToken(token).then((value) { print("----token val-- $value ------"); diff --git a/lib/pages/index/index_page.dart b/lib/pages/index/index_page.dart index 8f0afab..b71ec73 100644 --- a/lib/pages/index/index_page.dart +++ b/lib/pages/index/index_page.dart @@ -78,7 +78,8 @@ class _IndexPageState extends State { // drawer: DrawerPage(), appBar: AppBar( - title: Text("add_location".tr), + title: Obx(() => Text(indexController.selectedEventName.value)), + //title: Text("add_location".tr), actions: [ // IconButton( // onPressed: () { diff --git a/lib/pages/login/login_page.dart b/lib/pages/login/login_page.dart index 29743fe..c1ce842 100644 --- a/lib/pages/login/login_page.dart +++ b/lib/pages/login/login_page.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/widgets/helper_dialog.dart'; +import 'package:rogapp/services/api_service.dart'; // 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 // エラーメッセージをローカライズすることを検討してください。 @@ -16,9 +17,11 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { //class LoginPage extends StatelessWidget { final IndexController indexController = Get.find(); + final ApiService apiService = Get.find(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); + bool _obscureText = true; @override void initState() { @@ -31,6 +34,62 @@ class _LoginPageState extends State { }); } + void _showResetPasswordDialog() { + TextEditingController resetEmailController = TextEditingController(); + + Get.dialog( + AlertDialog( + title: Text('パスワードのリセット'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('パスワードをリセットするメールアドレスを入力してください。'), + SizedBox(height: 10), + TextField( + controller: resetEmailController, + decoration: InputDecoration( + labelText: 'メールアドレス', + border: OutlineInputBorder(), + ), + ), + ], + ), + actions: [ + TextButton( + child: Text('キャンセル'), + onPressed: () => Get.back(), + ), + ElevatedButton( + child: Text('リセット'), + onPressed: () async { + if (resetEmailController.text.isNotEmpty) { + bool success = await apiService.resetPassword(resetEmailController.text); + Get.back(); + if (success) { + Get.dialog( + AlertDialog( + title: Text('パスワードリセット'), + content: Text('パスワードリセットメールを送信しました。メールのリンクからパスワードを設定してください。'), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () => Get.back(), + ), + ], + ), + ); + } else { + Get.snackbar('エラー', 'パスワードリセットに失敗しました。もう一度お試しください。', + snackPosition: SnackPosition.BOTTOM); + } + } + }, + ), + ], + ), + ); + } + //LoginPage({Key? key}) : super(key: key); @override @@ -43,226 +102,243 @@ class _LoginPageState extends State { backgroundColor: Colors.white, automaticallyImplyLeading: false, ), - body: indexController.currentUser.isEmpty - ? SizedBox( - width: double.infinity, - - child: Column( - mainAxisAlignment: MainAxisAlignment.start, + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: indexController.currentUser.isEmpty + ? SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( 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, - ), - ], + Container( + height: MediaQuery.of(context).size.height / 6, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage( + 'assets/images/login_image.jpg'))), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Column( - children: [ - makeInput( - label: "email".tr, controller: emailController), - makeInput( - label: "password".tr, - controller: passwordController, - obsureText: true), - ], + const SizedBox( + height: 5, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + children: [ + makeInput( + label: "email".tr, controller: emailController), + makePasswordInput( + label: "password".tr, + controller: passwordController, + obscureText: _obscureText, + onToggleVisibility: () { + setState(() { + _obscureText = !_obscureText; + }); + }), + ], + ), + ), + 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), + ); + 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: 3, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: _showResetPasswordDialog, + child: Text( + "forgot_password".tr, + style: TextStyle(color: Colors.blue), ), ), - 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( + "app_developed_by_gifu_dx".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( - "rogaining_user_need_tosign_up".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( + 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"), - ), + ], + ), + ) + : TextButton( + onPressed: () { + indexController.logout(); + Get.offAllNamed(AppPages.LOGIN); + }, + child: const Text("Already Logged in, Click to logout"), + ), + ), ); } } +Widget makePasswordInput({ + required String label, + required TextEditingController controller, + required bool obscureText, + required VoidCallback onToggleVisibility, +}) { + 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: obscureText, + 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]!), + ), + suffixIcon: IconButton( + icon: Icon( + obscureText ? Icons.visibility : Icons.visibility_off, + color: Colors.grey, + ), + onPressed: onToggleVisibility, + ), + ), + ), + const SizedBox(height: 30.0) + ], + ); +} + Widget makeInput( {label, required TextEditingController controller, obsureText = false}) { return Column( diff --git a/lib/pages/login/login_page.dart_backup b/lib/pages/login/login_page.dart_backup new file mode 100644 index 0000000..b8464ef --- /dev/null +++ b/lib/pages/login/login_page.dart_backup @@ -0,0 +1,381 @@ +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'; +import 'package:rogapp/widgets/helper_dialog.dart'; +import 'package:rogapp/services/api_service.dart'; + +// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 +// エラーメッセージをローカライズすることを検討してください。 +// ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。 +// +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { +//class LoginPage extends StatelessWidget { + final IndexController indexController = Get.find(); + final ApiService apiService = Get.find(); + + TextEditingController emailController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); + bool _obscureText = true; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + showHelperDialog( + '参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。', + 'login_page' + ); + }); + } + + void _showResetPasswordDialog() { + TextEditingController resetEmailController = TextEditingController(); + + Get.dialog( + AlertDialog( + title: Text('パスワードのリセット'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('パスワードをリセットするメールアドレスを入力してください。'), + SizedBox(height: 10), + TextField( + controller: resetEmailController, + decoration: InputDecoration( + labelText: 'メールアドレス', + border: OutlineInputBorder(), + ), + ), + ], + ), + actions: [ + TextButton( + child: Text('キャンセル'), + onPressed: () => Get.back(), + ), + ElevatedButton( + child: Text('リセット'), + onPressed: () async { + if (resetEmailController.text.isNotEmpty) { + bool success = await apiService.resetPassword(resetEmailController.text); + Get.back(); + if (success) { + Get.dialog( + AlertDialog( + title: Text('パスワードリセット'), + content: Text('パスワードリセットメールを送信しました。メールのリンクからパスワードを設定してください。'), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () => Get.back(), + ), + ], + ), + ); + } else { + Get.snackbar('エラー', 'パスワードリセットに失敗しました。もう一度お試しください。', + snackPosition: SnackPosition.BOTTOM); + } + } + }, + ), + ], + ), + ); + } + + //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), + makePasswordInput( + label: "password".tr, + controller: passwordController, + obscureText: _obscureText, + onToggleVisibility: () { + setState(() { + _obscureText = !_obscureText; + }); + }), + ], + ), + ), + 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: 3, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: _showResetPasswordDialog, + child: Text( + "forgot_password".tr, + style: TextStyle(color: Colors.blue), + ), + ), + ], + ), + 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.logout(); + Get.offAllNamed(AppPages.LOGIN); + }, + child: const Text("Already Logged in, Click to logout"), + ), + ); + } +} + +Widget makePasswordInput({ + required String label, + required TextEditingController controller, + required bool obscureText, + required VoidCallback onToggleVisibility, +}) { + 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: obscureText, + 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]!), + ), + suffixIcon: IconButton( + icon: Icon( + obscureText ? Icons.visibility : Icons.visibility_off, + color: Colors.grey, + ), + onPressed: onToggleVisibility, + ), + ), + ), + const SizedBox(height: 30.0) + ], + ); +} + +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, + ) + ], + ); +} diff --git a/lib/pages/login_popup/login_popup_page.dart b/lib/pages/login_popup/login_popup_page.dart index c71bbd8..38527c7 100644 --- a/lib/pages/login_popup/login_popup_page.dart +++ b/lib/pages/login_popup/login_popup_page.dart @@ -207,7 +207,8 @@ class LoginPopupPage extends StatelessWidget { ) : TextButton( onPressed: () { - indexController.currentUser.clear(); + indexController.logout(); + Get.offAllNamed(AppPages.LOGIN); }, child: const Text("Already Logged in, Click to logout"), ), diff --git a/lib/pages/register/register_page.dart b/lib/pages/register/register_page.dart index 646f296..0499e69 100644 --- a/lib/pages/register/register_page.dart +++ b/lib/pages/register/register_page.dart @@ -16,6 +16,9 @@ class _RegisterPageState extends State { final TextEditingController passwordController = TextEditingController(); final TextEditingController confirmPasswordController = TextEditingController(); + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + @override void initState() { super.initState(); @@ -60,8 +63,28 @@ class _RegisterPageState extends State { ), const SizedBox(height: 30), makeInput(label: "email".tr, controller: emailController), - makeInput(label: "password".tr, controller: passwordController, obsureText: true), - makeInput(label: "confirm_password".tr, controller: confirmPasswordController, obsureText: true), + //makeInput(label: "password".tr, controller: passwordController, obsureText: true), + //makeInput(label: "confirm_password".tr, controller: confirmPasswordController, obsureText: true), + makePasswordInput( + label: "password".tr, + controller: passwordController, + obscureText: _obscurePassword, + onToggleVisibility: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + makePasswordInput( + label: "confirm_password".tr, + controller: confirmPasswordController, + obscureText: _obscureConfirmPassword, + onToggleVisibility: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), const SizedBox(height: 20), ElevatedButton( onPressed: _handleRegister, @@ -91,6 +114,38 @@ class _RegisterPageState extends State { ); } + Widget makePasswordInput({ + required String label, + required TextEditingController controller, + required bool obscureText, + required VoidCallback onToggleVisibility, + }) { + 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: obscureText, + 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]!)), + suffixIcon: IconButton( + icon: Icon( + obscureText ? Icons.visibility : Icons.visibility_off, + color: Colors.grey, + ), + onPressed: onToggleVisibility, + ), + ), + ), + const SizedBox(height: 20), + ], + ); + } + void _handleRegister() { if (passwordController.text != confirmPasswordController.text) { _showErrorSnackbar("no_match".tr, "password_does_not_match".tr); @@ -106,6 +161,7 @@ class _RegisterPageState extends State { indexController.register( emailController.text, passwordController.text, + confirmPasswordController.text, context ); // 登録が成功したと仮定し、ログインページに遷移 diff --git a/lib/pages/team/team_detail_page.dart b/lib/pages/team/team_detail_page.dart index 4ce558d..28db560 100644 --- a/lib/pages/team/team_detail_page.dart +++ b/lib/pages/team/team_detail_page.dart @@ -146,11 +146,6 @@ class _TeamDetailPageState extends State { onChanged: (value) => controller.updateCategory(value), ); }), - if (mode.value == 'edit') - Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Text('ゼッケン番号: ${controller.selectedTeam.value?.zekkenNumber ?? ""}'), - ), Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'), diff --git a/lib/pages/team/team_list_page.dart b/lib/pages/team/team_list_page.dart index 2f240f7..0cc5291 100644 --- a/lib/pages/team/team_list_page.dart +++ b/lib/pages/team/team_list_page.dart @@ -35,7 +35,7 @@ class TeamListPage extends GetWidget { final team = controller.teams[index]; return ListTile( title: Text(team.teamName), - subtitle: Text('${team.category.categoryName} - ${team.zekkenNumber}'), + subtitle: Text('${team.category.categoryName} '), onTap: () async { await Get.toNamed( AppPages.TEAM_DETAIL, diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index c0724dc..67ebbd6 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -114,9 +114,14 @@ class ApiService extends GetxService{ List categories = []; for (var categoryJson in categoriesJson) { - //print('\nCategory Data:'); - //_printDataComparison(categoryJson, NewCategory); - categories.add(NewCategory.fromJson(categoryJson)); + try { + //print('\nCategory Data:'); + //_printDataComparison(categoryJson, NewCategory); + categories.add(NewCategory.fromJson(categoryJson)); + }catch(e){ + print('Error parsing category: $e'); + print('Problematic JSON: $categoryJson'); + } } return categories; @@ -131,6 +136,26 @@ class ApiService extends GetxService{ } } + Future getZekkenNumber(int categoryId) async { + try { + final response = await http.post( + Uri.parse('$baseUrl/categories-viewset/$categoryId/get_zekken_number/'), + headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, + ); + if (response.statusCode == 200) { + final decodedResponse = utf8.decode(response.bodyBytes); + print('User Response body: $decodedResponse'); + final categoriesJson = json.decode(decodedResponse); + + return NewCategory.fromJson(categoriesJson); + } else { + throw Exception('Failed to increment category number'); + } + } catch (e) { + throw Exception('Error incrementing category number: $e'); + } + } + Future getCurrentUser() async { init(); getToken(); @@ -457,7 +482,7 @@ class ApiService extends GetxService{ } } - Future createEntry(int teamId, int eventId, int categoryId, DateTime date) async { + Future createEntry(int teamId, int eventId, int categoryId, DateTime date,String zekkenNumber) async { init(); getToken(); @@ -477,6 +502,7 @@ class ApiService extends GetxService{ 'event': eventId, 'category': categoryId, 'date': formattedDate, + 'zekken_number':zekkenNumber, }), ); @@ -508,7 +534,7 @@ class ApiService extends GetxService{ 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ - 'zekken_number': entry.team.zekkenNumber, + 'zekken_number': entry.zekkenNumber, 'event_code': entry.event.eventName, 'group': entry.team.category.categoryName, 'team_name': entry.team.teamName, @@ -608,4 +634,29 @@ class ApiService extends GetxService{ } } + Future resetPassword(String email) async { + init(); + + try { + final response = await http.post( + Uri.parse('$baseUrl/password-reset/'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: json.encode({ + 'email': email, + }), + ); + + if (response.statusCode == 200) { + return true; + } else { + print('Password reset failed with status code: ${response.statusCode}'); + return false; + } + } catch (e) { + print('Error in resetPassword: $e'); + return false; + } + } } \ No newline at end of file diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 1186cb5..ee2f40e 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -132,6 +132,7 @@ class AuthService { return cats; } + // ユーザー登録 // /* @@ -151,7 +152,7 @@ class AuthService { */ static Future> register( - String email, String password) async { + String email, String password, String password2) async { Map cats = {}; String serverUrl = ConstValues.currentServer(); String url = '$serverUrl/api/register/'; @@ -161,10 +162,10 @@ class AuthService { headers: { 'Content-Type': 'application/json; charset=UTF-8', }, - body: jsonEncode({'email': email, 'password': password}), + body: jsonEncode({'email': email, 'password': password, 'password2': password2}), ); //print(response.body); - if (response.statusCode == 200) { + if (response.statusCode == 201) { cats = json.decode(utf8.decode(response.bodyBytes)); } return cats; diff --git a/lib/services/external_service.dart b/lib/services/external_service.dart index 57f609e..1b6b0aa 100644 --- a/lib/services/external_service.dart +++ b/lib/services/external_service.dart @@ -4,8 +4,10 @@ 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/model/team.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart'; +import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/utils/database_gps.dart'; import 'package:rogapp/utils/database_helper.dart'; import 'dart:convert'; @@ -32,11 +34,19 @@ class ExternalService { Future> startRogaining() async { final IndexController indexController = Get.find(); + //final TeamController teamController = Get.find(); debugPrint("== startRogaining =="); Map res = {}; + //final teamController = TeamController(); + + //Team team0 = teamController.teams[0]; + //print("team={team0}"); + + //int teamId = indexController.currentUser[0]["user"]["team"]["id"]; + int userId = indexController.currentUser[0]["user"]["id"]; //print("--- Pressed -----"); String team = indexController.currentUser[0]["user"]['team_name']; @@ -60,8 +70,9 @@ class ExternalService { } else { debugPrint("== startRogaining processing=="); - String url = 'https://rogaining.sumasen.net/gifuroge/start_from_rogapp'; - //print('++++++++$url'); + String serverUrl = ConstValues.currentServer(); + String url = '$serverUrl/gifuroge/start_from_rogapp'; + print('++++++++$url'); final http.Response response = await http.post( Uri.parse(url), headers: { @@ -82,7 +93,7 @@ class ExternalService { } Future> makeCheckpoint( - int userId, + int userId, // 中身はteamId String token, String checkinTime, String teamname, @@ -93,9 +104,17 @@ class ExternalService { // print("~~~~ cp is $cp ~~~~"); //print("--cpcp-- ${cp}"); Map res = {}; - String url = 'https://rogaining.sumasen.net/gifuroge/checkin_from_rogapp'; + String serverUrl = ConstValues.currentServer(); + String url = '$serverUrl/gifuroge/checkin_from_rogapp'; //print('++++++++$url'); final IndexController indexController = Get.find(); + //final TeamController teamController = Get.find(); + + +// Team team0 = indexController.teamController.teams[0]; +// print("team={team0}"); + + //int teamId = indexController.teamController.teams[0]; if (imageurl != null) { if (indexController.connectionStatusName.value != "wifi" && @@ -160,7 +179,7 @@ class ExternalService { 'cp_number': cp.toString(), 'event_code': eventcode, 'image': res["checkinimage"].toString().replaceAll( - 'http://localhost:8100', 'http://rogaining.sumasen.net') + 'http://localhost:8100', serverUrl) //'http://rogaining.sumasen.net') }), ); var vv = jsonEncode({ @@ -168,7 +187,7 @@ class ExternalService { 'cp_number': cp.toString(), 'event_code': eventcode, 'image': res["checkinimage"].toString().replaceAll( - 'http://localhost:8100', 'http://rogaining.sumasen.net') + 'http://localhost:8100', serverUrl) //'http://rogaining.sumasen.net') }); //print("~~~~ api 2 values $vv ~~~~"); //print("--json-- $vv"); @@ -253,6 +272,9 @@ class ExternalService { final DestinationController destinationController = Get.find(); + // チームIDを取得 + int teamId = indexController.currentUser[0]["user"]["team"]["id"]; + debugPrint("== goal Rogaining =="); //if(indexController.connectionStatusName != "wifi" && indexController.connectionStatusName != "mobile"){ @@ -261,7 +283,7 @@ class ExternalService { id: 1, team_name: teamname, event_code: eventcode, - user_id: userId, + user_id: teamId, //userId, cp_number: -1, checkintime: DateTime.now().toUtc().microsecondsSinceEpoch, image: image, @@ -283,7 +305,7 @@ class ExternalService { }, // 'id', 'user', 'goalimage', 'goaltime', 'team_name', 'event_code','cp_number' body: jsonEncode({ - 'user': userId.toString(), + 'user': teamId.toString(), //userId.toString(), 'team_name': teamname, 'event_code': eventcode, 'goaltime': goalTime, @@ -292,7 +314,8 @@ class ExternalService { }), ); - String url = 'https://rogaining.sumasen.net/gifuroge/goal_from_rogapp'; + //String serverUrl = ConstValues.currentServer(); + String url = '$serverUrl/gifuroge/goal_from_rogapp'; //print('++++++++$url'); if (response.statusCode == 201) { Map res = json.decode(utf8.decode(response.bodyBytes)); @@ -308,7 +331,7 @@ class ExternalService { 'event_code': eventcode, 'goal_time': goalTime, 'image': res["goalimage"].toString().replaceAll( - 'http://localhost:8100', 'http://rogaining.sumasen.net') + 'http://localhost:8100', serverUrl) //'http://rogaining.sumasen.net') }), ); String rec = jsonEncode({ @@ -317,7 +340,7 @@ class ExternalService { 'goal_time': goalTime, 'image': res["goalimage"] .toString() - .replaceAll('http://localhost:8100', 'http://rogaining.sumasen.net') + .replaceAll('http://localhost:8100', serverUrl) //'http://rogaining.sumasen.net') }); //print("-- json -- $rec"); //print('----- response2 is $response2 --------'); @@ -343,8 +366,8 @@ class ExternalService { indexController.connectionStatusName.value != "mobile") { return Future.value(false); } else { - String url = - 'https://rogaining.sumasen.net/gifuroge/remove_checkin_from_rogapp'; + String serverUrl = ConstValues.currentServer(); + String url = '$serverUrl/gifuroge/remove_checkin_from_rogapp'; //print('++++++++$url'); final http.Response response = await http.post( Uri.parse(url), @@ -413,8 +436,8 @@ class ExternalService { //print("calling push gps step 2 ${payload}"); - String urlS = - 'https://rogaining.sumasen.net/gifuroge/get_waypoint_datas_from_rogapp'; + String serverUrl = ConstValues.currentServer(); + String urlS = '$serverUrl/gifuroge/get_waypoint_datas_from_rogapp'; //print('++++++++$url'); var url = Uri.parse(urlS); // Replace with your server URL var response = await http.post( @@ -440,8 +463,8 @@ class ExternalService { 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"; + String serverUrl = ConstValues.currentServer(); + String url = "$serverUrl/gifuroge/check_event_code?zekken_number=$teamcode&password=$password"; //print('++++++++$url'); final http.Response response = await http.get(Uri.parse(url), headers: { diff --git a/lib/utils/const.dart b/lib/utils/const.dart index be8d7ff..fe09a7e 100644 --- a/lib/utils/const.dart +++ b/lib/utils/const.dart @@ -2,10 +2,10 @@ class ConstValues{ - static const container_svr = "http://container.intranet.sumasen.net:8100"; - static const server_uri = "https://rogaining.intranet.sumasen.net"; - //static const container_svr = "http://container.sumasen.net:8100"; - //static const server_uri = "https://rogaining.sumasen.net"; + //static const container_svr = "http://container.intranet.sumasen.net:8100"; + //static const server_uri = "https://rogaining.intranet.sumasen.net"; + static const container_svr = "http://container.sumasen.net:8100"; + static const server_uri = "https://rogaining.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"; diff --git a/lib/utils/string_values.dart b/lib/utils/string_values.dart index f7ca32b..03dab47 100644 --- a/lib/utils/string_values.dart +++ b/lib/utils/string_values.dart @@ -213,6 +213,9 @@ class StringValues extends Translations{ '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.', + 'forgot_password':'Forgot password', + 'user_registration_successful':'Sent activation mail to you. Pls click activation link on the email.', + }, 'ja_JP': { 'drawer_title':'ロゲイニング参加者はログイン するとチェックポイントが参照 できます', @@ -428,6 +431,8 @@ class StringValues extends Translations{ 'reset_explain': 'すべてリセットされました。ロゲ開始から再開して下さい。', 'no_match': '不一致', 'password_does_not_match':'入力したパスワードが一致しません', + 'forgot_password':'パスワードを忘れた場合', + 'user_registration_successful':'ユーザー認証のメールをお届けしました。メール上のリンクをクリックして正式登録してください。', }, }; } diff --git a/lib/widgets/bottom_sheet_new.dart b/lib/widgets/bottom_sheet_new.dart index e9b9804..b5a945e 100644 --- a/lib/widgets/bottom_sheet_new.dart +++ b/lib/widgets/bottom_sheet_new.dart @@ -216,6 +216,7 @@ class BottomSheetNew extends GetView { ); saveGameState(); + //int teamId = indexController.teamId.value; // teamIdを使用 await ExternalService().startRogaining(); Get.back(); Get.back();// Close the dialog and potentially navigate away diff --git a/login_page.dart b/login_page.dart new file mode 100644 index 0000000..b8464ef --- /dev/null +++ b/login_page.dart @@ -0,0 +1,381 @@ +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'; +import 'package:rogapp/widgets/helper_dialog.dart'; +import 'package:rogapp/services/api_service.dart'; + +// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 +// エラーメッセージをローカライズすることを検討してください。 +// ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。 +// +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { +//class LoginPage extends StatelessWidget { + final IndexController indexController = Get.find(); + final ApiService apiService = Get.find(); + + TextEditingController emailController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); + bool _obscureText = true; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + showHelperDialog( + '参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。', + 'login_page' + ); + }); + } + + void _showResetPasswordDialog() { + TextEditingController resetEmailController = TextEditingController(); + + Get.dialog( + AlertDialog( + title: Text('パスワードのリセット'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('パスワードをリセットするメールアドレスを入力してください。'), + SizedBox(height: 10), + TextField( + controller: resetEmailController, + decoration: InputDecoration( + labelText: 'メールアドレス', + border: OutlineInputBorder(), + ), + ), + ], + ), + actions: [ + TextButton( + child: Text('キャンセル'), + onPressed: () => Get.back(), + ), + ElevatedButton( + child: Text('リセット'), + onPressed: () async { + if (resetEmailController.text.isNotEmpty) { + bool success = await apiService.resetPassword(resetEmailController.text); + Get.back(); + if (success) { + Get.dialog( + AlertDialog( + title: Text('パスワードリセット'), + content: Text('パスワードリセットメールを送信しました。メールのリンクからパスワードを設定してください。'), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () => Get.back(), + ), + ], + ), + ); + } else { + Get.snackbar('エラー', 'パスワードリセットに失敗しました。もう一度お試しください。', + snackPosition: SnackPosition.BOTTOM); + } + } + }, + ), + ], + ), + ); + } + + //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), + makePasswordInput( + label: "password".tr, + controller: passwordController, + obscureText: _obscureText, + onToggleVisibility: () { + setState(() { + _obscureText = !_obscureText; + }); + }), + ], + ), + ), + 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: 3, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: _showResetPasswordDialog, + child: Text( + "forgot_password".tr, + style: TextStyle(color: Colors.blue), + ), + ), + ], + ), + 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.logout(); + Get.offAllNamed(AppPages.LOGIN); + }, + child: const Text("Already Logged in, Click to logout"), + ), + ); + } +} + +Widget makePasswordInput({ + required String label, + required TextEditingController controller, + required bool obscureText, + required VoidCallback onToggleVisibility, +}) { + 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: obscureText, + 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]!), + ), + suffixIcon: IconButton( + icon: Icon( + obscureText ? Icons.visibility : Icons.visibility_off, + color: Colors.grey, + ), + onPressed: onToggleVisibility, + ), + ), + ), + const SizedBox(height: 30.0) + ], + ); +} + +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, + ) + ], + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 5be1174..15c2202 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 4.8.5+485 +version: 4.8.6+486 environment: sdk: ">=3.2.0 <4.0.0"