add refresh offer_page

This commit is contained in:
mohamadmahdi jebeli 2025-08-04 10:35:28 +03:30
parent f4cd446cde
commit 608222e8a3
7 changed files with 226 additions and 164 deletions

View File

@ -11,7 +11,7 @@ plugins {
android {
namespace = "com.example.proxibuy"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973"
compileOptions {
isCoreLibraryDesugaringEnabled = true

View File

@ -1,6 +1,7 @@
<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_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" />

View File

@ -13,7 +13,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
late final Dio _dio;
final _storage = const FlutterSecureStorage();
AuthBloc() : super(AuthInitial()) {
AuthBloc() : super(AuthUnknown()) {
_dio = Dio();
_dio.interceptors.add(
LogInterceptor(
@ -44,6 +44,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
}
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {

View File

@ -7,6 +7,8 @@ abstract class AuthState extends Equatable {
List<Object?> get props => [];
}
class AuthUnknown extends AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -130,7 +131,7 @@ class _NotificationPreferencesPageState
const SizedBox(width: 8),
],
),
body: BlocListener<NotificationPreferencesBloc,
body: BlocConsumer<NotificationPreferencesBloc,
NotificationPreferencesState>(
listener: (context, state) async {
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),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -212,9 +216,8 @@ class _NotificationPreferencesPageState
),
const SizedBox(height: 24),
Expanded(
child: BlocBuilder<NotificationPreferencesBloc,
NotificationPreferencesState>(
builder: (context, state) {
child: Builder(
builder: (context) {
if (state.categories.isEmpty && state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
@ -258,18 +261,13 @@ class _NotificationPreferencesPageState
},
),
),
BlocBuilder<NotificationPreferencesBloc,
NotificationPreferencesState>(
builder: (context, state) {
final areCategoriesSelected =
state.selectedCategoryIds.isNotEmpty;
if (areCategoriesSelected) {
return SizedBox(
if (state.selectedCategoryIds.isNotEmpty)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: !state.isLoading
? () async {
onPressed: state.isLoading
? null
: () async {
final bloc =
context.read<NotificationPreferencesBloc>();
@ -288,21 +286,17 @@ class _NotificationPreferencesPageState
selectedCategoryNames);
bloc.add(SubmitPreferences());
}
: null,
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm,
foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey,
disabledBackgroundColor: Colors.grey.withOpacity(0.5),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
),
child: state.isLoading
? const CircularProgressIndicator(
color: Colors.white)
: const Text(
child: const Text(
'اعمال',
style: TextStyle(
fontFamily: 'Dana',
@ -311,16 +305,23 @@ class _NotificationPreferencesPageState
),
),
),
);
} else {
return const SizedBox.shrink();
}
},
),
const SizedBox(height: 20),
],
),
),
if (state.isLoading)
Container(
color: Colors.black.withOpacity(0.4),
child: const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
],
);
},
),
);
}

View File

@ -1,4 +1,3 @@
// lib/presentation/pages/offers_page.dart
import 'dart:async';
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() {
return Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0),
@ -220,7 +252,7 @@ class _OffersPageState extends State<OffersPage> {
),
TextButton(
onPressed: () async {
await Navigator.of(context).push<bool>(
final result = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (context) => const NotificationPreferencesPage(
loadFavoritesOnStart: true,
@ -234,7 +266,11 @@ class _OffersPageState extends State<OffersPage> {
.read<NotificationPreferencesBloc>()
.add(ResetSubmissionStatus());
_loadPreferences();
await _loadPreferences();
if (result == true) {
_handleRefresh();
}
},
child: Row(
children: [
@ -362,7 +398,10 @@ class _OffersPageState extends State<OffersPage> {
const SizedBox(width: 8),
],
),
body: SingleChildScrollView(
body: RefreshIndicator(
onRefresh: _handleRefresh,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -370,11 +409,13 @@ class _OffersPageState extends State<OffersPage> {
OffersView(
isGpsEnabled: _isGpsEnabled,
isConnectedToInternet: _isConnectedToInternet,
selectedCategories: _selectedCategories,
),
],
),
),
),
),
);
}
}
@ -382,11 +423,13 @@ class _OffersPageState extends State<OffersPage> {
class OffersView extends StatelessWidget {
final bool isGpsEnabled;
final bool isConnectedToInternet;
final List<String> selectedCategories;
const OffersView({
super.key,
required this.isGpsEnabled,
required this.isConnectedToInternet,
required this.selectedCategories,
});
@override
@ -418,7 +461,13 @@ class OffersView extends StatelessWidget {
}
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(
height: 300,
child: Center(
@ -434,7 +483,7 @@ class OffersView extends StatelessWidget {
}
final groupedOffers = groupBy(
state.offers,
filteredOffers,
(OfferModel offer) => offer.category,
);
final categories = groupedOffers.keys.toList();

View File

@ -30,13 +30,7 @@ void onStart(ServiceInstance service) async {
});
}
mqttService.messages.listen((data) {
service.invoke('update', {'offers': data});
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
debugPrint("✅ Background Service: Sending location...");
Future<void> sendGpsData() async {
var locationStatus = await Permission.location.status;
if (!locationStatus.isGranted) {
debugPrint("Background Service: Location permission not granted.");
@ -69,6 +63,20 @@ void onStart(ServiceInstance service) async {
} catch (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();
});
}