add refresh offer_page
This commit is contained in:
parent
f4cd446cde
commit
608222e8a3
|
|
@ -11,7 +11,7 @@ plugins {
|
|||
android {
|
||||
namespace = "com.example.proxibuy"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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,151 +177,151 @@ class _NotificationPreferencesPageState
|
|||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
builder: (context, state) {
|
||||
return Stack(
|
||||
children: [
|
||||
const Text(
|
||||
'دریافت اعلان',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Divider(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 14,
|
||||
color: AppColors.hint,
|
||||
height: 1.5,
|
||||
),
|
||||
children: const <TextSpan>[
|
||||
TextSpan(
|
||||
text:
|
||||
'ترجیح میدی از کدام دستهبندیها اعلان تخفیف دریافت کنی؟ ',
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'دریافت اعلان',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'),
|
||||
const SizedBox(height: 4),
|
||||
const Divider(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 14,
|
||||
color: AppColors.hint,
|
||||
height: 1.5,
|
||||
),
|
||||
children: const <TextSpan>[
|
||||
TextSpan(
|
||||
text:
|
||||
'ترجیح میدی از کدام دستهبندیها اعلان تخفیف دریافت کنی؟ ',
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (state.categories.isEmpty && state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final double horizontalPadding = 24.0;
|
||||
final double crossAxisSpacing = 16.0;
|
||||
final int crossAxisCount = 3;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final itemWidth = (screenWidth -
|
||||
(horizontalPadding * 2) -
|
||||
(crossAxisSpacing * (crossAxisCount - 1))) /
|
||||
crossAxisCount;
|
||||
final itemHeight = itemWidth / 0.9;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: crossAxisSpacing,
|
||||
runSpacing: 24.0,
|
||||
alignment: WrapAlignment.center,
|
||||
children: state.categories.map((category) {
|
||||
final isSelected =
|
||||
state.selectedCategoryIds.contains(category.id);
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
child: CategorySelectionCard(
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
isSelected: isSelected,
|
||||
showSelectableIndicator:
|
||||
state.selectedCategoryIds.isNotEmpty,
|
||||
onTap: () {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
ToggleCategorySelection(category.id));
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.selectedCategoryIds.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: state.isLoading
|
||||
? null
|
||||
: () async {
|
||||
final bloc =
|
||||
context.read<NotificationPreferencesBloc>();
|
||||
|
||||
final selectedCategoryNames = bloc
|
||||
.state.categories
|
||||
.where((cat) => bloc.state
|
||||
.selectedCategoryIds
|
||||
.contains(cat.id))
|
||||
.map((cat) => cat.name)
|
||||
.toList();
|
||||
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(
|
||||
'user_selected_categories',
|
||||
selectedCategoryNames);
|
||||
|
||||
bloc.add(SubmitPreferences());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.confirm,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.grey.withOpacity(0.5),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'اعمال',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: BlocBuilder<NotificationPreferencesBloc,
|
||||
NotificationPreferencesState>(
|
||||
builder: (context, state) {
|
||||
if (state.categories.isEmpty && state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final double horizontalPadding = 24.0;
|
||||
final double crossAxisSpacing = 16.0;
|
||||
final int crossAxisCount = 3;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final itemWidth = (screenWidth -
|
||||
(horizontalPadding * 2) -
|
||||
(crossAxisSpacing * (crossAxisCount - 1))) /
|
||||
crossAxisCount;
|
||||
final itemHeight = itemWidth / 0.9;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: crossAxisSpacing,
|
||||
runSpacing: 24.0,
|
||||
alignment: WrapAlignment.center,
|
||||
children: state.categories.map((category) {
|
||||
final isSelected =
|
||||
state.selectedCategoryIds.contains(category.id);
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
child: CategorySelectionCard(
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
isSelected: isSelected,
|
||||
showSelectableIndicator:
|
||||
state.selectedCategoryIds.isNotEmpty,
|
||||
onTap: () {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
ToggleCategorySelection(category.id));
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
if (state.isLoading)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocBuilder<NotificationPreferencesBloc,
|
||||
NotificationPreferencesState>(
|
||||
builder: (context, state) {
|
||||
final areCategoriesSelected =
|
||||
state.selectedCategoryIds.isNotEmpty;
|
||||
|
||||
if (areCategoriesSelected) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: !state.isLoading
|
||||
? () async {
|
||||
final bloc =
|
||||
context.read<NotificationPreferencesBloc>();
|
||||
|
||||
final selectedCategoryNames = bloc
|
||||
.state.categories
|
||||
.where((cat) => bloc.state
|
||||
.selectedCategoryIds
|
||||
.contains(cat.id))
|
||||
.map((cat) => cat.name)
|
||||
.toList();
|
||||
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(
|
||||
'user_selected_categories',
|
||||
selectedCategoryNames);
|
||||
|
||||
bloc.add(SubmitPreferences());
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.confirm,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.grey,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
),
|
||||
child: state.isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white)
|
||||
: const Text(
|
||||
'اعمال',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Dana',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,16 +398,21 @@ class _OffersPageState extends State<OffersPage> {
|
|||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFavoriteCategoriesSection(),
|
||||
OffersView(
|
||||
isGpsEnabled: _isGpsEnabled,
|
||||
isConnectedToInternet: _isConnectedToInternet,
|
||||
),
|
||||
],
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _handleRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFavoriteCategoriesSection(),
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue