add refresh offer_page
This commit is contained in:
parent
f4cd446cde
commit
608222e8a3
|
|
@ -11,7 +11,7 @@ plugins {
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.proxibuy"
|
namespace = "com.example.proxibuy"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
late final Dio _dio;
|
late final Dio _dio;
|
||||||
final _storage = const FlutterSecureStorage();
|
final _storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
AuthBloc() : super(AuthInitial()) {
|
AuthBloc() : super(AuthUnknown()) {
|
||||||
_dio = Dio();
|
_dio = Dio();
|
||||||
_dio.interceptors.add(
|
_dio.interceptors.add(
|
||||||
LogInterceptor(
|
LogInterceptor(
|
||||||
|
|
@ -44,6 +44,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
|
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
|
||||||
emit(AuthLoading());
|
emit(AuthLoading());
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ abstract class AuthState extends Equatable {
|
||||||
List<Object?> get props => [];
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AuthUnknown extends AuthState {}
|
||||||
|
|
||||||
class AuthInitial extends AuthState {}
|
class AuthInitial extends AuthState {}
|
||||||
|
|
||||||
class AuthLoading extends AuthState {}
|
class AuthLoading extends AuthState {}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
@ -130,7 +131,7 @@ class _NotificationPreferencesPageState
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: BlocListener<NotificationPreferencesBloc,
|
body: BlocConsumer<NotificationPreferencesBloc,
|
||||||
NotificationPreferencesState>(
|
NotificationPreferencesState>(
|
||||||
listener: (context, state) async {
|
listener: (context, state) async {
|
||||||
if (state.submissionSuccess) {
|
if (state.submissionSuccess) {
|
||||||
|
|
@ -176,7 +177,10 @@ class _NotificationPreferencesPageState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
builder: (context, state) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -212,9 +216,8 @@ class _NotificationPreferencesPageState
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BlocBuilder<NotificationPreferencesBloc,
|
child: Builder(
|
||||||
NotificationPreferencesState>(
|
builder: (context) {
|
||||||
builder: (context, state) {
|
|
||||||
if (state.categories.isEmpty && state.isLoading) {
|
if (state.categories.isEmpty && state.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
@ -258,18 +261,13 @@ class _NotificationPreferencesPageState
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocBuilder<NotificationPreferencesBloc,
|
if (state.selectedCategoryIds.isNotEmpty)
|
||||||
NotificationPreferencesState>(
|
SizedBox(
|
||||||
builder: (context, state) {
|
|
||||||
final areCategoriesSelected =
|
|
||||||
state.selectedCategoryIds.isNotEmpty;
|
|
||||||
|
|
||||||
if (areCategoriesSelected) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: !state.isLoading
|
onPressed: state.isLoading
|
||||||
? () async {
|
? null
|
||||||
|
: () async {
|
||||||
final bloc =
|
final bloc =
|
||||||
context.read<NotificationPreferencesBloc>();
|
context.read<NotificationPreferencesBloc>();
|
||||||
|
|
||||||
|
|
@ -288,21 +286,17 @@ class _NotificationPreferencesPageState
|
||||||
selectedCategoryNames);
|
selectedCategoryNames);
|
||||||
|
|
||||||
bloc.add(SubmitPreferences());
|
bloc.add(SubmitPreferences());
|
||||||
}
|
},
|
||||||
: null,
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.confirm,
|
backgroundColor: AppColors.confirm,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
disabledBackgroundColor: Colors.grey,
|
disabledBackgroundColor: Colors.grey.withOpacity(0.5),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: state.isLoading
|
child: const Text(
|
||||||
? const CircularProgressIndicator(
|
|
||||||
color: Colors.white)
|
|
||||||
: const Text(
|
|
||||||
'اعمال',
|
'اعمال',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Dana',
|
fontFamily: 'Dana',
|
||||||
|
|
@ -311,16 +305,23 @@ class _NotificationPreferencesPageState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (state.isLoading)
|
||||||
|
Container(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// lib/presentation/pages/offers_page.dart
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
@ -205,6 +204,39 @@ class _OffersPageState extends State<OffersPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleRefresh() {
|
||||||
|
final completer = Completer<void>();
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
|
||||||
|
final timeout = Timer(const Duration(seconds: 20), () {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError('Request timed out.');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Request timed out. Please try again.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamSubscription<Map<String, dynamic>?> subscription =
|
||||||
|
service.on('update').listen((event) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
completer.future.whenComplete(() {
|
||||||
|
subscription.cancel();
|
||||||
|
timeout.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
service.invoke('force_refresh');
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildFavoriteCategoriesSection() {
|
Widget _buildFavoriteCategoriesSection() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0),
|
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0),
|
||||||
|
|
@ -220,7 +252,7 @@ class _OffersPageState extends State<OffersPage> {
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.of(context).push<bool>(
|
final result = await Navigator.of(context).push<bool>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const NotificationPreferencesPage(
|
builder: (context) => const NotificationPreferencesPage(
|
||||||
loadFavoritesOnStart: true,
|
loadFavoritesOnStart: true,
|
||||||
|
|
@ -234,7 +266,11 @@ class _OffersPageState extends State<OffersPage> {
|
||||||
.read<NotificationPreferencesBloc>()
|
.read<NotificationPreferencesBloc>()
|
||||||
.add(ResetSubmissionStatus());
|
.add(ResetSubmissionStatus());
|
||||||
|
|
||||||
_loadPreferences();
|
await _loadPreferences();
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
_handleRefresh();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -362,7 +398,10 @@ class _OffersPageState extends State<OffersPage> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: RefreshIndicator(
|
||||||
|
onRefresh: _handleRefresh,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -370,11 +409,13 @@ class _OffersPageState extends State<OffersPage> {
|
||||||
OffersView(
|
OffersView(
|
||||||
isGpsEnabled: _isGpsEnabled,
|
isGpsEnabled: _isGpsEnabled,
|
||||||
isConnectedToInternet: _isConnectedToInternet,
|
isConnectedToInternet: _isConnectedToInternet,
|
||||||
|
selectedCategories: _selectedCategories,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -382,11 +423,13 @@ class _OffersPageState extends State<OffersPage> {
|
||||||
class OffersView extends StatelessWidget {
|
class OffersView extends StatelessWidget {
|
||||||
final bool isGpsEnabled;
|
final bool isGpsEnabled;
|
||||||
final bool isConnectedToInternet;
|
final bool isConnectedToInternet;
|
||||||
|
final List<String> selectedCategories;
|
||||||
|
|
||||||
const OffersView({
|
const OffersView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.isGpsEnabled,
|
required this.isGpsEnabled,
|
||||||
required this.isConnectedToInternet,
|
required this.isConnectedToInternet,
|
||||||
|
required this.selectedCategories,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -418,7 +461,13 @@ class OffersView extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is OffersLoadSuccess) {
|
if (state is OffersLoadSuccess) {
|
||||||
if (state.offers.isEmpty) {
|
final filteredOffers = selectedCategories.isEmpty
|
||||||
|
? state.offers
|
||||||
|
: state.offers
|
||||||
|
.where((offer) => selectedCategories.contains(offer.category))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (filteredOffers.isEmpty) {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
height: 300,
|
height: 300,
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|
@ -434,7 +483,7 @@ class OffersView extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupedOffers = groupBy(
|
final groupedOffers = groupBy(
|
||||||
state.offers,
|
filteredOffers,
|
||||||
(OfferModel offer) => offer.category,
|
(OfferModel offer) => offer.category,
|
||||||
);
|
);
|
||||||
final categories = groupedOffers.keys.toList();
|
final categories = groupedOffers.keys.toList();
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,7 @@ void onStart(ServiceInstance service) async {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mqttService.messages.listen((data) {
|
Future<void> sendGpsData() async {
|
||||||
service.invoke('update', {'offers': data});
|
|
||||||
});
|
|
||||||
|
|
||||||
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
|
||||||
debugPrint("✅ Background Service: Sending location...");
|
|
||||||
|
|
||||||
var locationStatus = await Permission.location.status;
|
var locationStatus = await Permission.location.status;
|
||||||
if (!locationStatus.isGranted) {
|
if (!locationStatus.isGranted) {
|
||||||
debugPrint("Background Service: Location permission not granted.");
|
debugPrint("Background Service: Location permission not granted.");
|
||||||
|
|
@ -69,6 +63,20 @@ void onStart(ServiceInstance service) async {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("❌ Background Service Error: $e");
|
debugPrint("❌ Background Service Error: $e");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mqttService.messages.listen((data) {
|
||||||
|
service.invoke('update', {'offers': data});
|
||||||
|
});
|
||||||
|
|
||||||
|
service.on('force_refresh').listen((event) async {
|
||||||
|
debugPrint("✅ Background Service: Received force_refresh event.");
|
||||||
|
await sendGpsData();
|
||||||
|
});
|
||||||
|
|
||||||
|
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||||
|
debugPrint("✅ Background Service: Sending location via periodic timer...");
|
||||||
|
await sendGpsData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue