大幅変更&環境バージョンアップ

This commit is contained in:
2024-08-22 14:35:09 +09:00
parent 56e9861c7a
commit dc58dc0584
446 changed files with 29645 additions and 8315 deletions

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewPage extends StatelessWidget {
final String url;
const WebViewPage({super.key, required this.url});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('WebView'),
),
body: WebViewWidget(
controller: WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(url)),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
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:gifunavi/model/destination.dart';
class CustomCameraView extends StatefulWidget {
final Function(String) onImageCaptured;
final Destination? destination;
const CustomCameraView({super.key, required this.onImageCaptured, required this.destination});
@override
_CustomCameraViewState createState() => _CustomCameraViewState();
}
class _CustomCameraViewState extends State<CustomCameraView> {
CameraController? _controller;
late List<CameraDescription> _cameras;
int _selectedCameraIndex = 0;
double _currentScale = 1.0;
FlashMode _currentFlashMode = FlashMode.off;
Destination? destination;
@override
void initState() {
super.initState();
_initializeCamera();
destination = widget.destination;
}
Future<void> _initializeCamera() async {
_cameras = await availableCameras();
_controller = CameraController(_cameras[_selectedCameraIndex], ResolutionPreset.medium);
await _controller!.initialize();
setState(() {});
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
Future<void> _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,
),
],
),
),
],
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
class CategoryPage extends StatelessWidget {
const CategoryPage({Key? key}) : super(key: key);
const CategoryPage({super.key});
@override
Widget build(BuildContext context) {

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/widgets/debug_widget.dart';
class ChangePasswordPage extends StatelessWidget {
ChangePasswordPage({Key? key}) : super(key: key);
ChangePasswordPage({super.key});
LogManager logManager = LogManager();
IndexController indexController = Get.find<IndexController>();
@ -13,150 +16,174 @@ class ChangePasswordPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
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,)),
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: [
Container(
child: Text("change_password".tr, style: const TextStyle(fontSize: 24.0),),
),
const SizedBox(height: 30,),
],
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 40
),
child: Column(
body: SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
children: [
Column(
children: [
makeInput(label: "old_password".tr, controller: oldPasswordController),
makeInput(label: "new_password".tr, controller: newPasswordController, obsureText: true),
Text(
"change_password".tr,
style: const TextStyle(fontSize: 24.0),
),
const SizedBox(
height: 30,
),
],
),
),
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.is_loading == 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){
Get.snackbar(
"no_values".tr,
"values_required".tr,
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.is_loading.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: const Text("ログイン",style: TextStyle(
fontWeight: FontWeight.w600,fontSize: 16,color: Colors.white70
),
),
),
const SizedBox(height: 10.0,),
],
)
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),
],
),
),
)
),
const SizedBox(height: 20,),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
],
)
],
),
],
),
)
);
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: const 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])!,
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])!),
),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: (Colors.grey[400])!
),
),
),
),
const SizedBox(height: 30.0,)
],
);
const SizedBox(
height: 30.0,
)
],
);
}
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
class CityPage extends StatelessWidget {
const CityPage({Key? key}) : super(key: key);
const CityPage({super.key});
@override
Widget build(BuildContext context) {

View File

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import 'package:gifunavi/pages/debug/debug_controller.dart';
class DebugBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<DebugController>(() => DebugController());
}
}

View File

@ -0,0 +1,47 @@
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:gifunavi/utils/location_controller.dart';
class DebugController extends GetxController {
final LocationController locationController = Get.find<LocationController>();
final gpsSignalStrength = 'high'.obs;
final currentPosition = Rx<Position?>(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';
}
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/debug/debug_controller.dart';
class DebugPage extends GetView<DebugController> {
const DebugPage({super.key});
@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<String>(
value: controller.gpsSignalStrength.value,
onChanged: (value) {
controller.setGpsSignalStrength(value!);
},
items: ['high', 'medium', 'low']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
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
? 'シミュレーションモード'
: '標準モード'),
),
),
],
),
),
);
}
}

View File

@ -1,9 +1,11 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/destination/destination_controller.dart';
import 'package:gifunavi/main.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
class DestinationBinding extends Bindings {
@override
void dependencies() {
Get.put<DestinationController>(DestinationController());
restoreGame();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,212 +0,0 @@
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:rogapp/pages/destination/destination_controller.dart';
import 'package:rogapp/pages/destination_map/destination_map_page.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/destination_widget.dart';
class DestnationPage extends StatelessWidget {
DestnationPage({Key? key}) : super(key: key);
final DestinationController destinationController = Get.find<DestinationController>();
final IndexController indexController = Get.find<IndexController>();
final List<int> _items = List<int>.generate(50, (int index) => index);
Future<void> showCurrentPosition() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission != LocationPermission.whileInUse ||
permission != LocationPermission.always) {
permission = await Geolocator.requestPermission();
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
indexController.rogMapController.move(LatLng(position.latitude, position.longitude), 14);
}
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{
return Image(image: NetworkImage(destinationController.destinations[index].photos!));
}
}
Widget getRoutingImage(int route){
switch (route) {
case 0:
return const Image(image: AssetImage('assets/images/p4_9_man.png'), width: 35.0,);
case 1:
return const Image(image: AssetImage('assets/images/p4_8_car.png'), width: 35.0,);
case 2:
return const Image(image: AssetImage('assets/images/p4_10_train.png'), width: 35.0,);
default:
return const Image(image: AssetImage('assets/images/p4_9_man.png'), width: 35.0,);
}
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
final Color evenItemColor = colorScheme.primary.withOpacity(0.15);
return WillPopScope(
onWillPop: () async {
indexController.switchPage(AppPages.INITIAL);
return false;
},
child: Scaffold(
drawer: DrawerPage(),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left:13.0),
child: InkWell(
child: Obx((() => getRoutingImage(destinationController.travelMode.value))),
onTap: (){
Get.bottomSheet(
Obx(() =>
ListView(
children: [
Padding(
padding: const EdgeInsets.only(top:30.0, bottom: 30),
child: Center(child: Text("select_travel_mode".tr, style: const TextStyle(fontSize: 22.0, color:Colors.red, fontWeight:FontWeight.bold),),),
),
ListTile(
selected: destinationController.travelMode == 0 ? true : false,
selectedTileColor: Colors.amber.shade200,
leading: const Image(image: AssetImage('assets/images/p4_9_man.png'),),
title: Text("walking".tr),
onTap:(){
destinationController.travelMode.value = 0;
destinationController.PopulateDestinations();
Get.back();
},
),
ListTile(
selected: destinationController.travelMode == 1 ? true : false,
selectedTileColor: Colors.amber.shade200,
leading: const Image(image: AssetImage('assets/images/p4_8_car.png'),),
title: Text("driving".tr),
onTap:(){
destinationController.travelMode.value = 1;
destinationController.PopulateDestinations();
Get.back();
},
),
// ListTile(
// selected: destinationController.travelMode == 2 ? true : false,
// selectedTileColor: Colors.amber.shade200,
// leading: Image(image: AssetImage('assets/images/p4_10_train.png'),),
// title: Text("transit".tr),
// onTap:(){
// destinationController.travelMode.value = 2;
// destinationController.PopulateDestinations();
// Get.back();
// },
// ),
],
),
),
isScrollControlled:false,
backgroundColor: Colors.white,
);
//destinationController.PopulateDestinations();
}
),
)
,
IconButton(
icon: const Icon(Icons.travel_explore, size: 35,),
onPressed: (){
indexController.switchPage(AppPages.INITIAL);
}
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
//print("######");
indexController.toggleDestinationMode();
},
tooltip: 'Increment',
elevation: 4.0,
child: Obx(() =>
indexController.desination_mode == 1 ?
const Image(image: AssetImage('assets/images/list2.png'))
:
const Image(image: AssetImage('assets/images/map.png'))
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
appBar:AppBar(
automaticallyImplyLeading: true,
title: Text("app_title".tr),
actions: [
InkWell(
onTap: (){
Get.toNamed(AppPages.CAMERA_PAGE);
},
child: destinationController.is_in_rog == true ?
Image.asset("assets/images/basic-walking.gif",height: 10.0,)
:
destinationController.is_at_goal == true ?
IconButton(
onPressed:(){Get.toNamed(AppPages.CAMERA_PAGE);},
icon: const Icon(Icons.assistant_photo),
)
:
IconButton(
onPressed:(){Get.toNamed(AppPages.CAMERA_PAGE);},
icon: const Icon(Icons.accessibility),
),
),
// Obx(() =>
// Text(indexController.connectionStatusName.value)
// ),
Obx(() =>
ToggleButtons(
disabledColor: Colors.grey.shade200,
selectedColor: Colors.red,
onPressed: (int index) {
destinationController.is_gps_selected.value = !destinationController.is_gps_selected.value;
if(destinationController.is_gps_selected.value){
destinationController.chekcs = 0;
destinationController.skip_gps = false;
//destinationController.resetRogaining();
}
},
isSelected: [destinationController.is_gps_selected.value],
children: const <Widget>[
Icon(Icons.explore, size: 35.0,
)],
),
),
// IconButton(onPressed: (){
// showCurrentPosition();
// },
// icon: Icon(Icons.location_on_outlined))
],
),
body: Obx(() =>
indexController.desination_mode.value == 0 ?
DestinationWidget():
DestinationMapPage()
)
),
);
}
}

View File

@ -1,429 +1,207 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.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_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:gifunavi/model/destination.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/utils/text_util.dart';
import 'package:gifunavi/widgets/base_layer_widget.dart';
import 'package:gifunavi/widgets/bottom_sheet_new.dart';
//import 'package:gifunavi/widgets/bottom_sheets/bottom_sheet_start.dart';
//import 'package:gifunavi/widgets/bottom_sheets/bottom_sheet_goal.dart';
//import 'package:gifunavi/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);
DestinationMapPage({super.key});
final IndexController indexController = Get.find<IndexController>();
final DestinationController destinationController = Get.find<DestinationController>();
final DestinationController destinationController =
Get.find<DestinationController>();
StreamSubscription? subscription;
final PopupController _popupLayerController = PopupController();
//final PopupController _popupLayerController = PopupController();
List<LatLng>? getPoints(){
print("##### --- route point ${indexController.routePoints.length}");
List<LatLng> pts = [];
for(PointLatLng p in indexController.routePoints){
LatLng l = LatLng(p.latitude, p.longitude);
pts.add(l);
}
return pts;
List<LatLng>? getPoints() {
//print("##### --- route point ${indexController.routePoints.length}");
List<LatLng> pts = [];
for (PointLatLng p in indexController.routePoints) {
LatLng l = LatLng(p.latitude, p.longitude);
pts.add(l);
}
return pts;
}
List<Marker>? getMarkers() {
List<Marker> pts = [];
int index = -1;
for (int i = 0; i < destinationController.destinations.length; i++) {
Destination d = destinationController.destinations[i];
print("^^^^ $d ^^^^");
Marker m = Marker(
// 要検討:マーカーのタップイベントを処理する際に、エラーハンドリングが不十分です。例外が発生した場合の処理を追加することをお勧めします。
//
List<Marker>? getMarkers() {
List<Marker> 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!),
anchorPos: AnchorPos.align(AnchorAlign.center),
builder:(cts){
alignment: Alignment.center,
child: InkWell(
onTap: () {
//print("-- Destination is --- ${d.name} ------");
if (indexController.currentDestinationFeature.isNotEmpty) {
indexController.currentDestinationFeature.clear();
}
indexController.currentDestinationFeature.add(d);
//indexController.getAction();
return InkWell(
onTap: (){
print("-- Destination is --- ${d.name} ------");
if(indexController.currentDestinationFeature.isNotEmpty) {
indexController.currentDestinationFeature.clear();
}
indexController.currentDestinationFeature.add(d);
//indexController.getAction();
showModalBottomSheet(context: Get.context!, isScrollControlled: true,
builder:((context) => BottomSheetNew())
).whenComplete((){
print("---- set skip gps to false -----");
destinationController.skip_gps = 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),
),
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,
),
),
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;
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(),
),
],
)
));
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(),
),
],
)));
}
FlutterMap TravelMap() {
// 要検討MapOptionsのboundsプロパティにハードコードされた座標が使用されています。これを動的に設定できるようにすることを検討してください。
//
FlutterMap travelMap() {
return FlutterMap(
mapController: indexController.rogMapController,
options: MapOptions(
onMapReady: (){
indexController.is_rog_mapcontroller_loaded.value = true;
subscription = indexController.rogMapController.mapEventStream.listen((MapEvent mapEvent) {
if (mapEvent is MapEventMoveStart) {
}
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();
if (indexController.currentUser.isEmpty) {
indexController.loadLocationsBound(indexController.currentUser[0]["user"]["event_code"]);
}
}
});
} ,
bounds: indexController.currentBound.isNotEmpty ? indexController.currentBound[0]: LatLngBounds.fromPoints([LatLng(35.03999881162295, 136.40587119778962), LatLng(36.642756778706904, 137.95226720406063)]),
zoom: 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(),
},
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,
),
CurrentLocationLayer(),
MarkerLayer(
markers: getMarkers()!
),
],
);
children: [
const BaseLayer(),
Obx(
() => indexController.routePointLenght > 0
? PolylineLayer(
polylines: [
Polyline(
points: getPoints()!,
strokeWidth: 6.0,
color: Colors.indigo),
],
)
: Container(),
),
CurrentLocationLayer(),
MarkerLayer(markers: getMarkers()!),
],
);
}
}
// class DestinationMapPage extends StatefulWidget {
// DestinationMapPage({ Key? key }) : super(key: key);
// @override
// State<DestinationMapPage> createState() => _DestinationMapPageState();
// }
//class _DestinationMapPageState extends State<DestinationMapPage> {
// final IndexController indexController = Get.find<IndexController>();
// final DestinationController destinationController = Get.find<DestinationController>();
// StreamSubscription? subscription;
// final PopupController _popupLayerController = PopupController();
// List<LatLng>? getPoints(List<PointLatLng> ptts){
// //print("##### --- route point ${indexController.routePoints.length}");
// List<LatLng> pts = [];
// for(PointLatLng p in ptts){
// LatLng l = LatLng(p.latitude, p.longitude);
// pts.add(l);
// }
// return pts;
// }
// String getDisplaytext(Destination dp){
// String txt = "";
// if(dp.cp! > 0){
// txt = "${dp.cp}";
// if(dp.checkin_point != null && dp.checkin_point! > 0){
// txt = txt + "{${dp.checkin_point}}";
// }
// if(dp.buy_point != null && dp.buy_point! > 0){
// txt = txt + "[${dp.buy_point}]";
// }
// }
// return txt;
// }
// List<Marker>? getMarkers() {
// List<Marker> pts = [];
// int index = -1;
// for (int i = 0; i < destinationController.destinations.length; i++) {
// Destination d = destinationController.destinations[i];
// //for(Destination d in destinationController.destinations){
// //print("-----lat ${lat}, ----- lon ${lan}");
// Marker m = Marker(
// point: LatLng(d.lat!, d.lon!),
// anchorPos: AnchorPos.align(AnchorAlign.center),
// builder:(cts){
// return InkWell(
// onTap: (){
// print("-- Destination is --- ${d.name} ------");
// if(d != null){
// if(indexController.currentDestinationFeature.length > 0) {
// indexController.currentDestinationFeature.clear();
// }
// indexController.currentDestinationFeature.add(d);
// //indexController.getAction();
// showModalBottomSheet(context: context, isScrollControlled: true,
// //builder:((context) => BottomSheetWidget())
// builder:((context) => BottomSheetNew())
// );
// }
// },
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Container(
// width:20,
// height:20,
// decoration: BoxDecoration(
// color: Colors.red,
// shape: BoxShape.circle,
// border: new Border.all(
// color: Colors.white,
// width: d.checkin_radious != null ? d.checkin_radious! : 1,
// ),
// ),
// child: new Center(
// child: new Text(
// (i + 1).toString(),
// style: TextStyle(color: Colors.white),
// ),
// ),
// ),
// Container( color: Colors.yellow, child: Text(getDisplaytext(d), style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold, overflow: TextOverflow.visible),)),
// ],
// ),
// );
// //return Icon(Icons.pin_drop);
// // return IconButton(
// // onPressed: ()async {
// // Destination? fs = await destinationController.getDEstinationForLatLong(d.lat!, d.lon!);
// // print("-- Destination is --- ${fs!.name} ------");
// // if(fs != null){
// // if(indexController.currentDestinationFeature.length > 0) {
// // indexController.currentDestinationFeature.clear();
// // }
// // indexController.currentDestinationFeature.add(fs);
// // //indexController.getAction();
// // showModalBottomSheet(context: context, isScrollControlled: true,
// // //builder:((context) => BottomSheetWidget())
// // builder:((context) => BottomSheetNew())
// // );
// // }
// // },
// // icon: Container(
// // width: 60,
// // height: 60,
// // decoration: BoxDecoration(
// // borderRadius: BorderRadius.circular(d.checkin_radious ?? 0),
// // color: Colors.transparent,
// // border: BoxBorder()
// // ),
// // child: Icon(Icons.pin_drop)
// // )
// // );
// });
// pts.add(m);
// }
// return pts;
// }
// @override
// void initState() {
// //indexController.routePoints.clear();
// DestinationService.getDestinationLine(destinationController.destinations)?.then((value){
// //print("---- loading destination points ------ ${value}");
// setState(() {
// indexController.routePoints = value;
// });
// });
// super.initState();
// }
// void reload(){
// setState(() {
// });
// }
// @override
// Widget build(BuildContext context) {
// return Obx((() =>
// Stack(
// children: [
// indexController.is_rog_mapcontroller_loaded.value == false ?
// Center(child: CircularProgressIndicator())
// :
// BreadCrumbWidget(mapController:indexController.rogMapController),
// Padding(
// padding: const EdgeInsets.only(top:50.0),
// //child: TravelMap(),
// child:
// TravelMap(indexController.routePoints),
// ),
// // Positioned(
// // bottom: 200,
// // left: 10,
// // child: Container(
// // color: Colors.white,
// // child: Row(
// // children: [
// // Text(destinationController.gps[0]),
// // Text(destinationController.locationPermission[0])
// // ],
// // ),
// // )
// // ),
// ],
// )
// ));
// }
// FlutterMap TravelMap(List<PointLatLng> ptts) {
// return FlutterMap(
// options: MapOptions(
// onMapCreated: (c){
// indexController.rogMapController = c;
// indexController.rogMapController!.onReady.then((_) {
// indexController.is_rog_mapcontroller_loaded.value = true;
// subscription = indexController.rogMapController!.mapEventStream.listen((MapEvent mapEvent) {
// if (mapEvent is MapEventMoveStart) {
// //print(DateTime.now().toString() + ' [MapEventMoveStart] START');
// // do something
// }
// if (mapEvent is MapEventMoveEnd) {
// destinationController.isSelected.value = false;
// //print(DateTime.now().toString() + ' [MapEventMoveStart] END');
// //indexController.loadLocationsBound();
// }
// });
// });
// } ,
// bounds: indexController.currentBound.length > 0 ? indexController.currentBound[0]: LatLngBounds.fromPoints([LatLng(35.03999881162295, 136.40587119778962), LatLng(36.642756778706904, 137.95226720406063)]),
// zoom: 1,
// maxZoom: 42,
// interactiveFlags: InteractiveFlag.pinchZoom | InteractiveFlag.drag,
// //plugins: [LocationMarkerPlugin(),]
// ),
// children: [
// TileLayerWidget(
// options: TileLayerOptions(
// urlTemplate: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
// subdomains: ['a', 'b', 'c'],
// ),
// ),
// //Obx(() =>
// indexController.routePoints.length > 0 ?
// PolylineLayerWidget(
// options: PolylineLayerOptions(
// polylines: [
// Polyline(
// points: getPoints(ptts)!,
// strokeWidth: 6.0,
// color: Colors.indigo
// ),
// ],
// ),
// )
// :
// Container(),
// //),
// // PopupMarkerLayerWidget(
// // options: PopupMarkerLayerOptions(
// // popupController: _popupLayerController,
// // markers: _markers,
// // markerRotateAlignment:
// // PopupMarkerLayerOptions.rotationAlignmentFor(AnchorAlign.top),
// // popupBuilder: (BuildContext context, Marker marker) =>
// // examplePopup(marker),
// // ),
// // ),
// LocationMarkerLayerWidget(),
// MarkerLayerWidget(
// options: MarkerLayerOptions(
// markers: getMarkers()!
// ),
// ),
// ],
// );
// }
//}

View File

@ -1,128 +1,274 @@
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/services/auth_service.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/services/auth_service.dart';
import 'package:gifunavi/utils/database_helper.dart';
import 'package:gifunavi/widgets/debug_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gifunavi/pages/WebView/WebView_page.dart';
// SafeAreaウィジェットを使用して、画面の安全領域内にメニューを表示しています。
// Columnウィジェットを使用して、メニューアイテムを縦に並べています。
//
class DrawerPage extends StatelessWidget {
DrawerPage({ Key? key }) : super(key: key);
DrawerPage({super.key});
final IndexController indexController = Get.find<IndexController>();
LogManager logManager = LogManager();
// 要検討URLの起動に失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。
//
/*
void _launchURL(url) async {
if (!await launch(url)) throw 'Could not launch $url';
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(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
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),),
),
)
),
child: Obx(() => Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: indexController.currentUser.isEmpty
? Flexible(
child: Text(
"drawer_title".tr,
style: const TextStyle(
color: Colors.black, fontSize: 20),
))
: Text(
indexController.currentUser[0]['user']['email'],
style: const TextStyle(
color: Colors.black, fontSize: 20),
),
),
)),
),
Obx(() =>
indexController.currentUser.isEmpty ?
ListTile(
leading: const Icon(Icons.login),
title: Text("login".tr),
onTap: (){
Get.toNamed(AppPages.LOGIN);
},
) :
ListTile(
leading: const Icon(Icons.login),
title: Text("logout".tr),
onTap: (){
indexController.logout();
Get.toNamed(AppPages.TRAVEL);
},
)
),
indexController.currentUser.isNotEmpty ?
ListTile(
leading: const Icon(Icons.password),
title: Text("change_password".tr),
onTap: (){
Get.toNamed(AppPages.CHANGE_PASSWORD);
leading: const Icon(Icons.group),
title: const Text('チーム管理'),
onTap: () async{
//Get.back();
// スナックバーを安全に閉じる
await _safelyCloseSnackbar();
Get.toNamed(AppPages.TEAM_LIST);
},
) :
const SizedBox(width: 0, height: 0,),
indexController.currentUser.isEmpty ?
),
ListTile(
leading: const Icon(Icons.event),
title: const Text('エントリー管理'),
onTap: () {
Get.back();
Get.toNamed(AppPages.ENTRY_LIST);
},
),
ListTile(
leading: const Icon(Icons.event),
title: const Text('イベント参加'),
onTap: () {
Get.back(); // ドロワーを閉じる
Get.toNamed(AppPages.EVENT_ENTRY);
},
),
ListTile(
leading: const Icon(Icons.person),
title: Text("sign_up".tr),
onTap: (){
Get.toNamed(AppPages.REGISTER);
title: const Text("個人情報の修正"),
onTap: () {
Get.back(); // Close the drawer
Get.toNamed(AppPages.USER_DETAILS_EDIT);
},
) :
const SizedBox(width: 0, height: 0,),
indexController.currentUser.isNotEmpty ?
ListTile(
leading: const Icon(Icons.delete_forever),
title: Text("delete_account".tr),
onTap: (){
String token = indexController.currentUser[0]['token'];
AuthService.deleteUser(token).then((value){
if(value.isNotEmpty){
indexController.logout();
Get.toNamed(AppPages.TRAVEL);
Get.snackbar("accounted_deleted".tr, "account_deleted_message".tr);
}
});
},
) :
const SizedBox(width: 0, height: 0,),
// ListTile(
// leading: const Icon(Icons.person),
// title: Text("profile".tr),
// onTap: (){},
// ),
// ListTile(
// leading: const Icon(Icons.route),
// title: Text("recommended_route".tr),
// onTap: (){},
// ),
// ListTile(
// leading: const Icon(Icons.favorite_rounded),
// title: Text("point_rank".tr),
// onTap: (){},
// ),
indexController.currentUser.isNotEmpty ?
ListTile(
leading: const Icon(Icons.featured_video),
title: Text("rog_web".tr),
onTap: (){
_launchURL("https://www.gifuai.net/?page_id=17397");
},
) :
const SizedBox(width: 0, height: 0,),
),
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<DestinationController>();
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("https://rogaining.sumasen.net/api/privacy/");
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),
@ -138,4 +284,14 @@ class DrawerPage extends StatelessWidget {
),
);
}
}
Future<void> _safelyCloseSnackbar() async {
if (Get.isSnackbarOpen) {
try {
await Get.closeCurrentSnackbar();
} catch (e) {
print('Error closing snackbar: $e');
}
}
}
}

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/services/api_service.dart';
class EntryBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiService>(() => ApiService());
Get.lazyPut<EntryController>(() => EntryController());
}
}

View File

@ -0,0 +1,310 @@
// lib/entry/entry_controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/model/entry.dart';
import 'package:gifunavi/model/event.dart';
import 'package:gifunavi/model/team.dart';
import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/services/api_service.dart';
import '../index/index_controller.dart';
import 'package:timezone/timezone.dart' as tz;
class EntryController extends GetxController {
late ApiService _apiService;
final entries = <Entry>[].obs;
final events = <Event>[].obs;
final teams = <Team>[].obs;
final categories = <NewCategory>[].obs;
final selectedEvent = Rx<Event?>(null);
final selectedTeam = Rx<Team?>(null);
final selectedCategory = Rx<NewCategory?>(null);
final selectedDate = Rx<DateTime?>(null);
final currentEntry = Rx<Entry?>(null);
final isLoading = true.obs;
@override
void onInit() async {
super.onInit();
await initializeApiService();
await loadInitialData();
}
Future<void> 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<void> 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;
if (value != null) {
selectedCategory.value = value.category;
}
}
//void updateTeam(Team? value) => selectedTeam.value = value;
void updateCategory(NewCategory? value) => selectedCategory.value = value;
//void updateDate(DateTime value) => selectedDate.value = value;
void updateDate(DateTime value) {
selectedDate.value = tz.TZDateTime.from(value, tz.getLocation('Asia/Tokyo'));
}
/*
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<void> 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<void> fetchEvents() async {
try {
final fetchedEvents = await _apiService.getEvents();
events.assignAll(fetchedEvents.map((event) {
// end_dateの7日前を締め切りとして設定
final deadlineDateTime = event.endDatetime.subtract(const Duration(days: 7));
return Event(
id: event.id,
eventName: event.eventName,
startDatetime: event.startDatetime,
endDatetime: event.endDatetime,
deadlineDateTime: deadlineDateTime,
);
}).toList());
} catch (e) {
print('Error fetching events: $e');
Get.snackbar('Error', 'Failed to fetch events');
}
}
Future<void> fetchEvents_old() 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<void> 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<void> 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<void> createEntry() async {
if (selectedEvent.value == null || selectedTeam.value == null ||
selectedCategory.value == null || selectedDate.value == null) {
Get.snackbar('Error', 'Please fill all fields');
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<void> updateEntryAndRefreshMap() async {
await updateEntry();
// エントリーが正常に更新された後、マップをリフレッシュ
final indexController = Get.find<IndexController>();
final eventCode = currentEntry.value?.event.eventName ?? '';
indexController.reloadMap(eventCode);
}
bool isEntryEditable(Event event) {
return DateTime.now().isBefore(event.deadlineDateTime);
}
Future<void> updateEntry() async {
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for update');
return;
}
if (!isEntryEditable(currentEntry.value!.event)) {
Get.dialog(
AlertDialog(
title: Text('エントリー変更不可'),
content: Text('締め切りを過ぎているため、エントリーの変更はできません。変更が必要な場合は事務局にお問い合わせください。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
return;
}
try {
isLoading.value = true;
final updatedEntry = await _apiService.updateEntry(
currentEntry.value!.id,
currentEntry.value!.team.id,
selectedEvent.value!.id,
selectedCategory.value!.id,
selectedDate.value!,
currentEntry.value!.zekkenNumber,
);
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');
} finally {
isLoading.value = false;
}
}
Future<void> updateEntryCategory(int entryId, int newCategoryId) async {
try {
//await _apiService.updateEntryCategory(entryId, newCategoryId);
final updatedEntry = await _apiService.updateEntry(
currentEntry.value!.id,
currentEntry.value!.team.id,
selectedEvent.value!.id,
newCategoryId,
currentEntry.value!.date!,
currentEntry.value!.zekkenNumber,
);
await fetchEntries();
} catch (e) {
print('Error updating entry category: $e');
Get.snackbar('エラー', 'エントリーのカテゴリ更新に失敗しました');
}
}
Future<void> deleteEntry() async {
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for deletion');
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;
}
}
bool isOwner() {
// Implement logic to check if the current user is the owner of the entry
return true; // Placeholder
}
}

View File

@ -0,0 +1,193 @@
// lib/pages/entry/entry_detail_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/model/event.dart';
import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/model/team.dart';
import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart' as tz;
class EntryDetailPage extends GetView<EntryController> {
const EntryDetailPage({super.key});
@override
Widget build(BuildContext context) {
final Map<String, dynamic> 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 const Center(child: CircularProgressIndicator());
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDropdown<Event>(
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,
),
const SizedBox(height: 16),
_buildDropdown<Team>(
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,
),
const SizedBox(height: 16),
_buildCategoryDropdown(),
/*
_buildDropdown<NewCategory>()
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,
),
*/
const SizedBox(height: 16),
ListTile(
title: const Text('日付'),
subtitle: Text(
controller.selectedDate.value != null
? DateFormat('yyyy-MM-dd').format(tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo')))
: '日付を選択してください',
),
onTap: () async {
if (controller.selectedEvent.value == null) {
Get.snackbar('Error', 'Please select an event first');
return;
}
final tz.TZDateTime now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventStart = tz.TZDateTime.from(controller.selectedEvent.value!.startDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventEnd = tz.TZDateTime.from(controller.selectedEvent.value!.endDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime initialDate = controller.selectedDate.value != null
? tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo'))
: (now.isAfter(eventStart) ? now : eventStart);
// 選択可能な最初の日付を設定(今日かイベント開始日のうち、より後の日付)
final tz.TZDateTime firstDate = now.isAfter(eventStart) ? now : eventStart;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate.isAfter(firstDate) ? initialDate : firstDate,
firstDate: firstDate,
lastDate: eventEnd,
);
if (picked != null) {
controller.updateDate(tz.TZDateTime.from(picked, tz.getLocation('Asia/Tokyo')));
}
},
),
const SizedBox(height: 32),
if (mode == 'new')
ElevatedButton(
onPressed: () => controller.createEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
),
child: const Text('エントリーを作成'),
)
else
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => controller.deleteEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: const Size(0, 50),
),
child: const Text('エントリーを削除'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => controller.updateEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.lightBlue,
foregroundColor: Colors.white,
minimumSize: const Size(0, 50),
),
child: const Text('エントリーを更新'),
),
),
],
),
],
),
),
);
}),
);
}
Widget _buildDropdown<T>({
required String label,
required List<T> items,
required int? selectedId,
required void Function(int?) onChanged,
required String Function(T) getDisplayName,
required int Function(T) getId,
}) {
return DropdownButtonFormField<int>(
decoration: InputDecoration(labelText: label),
value: selectedId,
items: items.map((item) => DropdownMenuItem<int>(
value: getId(item),
child: Text(getDisplayName(item)),
)).toList(),
onChanged: onChanged,
);
}
Widget _buildCategoryDropdown() {
final eligibleCategories = controller.categories.where((c) =>
c.baseCategory == controller.selectedCategory.value?.baseCategory
).toList();
return DropdownButtonFormField<NewCategory>(
decoration: InputDecoration(labelText: 'カテゴリ'),
value: controller.selectedCategory.value,
items: eligibleCategories.map((category) => DropdownMenuItem<NewCategory>(
value: category,
child: Text(category.categoryName),
)).toList(),
onChanged: (value) => controller.updateCategory(value),
);
}
}

View File

@ -0,0 +1,116 @@
// lib/pages/entry/entry_list_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:timezone/timezone.dart' as tz;
class EntryListPage extends GetView<EntryController> {
const EntryListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('エントリー管理'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx(() {
if (controller.entries.isEmpty) {
return const 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: const 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 '日時未設定';
}
final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo'));
return DateFormat('yyyy-MM-dd').format(jstDate);
}
}
class EntryListPage_old extends GetView<EntryController> {
const EntryListPage_old({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('エントリー管理'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx((){
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
// エントリーを日付昇順にソート
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}),
);
},
);
}),
);
}
}

View File

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import 'package:gifunavi/pages/entry/event_entries_controller.dart';
class EventEntriesBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<EventEntriesController>(() => EventEntriesController());
}
}

View File

@ -0,0 +1,129 @@
import 'package:get/get.dart';
import 'package:gifunavi/model/entry.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/services/api_service.dart';
import 'package:flutter/material.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
class EventEntriesController extends GetxController {
final ApiService _apiService = Get.find<ApiService>();
final IndexController _indexController = Get.find<IndexController>();
late final DestinationController _destinationController;
final entries = <Entry>[].obs;
final filteredEntries = <Entry>[].obs;
final showTodayEntries = true.obs;
static bool _timezoneInitialized = false;
@override
void onInit() {
super.onInit();
_initializeTimezone();
// DestinationControllerが登録されていない場合に備えて、lazyPutを使用
Get.lazyPut<DestinationController>(() => DestinationController(), fenix: true);
_destinationController = Get.find<DestinationController>();
fetchEntries();
}
void _initializeTimezone() {
if (!_timezoneInitialized) {
tz.initializeTimeZones();
_timezoneInitialized = true;
}
}
Future<void> fetchEntries() async {
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 = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
filteredEntries.assignAll(entries.where((entry) {
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
return entryDate.year == now.year &&
entryDate.month == now.month &&
entryDate.day == now.day;
}));
}
void filterEntriesForToday_old() {
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<void> joinEvent(Entry entry) async {
//final now = DateTime.now();
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
bool isToday = entryDate.year == now.year &&
entryDate.month == now.month &&
entryDate.day == now.day;
_indexController.setReferenceMode(!isToday);
_indexController.setSelectedEventName(entry.event.eventName);
final userid = _indexController.currentUser[0]["user"]["id"];
await _apiService.updateUserInfo(userid,entry);
_indexController.currentUser[0]["user"]["event_date"] = entryDate; // 追加2024-8-9
_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.zekkenNumber;
Get.back(); // エントリー一覧ページを閉じる
//_indexController.isLoading.value = true;
_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);
}
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:gifunavi/pages/entry/event_entries_controller.dart';
import 'package:timezone/timezone.dart' as tz;
class EventEntriesPage_old extends GetView<EventEntriesController> {
const EventEntriesPage_old({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const 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),
);
},
)),
);
}
}
class EventEntriesPage extends GetView<EventEntriesController> {
const EventEntriesPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(() => Text(controller.showTodayEntries.value ? 'イベント参加' : 'イベント参照')),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => Text(
controller.showTodayEntries.value ? '本日のエントリー' : 'すべてのエントリー',
style: const 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 const 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: const 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 '日時未設定';
}
final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo'));
return DateFormat('yyyy-MM-dd').format(jstDate);
}
}

159
lib/pages/gps/gps_page.dart Normal file
View File

@ -0,0 +1,159 @@
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:gifunavi/model/gps_data.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/utils/database_gps.dart';
import 'package:gifunavi/widgets/base_layer_widget.dart';
class GpsPage extends StatefulWidget {
const GpsPage({super.key});
@override
State<GpsPage> createState() => _GpsPageState();
}
class _GpsPageState extends State<GpsPage> {
var gpsData = [].obs;
MapController? mapController;
StreamSubscription? subscription;
final IndexController indexController = Get.find<IndexController>();
final DestinationController destinationController =
Get.find<DestinationController>();
@override
void initState() {
super.initState();
loadGpsData();
}
// 要検討GPSデータの読み込みに失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。
//
void loadGpsData() async {
final teamName = indexController.currentUser[0]["user"]['team_name'];
final eventCode = indexController.currentUser[0]["user"]["event_code"];
GpsDatabaseHelper db = GpsDatabaseHelper.instance;
var data = await db.getGPSData(teamName, eventCode);
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([
const LatLng(35.03999881162295, 136.40587119778962),
const 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(),
// )
],
),
)),
);
}
}

View File

@ -1,8 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:rogapp/model/destination.dart';
import 'package:rogapp/utils/database_helper.dart';
import 'package:gifunavi/model/destination.dart';
import 'package:gifunavi/utils/database_helper.dart';
import 'package:get/get.dart';
class HistoryPage extends StatefulWidget {
const HistoryPage({super.key});
@ -18,12 +19,14 @@ class _HistoryPageState extends State<HistoryPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("History"),
title: Text("pass_history".tr),
),
body: SingleChildScrollView(
child: Column(
children: [
FutureBuilder(
// 要検討:スナップショットのエラーハンドリングが行われていますが、具体的なエラーメッセージを表示するようにすることをお勧めします。
//
future: db.getDestinations(),
builder: (BuildContext context,
AsyncSnapshot<List<Destination>> snapshot) {
@ -32,28 +35,48 @@ class _HistoryPageState extends State<HistoryPage> {
return Center(
child: Text(
'${snapshot.error} occurred',
style: TextStyle(fontSize: 18),
style: const TextStyle(fontSize: 18),
),
);
} else if (snapshot.hasData) {
final dests = snapshot.data;
if (dests!.length > 0) {
return Center(
child: ListView.builder(itemBuilder:(ctx, index){
return ListTile(
title: Text(dests[index].name?? ""),
subtitle: Text(dests[index].address ?? ""),
leading: dests[0].photos != null ? Image.file(File(dests[0].photos!)) : Container(),
);
}),
);
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"));
return Center(child: Text("no_checkin_yet".tr));
}
}
}
else if(snapshot.connectionState == ConnectionState.waiting){
return Center(child: CircularProgressIndicator(),);
} else if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Container();
}),
@ -63,3 +86,69 @@ class _HistoryPageState extends State<HistoryPage> {
);
}
}
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
),
],
),
),
],
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/home/home_controller.dart';
import 'package:gifunavi/pages/home/home_controller.dart';
class HomeBinding extends Bindings{
@override

View File

@ -1,44 +1,82 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/search/search_page.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:gifunavi/routes/app_pages.dart';
class HomePage extends GetView{
const HomePage({Key? key}) : super(key: key);
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _isLocationServiceEnabled = true;
@override
void initState() {
super.initState();
/*
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkLocationService(); // 非同期的に呼び出す
});
*/
_checkLocationService();
}
Future<void> _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(
elevation: 0,
backgroundColor: Colors.white,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("app_title".tr,
style: const TextStyle(
color: Colors.blue
),
),
InkWell(
onTap: (){
Navigator.push(context, MaterialPageRoute(builder: (context) => SearchPage()));
},
child: Container(
height: 32,
width: 75,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(25),
),
child: const Center(child: Icon(Icons.search),),
),
),
],
),
title: Text('home'.tr),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('welcome'.tr),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _isLocationServiceEnabled
? () => Get.offNamed(AppPages.INDEX)
: () => _showLocationDisabledDialog(),
child: Text('start_app'.tr),
),
],
),
body: Container(),
),
);
}
}

View File

@ -1,17 +1,14 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/utils/location_controller.dart';
class IndexBinding extends Bindings {
IndexBinding(this.token);
String? token;
@override
void dependencies() {
final IndexController indexController = IndexController();
indexController.userToken = token;
Get.put<IndexController>(indexController);
Get.lazyPut<IndexController>(() => IndexController());
//Get.put<IndexController>(IndexController());
Get.put<LocationController>(LocationController());
Get.put<DestinationController>(DestinationController());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,147 @@
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:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/drawer/drawer_page.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/widgets/list_widget.dart';
import 'package:gifunavi/widgets/map_widget.dart';
import 'package:gifunavi/utils/location_controller.dart';
class IndexPage extends GetView<IndexController> {
IndexPage({Key? key}) : super(key: key);
// index_page.dartファイルの主な内容です。
// このファイルは、アプリのメインページのUIを構築し、各機能へのナビゲーションを提供しています。
// また、IndexControllerとDestinationControllerを使用して、状態管理と各種機能の実装を行っています。
//
// MapWidgetとListWidgetは、それぞれ別のファイルで定義されているウィジェットであり、マップモードとリストモードの表示を担当しています。
//
// 全体的に、index_page.dartはアプリのメインページの構造を定義し、他のコンポーネントやページへの橋渡しを行っているファイルです。
//
// 要検討GPSデータの表示アイコンをタップした際のエラーハンドリングを追加することをお勧めします。
// MapWidgetとListWidgetの切り替えにObxを使用していますが、パフォーマンスを考慮して、必要な場合にのみウィジェットを再構築するようにしてください。
// DestinationControllerのisSimulationModeを使用してGPS信号の強弱をシミュレーションしていますが、本番環境では適切に実際のGPS信号を使用するようにしてください。
// IndexPageクラスは、GetView<IndexController>を継承したStatelessWidgetです。このクラスは、アプリのメインページを表すウィジェットです。
//
class IndexPage extends StatefulWidget {
const IndexPage({super.key});
@override
_IndexPageState createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
//checkLoginAndShowDialog();
});
}
void checkLoginAndShowDialog() {
if (indexController.currentUser.isEmpty) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('ログインが必要です'),
content: const Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('1) ログインされていません。ロゲに参加するにはログインが必要です。'),
SizedBox(height: 10),
Text('2) ログイン後、個人情報入力、チーム登録、エントリー登録を行なってください。'),
SizedBox(height: 10),
Text('3) エントリー登録は場所と日にちごとに行なってください。'),
],
),
actions: [
TextButton(
child: const Text('キャンセル'),
onPressed: () {
Navigator.of(context).pop();
},
),
ElevatedButton(
child: const Text('ログイン'),
onPressed: () {
Navigator.of(context).pop();
Get.toNamed(AppPages.LOGIN);
},
),
],
);
},
);
}
}
// class IndexPage extends GetView<IndexController> {
// IndexPage({Key? key}) : super(key: key);
// IndexControllerとDestinationControllerのインスタンスを取得しています。
//
final LocationController locationController = Get.find<LocationController>();
final IndexController indexController = Get.find<IndexController>();
final DestinationController destinationController =
Get.find<DestinationController>();
// buildメソッドは、ウィジェットのUIを構築するメソッドです。
// ここでは、WillPopScopeウィジェットを使用して、端末の戻るボタンが押された際の動作を制御しています。
//
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
indexController.switchPage(AppPages.INITIAL);
return false;
},
return PopScope(
canPop: false,
child: Scaffold(
//
// Scaffoldウィジェットを使用して、アプリのメインページのレイアウトを構築しています。
//
drawer: DrawerPage(),
appBar: AppBar(
// leading: IconButton(
// icon: const Icon(Icons.arrow_back_ios),
// onPressed: (){
// indexController.switchPage(AppPages.TRAVEL);
// },
// ),
//automaticallyImplyLeading: false,
title: Text("add_location".tr),
title: Obx(() => Text(indexController.selectedEventName.value)),
//title: Text("add_location".tr),
actions: [
// IconButton(
// onPressed: () {
// DatabaseService ds = DatabaseService();
// ds.updateDatabase();
// },
// icon: const Icon(Icons.ten_k_sharp)),
//
// AppBarには、タイトルとアクションアイコンが含まれています。
// アクションアイコンには、GPSデータの表示、履歴の表示、マップの更新、検索などの機能が含まれています。
//
IconButton(
onPressed: () async {
// GpsDatabaseHelper db = GpsDatabaseHelper.instance;
// List<GpsData> data = await db.getGPSData(
// indexController.currentUser[0]["user"]['team_name'],
// indexController.currentUser[0]["user"]["event_code"]);
// print("GPS data is ${data.length}");
Get.toNamed(AppPages.GPS);
},
icon: const Icon(Icons.telegram)),
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);
@ -41,6 +150,7 @@ class IndexPage extends GetView<IndexController> {
height: 32,
width: 75,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(25),
),
child: const Center(
@ -48,125 +158,129 @@ class IndexPage extends GetView<IndexController> {
),
),
),
IconButton(onPressed: () {
Get.toNamed(AppPages.HISTORY);
}, icon: const Icon(Icons.history))
//CatWidget(indexController: indexController,),
//
// デバッグ時のみリロードボタンの横にGPS信号レベルの設定ボタンを設置し、
// タップすることでGPS信号の強弱をシミュレーションできるようにする
// Akira 2024-4-5
//
/*
Obx(() {
if (locationController.isSimulationMode) {
return DropdownButton<String>(
value: locationController.getSimulatedSignalStrength(),
onChanged: (value) {
//debugPrint("DropDown changed!");
locationController.setSimulatedSignalStrength(value!);
},
items: ['low', 'medium', 'high', 'real']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
} else {
return Container();
}
}),
*/
],
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding:
const EdgeInsets.only(right: 10.0, top: 4.0, bottom: 4.0),
child: InkWell(
child:
Obx(() => destinationController.is_gps_selected == 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);
},
),
))),
),
],
),
),
// bottomNavigationBar: BottomAppBar(
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: <Widget>[
// Obx(
// () => destinationController.isInRog.value == true
// ? IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.run_circle,
// size: 44,
// color: Colors.green,
// ))
// : IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.run_circle,
// size: 44,
// color: Colors.black12,
// )),
// ),
// Padding(
// padding:
// const EdgeInsets.only(right: 10.0, top: 4.0, bottom: 4.0),
// child: InkWell(
// child: Obx(() => destinationController
// .isGpsSelected.value ==
// true
// ? Padding(
// padding: const EdgeInsets.only(
// right: 10.0, top: 4.0, bottom: 4.0),
// child: InkWell(
// child: const Image(
// image:
// AssetImage('assets/images/route3_off.png'),
// width: 35,
// height: 35,
// ),
// onTap: () {
// //indexController.switchPage(AppPages.TRAVEL);
// },
// ),
// )
// : Padding(
// padding: const EdgeInsets.only(
// right: 10.0, top: 4.0, bottom: 4.0),
// child: InkWell(
// child: const Image(
// image:
// AssetImage('assets/images/route2_on.png'),
// width: 35,
// height: 35,
// ),
// onTap: () {
// //indexController.switchPage(AppPages.TRAVEL);
// },
// ),
// ))),
// ),
// ],
// ),
// ),
//
// マップモードとリストモードを切り替えるためのボタンです。
//
floatingActionButton: FloatingActionButton(
onPressed: () {
indexController.toggleMode();
if (indexController.currentCat.isNotEmpty) {
print(indexController.currentCat[0].toString());
}
},
tooltip: 'Increment',
elevation: 4.0,
elevation: 1.0,
//
// Obxウィジェットを使用して、indexController.mode.valueの値に基づいて、MapWidgetまたはListWidgetを表示しています。
//
child: Obx(
() => indexController.mode == 0
() => 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: [
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 8.0),
// alignment: Alignment.centerLeft,
// height: 50.0,
// //child: SingleChildScrollView(
// // scrollDirection: Axis.horizontal,
// // child:Row(
// // mainAxisAlignment: MainAxisAlignment.start,
// // children: [
// // TextButton(child:Text("Main Pef >", style: TextStyle(fontSize:16.0, fontWeight: FontWeight.bold),), onPressed: (){Get.toNamed(AppPages.MAINPERF);},),
// // TextButton(child:Text("Sub Pef >", style: TextStyle(fontSize:16.0, fontWeight: FontWeight.bold),), onPressed: (){Get.toNamed(AppPages.SUBPERF);},),
// // TextButton(child:Text("Cities >", style: TextStyle(fontSize:16.0, fontWeight: FontWeight.bold),), onPressed: (){Get.toNamed(AppPages.CITY);},),
// // TextButton(child:Text("Categories", style: TextStyle(fontSize:16.0, fontWeight: FontWeight.bold),), onPressed: (){Get.toNamed(AppPages.CATEGORY);},),
// // ],
// // )
// // ),
// child: SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Obx(() =>
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// indexController.is_mapController_loaded.value == false ?
// Center(child: CircularProgressIndicator())
// :
// BreadCrumbWidget(mapController: indexController.mapController),
// Container(width: 24.0,),
// // Row(
// // children: [
// // indexController.currentCat.isNotEmpty ? Text(indexController.currentCat[0].toString()): Text(""),
// // indexController.currentCat.isNotEmpty ?
// // IconButton(
// // onPressed: (){
// // indexController.currentCat.clear();
// // indexController.loadLocationsBound();
// // },
// // icon: Icon(Icons.cancel, color: Colors.red,)
// // ) :
// // Container(width: 0, height: 0,)
// // ],
// // )
// ],
// )
// ),
// ),
// ),
Expanded(
child: Obx(
() => indexController.mode == 0 ? MapWidget() : ListWidget(),
() => indexController.mode.value == 0
? const MapWidget()
: const ListWidget(),
))
],
),

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:gifunavi/routes/app_pages.dart';
// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。
// ボタンのテキストをローカライズすることを検討してください。
//
class LandingPage extends StatefulWidget {
const LandingPage({ Key? key }) : super(key: key);
const LandingPage({ super.key });
@override
State<LandingPage> createState() => _LandingPageState();

View File

@ -1,16 +1,17 @@
import 'package:flutter/material.dart';
class LoadingPage extends StatelessWidget {
const LoadingPage({ Key? key }) : super(key: key);
const LoadingPage({super.key});
// 要検討ローディングインジケーターの値を固定値0.8)にしていますが、実際のローディング進捗に合わせて動的に変更することを検討してください。
//
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
margin: const EdgeInsets.only(top: 20),
child: const CircularProgressIndicator(
value: 0.8,
)
);
alignment: Alignment.center,
margin: const EdgeInsets.only(top: 20),
child: const CircularProgressIndicator(
value: 0.8,
));
}
}
}

View File

@ -1,224 +1,398 @@
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:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/widgets/helper_dialog.dart';
import 'package:gifunavi/services/api_service.dart';
class LoginPage extends StatelessWidget {
import 'package:package_info_plus/package_info_plus.dart';
// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。
// エラーメッセージをローカライズすることを検討してください。
// ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。
//
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
//class LoginPage extends StatelessWidget {
final IndexController indexController = Get.find<IndexController>();
final ApiService apiService = Get.find<ApiService>();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool _obscureText = true;
String _version = ''; // バージョン情報を保持する変数
LoginPage({Key? key}) : super(key: key);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。',
'login_page'
);
});
_getVersionInfo(); // バージョン情報を取得
}
// バージョン情報を取得するメソッド
Future<void> _getVersionInfo() async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
_version = packageInfo.version;
});
}
void _showResetPasswordDialog() {
TextEditingController resetEmailController = TextEditingController();
Get.dialog(
AlertDialog(
title: const Text('パスワードのリセット'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('パスワードをリセットするメールアドレスを入力してください。'),
const SizedBox(height: 10),
TextField(
controller: resetEmailController,
decoration: const InputDecoration(
labelText: 'メールアドレス',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
child: const Text('キャンセル'),
onPressed: () => Get.back(),
),
ElevatedButton(
child: const Text('リセット'),
onPressed: () async {
if (resetEmailController.text.isNotEmpty) {
bool success = await apiService.resetPassword(resetEmailController.text);
Get.back();
if (success) {
Get.dialog(
AlertDialog(
title: const Text('パスワードリセット'),
content: const Text('パスワードリセットメールを送信しました。メールのリンクからパスワードを設定してください。'),
actions: [
TextButton(
child: const 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,
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/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(
automaticallyImplyLeading: false,
),
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: [
makeInput(label: "email".tr, controller: emailController),
makeInput(label: "password".tr, controller: passwordController, obsureText: true),
Container(
height: MediaQuery.of(context).size.height / 6,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
'assets/images/login_image.jpg'))),
),
const SizedBox(
height: 5,
),
// バージョン情報を表示
Text(
'Version: $_version',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
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.is_loading == 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: (){
if(emailController.text.isEmpty || passwordController.text.isEmpty){
Get.snackbar(
"no_values".tr,
"email_and_password_required".tr,
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.is_loading.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: 5.0,),
MaterialButton(
minWidth: double.infinity,
height:40,
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:40,
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
),
),
),
],
)
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;
});
}),
],
),
),
)
),
const SizedBox(height: 5,),
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,
),),
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: const 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
),),
],
),
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),
),
),
),
),
],
)
],
),
],
),
):
Container(
child: TextButton(
onPressed: (){
indexController.currentUser.clear();
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"※第8回と第9回は、岐阜県の令和年度「清流の国ぎふ」SDGs推進ネットワーク連携促進補助金を受けています",
style: TextStyle(
fontSize: 10.0,
),
),
),
),
],
),
],
),
],
),
)
: TextButton(
onPressed: () {
indexController.logout();
Get.offAllNamed(AppPages.LOGIN);
},
child: const Text("Already Logged in, Click to logout"),
),
)
,
),
);
}
}
Widget makeInput({label, required TextEditingController controller, obsureText = false}){
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,),
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),
contentPadding:
const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: (Colors.grey[400])!,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: (Colors.grey[400])!
borderSide: BorderSide(color: (Colors.grey[400])!),
),
),
),
),
const SizedBox(height: 30.0,)
const SizedBox(
height: 30.0,
)
],
);
}

View File

@ -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<LoginPage> {
//class LoginPage extends StatelessWidget {
final IndexController indexController = Get.find<IndexController>();
final ApiService apiService = Get.find<ApiService>();
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回は、岐阜県の令和年度「清流の国ぎふ」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,
)
],
);
}

View File

@ -1,10 +1,14 @@
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:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。
// エラーメッセージをローカライズすることを検討してください。
// ポップアップを閉じるボタンを追加することを検討してください。
//
class LoginPopupPage extends StatelessWidget {
LoginPopupPage({Key? key}) : super(key: key);
LoginPopupPage({super.key});
final IndexController indexController = Get.find<IndexController>();
@ -15,195 +19,235 @@ class LoginPopupPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0,
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(
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: [
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.is_loading == 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,
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.is_loading.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
),
),
)
Container(
height: MediaQuery.of(context).size.height / 5,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
'assets/images/login_image.jpg'))),
),
const SizedBox(
height: 5,
),
],
)
),
),
)
),
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,
),),
),
),
],
)
],
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.logout();
Get.offAllNamed(AppPages.LOGIN);
},
child: const Text("Already Logged in, Click to logout"),
),
],
),
):
Container(
child: TextButton(
onPressed: (){
indexController.currentUser.clear();
},
child: const Text("Already Logged in, Click to logout"),
),
)
,
);
}
}
Widget makeInput({label, required TextEditingController controller, obsureText = false}){
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,),
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),
contentPadding:
const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: (Colors.grey[400])!,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: (Colors.grey[400])!
borderSide: BorderSide(color: (Colors.grey[400])!),
),
),
),
),
const SizedBox(height: 30.0,)
const SizedBox(
height: 30.0,
)
],
);
}

View File

@ -1,31 +1,31 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
// 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);
// class MainPerfPage extends StatelessWidget {
// MainPerfPage({Key? key}) : super(key: key);
IndexController indexController = Get.find<IndexController>();
// IndexController indexController = Get.find<IndexController>();
@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()),
);
},
),
);
}
}
// @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()),
// );
// },
// ),
// );
// }
// }

View File

@ -1,154 +1,311 @@
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/routes/app_pages.dart';
import 'dart:async';
class PermissionHandlerScreen extends StatefulWidget {
const PermissionHandlerScreen({Key? key}) : super(key: key);
@override
State<PermissionHandlerScreen> createState() => _PermissionHandlerScreenState();
}
class PermissionController {
class _PermissionHandlerScreenState extends State<PermissionHandlerScreen> {
static bool _isRequestingPermission = false;
static Completer<bool>? _permissionCompleter;
Future<void> _showMyDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: const Text('ロケーション許可'),
content: const SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text( 'このアプリでは、位置情報の収集を行います。'),
Text( 'このアプリでは、開始時点で位置情報を収集します。'),
],
),
),
actions: <Widget>[
TextButton(
child: const Text('わかった'),
onPressed: () {
//Navigator.of(context).pop();
Get.toNamed(AppPages.TRAVEL);
},
),
],
);
},
);
static Future<bool> 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);
}
@override
void initState() {
// TODO: implement initState
super.initState();
//permissionServiceCall();
}
static Future<bool> checkAndRequestPermissions() async {
if (_isRequestingPermission) {
return _permissionCompleter!.future;
}
Future<PermissionStatus> checkLocationPermission() async {
return await Permission.location.status;
}
_isRequestingPermission = true;
_permissionCompleter = Completer<bool>();
permissionServiceCall() async {
await permissionServices().then(
(value) {
if (value[Permission.location]!.isGranted ) {
/* ========= New Screen Added ============= */
Get.toNamed(AppPages.TRAVEL);
// Navigator.pushReplacement(
// context,
// MaterialPageRoute(builder: (context) => SplashScreen()),
// );
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{
_showMyDialog();
}
},
);
}
/*Permission services*/
Future<Map<Permission, PermissionStatus>> permissionServices() async {
// You can request multiple permissions at once.
Map<Permission, PermissionStatus> statuses = await [
Permission.location,
//add more permission to request here.
].request();
if (statuses[Permission.location]!.isPermanentlyDenied) {
await openAppSettings().then(
(value) async {
if (value) {
if (await Permission.location.status.isPermanentlyDenied == true &&
await Permission.location.status.isGranted == false) {
// openAppSettings();
permissionServiceCall(); /* opens app settings until permission is granted */
}
}
},
);
} else {
if (statuses[Permission.location]!.isDenied) {
permissionServiceCall();
} else {
print('User did not agree to location usage');
hasPermissions = false;
// アプリを終了
SystemNavigator.pop();
}
}
/*{Permission.camera: PermissionStatus.granted, Permission.storage: PermissionStatus.granted}*/
return statuses;
_isRequestingPermission = false;
_permissionCompleter!.complete(hasPermissions);
return _permissionCompleter!.future;
}
static Future<void> requestAllLocationPermissions() async {
await Permission.location.request();
await Permission.locationWhenInUse.request();
await Permission.locationAlways.request();
@override
Widget build(BuildContext context) {
var status = Permission.location.status.then((value){
if(value.isGranted == false){
Future.delayed(Duration.zero, () => showAlert(context));
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}'.");
}
else {
Get.toNamed(AppPages.TRAVEL);
}
});
return Scaffold(
body: Container(
child: const Text(""),
),
);
}
}
void showAlert(BuildContext context) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('ロケーション許可'),
content: const SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text( 'このアプリでは、位置情報の収集を行います。'),
Text('岐阜ナビアプリではチェックポイントの自動チェックインの機能を可能にするために、現在地のデータが収集されます。アプリを閉じている時や、使用していないときにも収集されます。位置情報は、個人を特定できない統計的な情報として、ユーザーの個人情報とは一切結びつかない形で送信されます。お知らせの配信、位置情報の利用を許可しない場合は、この後表示されるダイアログで「許可しない」を選択してください。'),
],
),
static Future<bool> showLocationDisclosure() async {
return await Get.dialog<bool>(
AlertDialog(
title: const Text('位置情報の使用について'),
content: const SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('このアプリでは、以下の目的で位置情報を使用します:'),
Text('• チェックポイントの自動チェックイン(アプリが閉じているときも含む)'),
Text('• 移動履歴の記録(バックグラウンドでも継続)'),
Text('• 現在地周辺の情報表示'),
Text('\nバックグラウンドでも位置情報を継続的に取得します。'),
Text('これにより、バッテリーの消費が増加する可能性があります。'),
Text('同意しない場合には、アプリは終了します。'),
],
),
actions: <Widget>[
TextButton(
child: const Text('わかった'),
onPressed: () {
permissionServiceCall();
},
),
],
)
),
actions: <Widget>[
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: const Text('キャンセル'),
onPressed: () => Get.back(),
),
TextButton(
child: const Text('設定'),
onPressed: () {
Get.back();
openAppSettings();
},
),
],
),
);
}
/*
static Future<bool> 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>();
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<bool> checkStoragePermission() async {
//debugPrint("(gifunavi)== checkStoragePermission ==");
final storagePermission = await Permission.storage.status;
return storagePermission == PermissionStatus.granted;
}
static Future<void> requestStoragePermission() async {
//debugPrint("(gifunavi)== requestStoragePermission ==");
final storagePermission = await Permission.storage.request();
if (storagePermission == PermissionStatus.granted) {
return;
}
if (storagePermission == PermissionStatus.permanentlyDenied) {
// リクエストが完了するまで待機
await Future.delayed(const Duration(milliseconds: 500));
showPermissionDeniedDialog('storage_permission_needed_title','storage_permission_needed_main');
}
}
/*
static Future<bool> checkLocationBasicPermission() async {
//debugPrint("(gifunavi)== checkLocationBasicPermission ==");
final locationPermission = await Permission.location.status;
return locationPermission == PermissionStatus.granted;
}
static Future<bool> checkLocationWhenInUsePermission() async {
//debugPrint("(gifunavi)== checkLocationWhenInUsePermission ==");
final whenInUsePermission = await Permission.locationWhenInUse.status;
return whenInUsePermission == PermissionStatus.granted;
}
static Future<bool> checkLocationAlwaysPermission() async {
//debugPrint("(gifunavi)== checkLocationAlwaysPermission ==");
final alwaysPermission = await Permission.locationAlways.status;
return alwaysPermission == PermissionStatus.granted;
}
static bool isBasicPermission=false;
static Future<void> 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<void> 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<void> 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<void> 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();
}
}
*/
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
class ProgressPage extends StatelessWidget {
const ProgressPage({Key? key}) : super(key: key);
const ProgressPage({super.key});
@override
Widget build(BuildContext context) {

View File

@ -1,143 +1,114 @@
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:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/widgets/helper_dialog.dart';
class RegisterPage extends StatelessWidget {
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@override
_RegisterPageState createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final IndexController indexController = Get.find<IndexController>();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController confirmPasswordController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController = TextEditingController();
RegisterPage({Key? key}) : super(key: key);
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'登録メールにアクティベーションメールが送信されます。メールにあるリンクをタップすると正式登録になります。',
'register_page'
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
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,)),
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black),
),
),
body: SafeArea(
child: SingleChildScrollView(
child: SizedBox(
child: Container(
height: MediaQuery.of(context).size.height,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text ("サインアップ", style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),),
const SizedBox(height: 20,),
Text("アカウントを作成し、無料です",style: TextStyle(
fontSize: 15,
color: Colors.grey[700],
),),
const SizedBox(height: 30,)
],
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 40
),
child: Column(
children: [
makeInput(label: "Eメール", controller: emailController),
makeInput(label: "パスワード", controller: passwordController,obsureText: true),
makeInput(label: "パスワードを認証する", controller: confirmPasswordController,obsureText: true)
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Container(
padding: const EdgeInsets.only(top: 3,left: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
border: const Border(
bottom: BorderSide(color: Colors.black),
top: BorderSide(color: Colors.black),
right: BorderSide(color: Colors.black),
left: BorderSide(color: Colors.black)
)
),
child: MaterialButton(
minWidth: double.infinity,
height:60,
onPressed: (){
if(passwordController.text != confirmPasswordController.text){
Get.snackbar(
"No match",
"Passwords does not match",
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"))
);
}
if(emailController.text.isEmpty || passwordController.text.isEmpty){
Get.snackbar(
"no_values".tr,
"email_and_password_required".tr,
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.is_loading.value = true;
indexController.register(emailController.text, passwordController.text, context);
},
color: Colors.redAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40)
),
child: Text("sign_up".tr,style: const TextStyle(
fontWeight: FontWeight.w600,fontSize: 16,
),),
),
),
),
const SizedBox(height: 20,),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Flexible(child: Text("すでにアカウントをお持ちですか?")),
TextButton(
onPressed: (){
Get.toNamed(AppPages.LOGIN);
},
child: const Text("ログイン",style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18
),),
),
],
)
],
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),
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,
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
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)),
),
],
)
],
),
),
@ -145,36 +116,96 @@ class RegisterPage extends StatelessWidget {
),
);
}
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);
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,
confirmPasswordController.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({label, required TextEditingController controller, obsureText = false}){
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,),
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])!
),
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,)
const SizedBox(height: 20),
],
);
}

View File

@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/model/user.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/services/api_service.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:intl/intl.dart';
import 'package:gifunavi/widgets/custom_date_picker.dart'; // 追加: 日付フォーマット用
class UserDetailsEditPage extends StatefulWidget {
const UserDetailsEditPage({super.key});
@override
_UserDetailsEditPageState createState() => _UserDetailsEditPageState();
}
class _UserDetailsEditPageState extends State<UserDetailsEditPage> {
final _formKey = GlobalKey<FormState>();
final IndexController indexController = Get.find<IndexController>();
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: const Text('個人情報の修正'),
automaticallyImplyLeading: false,
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
TextFormField(
controller: _lastnameController,
decoration: const InputDecoration(
labelText: '',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '姓を入力してください';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _firstnameController,
decoration: const InputDecoration(
labelText: '',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '名を入力してください';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _dateOfBirthController,
decoration: InputDecoration(
labelText: '生年月日 (YYYY/MM/DD)',
border: const OutlineInputBorder(),
hintText: 'YYYY/MM/DD',
suffixIcon: IconButton(
icon: const Icon(Icons.calendar_today),
onPressed: () => _selectDate(context),
),
),
keyboardType: TextInputType.number, // <=datetime,
onChanged: (value) {
// スラッシュを除去
value = value.replaceAll('/', '');
if (value.length <= 8) {
String formattedValue = '';
// 年の処理4桁
if (value.length >= 4) {
formattedValue += '${value.substring(0, 4)}/';
value = value.substring(4);
} else {
formattedValue += value;
value = '';
}
// 月の処理2桁
if (value.length >= 2) {
formattedValue += '${value.substring(0, 2)}/';
value = value.substring(2);
} else if (value.isNotEmpty) {
formattedValue += value;
value = '';
}
// 残りの日付
formattedValue += value;
// 末尾のスラッシュを削除
if (formattedValue.endsWith('/')) {
formattedValue = formattedValue.substring(0, formattedValue.length - 1);
}
_dateOfBirthController.value = TextEditingValue(
text: formattedValue,
selection: TextSelection.collapsed(offset: formattedValue.length),
);
}
},
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;
},
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('性別'),
subtitle: Text(_female ? '女性' : '男性'),
value: _female,
onChanged: (bool value) {
setState(() {
_female = value;
});
},
),
const SizedBox(height: 16),
TextFormField(
initialValue: _user.email,
decoration: const InputDecoration(
labelText: 'メールアドレス',
border: OutlineInputBorder(),
),
enabled: false,
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('アクティブ状態'),
value: _user.isActive,
onChanged: null,
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _updateUserDetails,
child: const Text('更新'),
),
],
),
),
);
}
// 日付選択用の関数を追加
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDialog<DateTime>(
context: context,
builder: (BuildContext context) {
return CustomDatePicker(
initialDate: _user.dateOfBirth ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
currentDateText: _dateOfBirthController.text,
);
},
);
if (picked != null) {
setState(() {
_dateOfBirthController.text = DateFormat('yyyy/MM/dd').format(picked);
});
}
}
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(
const SnackBar(content: Text('生年月日が無効です', style: TextStyle(color: Colors.red))),
);
return;
}
// 13歳以上かどうかをチェック
final now = DateTime.now();
final age = now.year - dateOfBirth.year -
(now.month > dateOfBirth.month ||
(now.month == dateOfBirth.month && now.day >= dateOfBirth.day) ? 0 : 1);
if (age < 13) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('13歳未満の方は登録できません', 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(
const SnackBar(content: Text('個人情報が更新されました')),
);
indexController.updateCurrentUser(updatedUser);
Get.offAllNamed(AppPages.INDEX);
//Get.back();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('更新に失敗しました', style: TextStyle(color: Colors.red))),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラーが発生しました: $e', style: const TextStyle(color: Colors.red))),
);
}
}
}
}

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/search/search_controller.dart';
import 'package:gifunavi/pages/search/search_controller.dart';
class SearchBinding extends Bindings {
@override

View File

@ -1,23 +1,21 @@
import 'package:geojson/geojson.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
class SearchBarController extends GetxController {
List<GeoJSONFeature> searchResults = <GeoJSONFeature>[].obs;
List<GeoJsonFeature> searchResults = <GeoJsonFeature>[].obs;
@override
@override
void onInit() {
IndexController indexController = Get.find<IndexController>();
if(indexController.locations.isNotEmpty){
for(int i=0; i<= indexController.locations[0].collection.length - 1; i++){
GeoJsonFeature p = indexController.locations[0].collection[i];
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();
}
}
}

View File

@ -1,28 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:geojson/geojson.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/pages/search/search_controller.dart';
import 'package:rogapp/widgets/bottom_sheet_new.dart';
import 'package:gifunavi/model/destination.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/pages/search/search_controller.dart';
//import 'package:gifunavi/widgets/bottom_sheets/bottom_sheet_start.dart';
//import 'package:gifunavi/widgets/bottom_sheets/bottom_sheet_goal.dart';
//import 'package:gifunavi/widgets/bottom_sheets/bottom_sheet_normal_point.dart';
import 'package:gifunavi/widgets/bottom_sheet_new.dart';
class SearchPage extends StatelessWidget {
SearchPage({Key? key}) : super(key: key);
SearchPage({super.key});
SearchBarController searchController = Get.find<SearchBarController>();
IndexController indexController = Get.find<IndexController>();
Image getImage(int index){
if(searchController.searchResults[index].properties!["photos"] == null || searchController.searchResults[index].properties!["photos"] == ""){
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{
} else {
return Image(
image: NetworkImage(searchController.searchResults[index].properties!["photos"]),
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
image: NetworkImage(
searchController.searchResults[index].properties!["photos"]),
errorBuilder:
(BuildContext context, Object exception, StackTrace? stackTrace) {
return Image.asset("assets/images/empty_image.png");
},
);
);
}
}
@ -33,64 +40,84 @@ class SearchPage extends StatelessWidget {
elevation: 0,
backgroundColor: Colors.white,
leading: IconButton(
onPressed:(){
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black,)),
title: TypeAheadField(
textFieldConfiguration: const TextFieldConfiguration(
autofocus: true,
),
suggestionsCallback: (pattern) async{
return searchController.searchResults.where((GeoJsonFeature element) => element.properties!["location_name"].toString().contains(pattern));
//return await
onPressed: () {
Get.back();
},
icon: const Icon(
Icons.arrow_back_ios_new,
color: Colors.black,
)),
centerTitle: true,
//title: const CupertinoSearchTextField(),
),
body: SingleChildScrollView(
child: TypeAheadField<GeoJSONFeature>(
// 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<DestinationController>();
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)
);
},
itemBuilder: (context, GeoJsonFeature suggestion){
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(""),
subtitle: suggestion.properties!["category"] != null
? Text(suggestion.properties!["category"])
: const Text(""),
//leading: getImage(index),
);
},
onSuggestionSelected: (GeoJsonFeature suggestion){
indexController.currentFeature.clear();
indexController.currentFeature.add(suggestion);
Get.back();
showModalBottomSheet(
isScrollControlled: true,
context: context,
//builder: (context) => BottomSheetWidget(),
builder:((context) => BottomSheetNew())
);
},
),
//title: const CupertinoSearchTextField(),
),
//body:
// Obx(() =>
// ListView.builder(
// itemCount: searchController.searchResults.length,
// itemBuilder: (context, index){
// return ListTile(
// title: searchController.searchResults[index].properties!["location_name"] != null ? Text(searchController.searchResults[index].properties!["location_name"]) : Text(""),
// subtitle: searchController.searchResults[index].properties!["category"] != null ? Text(searchController.searchResults[index].properties!["category"]) : Text(""),
// leading: getImage(index),
// onTap: (){
// indexController.currentFeature.clear();
// indexController.currentFeature.add(searchController.searchResults[index]);
// Get.back();
// showModalBottomSheet(
// isScrollControlled: true,
// context: context,
// //builder: (context) => BottomSheetWidget(),
// builder:((context) => BottomSheetNew())
// );
// },
// );
// },
// ),
// )
);
}
}
}

View File

@ -0,0 +1,12 @@
// lib/pages/settings/settings_binding.dart
import 'package:get/get.dart';
import 'package:gifunavi/pages/settings/settings_controller.dart';
class SettingsBinding extends Bindings {
@override
void dependencies() {
Get.put<SettingsController>(SettingsController()); // これを修正
//Get.lazyPut<SettingsController>(() => SettingsController());
}
}

View File

@ -0,0 +1,26 @@
// lib/pages/settings/settings_controller.dart
import 'package:get/get.dart';
import 'package:gifunavi/widgets/map_widget.dart';
class SettingsController extends GetxController {
var timerDuration = const 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!();
}
}

View File

@ -0,0 +1,63 @@
// lib/pages/settings/settings_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/settings/settings_controller.dart';
class SettingsPage extends GetView<SettingsController> {
const SettingsPage({super.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: const 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!);
},
),
),
],
),
),
);
}
}

View File

@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
class SubPerfPage extends StatelessWidget {
SubPerfPage({Key? key}) : super(key: key);
SubPerfPage({super.key});
IndexController indexController = Get.find<IndexController>();
@override
Widget build(BuildContext context) {
debugPrint("SubPerfPage ---->");
return Scaffold(
appBar: AppBar(
title: const Text("Select Sub Perfecture"),

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:gifunavi/pages/team/member_controller.dart';
import 'package:gifunavi/services/api_service.dart';
class MemberBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiService>(() => ApiService());
Get.lazyPut<MemberController>(() => MemberController());
}
}

View File

@ -0,0 +1,270 @@
// lib/controllers/member_controller.dart
import 'package:get/get.dart';
import 'package:gifunavi/model/user.dart';
import 'package:gifunavi/services/api_service.dart';
import 'package:gifunavi/pages/team/team_controller.dart';
class MemberController extends GetxController {
late final ApiService _apiService;
late final TeamController _teamController;
final selectedMember = Rx<User?>(null);
int teamId = 0;
final member = Rx<User?>(null);
final email = ''.obs;
final firstname = ''.obs;
final lastname = ''.obs;
final female = false.obs;
final dateOfBirth = Rx<DateTime?>(null);
final isLoading = false.obs; // isLoadingプロパティを追加
final isActive = false.obs;
//MemberController(this._apiService);
@override
void onInit() {
super.onInit();
_apiService = Get.find<ApiService>();
_teamController = Get.find<TeamController>();
ever(member, (_) => _initializeMemberData());
loadInitialData();
}
bool get isDummyEmail => email.value.startsWith('dummy_');
bool get isApproved => !email.value.startsWith('dummy_') && member.value?.isActive == true;
Future<void> 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) {
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<bool> 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;
await _teamController.updateTeamComposition();
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<bool> updateMember() async {
if (member.value == null) return false;
int? memberId = member.value?.id;
try {
final updatedMember = await _apiService.updateTeamMember(
teamId,
memberId,
firstname.value,
lastname.value,
dateOfBirth.value,
female.value,
);
member.value = updatedMember;
return true;
} catch (e) {
print('Error updating member: $e');
return false;
}
}
Future<bool> deleteMember() async {
if (member.value == null || member.value!.id == null) {
Get.snackbar('エラー', 'メンバー情報が不正です', snackPosition: SnackPosition.BOTTOM);
return false;
}
try {
isLoading.value = true;
await _apiService.deleteTeamMember(teamId, member.value!.id!);
await _teamController.updateTeamComposition();
Get.snackbar('成功', 'メンバーが削除されました', snackPosition: SnackPosition.BOTTOM);
member.value = null;
return true;
} catch (e) {
print('Error deleting member: $e');
Get.snackbar('エラー', 'メンバーの削除に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
isLoading.value = false;
return false;
} finally {
isLoading.value = false;
}
}
Future<void> 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 < 7) return '未就学';
if (yearsFromSchoolStart < 13) return '${yearsFromSchoolStart - 6}';
if (yearsFromSchoolStart < 16) return '${yearsFromSchoolStart - 12}';
if (yearsFromSchoolStart < 19) return '${yearsFromSchoolStart - 15}';
return '成人';
}
String getAgeAndGrade() {
final age = calculateAge();
final grade = calculateGrade();
return '$age歳/$grade';
}
bool isOver18() {
return calculateAge() >= 18;
}
}

View File

@ -0,0 +1,325 @@
// lib/pages/team/member_detail_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/team/member_controller.dart';
import 'package:gifunavi/pages/team/team_controller.dart';
import 'package:intl/intl.dart'; // この行を追加
import 'package:gifunavi/widgets/custom_date_picker.dart';
// 追加
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/model/user.dart';
class MemberDetailPage extends StatefulWidget {
const MemberDetailPage({super.key});
@override
_MemberDetailPageState createState() => _MemberDetailPageState();
}
class _MemberDetailPageState extends State<MemberDetailPage> {
final MemberController controller = Get.find<MemberController>();
final TeamController teamController = Get.find<TeamController>();
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)),
);
}
});
}
Future<void> _handleSaveAndNavigateBack_old() async {
bool success = await controller.saveMember();
if (success) {
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
// スナックバーが表示されるのを待つ
await Future.delayed(const Duration(seconds: 1));
// 現在のスナックバーを安全に閉じる
if (Get.isSnackbarOpen) {
await Get.closeCurrentSnackbar();
}
// リストページに戻る
//Get.until((route) => Get.currentRoute == '/team');
// または、リストページの具体的なルート名がある場合は以下を使用
// Get.offNamed('/team');
}
}
Future<void> _handleSaveAndNavigateBack() async {
if (!controller.validateInputs()) return;
try {
await controller.saveMember();
await teamController.updateTeamComposition();
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
await Future.delayed(const Duration(seconds: 1));
if (Get.isSnackbarOpen) {
await Get.closeCurrentSnackbar();
}
} catch (e) {
print('Error saving member: $e');
Get.snackbar('エラー', 'メンバーの保存に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
}
}
Future<void> _handleDeleteAndNavigateBack() async {
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('確認'),
content: const Text('このメンバーを削除してもよろしいですか?'),
actions: [
TextButton(
child: const Text('キャンセル'),
onPressed: () => Get.back(result: false),
),
TextButton(
child: const Text('削除'),
onPressed: () => Get.back(result: true),
),
],
),
);
if (confirmed == true) {
try {
if (controller.member.value != null && controller.member.value!.id != null) {
await teamController.removeMember(controller.member.value!.id!);
// スナックバーの処理を安全に行う
await _safelyCloseSnackbar();
// 画面遷移
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
} else {
Get.snackbar('エラー', 'メンバー情報が不正です', snackPosition: SnackPosition.BOTTOM);
}
} catch (e) {
print('Error deleting member: $e');
Get.snackbar('エラー', 'メンバーの削除に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
}
}
}
Future<void> _safelyCloseSnackbar() async {
if (Get.isSnackbarOpen) {
try {
await Get.closeCurrentSnackbar();
} catch (e) {
print('Error closing snackbar: $e');
}
}
}
@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 const 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: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mode == 'new')
TextFormField(
controller: _emailController,
onChanged: (value) => controller.updateEmail(value),
decoration: const InputDecoration(labelText: 'メールアドレス'),
keyboardType: TextInputType.emailAddress, // メールアドレス用のキーボードを表示
autocorrect: false, // 自動修正を無効化
enableSuggestions: false,
)
else if (controller.isDummyEmail)
const Text('メールアドレス: (メアド無し)')
else
Text('メールアドレス: ${controller.email.value}'),
if (controller.email.value.isEmpty || controller.isDummyEmail || mode == 'edit') ...[
TextFormField(
decoration: const InputDecoration(labelText: ''),
controller: _lastNameController,
onChanged: (value) => controller.updateLastName(value),
//controller: TextEditingController(text: controller.lastname.value),
),
TextFormField(
decoration: const InputDecoration(labelText: ''),
controller: _firstNameController,
//onChanged: (value) => controller.firstname.value = value,
onChanged: (value) => controller.updateFirstName(value),
//controller: TextEditingController(text: controller.firstname.value),
),
// 生年月日
if (controller.isDummyEmail || !controller.isOver18())
ListTile(
title: const Text('生年月日'),
subtitle: Text(controller.dateOfBirth.value != null
? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})'
: '未設定'),
onTap: () async {
final date = await showDialog<DateTime>(
context: context,
builder: (BuildContext context) {
return CustomDatePicker(
initialDate: controller.dateOfBirth.value ?? DateTime.now(),
firstDate: DateTime(1920),
lastDate: DateTime.now(),
currentDateText: controller.dateOfBirth.value != null
? DateFormat('yyyy/MM/dd').format(controller.dateOfBirth.value!)
: '',
);
},
);
if (date != null) {
controller.dateOfBirth.value = date;
}
},
)
else
const Text('18歳以上'),
SwitchListTile(
title: const Text('性別'),
subtitle: Text(controller.female.value ? '女性' : '男性'),
value: controller.female.value,
onChanged: (value) => controller.female.value = value,
),
],
]),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _handleDeleteAndNavigateBack,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('削除'),
),
if (!controller.isDummyEmail && !controller.isApproved && mode == 'edit')
ElevatedButton(
onPressed: () => controller.resendInvitation(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
child: const Text('招待再送信'),
),
ElevatedButton(
onPressed: _handleSaveAndNavigateBack,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: Text(controller.isDummyEmail ? '保存' :
(mode == 'new' && !controller.isDummyEmail) ? '保存・招待' :
'保存'),
),
],
),
),
],
);
}),
);
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:gifunavi/pages/team/team_controller.dart';
import 'package:gifunavi/services/api_service.dart';
class TeamBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiService>(() => ApiService());
Get.lazyPut<TeamController>(() => TeamController());
}
}

View File

@ -0,0 +1,692 @@
// lib/controllers/team_controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/model/team.dart';
import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/model/user.dart';
import 'package:gifunavi/model/entry.dart';
import 'package:gifunavi/services/api_service.dart';
import 'package:gifunavi/widgets/category_change_dialog.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/model/event.dart';
class TeamController extends GetxController {
late final ApiService _apiService;
late final EntryController _entryController;
final teams = <Team>[].obs;
final categories = <NewCategory>[].obs;
final RxList<User> teamMembers = <User>[].obs;
final teamEntries = <Entry>[].obs;
final selectedCategory = Rx<NewCategory?>(null);
final selectedTeam = Rx<Team?>(null);
final currentUser = Rx<User?>(null);
final teamName = ''.obs;
final isLoading = false.obs;
final error = RxString('');
@override
void onInit() async {
super.onInit();
try {
_apiService = Get.find<ApiService>();
if (!Get.isRegistered<EntryController>()) {
Get.put(EntryController());
}
_entryController = Get.find<EntryController>();
await loadInitialData();
} catch (e) {
error.value = e.toString();
print('Error in TeamController onInit: $e');
}
}
Future<void> loadInitialData() async {
try {
isLoading.value = true;
await Future.wait([
fetchCategories(),
fetchTeams(),
getCurrentUser(),
]);
if (categories.isNotEmpty && selectedCategory.value == null) {
selectedCategory.value = categories.first;
}
} catch (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<void> 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<void> fetchCategories() async {
try {
final fetchedCategories = await _apiService.getCategories();
categories.assignAll(fetchedCategories);
print("Fetched categories: ${categories.length}"); // デバッグ用
} catch (e) {
print('Error fetching categories: $e');
}
}
Future<void> getCurrentUser() async {
try {
final user = await _apiService.getCurrentUser();
currentUser.value = user;
} catch (e) {
print('Error getting current user: $e');
}
}
Future<Team> 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<Team> updateTeam(int teamId, String teamName, int categoryId) async {
// APIサービスを使用してチームを更新
final updatedTeam = await _apiService.updateTeam(teamId, teamName, categoryId);
return updatedTeam;
}
Future<void> deleteTeam(int teamId) async {
bool confirmDelete = await Get.dialog(
AlertDialog(
title: const Text('チーム削除の確認'),
content: const Text('このチームとそのすべてのメンバーを削除しますか?この操作は取り消せません。'),
actions: [
TextButton(
child: const Text('キャンセル'),
onPressed: () => Get.back(result: false),
),
TextButton(
child: const 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<void> fetchTeamMembers(int teamId) async {
try {
isLoading.value = true;
final members = await _apiService.getTeamMembers(teamId);
teamMembers.assignAll(members);
teamMembers.refresh(); // 明示的に更新を通知
} catch (e) {
error.value = 'メンバーの取得に失敗しました: $e';
print('Error fetching team members: $e');
} finally {
isLoading.value = false;
}
}
Future<void> 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<void> 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<void> 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<void> 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<void> deleteSelectedTeam() async {
if (selectedTeam.value != null) {
await deleteTeam(selectedTeam.value!.id);
selectedTeam.value = null;
}
}
List<NewCategory> getFilteredCategories_old() {
//List<User> teamMembers = getCurrentTeamMembers();
return categories.where((category) {
return isCategoryValid(category, teamMembers);
}).toList();
}
List<NewCategory> getFilteredCategories() {
if (teamMembers.isEmpty && currentUser.value != null) {
// ソロの場合
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else {
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
}
}
bool isElementarySchoolOrYounger(User user) {
final now = DateTime.now();
final age = now.year - user.dateOfBirth!.year;
return age <= 12;
}
bool isCategoryValid(NewCategory category, List<User> 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<User> teamMembers) {
// 家族かどうかを判断するロジック(例: 同じ姓を持つメンバーが2人以上いる場合は家族とみなす
Set<String> familyNames = teamMembers.map((member) => member.lastname).toSet();
return familyNames.length < teamMembers.length;
}
Future<void> updateTeamComposition() async {
NewCategory? oldCategory = selectedCategory.value;
String? oldTime = oldCategory?.time;
// メンバーリストの最新状態を取得
await fetchTeamMembers(selectedTeam.value!.id);
List<NewCategory> eligibleCategories = [];
if (teamMembers.isEmpty || teamMembers.length == 1) {
if (currentUser.value != null) {
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
} else {
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
// 同じ時間のカテゴリを優先的に選択
NewCategory? newCategory = eligibleCategories.firstWhereOrNull((c) => c.time == oldTime);
if (newCategory == null && eligibleCategories.isNotEmpty) {
// 同じ時間のカテゴリがない場合、最初のカテゴリを選択
newCategory = eligibleCategories.first;
// 警告メッセージを表示
Get.dialog(
AlertDialog(
title: Text('カテゴリ変更の通知'),
content: Text('旧カテゴリの時間($oldTime)と一致するカテゴリがないため、${newCategory.time ?? "時間指定なし"}を選択しました。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
}
if (newCategory != null && oldCategory != newCategory) {
selectedCategory.value = newCategory;
// チームのカテゴリを更新
if (selectedTeam.value != null) {
try {
final updatedTeam = await updateTeam(selectedTeam.value!.id, selectedTeam.value!.teamName, newCategory.id);
selectedTeam.value = updatedTeam;
// チームリストも更新
final index = teams.indexWhere((team) => team.id == updatedTeam.id);
if (index != -1) {
teams[index] = updatedTeam;
}
// エントリーの締め切りチェック
bool hasClosedEntries = await checkForClosedEntries(selectedTeam.value!.id);
if (hasClosedEntries) {
Get.dialog(
AlertDialog(
title: Text('警告'),
content: Text('締め切りを過ぎたエントリーがあります。カテゴリ変更が必要な場合は事務局にお問い合わせください。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
} else {
await checkAndHandleCategoryChange(oldCategory!, newCategory);
}
} catch (e) {
print('Error updating team category: $e');
Get.snackbar('エラー', 'チームのカテゴリ更新に失敗しました');
}
}
}
}
Future<bool> checkForClosedEntries(int teamId) async {
try {
final entries = await _apiService.getEntries();
return entries.any((entry) => !isEntryEditable(entry.event));
} catch (e) {
print('Error checking for closed entries: $e');
return false;
}
}
bool isEntryEditable(Event event) {
return DateTime.now().isBefore(event.deadlineDateTime);
}
Future<void> updateTeamComposition_old() async {
NewCategory? oldCategory = selectedCategory.value;
String? oldTime = oldCategory?.time;
// メンバーリストの最新状態を取得
await fetchTeamMembers(selectedTeam.value!.id);
List<NewCategory> eligibleCategories = [];
if (teamMembers.isEmpty) {
if (currentUser.value != null) {
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
} else {
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
// 同じ時間のカテゴリを優先的に選択
NewCategory? newCategory = eligibleCategories.firstWhereOrNull((c) => c.time == oldTime);
if (newCategory == null && eligibleCategories.isNotEmpty) {
// 同じ時間のカテゴリがない場合、最初のカテゴリを選択
newCategory = eligibleCategories.first;
// 警告メッセージを表示
Get.dialog(
AlertDialog(
title: Text('カテゴリ変更の通知'),
content: Text('旧カテゴリの時間($oldTime)と一致するカテゴリがないため、${newCategory.time ?? "時間指定なし"}を選択しました。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
}
if (newCategory != null && oldCategory != newCategory) {
selectedCategory.value = newCategory;
// チームのカテゴリを更新
if (selectedTeam.value != null) {
try {
final updatedTeam = await updateTeam(selectedTeam.value!.id, selectedTeam.value!.teamName, newCategory.id);
selectedTeam.value = updatedTeam;
// チームリストも更新
final index = teams.indexWhere((team) => team.id == updatedTeam.id);
if (index != -1) {
teams[index] = updatedTeam;
}
} catch (e) {
print('Error updating team category: $e');
Get.snackbar('エラー', 'チームのカテゴリ更新に失敗しました');
}
}
await checkAndHandleCategoryChange(oldCategory!, newCategory);
}
}
// メンバーの追加後に呼び出すメソッド
Future<void> addMember(User newMember) async {
try {
await _apiService.createTeamMember(selectedTeam.value!.id, newMember.email, newMember.firstname, newMember.lastname, newMember.dateOfBirth, newMember.female);
await updateTeamComposition();
} catch (e) {
print('Error adding member: $e');
Get.snackbar('エラー', 'メンバーの追加に失敗しました');
}
}
// メンバーの削除後に呼び出すメソッド
Future<void> removeMember(int memberId) async {
try {
await _apiService.deleteTeamMember(selectedTeam.value!.id, memberId);
// selectedTeamからメンバーを削除
if (selectedTeam.value != null) {
selectedTeam.value!.members.removeWhere((member) => member.id == memberId);
selectedTeam.refresh();
}
// メンバー削除後にチーム構成を更新
await updateTeamComposition();
// teamMembersを更新
await fetchTeamMembers(selectedTeam.value!.id);
} catch (e) {
print('Error removing member: $e');
Get.snackbar('エラー', 'メンバーの削除に失敗しました');
}
}
Future<void> checkAndHandleCategoryChange(NewCategory oldCategory, NewCategory newCategory) async {
try {
if (selectedTeam.value == null) {
print('No team selected');
return;
}
// エントリーの存在を確認
bool hasEntries = await checkIfTeamHasEntries(selectedTeam.value!.id);
if (hasEntries) {
bool shouldCreateNewTeam = await Get.dialog<bool>(
CategoryChangeDialog(
oldCategory: oldCategory.categoryName,
newCategory: newCategory.categoryName,
),
) ?? false;
if (shouldCreateNewTeam) {
await createNewTeamWithCurrentMembers();
} else {
await updateEntriesWithNewCategory();
}
}else{
// エントリーが存在しない場合は、カテゴリの更新のみを行う
await updateTeamCategory(newCategory);
}
} catch (e) {
print('Error in checkAndHandleCategoryChange: $e');
Get.snackbar('エラー', 'カテゴリ変更の処理中にエラーが発生しました');
}
}
Future<bool> checkIfTeamHasEntries(int teamId) async {
try {
final entries = await _apiService.getTeamEntries(teamId);
return entries.isNotEmpty;
} catch (e) {
print('Error checking if team has entries: $e');
return false;
}
}
Future<void> updateTeamCategory(NewCategory newCategory) async {
try {
if (selectedTeam.value != null) {
final updatedTeam = await updateTeam(selectedTeam.value!.id, selectedTeam.value!.teamName, newCategory.id);
selectedTeam.value = updatedTeam;
// チームリストも更新
final index = teams.indexWhere((team) => team.id == updatedTeam.id);
if (index != -1) {
teams[index] = updatedTeam;
}
Get.snackbar('成功', 'チームのカテゴリを更新しました');
}
} catch (e) {
print('Error updating team category: $e');
Get.snackbar('エラー', 'チームのカテゴリ更新に失敗しました');
}
}
Future<void> fetchTeamEntries(int teamId) async {
try {
final fetchedEntries = await _apiService.getEntries();
teamEntries.assignAll(fetchedEntries);
} catch (e) {
print('Error fetching team entries: $e');
teamEntries.clear(); // エラーが発生した場合、エントリーリストをクリア
//Get.snackbar('エラー', 'チームのエントリー取得に失敗しました');
}
}
Future<void> createNewTeamWithCurrentMembers() async {
String newTeamName = '${selectedTeam.value!.teamName}_${DateTime.now().millisecondsSinceEpoch}';
try {
Team newTeam = await _apiService.createTeam(newTeamName, selectedCategory.value!.id);
for (var member in teamMembers) {
await _apiService.createTeamMember(
newTeam.id,
member.email,
member.firstname,
member.lastname,
member.dateOfBirth,
member.female,
);
}
Get.offNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'edit', 'team': newTeam});
} catch (e) {
print('Error creating new team: $e');
Get.snackbar('エラー', '新しいチームの作成に失敗しました');
}
}
Future<void> updateEntriesWithNewCategory() async {
try {
for (var entry in teamEntries) {
//await _apiService.updateEntry(entry.id, selectedCategory.value!.id);
final updatedEntry = await _apiService.updateEntry(
entry.id,
entry.team.id,
entry.event.id,
selectedCategory.value!.id,
entry.date!,
entry.zekkenNumber,
);
}
Get.snackbar('成功', 'エントリーのカテゴリを更新しました');
} catch (e) {
print('Error updating entries: $e');
Get.snackbar('エラー', 'エントリーの更新に失敗しました');
}
}
void updateCurrentUserGender(bool isFemale) {
if (currentUser.value != null) {
currentUser.value!.female = isFemale;
updateTeamComposition();
}
}
}

View File

@ -0,0 +1,263 @@
// lib/pages/team/team_detail_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/team/team_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:gifunavi/model/team.dart';
import 'package:gifunavi/model/category.dart';
class TeamDetailPage extends StatefulWidget {
const TeamDetailPage({super.key});
@override
_TeamDetailPageState createState() => _TeamDetailPageState();
}
class _TeamDetailPageState extends State<TeamDetailPage> {
late TeamController controller;
late TextEditingController _teamNameController = TextEditingController();
final RxString mode = ''.obs;
final Rx<Team?> team = Rx<Team?>(null);
Worker? _teamNameWorker;
@override
void initState() {
super.initState();
controller = Get.find<TeamController>();
_teamNameController = TextEditingController();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeData();
});
}
void _initializeData() {
final args = Get.arguments;
if (args != null && args is Map<String, dynamic>) {
mode.value = args['mode'] as String? ?? '';
team.value = args['team'] as Team?;
if (mode.value == 'edit' && team.value != null) {
controller.setSelectedTeam(team.value!);
controller.fetchTeamMembers(team.value!.id); // メンバーを取得
} 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: const Icon(Icons.arrow_back),
onPressed: () {
controller.cleanupForNavigation();
Get.back();
},
),
actions: [
IconButton(
icon: const 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: const Icon(Icons.delete),
onPressed: () async {
try {
await controller.deleteSelectedTeam();
Get.back();
} catch (e) {
Get.snackbar('エラー', e.toString(),
snackPosition: SnackPosition.BOTTOM);
}
},
);
} else {
return const SizedBox.shrink();
}
}),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final filteredCategories = controller.getFilteredCategories();
final categoriesToDisplay = filteredCategories.isEmpty
? controller.categories
: filteredCategories;
// 選択されているカテゴリが表示リストに含まれていない場合、最初の項目を選択する
if (controller.selectedCategory.value == null ||
!categoriesToDisplay.contains(controller.selectedCategory.value)) {
controller.updateCategory(categoriesToDisplay.isNotEmpty
? categoriesToDisplay.first
: null);
}
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() =>
TextFormField(
decoration: const InputDecoration(labelText: 'チーム名'),
controller: TextEditingController(text: controller
.selectedTeam.value?.teamName ?? ''),
onChanged: (value) => controller.updateTeamName(value),
),
),
const SizedBox(height: 16),
if (categoriesToDisplay.isEmpty)
const Text('カテゴリデータを読み込めませんでした。',
style: TextStyle(color: Colors.red))
else
Obx(() =>
DropdownButtonFormField<NewCategory>(
decoration: const InputDecoration(
labelText: 'カテゴリ'),
value: controller.selectedCategory.value,
items: categoriesToDisplay.map((category) =>
DropdownMenuItem(
value: category,
child: Text(category.categoryName),
)).toList(),
onChanged: (value) => controller.updateCategory(value),
),
),
if (filteredCategories.isEmpty &&
controller.categories.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'注意: チーム構成に適さないカテゴリーも表示されています。',
style: TextStyle(color: Colors.orange[700], fontSize: 12),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text('所有者: ${controller.currentUser.value?.email ??
"未設定"}'),
),
if (mode.value == 'edit') ...[
const SizedBox(height: 24),
const Text('メンバーリスト', style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Obx(() {
final teamMembers = controller.teamMembers;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: teamMembers.length,
itemBuilder: (context, index) {
final member = teamMembers[index];
final isDummyEmail = member.email?.startsWith(
'dummy_') ?? false;
final isCurrentUser = member.email ==
controller.currentUser.value?.email;
final displayName = isDummyEmail
? '${member.lastname} ${member.firstname}'
: member.isActive
? '${member.lastname} ${member.firstname}'
: member.email?.split('@')[0] ?? ''; //(未承認)';
return ListTile(
title: Text(
'$displayName${isCurrentUser ? ' (自分)' : ''}'),
subtitle: isDummyEmail
? const Text('Email未設定')
: null,
onTap: isCurrentUser ? null : () async {
final result = await Get.toNamed(
AppPages.MEMBER_DETAIL,
arguments: {
'mode': 'edit',
'member': member,
'teamId': controller.selectedTeam.value?.id
},
);
await controller.fetchTeamMembers(
controller.selectedTeam.value!.id);
},
);
},
);
}),
const SizedBox(height: 16),
ElevatedButton(
child: const Text('新しいメンバーを追加'),
onPressed: () async {
final result = await Get.toNamed(
AppPages.MEMBER_DETAIL,
arguments: {
'mode': 'new',
'teamId': controller.selectedTeam.value?.id
},
);
if (result == true && controller.selectedTeam.value != null) {
await controller.fetchTeamMembers(controller.selectedTeam.value!.id);
controller.teamMembers.refresh(); // 明示的に更新を通知
}
},
),
],
],
),
),
);
}),
);
}
}

View File

@ -0,0 +1,57 @@
// lib/pages/team/team_list_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/team/team_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
class TeamListPage extends GetWidget<TeamController> {
const TeamListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('チーム管理'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
} else if (controller.error.value.isNotEmpty) {
return Center(child: Text(controller.error.value));
} else if (controller.teams.isEmpty) {
return const 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} '),
onTap: () async {
await Get.toNamed(
AppPages.TEAM_DETAIL,
arguments: {'mode': 'edit', 'team': team},
);
controller.fetchTeams();
},
);
},
),
);
}
}),
);
}
}