From edbf52825b6c17d6f2b08639fcfe8a09696fd91b Mon Sep 17 00:00:00 2001 From: Mohamed Nouffer Date: Mon, 1 Apr 2024 09:26:56 +0530 Subject: [PATCH] re factor rog --- android/app/build.gradle | 2 +- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/features/auth/login/login_page.dart | 71 +++ lib/features/data/checkpoint.dart | 397 +++++++++++++++++ lib/features/data/checkpoint_visitis.dart | 82 ++++ lib/features/data/data_provider.dart | 329 ++++++++++++++ lib/features/data/game_event.dart | 17 + lib/features/data/user.dart | 24 + lib/features/data/user_location.dart | 59 +++ lib/features/home/home_page.dart | 131 ++++++ lib/features/initializer/app_initializer.dart | 53 +++ lib/features/initializer/camera_check.dart | 19 + lib/features/initializer/icheck.dart | 17 + lib/features/initializer/location_check.dart | 19 + lib/features/initializer/network_check.dart | 10 + lib/features/landing/denied_screen.dart | 19 + lib/features/landing/landing_page.dart | 172 ++++++++ lib/features/location/location_provider.dart | 109 +++++ lib/features/services/auth_repo.dart | 14 + lib/features/services/auth_service.dart | 44 ++ lib/features/services/checkpoint_repo.dart | 32 ++ lib/features/services/checkpoint_service.dart | 27 ++ lib/features/state/game_state.dart | 98 +++++ lib/features/state/game_view_model.dart | 199 +++++++++ lib/features/state/user_state.dart | 18 + lib/features/utils/const.dart | 13 + lib/features/utils/distance_calculator.dart | 29 ++ lib/features/utils/error_reporter.dart | 17 + lib/features/utils/params.dart | 2 + lib/main.dart | 194 ++------- lib/main_mapsix.dart | 146 +++++++ lib/main_nrog.dart | 108 ++--- lib/nrog/pages/home_page.dart | 2 +- .../destination/destination_binding.dart | 2 +- .../destination/destination_controller.dart | 2 +- .../destination_map/destination_map_page.dart | 2 +- lib/pages/gps/gps_page.dart | 2 +- lib/pages/index/index_controller.dart | 22 +- lib/services/location_service.dart | 2 +- lib/widgets/base_layer_widget.dart | 42 +- lib/widgets/bottom_sheet_new.dart | 2 +- lib/widgets/map_widget.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- pubspec.lock | 412 ++++++++++++------ pubspec.yaml | 20 +- test/features/services/auth_service_test.dart | 1 + test/widget_test.dart | 29 -- .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 54 files changed, 2597 insertions(+), 435 deletions(-) create mode 100644 lib/features/auth/login/login_page.dart create mode 100644 lib/features/data/checkpoint.dart create mode 100644 lib/features/data/checkpoint_visitis.dart create mode 100644 lib/features/data/data_provider.dart create mode 100644 lib/features/data/game_event.dart create mode 100644 lib/features/data/user.dart create mode 100644 lib/features/data/user_location.dart create mode 100644 lib/features/home/home_page.dart create mode 100644 lib/features/initializer/app_initializer.dart create mode 100644 lib/features/initializer/camera_check.dart create mode 100644 lib/features/initializer/icheck.dart create mode 100644 lib/features/initializer/location_check.dart create mode 100644 lib/features/initializer/network_check.dart create mode 100644 lib/features/landing/denied_screen.dart create mode 100644 lib/features/landing/landing_page.dart create mode 100644 lib/features/location/location_provider.dart create mode 100644 lib/features/services/auth_repo.dart create mode 100644 lib/features/services/auth_service.dart create mode 100644 lib/features/services/checkpoint_repo.dart create mode 100644 lib/features/services/checkpoint_service.dart create mode 100644 lib/features/state/game_state.dart create mode 100644 lib/features/state/game_view_model.dart create mode 100644 lib/features/state/user_state.dart create mode 100644 lib/features/utils/const.dart create mode 100644 lib/features/utils/distance_calculator.dart create mode 100644 lib/features/utils/error_reporter.dart create mode 100644 lib/features/utils/params.dart create mode 100644 lib/main_mapsix.dart create mode 100644 test/features/services/auth_service_test.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index edac07b..9c24eef 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) { android { - compileSdkVersion 33 + compileSdkVersion 34 lintOptions { checkReleaseBuilds false diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 604a8c4..2dd7792 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826d..5e31d3d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + bool _isLoading = false; // Track loading state + + void _login() { + setState(() { + _isLoading = true; // Start loading + }); + String email = _emailController.text.trim(); + String password = _passwordController.text; + + ref.read(authProvider("$email|$password").future).then((user) { + setState(() { + _isLoading = false; // Stop loading + }); + if (user != null) { + ref.read(userNotifierProvider.notifier).setUser(user); + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => const HomePage(), + )); + } else { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Login failed'))); + } + }).catchError((error) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Error: $error'))); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Login')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextFormField( + controller: _emailController, + decoration: const InputDecoration(labelText: 'Email'), + ), + TextFormField( + controller: _passwordController, + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + ), + _isLoading + ? CircularProgressIndicator() + : ElevatedButton( + onPressed: _login, + child: const Text('Login'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/data/checkpoint.dart b/lib/features/data/checkpoint.dart new file mode 100644 index 0000000..57b0726 --- /dev/null +++ b/lib/features/data/checkpoint.dart @@ -0,0 +1,397 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +class FeatureCollection { + final String type; + final List features; + + FeatureCollection({required this.type, required this.features}); + + factory FeatureCollection.fromJson(Map json) { + return FeatureCollection( + type: json['type'], + features: + List.from(json['features'].map((x) => Feature.fromJson(x))), + ); + } +} + +class Feature { + final int id; + final String type; + final Geometry geometry; + final Properties properties; + + Feature( + {required this.id, + required this.type, + required this.geometry, + required this.properties}); + + factory Feature.fromJson(Map json) { + return Feature( + id: json['id'], + type: json['type'], + geometry: Geometry.fromJson(json['geometry']), + properties: Properties.fromJson(json['properties']), + ); + } + + String toJson() => json.encode(toMap()); + + Map toMap() { + return { + 'id': id, + 'type': type, + 'geometry': geometry.toMap(), // Assuming Geometry has a toMap method + 'properties': + properties.toMap(), // Assuming Properties has a toMap method + }; + } + + Feature.fromMap(Map map) + : id = map['id'], + type = map['type'], + geometry = Geometry.fromMap(map['geometry']), + properties = Properties.fromJson(map['properties']); + + static empty() {} +} + +class Geometry { + final String type; + final List> coordinates; + + Geometry({required this.type, required this.coordinates}); + + Geometry copyWith({ + String? type, + List>? coordinates, + }) { + return Geometry( + type: type ?? this.type, + coordinates: coordinates ?? this.coordinates, + ); + } + + Map toMap() { + return { + 'type': type, + 'coordinates': coordinates, + }; + } + + factory Geometry.fromMap(Map map) { + return Geometry( + type: map['type'] as String, + coordinates: List>.from( + (map['coordinates'] as List).map>( + (coordinateList) { + // Ensure 'coordinateList' is treated as a List of dynamic elements. + // Then, for each element in the list, explicitly cast or convert it to double. + return List.from(coordinateList.map((coordinate) { + // 'coordinate' is dynamic, needs explicit conversion to double. + // The conversion depends on the original data type of 'coordinate'; + // if it's already a number, you can use .toDouble(); if it's a String, you might need double.parse. + return double.parse(coordinate.toString()); + })); + }, + ), + ), + ); + } + + String toJson() => json.encode(toMap()); + + factory Geometry.fromJson(Map source) { + final geom = Geometry.fromMap(source); + return geom; + } + + get latitude => coordinates[0][1]; + get longitude => coordinates[0][0]; +} + +class Properties { + // Include all properties fields + final int locationId; + final String subLocId; + final double cp; + final String? name; + final String? address; + final String? phone; + final String? email; + final String? webcontents; + final String? videos; + final String? category; + final int? series; + final double? lat; + final double? lon; + final int? listOrder; + final String? photos; + final double? checkinRadious; + final bool? autoCheckin; + bool? selected = false; + bool? checkedin = false; + final double? checkinPoint; + final double? buyPoint; + final bool? hiddenLocation; + String? checkinImage; + String? buypointImage; + bool? forcedCheckin = false; + final String? tags; + + Properties( + {required this.locationId, + required this.subLocId, + required this.cp, + this.name, + this.address, + this.phone, + this.email, + this.webcontents, + this.videos, + this.category, + this.series, + this.lat, + this.lon, + this.listOrder, + this.photos, + this.checkinRadious, + this.autoCheckin, + this.selected, + this.checkedin, + this.checkinPoint, + this.buyPoint, + this.hiddenLocation, + this.checkinImage, + this.buypointImage, + this.forcedCheckin, + this.tags}); + + factory Properties.fromJson(Map json) { + Properties prop; + try { + prop = Properties( + locationId: json['location_id'], + subLocId: json['sub_loc_id'], + cp: json['cp'], + name: json['name'] ?? '', + address: json['address'] ?? '', + phone: json['phone'] ?? '', + email: json['email'] ?? '', + webcontents: json['webcontents'] ?? '', + videos: json['videos'] ?? '', + category: json['category'] ?? '', + series: json['series'] ?? 0, + lat: json['lat'] ?? 0.0, + lon: json['lon'] ?? 0.0, + listOrder: json['list_order'] ?? 0, + photos: json['photos'] ?? '', + checkinRadious: json['checkin_radious'] ?? 0.0, + autoCheckin: json['auto_checkin'] == 0 ? false : true, + selected: json['selected'] == 0 ? false : true, + checkedin: json['checkedin'] == 0 ? false : true, + checkinPoint: json['checkin_point'] ?? 0.0, + buyPoint: json['buy_point'] ?? 0.0, + hiddenLocation: json['hidden_location'] == 0 ? false : true, + checkinImage: json['checkin_image'] ?? '', + buypointImage: json['buypoint_image'] ?? '', + forcedCheckin: json['forced_checkin'] == 0 ? false : true, + tags: json['tags'] ?? ''); + //print("from json --- $prop"); + } catch (e) { + //print("from json --- $e"); + return Properties(locationId: 0, subLocId: '', cp: 0.0, name: ''); + } + return prop; + } + + factory Properties.toJson(Properties prop) { + return Properties( + locationId: prop.locationId, + subLocId: prop.subLocId, + cp: prop.cp, + name: prop.name, + address: prop.address, + phone: prop.phone, + email: prop.email, + webcontents: prop.webcontents, + videos: prop.videos, + category: prop.category, + series: prop.series, + lat: prop.lat, + lon: prop.lon, + listOrder: prop.listOrder, + photos: prop.photos, + checkinRadious: prop.checkinRadious, + autoCheckin: prop.autoCheckin, + selected: prop.selected, + checkedin: prop.checkedin, + checkinPoint: prop.checkinPoint, + buyPoint: prop.buyPoint, + hiddenLocation: prop.hiddenLocation, + checkinImage: prop.checkinImage, + buypointImage: prop.buypointImage, + forcedCheckin: prop.forcedCheckin, + tags: prop.tags); + } + + Properties copyWith({ + int? locationId, + String? subLocId, + double? cp, + String? name, + String? address, + String? phone, + String? email, + String? webcontents, + String? videos, + String? category, + int? series, + double? lat, + double? lon, + int? listOrder, + String? photos, + double? checkinRadious, + bool? autoCheckin, + bool? selected, + bool? checkedin, + double? checkinPoint, + double? buyPoint, + bool? hiddenLocation, + String? checkinImage, + String? buypointImage, + bool? forcedCheckin, + String? tags, + }) { + return Properties( + locationId: locationId ?? this.locationId, + subLocId: subLocId ?? this.subLocId, + cp: cp ?? this.cp, + name: name ?? this.name, + address: address ?? this.address, + phone: phone ?? this.phone, + email: email ?? this.email, + webcontents: webcontents ?? this.webcontents, + videos: videos ?? this.videos, + category: category ?? this.category, + series: series ?? this.series, + lat: lat ?? this.lat, + lon: lon ?? this.lon, + listOrder: listOrder ?? this.listOrder, + photos: photos ?? this.photos, + checkinRadious: checkinRadious ?? this.checkinRadious, + autoCheckin: autoCheckin ?? this.autoCheckin, + selected: selected ?? this.selected, + checkedin: checkedin ?? this.checkedin, + checkinPoint: checkinPoint ?? this.checkinPoint, + buyPoint: buyPoint ?? this.buyPoint, + hiddenLocation: hiddenLocation ?? this.hiddenLocation, + checkinImage: checkinImage ?? this.checkinImage, + buypointImage: buypointImage ?? this.buypointImage, + forcedCheckin: forcedCheckin ?? this.forcedCheckin, + tags: tags ?? this.tags, + ); + } + + Map toMap() { + return { + 'locationId': locationId, + 'subLocId': subLocId, + 'cp': cp, + 'name': name, + 'address': address, + 'phone': phone, + 'email': email, + 'webcontents': webcontents, + 'videos': videos, + 'category': category, + 'series': series, + 'lat': lat, + 'lon': lon, + 'listOrder': listOrder, + 'photos': photos, + 'checkinRadious': checkinRadious, + 'autoCheckin': autoCheckin, + 'selected': selected, + 'checkedin': checkedin, + 'checkinPoint': checkinPoint, + 'buyPoint': buyPoint, + 'hiddenLocation': hiddenLocation, + 'checkinImage': checkinImage, + 'buypointImage': buypointImage, + 'forcedCheckin': forcedCheckin, + 'tags': tags, + }; + } + + String toJson() => json.encode(toMap()); + + @override + String toString() { + return 'Properties(locationId: $locationId, subLocId: $subLocId, cp: $cp, name: $name, address: $address, phone: $phone, email: $email, webcontents: $webcontents, videos: $videos, category: $category, series: $series, lat: $lat, lon: $lon, listOrder: $listOrder, photos: $photos, checkinRadious: $checkinRadious, autoCheckin: $autoCheckin, selected: $selected, checkedin: $checkedin, checkinPoint: $checkinPoint, buyPoint: $buyPoint, hiddenLocation: $hiddenLocation, checkinImage: $checkinImage, buypointImage: $buypointImage, forcedCheckin: $forcedCheckin, tags: $tags)'; + } + + @override + bool operator ==(covariant Properties other) { + if (identical(this, other)) return true; + + return other.locationId == locationId && + other.subLocId == subLocId && + other.cp == cp && + other.name == name && + other.address == address && + other.phone == phone && + other.email == email && + other.webcontents == webcontents && + other.videos == videos && + other.category == category && + other.series == series && + other.lat == lat && + other.lon == lon && + other.listOrder == listOrder && + other.photos == photos && + other.checkinRadious == checkinRadious && + other.autoCheckin == autoCheckin && + other.selected == selected && + other.checkedin == checkedin && + other.checkinPoint == checkinPoint && + other.buyPoint == buyPoint && + other.hiddenLocation == hiddenLocation && + other.checkinImage == checkinImage && + other.buypointImage == buypointImage && + other.forcedCheckin == forcedCheckin && + other.tags == tags; + } + + @override + int get hashCode { + return locationId.hashCode ^ + subLocId.hashCode ^ + cp.hashCode ^ + name.hashCode ^ + address.hashCode ^ + phone.hashCode ^ + email.hashCode ^ + webcontents.hashCode ^ + videos.hashCode ^ + category.hashCode ^ + series.hashCode ^ + lat.hashCode ^ + lon.hashCode ^ + listOrder.hashCode ^ + photos.hashCode ^ + checkinRadious.hashCode ^ + autoCheckin.hashCode ^ + selected.hashCode ^ + checkedin.hashCode ^ + checkinPoint.hashCode ^ + buyPoint.hashCode ^ + hiddenLocation.hashCode ^ + checkinImage.hashCode ^ + buypointImage.hashCode ^ + forcedCheckin.hashCode ^ + tags.hashCode; + } +} diff --git a/lib/features/data/checkpoint_visitis.dart b/lib/features/data/checkpoint_visitis.dart new file mode 100644 index 0000000..077b65f --- /dev/null +++ b/lib/features/data/checkpoint_visitis.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; + +class CheckpointVisit { + final String id; + final String checkpointId; + final String teamName; + final String userName; + final String eventCode; + final DateTime visitTime; + + CheckpointVisit({ + required this.id, + required this.checkpointId, + required this.teamName, + required this.userName, + required this.eventCode, + required this.visitTime, + }); + + CheckpointVisit copyWith({ + String? id, + String? checkpointId, + String? teamName, + String? userName, + String? eventCode, + DateTime? visitTime, + }) { + return CheckpointVisit( + id: id ?? this.id, + checkpointId: checkpointId ?? this.checkpointId, + teamName: teamName ?? this.teamName, + userName: userName ?? this.userName, + eventCode: eventCode ?? this.eventCode, + visitTime: visitTime ?? this.visitTime, + ); + } + + Map toMap() { + return { + 'id': id, + 'checkpointId': checkpointId, + 'teamName': teamName, + 'userName': userName, + 'eventCode': eventCode, + 'visitTime': visitTime.millisecondsSinceEpoch, + }; + } + + factory CheckpointVisit.fromMap(Map map) { + return CheckpointVisit( + id: map['id'], + checkpointId: map['checkpointId'], + teamName: map['teamName'], + userName: map['userName'], + eventCode: map['eventCode'], + visitTime: DateTime.fromMillisecondsSinceEpoch(map['visitTime']), + ); + } + + String toJson() => json.encode(toMap()); + + factory CheckpointVisit.fromJson(String source) => + CheckpointVisit.fromMap(json.decode(source)); + + @override + String toString() { + return 'CheckpointVisit(id: $id, checkpointId: $checkpointId, teamName: $teamName, userName: $userName, eventCode: $eventCode, visitTime: $visitTime)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CheckpointVisit && + other.id == id && + other.checkpointId == checkpointId && + other.teamName == teamName && + other.userName == userName && + other.eventCode == eventCode && + other.visitTime == visitTime; + } +} diff --git a/lib/features/data/data_provider.dart b/lib/features/data/data_provider.dart new file mode 100644 index 0000000..aad1739 --- /dev/null +++ b/lib/features/data/data_provider.dart @@ -0,0 +1,329 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:path/path.dart'; +import 'package:rogapp/features/data/checkpoint.dart'; +import 'package:rogapp/features/data/user_location.dart'; + +final dataProviderProvider = Provider((ref) { + return DataProvider(); +}); + +// Convert a Feature object to a Map +Map featureToMap(Feature feature) { + var properties = feature.properties; + + try { + return { + 'id': feature.id, + 'type': feature.type, + 'name': properties.name ?? '', + 'address': properties.address ?? '', + 'phone': properties.phone ?? '', + 'email': properties.email ?? '', + 'webcontents': properties.webcontents ?? '', + 'videos': properties.videos ?? '', + 'category': properties.category ?? '', + 'series': properties.series, + 'lat': feature.geometry.latitude, + 'lon': feature.geometry.longitude, + 'list_order': properties.listOrder, + 'photos': properties.photos ?? '', + 'checkin_radious': properties.checkinRadious, + 'sub_loc_id': properties.subLocId, + 'auto_checkin': properties.autoCheckin! ? 1 : 0, + 'selected': properties.selected == null + ? 0 + : properties.selected! + ? 1 + : 0, + 'checkedin': properties.checkedin == null + ? 0 + : properties.checkedin! + ? 1 + : 0, + 'cp': properties.cp, + 'checkin_point': properties.checkinPoint, + 'buy_point': properties.buyPoint, + 'hidden_location': properties.hiddenLocation, + 'checkin_image': properties.checkinImage, + 'buypoint_image': properties.buypointImage, + 'forced_checkin': properties.forcedCheckin == null + ? 0 + : properties.forcedCheckin! + ? 1 + : 0, + 'tags': properties.tags ?? '', + 'location_id': properties.locationId, + }; + } catch (e) { + //print("--- feature to map Error: $e"); + return {}; + } +} + +// Convert a Map to a Feature object +Feature mapToFeature(Map map) { + Feature feat; + //print("mapToFeature id --- ${Properties.fromJson(map)}"); + //print("mapToFeature --- ${map['type']}"); + feat = Feature( + id: map['id'], + type: map['type'], + geometry: Geometry( + type: map['type'] ?? 'Point', + coordinates: [ + [map['lon'], map['lat']] + ], + ), + properties: Properties.fromJson(map), + ); + + //print("mapToFeature --- $feat"); + + return feat; +} + +class DataProvider { + static Database? _database; + + Future get database async { + if (_database != null) return _database!; + _database = await initDB(); + return _database!; + } + + initDB() async { + var documentsDirectory = await getApplicationDocumentsDirectory(); + String path = join(documentsDirectory.path, "GameDB.db"); + return await openDatabase(path, + version: 1, onOpen: (db) {}, onCreate: _onCreate); + } + + _onCreate(Database db, int version) async { + await db.execute(''' + CREATE TABLE Checkpoints ( + location_id INTEGER PRIMARY KEY, + id INTEGER, + type TEXT, + name TEXT, + address TEXT, + phone TEXT, + email TEXT, + webcontents TEXT, + videos TEXT, + category TEXT, + series INTEGER, + lat REAL, + lon REAL, + list_order INTEGER, + photos TEXT, + checkin_radious REAL, + sub_loc_id TEXT, + auto_checkin INTEGER, + selected INTEGER, + checkedin INTEGER, + cp REAL, + checkin_point REAL, + buy_point REAL, + hidden_location INTEGER, + checkin_image TEXT, + buypoint_image TEXT, + forced_checkin INTEGER, + recipt_times INTEGER, + tags TEXT + ) + '''); + await db.execute(''' + CREATE TABLE GPSLocations ( + timestamp TEXT PRIMARY KEY, + latitude REAL, + longitude REAL, + event_code TEXT, + team_name TEXT, + user_name TEXT, + attempt_count INTEGER + ) + '''); + await db.execute(''' + CREATE TABLE GameState ( + key TEXT PRIMARY KEY, + value TEXT, + event_code TEXT, + team_name TEXT, + user_name TEXT, + attempt_count INTEGER + ) + '''); + await db.execute(''' + CREATE TABLE CheckpointVisits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + checkpointId INTEGER, + photoTaken BOOLEAN, + receiptPhotoTaken BOOLEAN, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + event_code TEXT, + team_name TEXT, + user_name TEXT, + attempt_count INTEGER DEFAULT 1, + FOREIGN KEY (checkpointId) REFERENCES Checkpoints(location_id) + ) + '''); + } + + Future recordCheckpointVisit( + int locationId, String userName, String teamName, String eventCode, + {bool photoTaken = false, bool? receiptPhotoTaken}) async { + final db = await database; + + // Fetching buy_point to determine if a receipt is required + var checkpointQueryResult = await db.query( + 'Checkpoints', + columns: ['buy_point'], + where: 'location_id = ?', + whereArgs: [locationId], + ); + + var buyPoint = checkpointQueryResult.isNotEmpty + ? checkpointQueryResult.first['buy_point'] as double? + : null; + var requiresReceipt = buyPoint != null && buyPoint > 0.0; + + await db.insert( + 'CheckpointVisits', + { + 'checkpointId': locationId, + 'user_name': userName, + 'team_name': teamName, + 'event_code': eventCode, + 'photoTaken': photoTaken ? 1 : 0, + 'receiptPhotoTaken': + requiresReceipt ? (receiptPhotoTaken == true ? 1 : 0) : null, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future> getIncompleteTasks(int locationId, + String userName, String teamName, String eventCode) async { + final db = await database; + var checkpoint = await db.query( + 'Checkpoints', + where: 'location_id = ?', + whereArgs: [locationId], + ); + + var buyPointValue = + checkpoint.isNotEmpty ? checkpoint.first['buy_point'] as double? : null; + var requiresReceipt = buyPointValue != null && buyPointValue > 0.0; + + // Fetch visits considering all relevant identifiers + List visits = await db.query( + 'CheckpointVisits', + where: + 'checkpointId = ? AND user_name = ? AND team_name = ? AND event_code = ?', + whereArgs: [locationId, userName, teamName, eventCode], + ); + + if (visits.isNotEmpty) { + var lastVisit = visits.last; + return { + 'photoTaken': lastVisit['photoTaken'] == 1, + 'receiptPhotoTaken': + requiresReceipt ? lastVisit['receiptPhotoTaken'] == 1 : true, + }; + } + + // Default return values when no visits found + return { + 'photoTaken': false, + 'receiptPhotoTaken': requiresReceipt ? false : true, + }; + } + + Future insertMarker(Feature marker) async { + //print("--- inserting ${featureToMap(marker)} ---"); + final db = await database; + try { + await db.insert( + 'Checkpoints', + featureToMap(marker), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } catch (e) { + print("--- ${e.toString()} ---"); + } + } + + Future> getMarkers() async { + final db = await database; + var res = await db.query('Checkpoints'); + List list = + res.isNotEmpty ? res.map((c) => mapToFeature(c)).toList() : []; + //print("--- checkpoints ${res} ---"); + return list; + } + + Future deleteMarker(int id) async { + final db = await database; + return db.delete('Checkpoints', where: 'id = ?', whereArgs: [id]); + } + + Future insertGPSLocation(UserLocation location) async { + final db = await database; + await db.insert( + 'GPSLocations', + location + .toMap(), // Implement a method in UserLocation to convert the object to a Map + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future> getGPSLocations() async { + final db = await database; + var res = await db.query('GPSLocations'); + List locations = + res.isNotEmpty ? res.map((c) => UserLocation.fromMap(c)).toList() : []; + return locations; + } + + Future updateGameState( + String key, + String value, + String userName, + String teamName, + String eventCode, + ) async { + final db = await database; + await db.insert( + 'GameState', + { + 'key': key, + 'value': value, + 'team_name': teamName, + 'event_code': eventCode, + 'user_name': userName, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future getGameState( + String key, + String userName, + String teamName, + String eventCode, + ) async { + final db = await database; + List> results = await db.query( + 'GameState', + where: 'key = ? AND team_name = ? AND event_code = ? AND user_name = ?', + whereArgs: [key, teamName, eventCode, userName], + ); + + if (results.isNotEmpty) { + return results.first['value'] as String?; + } + return null; + } +} diff --git a/lib/features/data/game_event.dart b/lib/features/data/game_event.dart new file mode 100644 index 0000000..b9ef4bf --- /dev/null +++ b/lib/features/data/game_event.dart @@ -0,0 +1,17 @@ +enum EventType { + atStart, + starting, + started, + inCheckin, + checkiningIn, + checkedIn, + atGoal, + finishingGoal, + finishedGoal, +} + +class GameEvent { + final EventType type; + final DateTime timestamp; + GameEvent({this.type = EventType.atStart, required this.timestamp}); +} diff --git a/lib/features/data/user.dart b/lib/features/data/user.dart new file mode 100644 index 0000000..3e55c97 --- /dev/null +++ b/lib/features/data/user.dart @@ -0,0 +1,24 @@ +class User { + User.User(); + + //AuthUser.from({required this.id, required this.email, required this.is_rogaining, required this.group, required this.zekken_number, required this.event_code, required this.team_name}); + + User.fromMap(Map map) + : id = int.parse(map["id"].toString()), + email = map["email"].toString(), + is_rogaining = bool.parse(map["is_rogaining"].toString()), + group = map["group"].toString(), + zekken_number = map["zekken_number"].toString(), + event_code = map["event_code"].toString(), + team_name = map["team_name"].toString(), + auth_token = map["token"]; + + int? id; + String? email; + bool? is_rogaining; + String? group; + String? zekken_number; + String? event_code; + String? team_name; + String? auth_token; +} diff --git a/lib/features/data/user_location.dart b/lib/features/data/user_location.dart new file mode 100644 index 0000000..d074b6c --- /dev/null +++ b/lib/features/data/user_location.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:geolocator/geolocator.dart'; + +// ignore_for_file: public_member_api_docs, sort_constructors_first +class UserLocation { + final double latitude; + final double longitude; + final DateTime timestamp; + UserLocation( + {required this.latitude, + required this.longitude, + required this.timestamp}); + + static UserLocation empty() { + return UserLocation( + latitude: 0.0, longitude: 0.0, timestamp: DateTime.now()); + } + + factory UserLocation.fromPosition(Position position) { + return UserLocation( + latitude: position.latitude, + longitude: position.longitude, + timestamp: position.timestamp); + } + + UserLocation copyWith({ + double? latitude, + double? longitude, + DateTime? timestamp, + }) { + return UserLocation( + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + timestamp: timestamp ?? this.timestamp, + ); + } + + Map toMap() { + return { + 'latitude': latitude, + 'longitude': longitude, + 'timestamp': timestamp.millisecondsSinceEpoch, + }; + } + + factory UserLocation.fromMap(Map map) { + return UserLocation( + latitude: map['latitude'] as double, + longitude: map['longitude'] as double, + timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp'] as int), + ); + } + + String toJson() => json.encode(toMap()); + + factory UserLocation.fromJson(String source) => + UserLocation.fromMap(json.decode(source) as Map); +} diff --git a/lib/features/home/home_page.dart b/lib/features/home/home_page.dart new file mode 100644 index 0000000..dc12579 --- /dev/null +++ b/lib/features/home/home_page.dart @@ -0,0 +1,131 @@ +import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_cache/flutter_map_cache.dart'; +import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:rogapp/features/location/location_provider.dart'; +import 'package:rogapp/features/state/game_state.dart'; +import 'package:rogapp/features/state/game_view_model.dart'; +import 'package:path_provider/path_provider.dart'; + +class HomePage extends ConsumerStatefulWidget { + const HomePage({super.key}); + + @override + ConsumerState createState() => _HomePageState(); +} + +class _HomePageState extends ConsumerState { + MapController mapController = MapController(); + String? _cachePath; + + @override + void initState() { + super.initState(); + //ref.read(featureCheckpointCollectionProvider.future); + ref.read(locationNotifierProvider.notifier).startListening(); + _initializeCachePath(); + } + + Future _initializeCachePath() async { + _cachePath = await getTemporaryDirectory().then((dir) => dir.path); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final gameState = ref.watch(gameViewModelProvider); + final locationState = ref.watch(locationNotifierProvider); + + if (_cachePath == null || gameState.markers.isEmpty) { + return Scaffold( + appBar: AppBar(), + body: Center(child: CircularProgressIndicator()), + ); + } + return Scaffold( + appBar: AppBar( + actions: [ + Text( + 'Current Latitude: ${locationState.currentPosition?.latitude ?? 'No data'}'), + IconButton( + icon: Icon(MdiIcons.logout), + onPressed: () => _logGameState(gameState), + ), + Text(gameState.currentLocation?.latitude.toString() ?? + '--- No location'), + ], + ), + body: Column( + children: [ + Expanded(child: _buildMap(gameState, _cachePath!)), + ], + ), + ); + } + + void _logGameState(GameState gameState) { + print("Checkpoint photo taken: ${gameState.checkpointPhotoTaken}"); + print("Current checkpoint: ${gameState.currentCheckpoint}"); + print("Has left start area: ${gameState.hasLeftStartArea}"); + print("Has visited checkpoint: ${gameState.hasVisitedCheckpoint}"); + } + + Widget _buildMap(GameState gameState, String cachePath) { + return FlutterMap( + mapController: mapController, + options: MapOptions( + interactionOptions: const InteractionOptions( + enableMultiFingerGestureRace: true, + flags: InteractiveFlag.doubleTapDragZoom | + InteractiveFlag.doubleTapZoom | + InteractiveFlag.drag | + InteractiveFlag.flingAnimation | + InteractiveFlag.pinchZoom | + InteractiveFlag.scrollWheelZoom, + ), + maxZoom: 18.4, + onMapReady: () => mapController.move(LatLng(37.153196, 139.587659), 4), + ), + children: [ + TileLayer( + urlTemplate: + 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', + tileProvider: CachedTileProvider( + maxStale: Duration(days: 30), + store: HiveCacheStore(cachePath, hiveBoxName: 'HiveCacheStore'), + ), + ), + CurrentLocationLayer( + alignDirectionOnUpdate: AlignOnUpdate.never, + style: const LocationMarkerStyle( + marker: DefaultLocationMarker( + child: Icon( + Icons.navigation, + color: Colors.white, + size: 15, + ), + ), + markerSize: Size(27, 27), + markerDirection: MarkerDirection.heading, + ), + ), + MarkerLayer( + markers: gameState.markers.map((feature) { + return Marker( + child: const Icon(Icons.location_on), + // Convert each Feature to a Marker + width: 80.0, + height: 80.0, + point: + LatLng(feature.geometry.latitude, feature.geometry.longitude), + ); + }).toList(), + ), + ], + ); + } +} diff --git a/lib/features/initializer/app_initializer.dart b/lib/features/initializer/app_initializer.dart new file mode 100644 index 0000000..0b267e3 --- /dev/null +++ b/lib/features/initializer/app_initializer.dart @@ -0,0 +1,53 @@ +import 'package:rogapp/features/initializer/camera_check.dart'; +import 'package:rogapp/features/initializer/icheck.dart'; +import 'package:rogapp/features/initializer/location_check.dart'; + +class AppInitializer { + final List checks; + + AppInitializer() + : checks = [ + LocationCheck(), + CameraCheck(), + //NetworkCheck(), + ]; + + Future permissionStatus() async { + return await LocationCheck().locationPermissionStatus() && + await CameraCheck().cameraPermissionStatus(); + } + + Future initializeApp() async { + if (!await runPreFlightChecks()) { + return false; + } + + //await initializeTileCaching(); + await loadInitialData(); + await configureGlobalSettings(); + // Add other initialization tasks as needed + + return true; + } + + Future runPreFlightChecks() async { + for (var check in checks) { + if (!await check.check()) { + return false; + } + } + return true; + } + + Future initializeTileCaching() async {} + + Future loadInitialData() async { + // Logic to preload data that your app will need + } + + Future configureGlobalSettings() async { + // Logic to set up any global configurations for your app + } + + // You can add more methods for other initialization tasks +} diff --git a/lib/features/initializer/camera_check.dart b/lib/features/initializer/camera_check.dart new file mode 100644 index 0000000..3a767dc --- /dev/null +++ b/lib/features/initializer/camera_check.dart @@ -0,0 +1,19 @@ +import 'package:permission_handler/permission_handler.dart'; +import 'package:rogapp/features/initializer/icheck.dart'; + +class CameraCheck implements ICameraCheck { + @override + Future cameraPermissionStatus() async { + var permission = await Permission.camera.status; + return permission.isGranted; + } + + @override + Future check() async { + var permission = await Permission.camera.status; + if (!permission.isGranted) { + permission = await Permission.camera.request(); + } + return permission.isGranted; + } +} diff --git a/lib/features/initializer/icheck.dart b/lib/features/initializer/icheck.dart new file mode 100644 index 0000000..134047c --- /dev/null +++ b/lib/features/initializer/icheck.dart @@ -0,0 +1,17 @@ +abstract class ICheck { + Future check(); +} + +abstract class ILocationCheck extends ICheck { + Future locationPermissionStatus(); + // Additional location-specific methods can be defined here +} + +abstract class ICameraCheck extends ICheck { + Future cameraPermissionStatus(); + // Additional camera-specific methods can be defined here +} + +abstract class INetworkCheck extends ICheck { + // Additional network-specific methods can be defined here +} diff --git a/lib/features/initializer/location_check.dart b/lib/features/initializer/location_check.dart new file mode 100644 index 0000000..16c25a0 --- /dev/null +++ b/lib/features/initializer/location_check.dart @@ -0,0 +1,19 @@ +import 'package:permission_handler/permission_handler.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:rogapp/features/initializer/icheck.dart'; + +class LocationCheck implements ILocationCheck { + @override + Future check() async { + var permission = await Permission.locationWhenInUse.status; + if (!permission.isGranted) { + permission = await Permission.locationWhenInUse.request(); + } + return permission.isGranted && await Geolocator.isLocationServiceEnabled(); + } + + @override + Future locationPermissionStatus() { + return Permission.locationWhenInUse.isGranted; + } +} diff --git a/lib/features/initializer/network_check.dart b/lib/features/initializer/network_check.dart new file mode 100644 index 0000000..2ad4b61 --- /dev/null +++ b/lib/features/initializer/network_check.dart @@ -0,0 +1,10 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:rogapp/features/initializer/icheck.dart'; + +class NetworkCheck implements INetworkCheck { + @override + Future check() async { + var connectivityResult = await Connectivity().checkConnectivity(); + return connectivityResult != ConnectivityResult.none; + } +} diff --git a/lib/features/landing/denied_screen.dart b/lib/features/landing/denied_screen.dart new file mode 100644 index 0000000..9e81580 --- /dev/null +++ b/lib/features/landing/denied_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +// This screen shows when user denied and intented to close the app + +class DeniedScreen extends StatefulWidget { + const DeniedScreen({super.key}); + + @override + State createState() => _DeniedScreenState(); +} + +class _DeniedScreenState extends State { + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Text("Dnied"), + ); + } +} diff --git a/lib/features/landing/landing_page.dart b/lib/features/landing/landing_page.dart new file mode 100644 index 0000000..da03ec4 --- /dev/null +++ b/lib/features/landing/landing_page.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rogapp/features/data/checkpoint.dart'; +import 'package:rogapp/features/data/data_provider.dart'; +import 'package:rogapp/features/home/home_page.dart'; +import 'package:rogapp/features/landing/denied_screen.dart'; +import 'package:rogapp/features/initializer/app_initializer.dart'; +import 'package:rogapp/features/services/checkpoint_repo.dart'; +import 'package:rogapp/features/state/game_state.dart'; +import 'package:rogapp/features/state/game_view_model.dart'; + +// LandingPage serves as the initial screen displayed by the application to perform necessary checks, initialize required values, and set up services essential for the app's functionality. + +class LandingPage extends ConsumerStatefulWidget { + const LandingPage({super.key}); + + @override + ConsumerState createState() => _LandingPageState(); +} + +class _LandingPageState extends ConsumerState { + bool? _isInitialized; + + @override + void initState() { + super.initState(); + //load only if network available + ref.read(featureCheckpointCollectionProvider.future); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeApp(); + }); + } + + @override + Widget build(BuildContext context) { + AsyncValue featureCollection = + ref.watch(featureCheckpointCollectionProvider); + + return featureCollection.when( + data: (data) { + if (_isInitialized == null) { + return const MaterialApp( + home: Scaffold(body: Center(child: CircularProgressIndicator()))); + } + + return _isInitialized! ? const HomePage() : const DeniedScreen(); + }, + loading: () => const MaterialApp( + home: Scaffold(body: Center(child: CircularProgressIndicator()))), + error: (error, stack) => ErrorScreen(error: error), + ); + } + + Future _restoreAppState() async { + final dataProvider = ref.read(dataProviderProvider); + + // Example values, replace these with actual values from user and event context + String userName = + "current_user"; // Fetch the current user's name or identifier + String teamName = "user_team"; // Fetch the current user's team + String eventCode = "event_code"; // Fetch the current event code + + // Load and restore game state + String? gameStateKey = "current_game_state"; + String? gameStateValue = await dataProvider.getGameState( + gameStateKey, userName, teamName, eventCode); + if (gameStateValue != null) { + Map gameStateData = jsonDecode(gameStateValue); + GameState gameState = GameState.fromMap(gameStateData); + ref.read(gameViewModelProvider.notifier).loadGameState(gameState); + } + + // After restoring all necessary states, proceed with the app + setState(() { + _isInitialized = true; + }); + } + + // Future _restoreAppState() async { + // await _initializeApp(); + // // Add additional restoration logic if needed, for example: + // // Restore game state, user progress, etc., from the DataProvider. + // // Example: + // // var gameState = await _dataProvider.getGameState('current_state'); + // // Use the gameState to decide what to do next, e.g., navigate to a specific screen. + + // if (_isInitialized != true) { + // // If initialization is not done, try to initialize the app. + // _isInitialized = await _initializeApp(); + // } + + // if (!_isInitialized!) { + // await _showRetryCloseDialog(); + // } else { + // setState(() { + // _isInitialized = true; + // }); + // } + // } + + Future _initializeApp() async { + AppInitializer initializer = AppInitializer(); + + // Check if permissions are already granted + bool permissionsAlreadyGranted = await initializer.permissionStatus(); + if (!permissionsAlreadyGranted) { + await _showPermissionRequestMessage(); + } + + // Initialize the app and return the success status. + return await initializer.initializeApp(); + } + + Future _showPermissionRequestMessage() async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => AlertDialog( + title: const Text('Permissions Required'), + content: const Text( + 'This app requires certain permissions to function properly. Please grant these permissions in the next steps.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Okay'), + ), + ], + ), + ); + } + + Future _requestPermissionsAndInitialize() async { + AppInitializer initializer = AppInitializer(); + return await initializer.initializeApp(); + } + + Future _showRetryCloseDialog() async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => AlertDialog( + title: const Text('Initialization Failed'), + content: const Text( + 'The app was unable to start properly due to missing permissions.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + bool retrySuccess = await _requestPermissionsAndInitialize(); + if (!retrySuccess) { + await _showRetryCloseDialog(); + } else { + setState(() { + _isInitialized = true; + }); + } + }, + child: const Text('Retry'), + ), + ], + ), + ); + } + + ErrorScreen({required Object error}) {} +} diff --git a/lib/features/location/location_provider.dart b/lib/features/location/location_provider.dart new file mode 100644 index 0000000..dccc893 --- /dev/null +++ b/lib/features/location/location_provider.dart @@ -0,0 +1,109 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:rogapp/features/data/user_location.dart'; +import 'package:rogapp/features/state/game_view_model.dart'; + +/// The state for the location feature +class LocationState { + final Position? currentPosition; + final bool isStreamPaused; + + LocationState({ + this.currentPosition, + this.isStreamPaused = false, + }); + + LocationState copyWith({ + Position? currentPosition, + bool? isStreamPaused, + }) { + return LocationState( + currentPosition: currentPosition ?? this.currentPosition, + isStreamPaused: isStreamPaused ?? this.isStreamPaused, + ); + } +} + +/// A StateNotifier that manages the location state +class LocationNotifier extends StateNotifier { + final Ref ref; + LocationNotifier(this.ref) : super(LocationState()) { + startListening(); + startPeriodicUpdates(); + } + + StreamSubscription? _positionSubscription; + Position? _lastPosition; + final double _distanceThreshold = 220.0; + Timer? _periodicUpdateTimer; + + void startPeriodicUpdates() { + _periodicUpdateTimer?.cancel(); + _periodicUpdateTimer = Timer.periodic(Duration(seconds: 5), (timer) { + if (state.currentPosition != null) { + // Convert Position to UserLocation + UserLocation userLocation = + UserLocation.fromPosition(state.currentPosition!); + ref.read(gameViewModelProvider.notifier).updateLocation(userLocation); + } + }); + } + + void startListening() { + _positionSubscription = + Geolocator.getPositionStream().listen((Position position) { + if (_shouldUpdatePosition(position)) { + state = state.copyWith(currentPosition: position); + _lastPosition = position; + } + }); + } + + bool _shouldUpdatePosition(Position newPosition) { + if (_lastPosition == null) return true; + + double distance = Geolocator.distanceBetween( + _lastPosition!.latitude, + _lastPosition!.longitude, + newPosition.latitude, + newPosition.longitude, + ); + + return distance < _distanceThreshold; + } + + void stopListening() { + _positionSubscription?.cancel(); + _positionSubscription = null; + state = state.copyWith( + currentPosition: null); // Reset the position state or handle as needed + } + + void pauseListening() { + _positionSubscription?.pause(); + } + + void resumeListening() { + _positionSubscription?.resume(); + } + + void updateCurrentPosition(Position? position) { + state = state.copyWith(currentPosition: position); + } + + void updateStreamPaused(bool isPaused) { + state = state.copyWith(isStreamPaused: isPaused); + } + + @override + void dispose() { + _positionSubscription?.cancel(); + super.dispose(); + } +} + +final locationNotifierProvider = + StateNotifierProvider((ref) { + return LocationNotifier(ref); // Pass ref directly +}); diff --git a/lib/features/services/auth_repo.dart b/lib/features/services/auth_repo.dart new file mode 100644 index 0000000..4aeacea --- /dev/null +++ b/lib/features/services/auth_repo.dart @@ -0,0 +1,14 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rogapp/features/data/user.dart'; +import 'package:rogapp/features/services/auth_service.dart'; + +final authProvider = + FutureProvider.family((ref, credentials) async { + // Assuming credentials is a combined string of "email|password" + List parts = credentials.split('|'); + String email = parts[0]; + String password = parts[1]; + + AuthService authService = AuthService(); + return await authService.userLogin(email, password); +}); diff --git a/lib/features/services/auth_service.dart b/lib/features/services/auth_service.dart new file mode 100644 index 0000000..e327109 --- /dev/null +++ b/lib/features/services/auth_service.dart @@ -0,0 +1,44 @@ +import 'package:rogapp/features/data/user.dart'; + +import '../utils/const.dart'; + +import 'package:dio/dio.dart'; + +class AuthService { + Dio dio = Dio(); + + Future userLogin(String email, String password) async { + final String serverUrl = ConstValues.currentServer(); + final String url = '$serverUrl/api/login/'; + + try { + final response = await dio.post( + url, + data: { + 'email': email, + 'password': password, + }, + options: Options( + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + ), + ); + + if (response.statusCode == 200 && response.data != null) { + final data = response.data; + User user = User.fromMap(data["user"]); + final String token = data["token"]; + user.auth_token = token; + return user; + } + return null; + } on DioException catch (e) { + print("Dio error: ${e.response?.statusCode} - ${e.message}"); + return null; + } catch (e) { + print("Unexpected error: $e"); + return null; + } + } +} diff --git a/lib/features/services/checkpoint_repo.dart b/lib/features/services/checkpoint_repo.dart new file mode 100644 index 0000000..5f644c6 --- /dev/null +++ b/lib/features/services/checkpoint_repo.dart @@ -0,0 +1,32 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rogapp/features/data/checkpoint.dart'; +import 'package:rogapp/features/data/data_provider.dart'; +import 'package:rogapp/features/services/checkpoint_service.dart'; // Import your ApiService file + +Future storeFetchedFeatures(FeatureCollection featureCollection) async { + final dataProvider = + DataProvider(); // Ensure DataProvider is initialized appropriately + + for (Feature feature in featureCollection.features) { + await dataProvider.insertMarker(feature); + } +} + +final checkpointProvider = FutureProvider>((ref) async { + final dataProvider = + DataProvider(); // Assuming you create or access an instance here + final markers = await dataProvider.getMarkers(); + return markers; +}); + +final featureCheckpointCollectionProvider = + FutureProvider((ref) async { + final apiService = + ApiService(); // Ensure ApiService is initialized appropriately + final featureCollection = await apiService.fetchFeatures(); + + // Store the fetched features in the local database + await storeFetchedFeatures(featureCollection); + + return featureCollection; // Return the fetched feature collection +}); diff --git a/lib/features/services/checkpoint_service.dart b/lib/features/services/checkpoint_service.dart new file mode 100644 index 0000000..cc9fdc0 --- /dev/null +++ b/lib/features/services/checkpoint_service.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; +import 'package:rogapp/features/data/checkpoint.dart'; + +class ApiService { + final Dio _dio = Dio(); + + Future fetchFeatures() async { + try { + final tmp_url = + "https://rogaining.sumasen.net/api/inbound?rog=True&grp=養老ロゲ&ln1=136.52675654298724&la1=35.19710769333327&ln2=136.52675654298724&la2=35.38264844179004&ln3=136.65061968584442&la3=35.38264844179004&ln4=136.65061968584442&la4=35.19710769333327"; + final response = await _dio.get(tmp_url); + if (response.statusCode == 200) { + return FeatureCollection.fromJson(response.data); + } else { + throw Exception('Failed to load features'); + } + } on DioException catch (e) { + // Handle Dio errors here + print('Dio error: $e'); + throw Exception('Failed to load features'); + } catch (e) { + // Handle any other errors + print('General error: $e'); + throw Exception('Failed to load features'); + } + } +} diff --git a/lib/features/state/game_state.dart b/lib/features/state/game_state.dart new file mode 100644 index 0000000..5caa079 --- /dev/null +++ b/lib/features/state/game_state.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:rogapp/features/data/checkpoint.dart'; +import 'package:rogapp/features/data/user_location.dart'; + +enum GameStatus { + notStarted, + started, + inProgress, + atCheckpoint, + gameCompleted, +} + +enum PendingAction { + none, + checkInPhoto, + receiptPhoto, + endGamePhoto, +} + +@immutable +class GameState { + final GameStatus status; + final List markers; + final UserLocation? currentLocation; + final Set visitedMarkers; + final bool isWithinStartRadius; + final bool hasLeftStartArea; + final bool hasVisitedCheckpoint; + final Feature? currentCheckpoint; + final bool checkpointPhotoTaken; + final bool receiptPhotoTaken; + final PendingAction pendingAction; + + const GameState({ + this.status = GameStatus.notStarted, + this.markers = const [], + this.currentLocation, + this.visitedMarkers = const {}, + this.isWithinStartRadius = false, + this.hasLeftStartArea = false, + this.hasVisitedCheckpoint = false, + this.currentCheckpoint, + this.checkpointPhotoTaken = false, + this.receiptPhotoTaken = false, + this.pendingAction = PendingAction.none, + }); + + LatLng? getUserLatLng() { + return currentLocation != null + ? LatLng(currentLocation!.latitude, currentLocation!.longitude) + : null; + } + + GameState.fromMap(Map map) + : status = GameStatus.values[map['status']], + markers = + List.from(map['markers'].map((x) => Feature.fromMap(x))), + currentLocation = UserLocation.fromMap(map['currentLocation']), + visitedMarkers = Set.from( + map['visitedMarkers'].map((x) => Feature.fromMap(x))), + isWithinStartRadius = map['isWithinStartRadius'], + hasLeftStartArea = map['hasLeftStartArea'], + hasVisitedCheckpoint = map['hasVisitedCheckpoint'], + currentCheckpoint = map['currentCheckpoint'] != null + ? Feature.fromMap(map['currentCheckpoint']) + : null, + checkpointPhotoTaken = map['checkpointPhotoTaken'], + receiptPhotoTaken = map['receiptPhotoTaken'], + pendingAction = PendingAction.values[map['pendingAction']]; + + GameState copyWith({ + GameStatus? status, + List? markers, + UserLocation? currentLocation, + Set? visitedMarkers, + bool? isWithinStartRadius, + bool? hasLeftStartArea, + bool? hasVisitedCheckpoint, + Feature? currentCheckpoint, + bool? checkpointPhotoTaken, + bool? receiptPhotoTaken, + PendingAction? pendingAction, + }) { + return GameState( + status: status ?? this.status, + markers: markers ?? this.markers, + currentLocation: currentLocation ?? this.currentLocation, + visitedMarkers: visitedMarkers ?? this.visitedMarkers, + isWithinStartRadius: isWithinStartRadius ?? this.isWithinStartRadius, + hasLeftStartArea: hasLeftStartArea ?? this.hasLeftStartArea, + hasVisitedCheckpoint: hasVisitedCheckpoint ?? this.hasVisitedCheckpoint, + currentCheckpoint: currentCheckpoint ?? this.currentCheckpoint, + checkpointPhotoTaken: checkpointPhotoTaken ?? this.checkpointPhotoTaken, + receiptPhotoTaken: receiptPhotoTaken ?? this.receiptPhotoTaken, + pendingAction: pendingAction ?? this.pendingAction); + } +} diff --git a/lib/features/state/game_view_model.dart b/lib/features/state/game_view_model.dart new file mode 100644 index 0000000..e7575a5 --- /dev/null +++ b/lib/features/state/game_view_model.dart @@ -0,0 +1,199 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:rogapp/features/data/checkpoint.dart'; +import 'package:rogapp/features/data/data_provider.dart'; +import 'package:rogapp/features/data/user_location.dart'; +import 'package:rogapp/features/state/game_state.dart'; +import 'package:rogapp/features/utils/params.dart'; + +final gameViewModelProvider = + StateNotifierProvider((ref) { + final dataProvider = DataProvider(); + return GameViewModel(dataProvider); +}); + +class GameViewModel extends StateNotifier { + final DataProvider _dataProvider; + + GameViewModel(this._dataProvider) : super(const GameState()) { + loadInitialData(); + } + + Future loadInitialData() async { + List markers = await _dataProvider.getMarkers(); + state = state.copyWith(markers: markers); + } + + void setMarkers(List markers) { + state = state.copyWith(markers: markers); + } + + void startGame() { + if (state.isWithinStartRadius) { + state = state.copyWith(status: GameStatus.started); + } + } + + void updateLocation(UserLocation location) { + Feature startMarker = state.markers.firstWhere( + (marker) => marker.properties.cp == -1, + orElse: () => Feature.empty()); + List checkpoints = + state.markers.where((marker) => marker.properties.cp != -1).toList(); + + bool isWithinStartRadius = + _checkProximity(location, startMarker /* start marker location */ + ); + bool hasLeftArea = + state.hasLeftStartArea || _checkHasLeftStartArea(location, startMarker); + + Feature? atCheckpoint = _checkAtAnyCheckpoint(location, checkpoints); + + // Check if at any checkpoint (except start point after game started) + bool isAtCheckpoint = + state.status == GameStatus.started && atCheckpoint != null; + if (isAtCheckpoint) { + _handleCheckpointArrival(location, atCheckpoint); + } + + // Update game state based on the new location + state = state.copyWith( + currentLocation: location, + isWithinStartRadius: isWithinStartRadius, + hasLeftStartArea: hasLeftArea, + status: isAtCheckpoint ? GameStatus.atCheckpoint : state.status, + ); + } + + void _handleCheckpointArrival(UserLocation location, Feature marker) { + // Directly set the current checkpoint; no need to check if the photo was taken here + state = state.copyWith(currentCheckpoint: marker); + + // If no checkpoint photo has been taken yet, set the pending action to take a checkpoint photo + if (!state.checkpointPhotoTaken) { + state = state.copyWith(pendingAction: PendingAction.checkInPhoto); + } + + // Additional actions specific to buy points can be prepared here, but it's usually better + // to handle them after the checkpoint photo is submitted, not at the moment of arrival. + } + + void submitCheckInPhoto(String photoPath) { + // Logic to save or upload the check-in photo... + state = state.copyWith(checkpointPhotoTaken: true); + + // Determine the next action based on the checkpoint type + if (state.currentCheckpoint != null && + isBuyPoint(state.currentCheckpoint!)) { + state = state.copyWith(pendingAction: PendingAction.receiptPhoto); + } else { + // If it's not a buy point, mark the checkpoint visit as complete + _markCheckpointVisited(state.currentCheckpoint!); + } + } + + void submitReceiptPhoto(String photoPath) { + // Logic to save or upload the receipt photo... + state = state.copyWith(receiptPhotoTaken: true); + + // After submitting the receipt photo, mark the checkpoint visit as complete + _markCheckpointVisited(state.currentCheckpoint!); + } + + void _markCheckpointVisited(Feature checkpoint) { + state = state.copyWith( + visitedMarkers: Set.from(state.visitedMarkers)..add(checkpoint), + hasVisitedCheckpoint: true, + pendingAction: PendingAction.none, // Reset the pending action + ); + + // Additional logic to determine if the game is completed... + } + + bool isBuyPoint(Feature checkpoint) { + if (checkpoint.properties.buyPoint != null) { + return true; + } + return false; + } + + bool _checkProximity(UserLocation location, Feature startMarker) { + const Distance distance = Distance(); + return distance( + LatLng(location.latitude, location.longitude), + LatLng(startMarker.geometry.latitude, + startMarker.geometry.longitude)) <= + startMinDistance; + } + + bool _checkHasLeftStartArea(UserLocation location, Feature startMarker) { + const Distance distance = Distance(); + return distance( + LatLng(location.latitude, location.longitude), + LatLng(startMarker.geometry.latitude, + startMarker.geometry.longitude)) >= + minAwayfromStartDistance; + } + + Feature? _checkAtAnyCheckpoint(UserLocation location, List markers) { + const Distance distance = Distance(); + + for (Feature marker in markers) { + final double distanceInMeters = distance( + LatLng(location.latitude, location.longitude), + LatLng(marker.geometry.latitude, marker.geometry.longitude), + ); + if (marker.properties.checkinRadious != null && + marker.properties.checkinRadious! >= distanceInMeters) { + return marker; + } + } + return null; + } + + Feature? _getCheckpointFromLocation(UserLocation location) { + const thresholdDistance = 50.0; // meters, adjust based on your game design + const Distance distance = Distance(); + + for (final marker in state.markers) { + final double distanceInMeters = distance( + LatLng(location.latitude, location.longitude), + LatLng(marker.geometry.latitude, marker.geometry.longitude), + ); + + if (distanceInMeters <= + (marker.properties.checkinRadious ?? thresholdDistance)) { + return marker; + } + } + + return null; // No checkpoint is close enough to the user's location + } + + bool _isBackAtStart(UserLocation location) { + const startThresholdDistance = 150.0; // Define proximity threshold + const Distance distance = Distance(); + + // Assuming the start point is the first marker + // Adjust based on your game's logic + final startPoint = state.markers.first; + + final double distanceToStart = distance( + LatLng(location.latitude, location.longitude), + LatLng(startPoint.geometry.latitude, startPoint.geometry.longitude), + ); + + // User is considered back at the start if within the threshold distance + return distanceToStart <= startThresholdDistance; + } + + void endGame() { + if (state.status == GameStatus.gameCompleted) { + // Perform any end game actions + } + } + + void loadGameState(GameState gameState) { + state = gameState; + } +} diff --git a/lib/features/state/user_state.dart b/lib/features/state/user_state.dart new file mode 100644 index 0000000..763ab49 --- /dev/null +++ b/lib/features/state/user_state.dart @@ -0,0 +1,18 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rogapp/features/data/user.dart'; + +class UserNotifier extends StateNotifier { + UserNotifier() : super(null); + + void setUser(User? user) { + state = user; + } + + void clearUser() { + state = null; + } +} + +final userNotifierProvider = StateNotifierProvider((ref) { + return UserNotifier(); +}); diff --git a/lib/features/utils/const.dart b/lib/features/utils/const.dart new file mode 100644 index 0000000..216cf5a --- /dev/null +++ b/lib/features/utils/const.dart @@ -0,0 +1,13 @@ +class ConstValues { + static const container_svr = "http://container.intranet.sumasen.net:8100"; + static const server_uri = "https://rogaining.sumasen.net"; + static const dev_server = "http://localhost:8100"; + static const dev_ip_server = "http://192.168.8.100:8100"; + static const dev_home_ip_server = "http://172.20.10.9:8100"; + static const dev_home_ip_mserver = "http://192.168.1.10:8100"; + + static String currentServer() { + //return dev_ip_server; + return server_uri; + } +} diff --git a/lib/features/utils/distance_calculator.dart b/lib/features/utils/distance_calculator.dart new file mode 100644 index 0000000..6a47d67 --- /dev/null +++ b/lib/features/utils/distance_calculator.dart @@ -0,0 +1,29 @@ +import 'dart:math'; +import 'package:latlong2/latlong.dart'; + +class DistanceCalculatorHS { + static const int _radius = 6371; // Radius of the Earth in kilometers + + static double _degreeToRadian(double degree) { + return degree * pi / 180; + } + + static double calculateDistance(LatLng location1, LatLng location2) { + double latDistance = + _degreeToRadian(location2.latitude - location1.latitude); + double lonDistance = + _degreeToRadian(location2.longitude - location1.longitude); + + double a = sin(latDistance / 2) * sin(latDistance / 2) + + cos(_degreeToRadian(location1.latitude)) * + cos(_degreeToRadian(location2.latitude)) * + sin(lonDistance / 2) * + sin(lonDistance / 2); + + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + + double distance = _radius * c * 1000; // Convert to meters + + return distance; + } +} diff --git a/lib/features/utils/error_reporter.dart b/lib/features/utils/error_reporter.dart new file mode 100644 index 0000000..39f814b --- /dev/null +++ b/lib/features/utils/error_reporter.dart @@ -0,0 +1,17 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final errorReportingProvider = Provider((ref) { + return ErrorReporterImpl(); +}); + +class ErrorReporter { + void reportError(Object error, StackTrace stackTrace) {} +} + +class ErrorReporterImpl extends ErrorReporter { + @override + void reportError(Object error, StackTrace stackTrace) { + // Send error and stackTrace to the server + // Include app state if necessary + } +} diff --git a/lib/features/utils/params.dart b/lib/features/utils/params.dart new file mode 100644 index 0000000..6fa270c --- /dev/null +++ b/lib/features/utils/params.dart @@ -0,0 +1,2 @@ +int startMinDistance = 150; +int minAwayfromStartDistance = 300; diff --git a/lib/main.dart b/lib/main.dart index f29f4e6..cb77807 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,147 +1,47 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:get/get.dart'; -import 'package:rogapp/pages/destination/destination_controller.dart'; -import 'package:rogapp/pages/index/index_binding.dart'; -import 'package:rogapp/routes/app_pages.dart'; -import 'package:rogapp/utils/location_controller.dart'; -import 'package:rogapp/utils/string_values.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -// import 'package:is_lock_screen/is_lock_screen.dart'; - -void saveGameState() async { - DestinationController destinationController = - Get.find(); - SharedPreferences pref = await SharedPreferences.getInstance(); - pref.setBool("is_in_rog", destinationController.isInRog.value); - pref.setBool( - "rogaining_counted", destinationController.rogainingCounted.value); - pref.setBool("ready_for_goal", DestinationController.ready_for_goal); -} - -void restoreGame() async { - SharedPreferences pref = await SharedPreferences.getInstance(); - DestinationController destinationController = - Get.find(); - destinationController.skipGps = false; - destinationController.isInRog.value = pref.getBool("is_in_rog") ?? false; - destinationController.rogainingCounted.value = - pref.getBool("rogaining_counted") ?? false; - DestinationController.ready_for_goal = - pref.getBool("ready_for_goal") ?? false; - //print( - // "--restored -- destinationController.isInRog.value ${pref.getBool("is_in_rog")} -- ${pref.getBool("rogaining_counted")}"); -} - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - await FlutterMapTileCaching.initialise(); - final StoreDirectory instanceA = FMTC.instance('OpenStreetMap (A)'); - await instanceA.manage.createAsync(); - await instanceA.metadata.addAsync( - key: 'sourceURL', - value: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', - ); - await instanceA.metadata.addAsync( - key: 'validDuration', - value: '14', - ); - await instanceA.metadata.addAsync( - key: 'behaviour', - value: 'cacheFirst', - ); - runApp(const MyApp()); -} - -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State with WidgetsBindingObserver { - // This widget is the root of your application. - - @override - void initState() { - super.initState(); - if (context.mounted) { - restoreGame(); - } - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - // void saveGameState() async { - // DestinationController destinationController = Get.find(); - // SharedPreferences pref = await SharedPreferences.getInstance(); - // pref.setBool("is_in_rog", destinationController.is_in_rog.value); - // pref.setBool("rogaining_counted", destinationController.rogaining_counted.value); - // } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - LocationController locationController = Get.find(); - - DestinationController destinationController = - Get.find(); - switch (state) { - case AppLifecycleState.resumed: - locationController.resumePositionStream(); - //print("RESUMED"); - restoreGame(); - break; - case AppLifecycleState.inactive: - locationController.resumePositionStream(); - //print("INACTIVE"); - break; - case AppLifecycleState.paused: - locationController.resumePositionStream(); - //print("PAUSED"); - saveGameState(); - break; - case AppLifecycleState.detached: - locationController.resumePositionStream(); - //print("DETACHED"); - saveGameState(); - break; - case AppLifecycleState.hidden: - locationController.resumePositionStream(); - //print("DETACHED"); - saveGameState(); - break; - } - } - - @override - Widget build(BuildContext context) { - return GetMaterialApp( - translations: StringValues(), - locale: const Locale('ja', 'JP'), - //locale: const Locale('en', 'US'), - fallbackLocale: const Locale('en', 'US'), - title: 'ROGAINING', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color.fromARGB(255, 36, 135, 221)), - useMaterial3: true, - ), - debugShowCheckedModeBanner: false, - defaultTransition: Transition.cupertino, - opaqueRoute: Get.isOpaqueRouteDefault, - popGesture: Get.isPopGestureEnable, - transitionDuration: const Duration(milliseconds: 230), - initialBinding: IndexBinding(), //HomeBinding(), - initialRoute: AppPages.PERMISSION, - getPages: AppPages.routes, - enableLog: true, - ); - } -} +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:rogapp/features/landing/landing_page.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rogapp/features/utils/error_reporter.dart'; + +void reportError( + ProviderContainer container, Object error, StackTrace stackTrace) { + // Log the error locally or report to a server + print('Caught an error: $error'); + container.read(errorReportingProvider).reportError(error, stackTrace); +} + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + final container = ProviderContainer(); + runZonedGuarded(() { + runApp(UncontrolledProviderScope(container: container, child: const App())); + FlutterError.onError = (FlutterErrorDetails details) { + final error = details.exception; + final stackTrace = details.stack ?? StackTrace.current; + reportError(container, error, stackTrace); + }; + }, (error, stackTrace) { + reportError(container, error, stackTrace ?? StackTrace.current); + }); +} + +class App extends StatelessWidget { + const App({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color.fromARGB(255, 0, 35, 140)), //Samurai Blue + useMaterial3: true, + ), + home: const LandingPage(), + ); + } +} diff --git a/lib/main_mapsix.dart b/lib/main_mapsix.dart new file mode 100644 index 0000000..46a85c8 --- /dev/null +++ b/lib/main_mapsix.dart @@ -0,0 +1,146 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +// import 'package:get/get.dart'; +// import 'package:rogapp/pages/destination/destination_controller.dart'; +// import 'package:rogapp/pages/index/index_binding.dart'; +// import 'package:rogapp/routes/app_pages.dart'; +// import 'package:rogapp/utils/location_controller.dart'; +// import 'package:rogapp/utils/string_values.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; +// // import 'package:is_lock_screen/is_lock_screen.dart'; + +// void saveGameState() async { +// DestinationController destinationController = +// Get.find(); +// SharedPreferences pref = await SharedPreferences.getInstance(); +// pref.setBool("is_in_rog", destinationController.isInRog.value); +// pref.setBool( +// "rogaining_counted", destinationController.rogainingCounted.value); +// pref.setBool("ready_for_goal", DestinationController.ready_for_goal); +// } + +// void restoreGame() async { +// SharedPreferences pref = await SharedPreferences.getInstance(); +// DestinationController destinationController = +// Get.find(); +// destinationController.skipGps = false; +// destinationController.isInRog.value = pref.getBool("is_in_rog") ?? false; +// destinationController.rogainingCounted.value = +// pref.getBool("rogaining_counted") ?? false; +// DestinationController.ready_for_goal = +// pref.getBool("ready_for_goal") ?? false; +// //print( +// // "--restored -- destinationController.isInRog.value ${pref.getBool("is_in_rog")} -- ${pref.getBool("rogaining_counted")}"); +// } + +// void main() async { +// WidgetsFlutterBinding.ensureInitialized(); +// await FlutterMapTileCaching.initialise(); +// final StoreDirectory instanceA = FMTC.instance('OpenStreetMap (A)'); +// await instanceA.manage.createAsync(); +// await instanceA.metadata.addAsync( +// key: 'sourceURL', +// value: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', +// ); +// await instanceA.metadata.addAsync( +// key: 'validDuration', +// value: '14', +// ); +// await instanceA.metadata.addAsync( +// key: 'behaviour', +// value: 'cacheFirst', +// ); +// runApp(const MyApp()); +// } + +// class MyApp extends StatefulWidget { +// const MyApp({Key? key}) : super(key: key); + +// @override +// State createState() => _MyAppState(); +// } + +// class _MyAppState extends State with WidgetsBindingObserver { +// // This widget is the root of your application. + +// @override +// void initState() { +// super.initState(); +// if (context.mounted) { +// restoreGame(); +// } +// WidgetsBinding.instance.addObserver(this); +// } + +// @override +// void dispose() { +// WidgetsBinding.instance.removeObserver(this); +// super.dispose(); +// } + +// // void saveGameState() async { +// // DestinationController destinationController = Get.find(); +// // SharedPreferences pref = await SharedPreferences.getInstance(); +// // pref.setBool("is_in_rog", destinationController.is_in_rog.value); +// // pref.setBool("rogaining_counted", destinationController.rogaining_counted.value); +// // } + +// @override +// void didChangeAppLifecycleState(AppLifecycleState state) { +// LocationController locationController = Get.find(); + +// DestinationController destinationController = +// Get.find(); +// switch (state) { +// case AppLifecycleState.resumed: +// locationController.resumePositionStream(); +// //print("RESUMED"); +// restoreGame(); +// break; +// case AppLifecycleState.inactive: +// locationController.resumePositionStream(); +// //print("INACTIVE"); +// break; +// case AppLifecycleState.paused: +// locationController.resumePositionStream(); +// //print("PAUSED"); +// saveGameState(); +// break; +// case AppLifecycleState.detached: +// locationController.resumePositionStream(); +// //print("DETACHED"); +// saveGameState(); +// break; +// case AppLifecycleState.hidden: +// locationController.resumePositionStream(); +// //print("DETACHED"); +// saveGameState(); +// break; +// } +// } + +// @override +// Widget build(BuildContext context) { +// return GetMaterialApp( +// translations: StringValues(), +// locale: const Locale('ja', 'JP'), +// //locale: const Locale('en', 'US'), +// fallbackLocale: const Locale('en', 'US'), +// title: 'ROGAINING', +// theme: ThemeData( +// colorScheme: ColorScheme.fromSeed( +// seedColor: const Color.fromARGB(255, 36, 135, 221)), +// useMaterial3: true, +// ), +// debugShowCheckedModeBanner: false, +// defaultTransition: Transition.cupertino, +// opaqueRoute: Get.isOpaqueRouteDefault, +// popGesture: Get.isPopGestureEnable, +// transitionDuration: const Duration(milliseconds: 230), +// initialBinding: IndexBinding(), //HomeBinding(), +// initialRoute: AppPages.PERMISSION, +// getPages: AppPages.routes, +// enableLog: true, +// ); +// } +// } diff --git a/lib/main_nrog.dart b/lib/main_nrog.dart index bef21d2..673fd0a 100644 --- a/lib/main_nrog.dart +++ b/lib/main_nrog.dart @@ -1,61 +1,61 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:rogapp/nrog/pages/permission_page.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:rogapp/nrog/pages/permission_page.dart'; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); +// void main() async { +// WidgetsFlutterBinding.ensureInitialized(); - await FlutterMapTileCaching.initialise(); - final StoreDirectory instanceA = FMTC.instance('OpenStreetMap (A)'); - await instanceA.manage.createAsync(); - await instanceA.metadata.addAsync( - key: 'sourceURL', - value: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', - ); - await instanceA.metadata.addAsync( - key: 'validDuration', - value: '14', - ); - await instanceA.metadata.addAsync( - key: 'behaviour', - value: 'cacheFirst', - ); - runApp( - const ProviderScope(child: MyApp()), - ); -} +// await FlutterMapTileCaching.initialise(); +// final StoreDirectory instanceA = FMTC.instance('OpenStreetMap (A)'); +// await instanceA.manage.createAsync(); +// await instanceA.metadata.addAsync( +// key: 'sourceURL', +// value: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', +// ); +// await instanceA.metadata.addAsync( +// key: 'validDuration', +// value: '14', +// ); +// await instanceA.metadata.addAsync( +// key: 'behaviour', +// value: 'cacheFirst', +// ); +// runApp( +// const ProviderScope(child: MyApp()), +// ); +// } -class MyApp extends StatefulWidget { - const MyApp({super.key}); +// class MyApp extends StatefulWidget { +// const MyApp({super.key}); - @override - State createState() => _MyAppState(); -} +// @override +// State createState() => _MyAppState(); +// } -class _MyAppState extends State with WidgetsBindingObserver { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } +// class _MyAppState extends State with WidgetsBindingObserver { +// @override +// void initState() { +// super.initState(); +// WidgetsBinding.instance.addObserver(this); +// } - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } +// @override +// void dispose() { +// WidgetsBinding.instance.removeObserver(this); +// super.dispose(); +// } - @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color.fromARGB(255, 124, 156, 9)), - useMaterial3: true, - ), - home: const PermissionPage()); - } -} +// @override +// Widget build(BuildContext context) { +// return MaterialApp( +// debugShowCheckedModeBanner: false, +// title: 'Flutter Demo', +// theme: ThemeData( +// colorScheme: ColorScheme.fromSeed( +// seedColor: const Color.fromARGB(255, 124, 156, 9)), +// useMaterial3: true, +// ), +// home: const PermissionPage()); +// } +// } diff --git a/lib/nrog/pages/home_page.dart b/lib/nrog/pages/home_page.dart index b8f6211..6a3327d 100644 --- a/lib/nrog/pages/home_page.dart +++ b/lib/nrog/pages/home_page.dart @@ -95,7 +95,7 @@ class _HomePageState extends ConsumerState { // .hideAllPopups(), // Hide popup when the map is tapped. ), children: [ - const BaseLayer(), + //const BaseLayer(), CurrentLocationLayer( followOnLocationUpdate: FollowOnLocationUpdate.once, turnOnHeadingUpdate: TurnOnHeadingUpdate.never, diff --git a/lib/pages/destination/destination_binding.dart b/lib/pages/destination/destination_binding.dart index 56e214d..abb7b72 100644 --- a/lib/pages/destination/destination_binding.dart +++ b/lib/pages/destination/destination_binding.dart @@ -6,6 +6,6 @@ class DestinationBinding extends Bindings { @override void dependencies() { Get.put(DestinationController()); - restoreGame(); + //restoreGame(); } } diff --git a/lib/pages/destination/destination_controller.dart b/lib/pages/destination/destination_controller.dart index e38d51d..31810bb 100644 --- a/lib/pages/destination/destination_controller.dart +++ b/lib/pages/destination/destination_controller.dart @@ -650,7 +650,7 @@ class DestinationController extends GetxController { } isInRog.value = true; - saveGameState(); + //saveGameState(); } Future cancelBuyPoint(Destination destination) async { diff --git a/lib/pages/destination_map/destination_map_page.dart b/lib/pages/destination_map/destination_map_page.dart index 75c9b05..b851cbd 100644 --- a/lib/pages/destination_map/destination_map_page.dart +++ b/lib/pages/destination_map/destination_map_page.dart @@ -155,7 +155,7 @@ class DestinationMapPage extends StatelessWidget { interactiveFlags: InteractiveFlag.pinchZoom | InteractiveFlag.drag, ), children: [ - const BaseLayer(), + //const BaseLayer(), Obx( () => indexController.routePointLenght > 0 ? PolylineLayer( diff --git a/lib/pages/gps/gps_page.dart b/lib/pages/gps/gps_page.dart index 838f07f..26e0b24 100644 --- a/lib/pages/gps/gps_page.dart +++ b/lib/pages/gps/gps_page.dart @@ -123,7 +123,7 @@ class _GpsPageState extends State { onTap: (tapPos, cord) {}, // Hide popup when the map is tapped. ), children: [ - const BaseLayer(), + //const BaseLayer(), MarkerLayer( markers: gpsData.map((i) { return Marker( diff --git a/lib/pages/index/index_controller.dart b/lib/pages/index/index_controller.dart index f381bc3..52c0784 100644 --- a/lib/pages/index/index_controller.dart +++ b/lib/pages/index/index_controller.dart @@ -116,8 +116,8 @@ class IndexController extends GetxController { @override void onInit() { - _connectivitySubscription = - _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); + // _connectivitySubscription = + // _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); super.onInit(); } @@ -133,16 +133,16 @@ class IndexController extends GetxController { } Future initConnectivity() async { - late ConnectivityResult result; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - result = await _connectivity.checkConnectivity(); - } on PlatformException catch (_) { - //print('Couldn\'t check connectivity status --- $e'); - return; - } + // late ConnectivityResult result; + // // Platform messages may fail, so we use a try/catch PlatformException. + // try { + // //result = await _connectivity.checkConnectivity(); + // } on PlatformException catch (_) { + // //print('Couldn\'t check connectivity status --- $e'); + // return; + // } - return _updateConnectionStatus(result); + // return _updateConnectionStatus(result); } LatLngBounds boundsFromLatLngList(List list) { diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart index 54005f4..0f384fc 100644 --- a/lib/services/location_service.dart +++ b/lib/services/location_service.dart @@ -107,7 +107,7 @@ class LocationService { '$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; } } - //print('++++++++$url'); + print('++++++++$url'); final response = await http.get( Uri.parse(url), headers: { diff --git a/lib/widgets/base_layer_widget.dart b/lib/widgets/base_layer_widget.dart index 6e025e7..8677af4 100644 --- a/lib/widgets/base_layer_widget.dart +++ b/lib/widgets/base_layer_widget.dart @@ -1,24 +1,24 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_map/flutter_map.dart'; +// import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -class BaseLayer extends StatelessWidget { - const BaseLayer({Key? key}) : super(key: key); +// class BaseLayer extends StatelessWidget { +// const BaseLayer({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - return TileLayer( - urlTemplate: "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png", - tileProvider: FMTC.instance('OpenStreetMap (A)').getTileProvider( - settings: FMTCTileProviderSettings( - behavior: CacheBehavior.values.byName('cacheFirst'), - cachedValidDuration: const Duration(days: 14), - ), - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return TileLayer( +// urlTemplate: "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png", +// tileProvider: FMTC.instance('OpenStreetMap (A)').getTileProvider( +// settings: FMTCTileProviderSettings( +// behavior: CacheBehavior.values.byName('cacheFirst'), +// cachedValidDuration: const Duration(days: 14), +// ), +// ), +// ); +// } +// } -// var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { -// attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' -// }); \ No newline at end of file +// // var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { +// // attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' +// // }); \ No newline at end of file diff --git a/lib/widgets/bottom_sheet_new.dart b/lib/widgets/bottom_sheet_new.dart index d5f63b5..9c9a5f6 100644 --- a/lib/widgets/bottom_sheet_new.dart +++ b/lib/widgets/bottom_sheet_new.dart @@ -195,7 +195,7 @@ class BottomSheetNew extends GetView { destinationController.currentLon, destination.location_id!, ); - saveGameState(); + //saveGameState(); await ExternalService().startRogaining(); Get.back(); // Close the dialog and potentially navigate away }, diff --git a/lib/widgets/map_widget.dart b/lib/widgets/map_widget.dart index 588e81f..4ce4cd7 100644 --- a/lib/widgets/map_widget.dart +++ b/lib/widgets/map_widget.dart @@ -237,7 +237,7 @@ class _MapWidgetState extends State { .hideAllPopups(), // Hide popup when the map is tapped. ), children: [ - const BaseLayer(), + //const BaseLayer(), Obx( () => indexController.routePointLenght > 0 ? PolylineLayer( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 31124ea..7299b5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,16 +7,12 @@ #include "generated_plugin_registrant.h" #include -#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); - isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 00d762d..786ff5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux - isar_flutter_libs url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0d0cd75..c3a1d0a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import connectivity_plus import file_selector_macos import geolocator_apple -import isar_flutter_libs import path_provider_foundation import shared_preferences_foundation import sqflite @@ -18,7 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) - IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 727369a..9ef94f2 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -259,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8af8f32..b8fd570 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.2.3 <4.0.0" - flutter: ">=3.16.6" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml index d60a34b..107611e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: sqflite: ^2.0.1 get: ^4.6.6 flutter_map: ^6.0.1 - geolocator: ^10.1.0 + geolocator: ^11.0.0 permission_handler: ^11.1.0 google_api_availability: ^5.0.0 tuple: ^2.0.0 @@ -49,36 +49,33 @@ dependencies: proj4dart: ^2.0.0 meta: ^1.7.0 collection: ^1.15.0 - path_provider: ^2.0.8 - flutter_map_location_marker: any + path_provider: ^2.1.2 + flutter_map_location_marker: ^8.0.8 flutter_map_marker_cluster: any material_design_icons_flutter: ^7.0.7296 google_fonts: ^6.1.0 keyboard_dismisser: ^3.0.0 image_picker: ^1.0.4 geojson_vi: ^2.2.1 - #geojson: ^1.0.0 url_launcher: ^6.0.20 flutter_breadcrumb: ^1.0.1 timeline_tile: ^2.0.0 - # google_maps_flutter: ^2.5.0 - #flutter_map_marker_popup: any flutter_polyline_points: ^2.0.0 - #google_maps_webservice: ^0.0.20-nullsafety.5 flutter_typeahead: ^5.0.1 flutter_launcher_icons: ^0.13.1 rename: ^3.0.1 circular_menu: ^2.0.1 camera_camera: ^3.0.0 - intl: ^0.18.1 + intl: ^0.19.0 modal_bottom_sheet: ^3.0.0-pre - connectivity_plus: ^5.0.2 - flutter_map_tile_caching: ^9.0.0-dev.5 shared_preferences: ^2.0.15 - # gallery_saver: ^2.3.2 image_gallery_saver: ^2.0.3 flutter_riverpod: ^2.4.0 http: ^1.1.0 + dio: ^5.4.1 + dio_cache_interceptor_hive_store: ^3.2.1 + flutter_map_cache: ^1.3.0 + connectivity_plus: ^5.0.2 flutter_icons: android: true @@ -95,6 +92,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^3.0.1 + mockito: ^5.4.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/features/services/auth_service_test.dart b/test/features/services/auth_service_test.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/features/services/auth_service_test.dart @@ -0,0 +1 @@ + diff --git a/test/widget_test.dart b/test/widget_test.dart index b5f1975..d3f5a12 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:rogapp/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bc61c87..df3c158 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -20,8 +19,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); - IsarFlutterLibsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1834cc0..6f341d1 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus file_selector_windows geolocator_windows - isar_flutter_libs permission_handler_windows url_launcher_windows )