1043 lines
44 KiB
Dart
1043 lines
44 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert'; // この行を追加または確認
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:intl/intl.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:gifunavi/model/destination.dart';
|
||
import 'package:gifunavi/pages/destination/destination_controller.dart';
|
||
import 'package:gifunavi/pages/index/index_controller.dart';
|
||
import 'package:gifunavi/services/external_service.dart';
|
||
import 'package:gifunavi/utils/const.dart';
|
||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||
|
||
import 'package:http/http.dart' as http;
|
||
|
||
import '../../routes/app_pages.dart'; // この行を追加
|
||
import 'package:timezone/data/latest.dart' as tz;
|
||
import 'package:timezone/timezone.dart' as tz;
|
||
|
||
// 関数 getTagText は、特定の条件に基づいて文字列から特定の部分を抽出し、返却するためのものです。
|
||
// 関数は2つのパラメータを受け取り、条件分岐を通じて結果を返します。
|
||
//
|
||
// この関数は、タグのリスト(空白を含む文字列)を適切に解析し、条件に応じて特定のタグを抽出するために設計されています。
|
||
// 異なる種類の空白文字(半角、全角)で異なる分割を行い、与えられた条件(isRecept)に応じて適切なタグを選択して返却します。
|
||
//
|
||
String getTagText(bool isRecept, String? tags) {
|
||
// bool isRecept: 真偽値を受け取り、この値によって処理の分岐が行われます。
|
||
// String? tags: オプショナルな文字列(null が許容される)。空白文字を含む可能性のあるタグのリストを表します。
|
||
|
||
// 空のチェック:
|
||
// tags が null または空文字列 ("") の場合、何も含まれていないことを意味し、関数はただちに空文字列を返します。
|
||
//
|
||
if (tags == null || tags.isEmpty) {
|
||
return "";
|
||
}
|
||
|
||
// タグの分割:
|
||
// tags が空ではない場合、文字列を空白文字で分割します。
|
||
// ここで2種類の空白文字(半角 " " と全角 " ")に対応するため、2回分割を行っています。
|
||
// tts: 半角スペース " " で分割した結果のリスト。
|
||
// ttt: 全角スペース " " で分割した結果のリスト。
|
||
//
|
||
List<String> tts = tags.split(" ");
|
||
List<String> ttt = tags.split(" ");
|
||
|
||
// 条件分岐:
|
||
// isRecept の値によって、処理が分岐します。
|
||
//
|
||
if (isRecept) {
|
||
// isRecept が true の場合:
|
||
// 全角スペースで分割した結果 (ttt) の長さが半角スペースで分割した結果 (tts) の長さより大きく、
|
||
// かつ ttt が1つ以上の要素を持つ場合、ttt[1] (全角スペースで分割後の2番目の要素)を返します。
|
||
if (ttt.length > tts.length && ttt.length > 1) {
|
||
return ttt[1];
|
||
}
|
||
}
|
||
if (!isRecept) {
|
||
// isRecept が false の場合:
|
||
// 全角スペースで分割したリストが半角スペースで分割したリストよりも長い場合、ttt[0] (全角スペースで分割後の最初の要素)を返します。
|
||
// 上記の条件に当てはまらない場合、半角スペースで分割したリストの最初の要素 tts[0] を返します。
|
||
//
|
||
if (ttt.length > tts.length && ttt.length > 1) {
|
||
return ttt[0];
|
||
}
|
||
}
|
||
if (!isRecept) {
|
||
// 最終的な返却:
|
||
// 上記の条件に何も該当しない場合(主に isRecept が true であり、全角スペースの条件に該当しない場合)、空文字列 "" を返します。
|
||
return tts[0];
|
||
}
|
||
return "";
|
||
}
|
||
|
||
// 要修正:画像の読み込みエラーが発生した場合のエラーハンドリングが不十分です。エラーメッセージを表示するなどの処理を追加してください。
|
||
// getDisplayImage は、Destination オブジェクトを受け取り、特定の条件に基づいて表示する画像を返す機能を持っています。
|
||
// Flutterの Image ウィジェットを使用して、適切な画像を表示します。
|
||
//
|
||
// この関数は、提供された Destination オブジェクトに基づいて適切な画像を動的に選択し、
|
||
// その画像を表示するための Image ウィジェットを生成します。
|
||
// デフォルトの画像、完全なURL、またはサーバーURLと組み合わされた画像パスを使用して、条件に応じた画像の取得を試みます。
|
||
// また、エラー発生時にはデフォルト画像にフォールバックすることでユーザー体験を向上させます。
|
||
//
|
||
Image getDisplayImage(Destination destination) {
|
||
// Destination destination: これは Destination クラスのインスタンスで、
|
||
// CheckPointのデータを持っているオブジェクトです。
|
||
// このクラスには少なくとも phone と photos というプロパティが含まれている
|
||
//
|
||
|
||
// サーバーURLの取得:
|
||
// serverUrl 変数には ConstValues.currentServer() メソッドから現在のサーバーのURLが取得されます。
|
||
// これは画像を取得する際の基本URLとして使用される可能性があります。
|
||
//
|
||
String serverUrl = ConstValues.currentServer();
|
||
|
||
// デフォルト画像の設定:
|
||
// img 変数にはデフォルトの画像が設定されます。
|
||
// これは、アセットから "assets/images/empty_image.png" をロードするための Image.asset コンストラクタを使用しています。
|
||
//
|
||
Image img = Image.asset("assets/images/empty_image.png");
|
||
|
||
// 電話番号のチェック:
|
||
// destination.phone が null の場合、関数は img(デフォルト画像)を返します。
|
||
// これは、phone プロパティが画像URLの代用として何らかの形で使用されることを示唆していますが、
|
||
// それが null であればデフォルト画像を使用するという意味です。
|
||
//
|
||
if (destination.phone == null) {
|
||
return img;
|
||
}
|
||
|
||
// 画像URLの構築と画像の返却:
|
||
// destination.photos が http を含む場合、これはすでに完全なURLとして提供されていることを意味します。
|
||
// このURLを NetworkImage コンストラクタに渡し、Image ウィジェットを生成して返します。
|
||
// そうでない場合は、serverUrl と destination.photos を組み合わせたURLを生成して NetworkImage に渡し、画像を取得します。
|
||
//
|
||
if (destination.photos!.contains('http')) {
|
||
return Image(
|
||
image: NetworkImage(
|
||
destination.phone!,
|
||
),
|
||
errorBuilder:
|
||
(BuildContext context, Object exception, StackTrace? stackTrace) {
|
||
return Image.asset("assets/images/empty_image.png");
|
||
},
|
||
);
|
||
} else {
|
||
return Image(
|
||
image: NetworkImage(
|
||
'$serverUrl/media/compressed/${destination.photos}',
|
||
),
|
||
errorBuilder:
|
||
(BuildContext context, Object exception, StackTrace? stackTrace) {
|
||
return Image.asset("assets/images/empty_image.png");
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
// getFinishImage は、ImageProvider 型のオブジェクトを返す関数で、Flutterアプリケーションで使用される画像を提供します。
|
||
// この関数は、DestinationController というクラスのインスタンスに依存しており、特定の状態に基づいて適切な画像を返します。
|
||
//
|
||
// この関数は、アプリケーションの現在の状態に依存して動的に画像を提供します。
|
||
// DestinationController の photos リストに基づいて画像を選択し、リストが空の場合はデフォルトの画像を提供します。
|
||
// これにより、画像の動的な管理が可能になり、ユーザーインターフェースの柔軟性が向上します。
|
||
// また、ImageProvider クラスを使用することで、
|
||
// 画像の具体的な取得方法(ファイルからの読み込みやアセットからのロードなど)を抽象化し、
|
||
// Flutterの Image ウィジェットで直接使用できる形式で画像を返します。
|
||
//
|
||
ImageProvider getFinishImage() {
|
||
|
||
// DestinationControllerの取得:
|
||
// destinationController は Get.find<DestinationController>() を使用して取得されます。
|
||
// これは、GetXというFlutterの状態管理ライブラリの機能を使用して、
|
||
// 現在のアプリケーションコンテキストから DestinationController タイプのインスタンスを取得するものです。
|
||
// これにより、アプリケーションの他の部分で共有されている DestinationController の現在のインスタンスにアクセスします。
|
||
//
|
||
DestinationController destinationController =
|
||
Get.find<DestinationController>();
|
||
|
||
// 画像の決定:
|
||
// destinationController.photos リストが空でないかどうかをチェックします。
|
||
// このリストは、ファイルパスまたは画像リソースへの参照を含む可能性があります。
|
||
//
|
||
if (destinationController.photos.isNotEmpty) {
|
||
// リストが空でない場合、最初の要素 (destinationController.photos[0]) が使用されます。
|
||
// FileImage コンストラクタを使用して、このパスから ImageProvider を生成します。
|
||
// これは、ローカルファイルシステム上の画像ファイルを参照するためのものです。
|
||
//
|
||
return FileImage(destinationController.photos[0]);
|
||
|
||
} else {
|
||
// destinationController.photos が空の場合、
|
||
// AssetImage を使用してアプリケーションのアセットからデフォルトの画像('assets/images/empty_image.png')を
|
||
// ロードします。これはビルド時にアプリケーションに組み込まれる静的なリソースです。
|
||
//
|
||
return const AssetImage('assets/images/empty_image.png');
|
||
}
|
||
}
|
||
|
||
// getReceiptImage は、ImageProvider 型を返す関数です。
|
||
// この関数は DestinationController オブジェクトに依存しており、条件に応じて特定の画像を返します。
|
||
// この関数は getFinishImage 関数と非常に似ており、ほぼ同じロジックを使用していますが、返されるデフォルトの画像が異なります。
|
||
//
|
||
ImageProvider getReceiptImage() {
|
||
DestinationController destinationController =
|
||
Get.find<DestinationController>();
|
||
if (destinationController.photos.isNotEmpty) {
|
||
return FileImage(destinationController.photos[0]);
|
||
} else {
|
||
return const AssetImage('assets/images/money.png');
|
||
}
|
||
}
|
||
|
||
// CameraPageクラスは、目的地に応じて適切なカメラ機能とアクションボタンを提供します。
|
||
// 手動チェックイン、ゴール撮影、購入ポイント撮影など、様々なシナリオに対応しています。
|
||
// また、ロゲイニングが開始されていない場合は、StartRogainingウィジェットを表示して、ユーザーにロゲイニングの開始を促します。
|
||
// CameraPageクラスは、IndexControllerとDestinationControllerを使用して、
|
||
// 現在の状態や目的地の情報を取得し、適切なUIを構築します。
|
||
// また、写真の撮影や購入ポイントの処理など、様々な機能を提供しています。
|
||
//
|
||
class CameraPage extends StatelessWidget {
|
||
bool? manulaCheckin = false; // 手動チェックインを示すブール値(デフォルトはfalse)
|
||
bool? buyPointPhoto = false; // 購入ポイントの写真を示すブール値(デフォルトはfalse)
|
||
Destination destination; // 目的地オブジェクト
|
||
Destination? dbDest; // データベースから取得した目的地オブジェクト(オプショナル)
|
||
String? initImage; // 初期画像のパス(オプショナル)
|
||
bool? buyQrCode = false;
|
||
|
||
CameraPage(
|
||
{super.key,
|
||
required this.destination,
|
||
this.dbDest,
|
||
this.manulaCheckin,
|
||
this.buyPointPhoto,
|
||
this.initImage});
|
||
DestinationController destinationController =
|
||
Get.find<DestinationController>();
|
||
IndexController indexController = Get.find<IndexController>();
|
||
|
||
var settingGoal = false.obs;
|
||
|
||
Timer? timer;
|
||
|
||
bool isValidEventParticipation() {
|
||
final eventCode = indexController.currentUser[0]["user"]["event_code"];
|
||
final teamName = indexController.currentUser[0]["user"]["team_name"];
|
||
final dateString = indexController.currentUser[0]["user"]["event_date"];
|
||
//final parsedDate = DateTime.parse(dateString);
|
||
|
||
//final eventDate = tz.TZDateTime.from(parsedDate, tz.getLocation('Asia/Tokyo'));
|
||
|
||
//final today = DateTime.now();
|
||
|
||
return eventCode != null &&
|
||
teamName != null &&
|
||
dateString != null ;
|
||
|
||
// isSameDay(eventDate, today);
|
||
}
|
||
|
||
bool isSameDay(DateTime date1, DateTime date2) {
|
||
return date1.year == date2.year &&
|
||
date1.month == date2.month &&
|
||
date1.day == date2.day;
|
||
}
|
||
|
||
void showEventParticipationWarning(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (BuildContext context) {
|
||
return AlertDialog(
|
||
title: const Text("警告"),
|
||
content: const Text("今日のイベントにまず参加しないと事前チェックインはできません。サブメニューからイベント参加をタップして今日のイベントに参加してください。"),
|
||
actions: <Widget>[
|
||
TextButton(
|
||
child: const Text("キャンセル"),
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
TextButton(
|
||
child: const Text("参加する"),
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
Get.toNamed(AppPages.EVENT_ENTRY);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
// 現在の状態に基づいて、適切なアクションボタンを返します。
|
||
// 要修正:エラーハンドリングが不十分です。例外が発生した場合の処理を追加することをお勧めします。
|
||
//
|
||
Widget getAction(BuildContext context) {
|
||
if (!isValidEventParticipation()) {
|
||
return ElevatedButton(
|
||
onPressed: () => showEventParticipationWarning(context),
|
||
child: const Text("チェックイン"),
|
||
);
|
||
}
|
||
|
||
if (manulaCheckin == true) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||
child: Wrap(
|
||
spacing: 16.0,
|
||
runSpacing: 8.0,
|
||
children: [
|
||
Obx(() => ElevatedButton(
|
||
onPressed: () {
|
||
destinationController.openCamera(context, destination);
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
shape: const CircleBorder(),
|
||
padding: const EdgeInsets.all(20),
|
||
foregroundColor: Colors.white,
|
||
backgroundColor: destinationController.photos.isEmpty
|
||
? Colors.red
|
||
: Colors.grey[300],
|
||
),
|
||
child: destinationController.photos.isEmpty
|
||
? const Text("撮影", style: TextStyle(color: Colors.white))
|
||
: const Text("再撮影", style: TextStyle(color: Colors.black)),
|
||
)),
|
||
Obx(() => destinationController.photos.isNotEmpty
|
||
? ElevatedButton(
|
||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||
onPressed: () async {
|
||
await destinationController.makeCheckin(destination, true, // チェクインボタン
|
||
destinationController.photos[0].path);
|
||
if( destinationController.isInRog.value==true ) {
|
||
destinationController.rogainingCounted.value = true; // ロゲ開始後のみ許可
|
||
}
|
||
destinationController.skipGps = false;
|
||
destinationController.isPhotoShoot.value = false;
|
||
|
||
Get.snackbar("チェックインしました。",
|
||
"${destination.sub_loc_id} : ${destination.name}",
|
||
backgroundColor: Colors.green,
|
||
colorText: Colors.white,
|
||
duration: const Duration(seconds: 2),
|
||
);
|
||
await Future.delayed(const Duration(seconds: 2));
|
||
|
||
Navigator.of(context).pop(true);
|
||
},
|
||
child: const Text("チェックイン", style: TextStyle(color: Colors.white)),
|
||
)
|
||
: Container())
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
if (destinationController.isAtGoal.value &&
|
||
destinationController.isInRog.value &&
|
||
destination.cp == -1) {
|
||
// isAtGoalがtrueで、isInRogがtrue、destination.cpが-1の場合は、ゴール用の撮影ボタンとゴール完了ボタンを表示します。
|
||
//goal
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
if (settingGoal.value == false) {
|
||
destinationController.openCamera(context, destination);
|
||
}
|
||
},
|
||
child: Text("take_photo of the clock".tr)),
|
||
Obx(() => destinationController.photos.isNotEmpty
|
||
? settingGoal.value == false
|
||
? ElevatedButton(
|
||
style:
|
||
ElevatedButton.styleFrom(
|
||
foregroundColor: Colors.white,
|
||
backgroundColor: Colors.red
|
||
),
|
||
onPressed: () async {
|
||
// print(
|
||
// "----- user isss ${indexController.currentUser[0]} -----");
|
||
|
||
settingGoal.value = true;
|
||
try {
|
||
int userId =
|
||
indexController.currentUser[0]["user"]["id"];
|
||
//print("--- Pressed -----");
|
||
String team = indexController.currentUser[0]["user"]
|
||
['team_name'];
|
||
//print("--- _team : ${_team}-----");
|
||
String eventCode = indexController.currentUser[0]
|
||
["user"]["event_code"];
|
||
//print("--- _event_code : ${_event_code}-----");
|
||
String token =
|
||
indexController.currentUser[0]["token"];
|
||
//print("--- _token : ${_token}-----");
|
||
DateTime now = DateTime.now();
|
||
String formattedDate =
|
||
DateFormat('yyyy-MM-dd HH:mm:ss').format(now);
|
||
|
||
await ExternalService()
|
||
.makeGoal(
|
||
userId,
|
||
token,
|
||
team,
|
||
destinationController.photos[0].path,
|
||
formattedDate,
|
||
eventCode)
|
||
.then((value) {
|
||
// print(
|
||
// "---called ext api ${value['status']} ------");
|
||
if (value['status'] == 'OK') {
|
||
Get.back();
|
||
destinationController.skipGps = false;
|
||
Get.snackbar("目標が保存されました", "目標が正常に追加されました",
|
||
backgroundColor: Colors.green,
|
||
colorText: Colors.white
|
||
);
|
||
destinationController.resetRogaining(
|
||
isgoal: true);
|
||
} else {
|
||
//print("---- status ${value['status']} ---- ");
|
||
Get.snackbar(value["detail"], "ERROR",
|
||
backgroundColor: Colors.green,
|
||
colorText: Colors.white
|
||
);
|
||
}
|
||
});
|
||
} on Exception catch (_) {
|
||
settingGoal.value = false;
|
||
} finally {
|
||
settingGoal.value = false;
|
||
}
|
||
},
|
||
child: Text("finish_goal".tr))
|
||
: const Center(
|
||
child: CircularProgressIndicator(),
|
||
)
|
||
: Container())
|
||
],
|
||
);
|
||
|
||
} else if ((destinationController.isInRog.value || (destination.buy_point != null && destination.buy_point! > 0)) &&
|
||
dbDest?.checkedin != null &&
|
||
destination.cp != -1 &&
|
||
dbDest?.checkedin == true) {
|
||
// isInRogがtrueで、dbDest?.checkedinがtrue、destination.cpが-1以外の場合は、購入ポイントの撮影ボタンと完了ボタンを表示します。
|
||
//make buypoint image
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
Obx(() => ElevatedButton(
|
||
onPressed: () {
|
||
destinationController.openCamera(context, destination);
|
||
},
|
||
child: destinationController.photos.isNotEmpty
|
||
? const Text("再撮影")
|
||
: const Text("撮影"))),
|
||
Obx(() => destinationController.photos.isNotEmpty
|
||
? ElevatedButton(
|
||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||
onPressed: () async {
|
||
// print(
|
||
// "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######");
|
||
await destinationController.makeBuyPoint(
|
||
destination, destinationController.photos[0].path);
|
||
Get.back();
|
||
if( destinationController.isInRog.value==true ) {
|
||
destinationController.rogainingCounted.value = true; // ロゲ開始後のみ許可
|
||
}
|
||
destinationController.skipGps = false;
|
||
destinationController.isPhotoShoot.value = false;
|
||
Get.snackbar("お買い物加点を行いました。",
|
||
"${destination.sub_loc_id} : ${destination.name}",
|
||
backgroundColor: Colors.green,
|
||
colorText: Colors.white
|
||
);
|
||
Navigator.of(context).pop(true); // ここを修正
|
||
},
|
||
child: const Text("レシートの写真を撮ってください"))
|
||
: Container())
|
||
],
|
||
);
|
||
|
||
} else if ((destinationController.isInRog.value || (destination.buy_point != null && destination.buy_point! > 0)) &&
|
||
dbDest?.checkedin != null &&
|
||
destination.cp != -1 &&
|
||
destination.use_qr_code == true &&
|
||
dbDest?.checkedin == true) {
|
||
// isInRogがtrueで、dbDest?.checkedinがtrue、destination.cpが-1以外、qrCode == true の場合は、
|
||
// QRCode 撮影ボタンを表示
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
Obx(() => ElevatedButton(
|
||
onPressed: () {
|
||
destinationController.openCamera(context, destination);
|
||
},
|
||
child: destinationController.photos.isNotEmpty
|
||
? const Text("再QR読込")
|
||
: const Text("QR読込"))),
|
||
Obx(() => destinationController.photos.isNotEmpty
|
||
? ElevatedButton(
|
||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||
onPressed: () async {
|
||
// print(
|
||
// "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######");
|
||
await destinationController.makeBuyPoint(
|
||
destination, destinationController.photos[0].path);
|
||
Get.back();
|
||
if( destinationController.isInRog.value==true ) {
|
||
destinationController.rogainingCounted.value = true; //ロゲ開始後のみ許可
|
||
}
|
||
destinationController.skipGps = false;
|
||
destinationController.isPhotoShoot.value = false;
|
||
Get.snackbar("お買い物加点を行いました。",
|
||
"${destination.sub_loc_id} : ${destination.name}",
|
||
backgroundColor: Colors.green,
|
||
colorText: Colors.white
|
||
);
|
||
},
|
||
child: const Text("QRコードを読み取ってください"))
|
||
: Container())
|
||
],
|
||
);
|
||
} else {
|
||
// それ以外の場合は、撮影ボタンとチェックインボタンを表示します。
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
Obx(() => ElevatedButton(
|
||
onPressed: () {
|
||
destinationController.openCamera(context, destination);
|
||
},
|
||
child: destinationController.photos.isNotEmpty
|
||
? const Text("再撮影")
|
||
: const Text("撮影"))),
|
||
Obx(() => destinationController.photos.isNotEmpty
|
||
? ElevatedButton(
|
||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||
onPressed: () async {
|
||
// print(
|
||
|
||
// "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######");
|
||
await destinationController.makeCheckin( // チェックイン確定
|
||
indexController.currentDestinationFeature[0],
|
||
true,
|
||
destinationController.photos[0].path);
|
||
//Get.back();
|
||
if( destinationController.isInRog.value==true ) {
|
||
destinationController.rogainingCounted.value = true; //ロゲ開始後のみ許可
|
||
}
|
||
destinationController.skipGps = false;
|
||
destinationController.isPhotoShoot.value = false;
|
||
|
||
|
||
Get.snackbar(
|
||
"チェックインしました",
|
||
indexController.currentDestinationFeature[0].name ??
|
||
"",
|
||
backgroundColor: Colors.green,
|
||
colorText: Colors.white,
|
||
duration: const Duration(seconds: 2), // 表示時間を1秒に設定
|
||
);
|
||
// SnackBarの表示が終了するのを待ってからCameraPageを閉じる
|
||
await Future.delayed(const Duration(seconds: 2));
|
||
|
||
Navigator.of(context).pop(true); // ここを修正
|
||
},
|
||
child: const Text("チェックイン確定"))
|
||
: Container())
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
// void finishRog(){
|
||
// destinationController.addToRogaining(destinationController.current_lat, destinationController.current_lon, destination_id)
|
||
// }
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
|
||
//print("---- photos ${destination.photos} ----");
|
||
if (buyPointPhoto == true) {
|
||
// buyPointPhotoがtrueの場合は、BuyPointCameraウィジェットを返します。
|
||
//print("--- buy point camera ${destination.toString()}");
|
||
//return BuyPointCamera(destination: destination);
|
||
|
||
return SwitchableBuyPointCamera(destination: destination);
|
||
|
||
//}else if(destination.use_qr_code){
|
||
// return QRCodeScannerPage(destination: destination);
|
||
} else if (destinationController.isInRog.value || (destination.buy_point != null && destination.buy_point! > 0)) {
|
||
// isInRogがtrueの場合は、カメラページのUIを構築します。
|
||
// AppBarには、目的地の情報を表示します。
|
||
// ボディには、目的地の画像、タグ、アクションボタンを表示します。
|
||
//print("-----tags camera page----- ${destination.tags}");
|
||
//print("--- in normal camera ${destination.toString()}");
|
||
return Scaffold(
|
||
appBar: destinationController.isInRog.value &&
|
||
destinationController.rogainingCounted.value == true
|
||
? AppBar(
|
||
automaticallyImplyLeading: false,
|
||
title: destination.cp == -1
|
||
? Text("finishing_rogaining".tr)
|
||
: Text("${destination.sub_loc_id} : ${destination.name}"),
|
||
leading: IconButton(
|
||
icon: Text("cancel".tr),
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
destinationController.skip_10s = true;
|
||
timer =
|
||
Timer.periodic(const Duration(seconds: 10), (Timer t) {
|
||
destinationController.skip_10s = false;
|
||
});
|
||
},
|
||
),
|
||
centerTitle: true,
|
||
)
|
||
: AppBar(
|
||
automaticallyImplyLeading: false,
|
||
title: Text("${destination.sub_loc_id} : ${destination.name}"),
|
||
),
|
||
body: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(8.0),
|
||
child: Center(
|
||
child: Obx(
|
||
() => Container(
|
||
width: MediaQuery.of(context).size.width,
|
||
height: 370,
|
||
decoration: BoxDecoration(
|
||
image: DecorationImage(
|
||
image: destinationController.photos.isEmpty
|
||
? getDisplayImage(destination).image
|
||
: getFinishImage(),
|
||
fit: BoxFit.cover)),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||
child: Text(getTagText(
|
||
false,
|
||
destination.tags,
|
||
))
|
||
// child: Obx(() => destinationController.photos.isEmpty == true
|
||
// ? const Text("撮影してチェックインしてください。")
|
||
// : const Text("チェックインをタップしてください。")),
|
||
),
|
||
getAction(context),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
} else {
|
||
// isInRogがfalseの場合は、StartRogainingウィジェットを返します。
|
||
return StartRogaining();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ロゲイニングが開始されていない場合に表示されるウィジェットです。
|
||
// "You have not started rogaining yet."というメッセージと、戻るボタンを表示します。
|
||
//
|
||
class StartRogaining extends StatelessWidget {
|
||
StartRogaining({super.key});
|
||
|
||
DestinationController destinationController =
|
||
Get.find<DestinationController>();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
automaticallyImplyLeading: false,
|
||
title: Text(
|
||
"Not started yet".tr,
|
||
),
|
||
),
|
||
body: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text("You have not started rogaining yet.".tr,
|
||
style: const TextStyle(fontSize: 24)),
|
||
const SizedBox(
|
||
height: 40.0,
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
Get.back();
|
||
destinationController.skipGps = false;
|
||
},
|
||
child: const Text("Back"),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 購入ポイントの写真撮影用のウィジェットです。
|
||
// 目的地の画像、タグ、撮影ボタン、完了ボタン、購入なしボタンを表示します。
|
||
// 撮影ボタンをタップすると、カメラが起動します。
|
||
// 完了ボタンをタップすると、購入ポイントの処理が行われます。
|
||
// 購入なしボタンをタップすると、購入ポイントがキャンセルされます。
|
||
//
|
||
class SwitchableBuyPointCamera extends StatefulWidget {
|
||
final Destination destination;
|
||
|
||
const SwitchableBuyPointCamera({super.key, required this.destination});
|
||
|
||
@override
|
||
_SwitchableBuyPointCameraState createState() => _SwitchableBuyPointCameraState();
|
||
}
|
||
|
||
class _SwitchableBuyPointCameraState extends State<SwitchableBuyPointCamera> {
|
||
bool isQRMode = true;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final screenWidth = MediaQuery.of(context).size.width;
|
||
final screenHeight = MediaQuery.of(context).size.height;
|
||
final qrViewWidth = screenWidth * 2 / 3;
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
automaticallyImplyLeading: false,
|
||
title: Text("${widget.destination.sub_loc_id} : ${widget.destination.name}"),
|
||
),
|
||
body: SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
if (isQRMode)
|
||
Column(
|
||
children: [
|
||
SizedBox(height: screenHeight * 0.1),
|
||
Center(
|
||
child: SizedBox(
|
||
width: qrViewWidth,
|
||
height: qrViewWidth,
|
||
child: BuyPointCamera_QR(destination: widget.destination),
|
||
),
|
||
),
|
||
const Expanded(
|
||
child: Align(
|
||
alignment: Alignment.topCenter,
|
||
child: Padding(
|
||
padding: EdgeInsets.only(top: 16.0),
|
||
child: Text(
|
||
"岐阜ロゲQRコードにかざしてください。",
|
||
style: TextStyle(fontSize: 16),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
)
|
||
else
|
||
Positioned.fill(
|
||
child: BuyPointCamera(destination: widget.destination),
|
||
),
|
||
Positioned(
|
||
right: 16,
|
||
bottom: 16,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.7),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(isQRMode ? "カメラへ" : "QRへ"),
|
||
Switch(
|
||
value: isQRMode,
|
||
onChanged: (value) {
|
||
setState(() {
|
||
isQRMode = value;
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
class BuyPointCamera extends StatelessWidget {
|
||
BuyPointCamera({super.key, required this.destination});
|
||
|
||
Destination destination;
|
||
DestinationController destinationController =
|
||
Get.find<DestinationController>();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(8.0),
|
||
child: Center(
|
||
child: Obx(
|
||
() =>
|
||
Container(
|
||
width: MediaQuery
|
||
.of(context)
|
||
.size
|
||
.width,
|
||
height: 370,
|
||
decoration: BoxDecoration(
|
||
image: DecorationImage(
|
||
// 要修正:getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。
|
||
//
|
||
image: getReceiptImage(), fit: BoxFit.cover)),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
Padding(
|
||
padding: const EdgeInsets.all(8.0),
|
||
child: Text(getTagText(true, destination.tags)),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||
child: Wrap(
|
||
spacing: 16.0,
|
||
runSpacing: 8.0,
|
||
children: [
|
||
Obx(() =>
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
destinationController.openCamera(context, destination);
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
shape: const CircleBorder(),
|
||
padding: const EdgeInsets.all(20),
|
||
backgroundColor: destinationController.photos.isEmpty
|
||
? Colors.red
|
||
: Colors.grey[300],
|
||
),
|
||
child: destinationController.photos.isEmpty
|
||
? const Text("撮影",
|
||
style: TextStyle(color: Colors.white))
|
||
: const Text("再撮影",
|
||
style: TextStyle(color: Colors.black)),
|
||
)),
|
||
ElevatedButton(
|
||
onPressed: () async {
|
||
await destinationController.cancelBuyPoint(destination);
|
||
Navigator.of(Get.context!).pop();
|
||
if( destinationController.isInRog.value==true ) {
|
||
destinationController.rogainingCounted.value = true; // ロゲ開始後のみ許可
|
||
}
|
||
destinationController.skipGps = false;
|
||
destinationController.isPhotoShoot.value = false;
|
||
},
|
||
child: const Text("買い物なし")),
|
||
Obx(() =>
|
||
destinationController.photos.isNotEmpty
|
||
? ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.red),
|
||
onPressed: () async {
|
||
await destinationController.makeBuyPoint(
|
||
destination,
|
||
destinationController.photos[0].path);
|
||
Get.back();
|
||
destinationController.rogainingCounted.value = true;
|
||
destinationController.skipGps = false;
|
||
destinationController.isPhotoShoot.value = false;
|
||
Get.snackbar("お買い物加点を行いました",
|
||
"${destination.sub_loc_id} : ${destination.name}",
|
||
backgroundColor: Colors.green,
|
||
colorText: Colors.white);
|
||
},
|
||
child: const Text("完了",
|
||
style: TextStyle(color: Colors.white)))
|
||
: Container())
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
class BuyPointCamera_QR extends StatefulWidget {
|
||
final Destination destination;
|
||
|
||
const BuyPointCamera_QR({super.key, required this.destination});
|
||
|
||
@override
|
||
_BuyPointCamera_QRState createState() => _BuyPointCamera_QRState();
|
||
}
|
||
|
||
|
||
|
||
class _BuyPointCamera_QRState extends State<BuyPointCamera_QR> {
|
||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||
QRViewController? controller;
|
||
bool isQRScanned = false;
|
||
|
||
final DestinationController destinationController = Get.find<DestinationController>();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return QRView(
|
||
key: qrKey,
|
||
onQRViewCreated: _onQRViewCreated,
|
||
);
|
||
}
|
||
|
||
void _onQRViewCreated(QRViewController controller) {
|
||
this.controller = controller;
|
||
controller.scannedDataStream.listen((scanData) {
|
||
if (!isQRScanned && scanData.code != null && scanData.code!.startsWith('https://rogaining.sumasen.net/api/activate_buy_point/')) {
|
||
isQRScanned = true;
|
||
_processBuyPoint();
|
||
//_activateBuyPoint(scanData.code!);
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<String> getImageFilePathFromAssets(String assetPath) async {
|
||
final byteData = await rootBundle.load(assetPath);
|
||
final buffer = byteData.buffer;
|
||
Directory tempDir = await getTemporaryDirectory();
|
||
String tempPath = tempDir.path;
|
||
var filePath = '$tempPath/temp_qr_receipt.png';
|
||
return (await File(filePath).writeAsBytes(
|
||
buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)
|
||
)).path;
|
||
}
|
||
|
||
|
||
void _processBuyPoint() async {
|
||
// アセットの画像をテンポラリファイルにコピー
|
||
String predefinedImagePath = await getImageFilePathFromAssets('assets/images/QR_certificate.png');
|
||
|
||
try {
|
||
await destinationController.makeBuyPoint(widget.destination, predefinedImagePath);
|
||
Get.snackbar('成功', 'お買い物ポイントが有効化されました');
|
||
Navigator.of(context).pop();
|
||
} catch (e) {
|
||
Get.snackbar('エラー', 'お買い物ポイントの有効化に失敗しました');
|
||
} finally {
|
||
isQRScanned = false;
|
||
}
|
||
}
|
||
|
||
void _activateBuyPoint(String qrCode) async {
|
||
final IndexController indexController = Get.find<IndexController>();
|
||
|
||
final userId = indexController.currentUser[0]["user"]["id"];
|
||
final token = indexController.currentUser[0]["token"];
|
||
final teamName = indexController.currentUser[0]["user"]['team_name'];
|
||
final eventCode = indexController.currentUser[0]["user"]["event_code"];
|
||
//final cpNumber = destinationController.currentDestinationFeature[0].cp;
|
||
final cpNumber = widget.destination.cp;
|
||
|
||
try {
|
||
final response = await http.post(
|
||
Uri.parse('https://rogaining.sumasen.net/api/activate_buy_point/'),
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Token $token',
|
||
},
|
||
body: jsonEncode({
|
||
'user_id': userId,
|
||
'team_name': teamName,
|
||
'event_code': eventCode,
|
||
'cp_number': cpNumber,
|
||
'qr_code': qrCode,
|
||
}),
|
||
);
|
||
|
||
if (response.statusCode == 200) {
|
||
Get.snackbar('成功', 'お買い物ポイントが有効化されました');
|
||
Navigator.of(context).pop();
|
||
} else {
|
||
Get.snackbar('エラー', 'お買い物ポイントの有効化に失敗しました');
|
||
}
|
||
} catch (e) {
|
||
Get.snackbar('エラー', 'ネットワークエラーが発生しました');
|
||
} finally {
|
||
isQRScanned = false;
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
controller?.dispose();
|
||
super.dispose();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
class QRCodeScannerPage extends StatefulWidget {
|
||
|
||
QRCodeScannerPage({super.key, required this.destination});
|
||
|
||
Destination destination;
|
||
|
||
@override
|
||
_QRCodeScannerPageState createState() => _QRCodeScannerPageState();
|
||
}
|
||
|
||
class _QRCodeScannerPageState extends State<QRCodeScannerPage> {
|
||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||
QRViewController? controller;
|
||
|
||
@override
|
||
void dispose() {
|
||
controller?.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _onQRViewCreated(QRViewController controller) {
|
||
this.controller = controller;
|
||
controller.scannedDataStream.listen((scanData) {
|
||
// QRコードのデータを処理する
|
||
debugPrint("scan data = $scanData");
|
||
String? qrCodeData = scanData.code;
|
||
// qrCodeDataを使用してチェックポイントの処理を行う
|
||
// 例えば、qrCodeDataからCPのIDと店名を取得し、加点処理を行う
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
body: QRView(
|
||
key: qrKey,
|
||
onQRViewCreated: _onQRViewCreated,
|
||
),
|
||
);
|
||
}
|
||
} |