Начальный коммит
This commit is contained in:
170
lib/services/api/api_service.dart
Normal file
170
lib/services/api/api_service.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
// lib/services/api/api_service.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:Stocky/models/task_adapter.dart';
|
||||
import 'package:Stocky/services/local/StorageService.dart';
|
||||
import 'package:Stocky/utils/constants.dart';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ApiService extends GetxService {
|
||||
static final ApiService _instance = ApiService._internal();
|
||||
|
||||
ApiService._internal() {
|
||||
_setupDio();
|
||||
}
|
||||
|
||||
final Dio _dio = Dio();
|
||||
|
||||
String? get accessToken => Get.find<LocalStorageService>().getAccessToken();
|
||||
|
||||
factory ApiService() => _instance;
|
||||
|
||||
void _setupDio() {
|
||||
_dio.options.baseUrl = Constants.BASE_URL;
|
||||
_dio.options.connectTimeout = const Duration(seconds: 10);
|
||||
_dio.options.receiveTimeout = const Duration(seconds: 10);
|
||||
_dio.options.contentType = Headers.jsonContentType;
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
final token = accessToken;
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) => handler.next(response),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future registerByPhone(String phone) async {
|
||||
try {
|
||||
await _dio.post(
|
||||
'/register',
|
||||
data: {'application': 'warehouse', 'phone': phone},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Регистрация по телефону: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> loginByPhone(String phone, String digCode) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/login',
|
||||
data: {
|
||||
'phone': phone,
|
||||
'digCode': digCode,
|
||||
'application': 'warehouse',
|
||||
}, // ← убран пробел
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
final token = data['token'];
|
||||
final username = data['name'];
|
||||
Get.find<LocalStorageService>().saveAccessToken(token);
|
||||
Get.find<LocalStorageService>().putUsername(username);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Логин по телефону: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<Task>> fetchTasks() async {
|
||||
try {
|
||||
final username = await Get.find<LocalStorageService>().getUsername();
|
||||
final response = await _dio.get('/warehouse/tasks/$username');
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> list = response.data;
|
||||
return list.map((item) => Task.fromMap(item)).toList();
|
||||
} else {
|
||||
debugPrint(
|
||||
'Некорректный статус при загрузке задач: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка загрузки задач: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<Task?> upsertTask(Task task) async {
|
||||
if (task.isNew == true || task.id.isEmpty) {
|
||||
final saved = await saveTask(task);
|
||||
return saved;
|
||||
} else {
|
||||
final success = await updateTask(task);
|
||||
return success ? task : null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Task?> saveTask(Task task) async {
|
||||
try {
|
||||
final response = await _dio.post('/warehouse/task', data: task.toMap());
|
||||
if (response.statusCode == 200) {
|
||||
return response.data;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка сохранения задачи: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> updateTask(Task task) async {
|
||||
try {
|
||||
debugPrint('Обновление задачи: ${task.toMap()}');
|
||||
final response = await _dio.put('/warehouse/task', data: task.toMap());
|
||||
return response.statusCode == 201;
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка обновления задачи: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> deleteTask(String taskId) async {
|
||||
try {
|
||||
final response = await _dio.delete('/warehouse/task/$taskId');
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка удаления задачи: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void clearAuth() {
|
||||
Get.find<LocalStorageService>().clearAuth();
|
||||
}
|
||||
|
||||
static Future<ApiService> init() async {
|
||||
final service = ApiService();
|
||||
Get.put(service, permanent: true);
|
||||
return service;
|
||||
}
|
||||
|
||||
Future<bool> setFcmToken(String token) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
'/warehouse/user',
|
||||
data: {
|
||||
"number": await Get.find<LocalStorageService>().getNumber(),
|
||||
"notification": {
|
||||
"token": token.toString(),
|
||||
"enable": true,
|
||||
"type": "FCM",
|
||||
},
|
||||
},
|
||||
);
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка обновление токена: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
151
lib/services/api/push_notification_service.dart
Normal file
151
lib/services/api/push_notification_service.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
// lib/services/api/push_notification_service.dart
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:Stocky/Pages/controllers/MainController.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
import 'api_service.dart'; // Убедитесь, что путь правильный
|
||||
|
||||
class PushNotificationService extends GetxService {
|
||||
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
|
||||
|
||||
final StreamController<String?> _tokenController =
|
||||
StreamController<String?>.broadcast();
|
||||
|
||||
final StreamController<RemoteMessage> _foregroundMessageController =
|
||||
StreamController<RemoteMessage>.broadcast();
|
||||
|
||||
// Поток для уведомлений, пришедших при открытом приложении
|
||||
Stream<RemoteMessage> get foregroundMessageStream =>
|
||||
_foregroundMessageController.stream;
|
||||
|
||||
// Поток FCM-токена
|
||||
Stream<String?> get tokenStream => _tokenController.stream;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
_initFirebase();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_tokenController.close();
|
||||
_foregroundMessageController.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> _initFirebase() async {
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
|
||||
// Запрашиваем разрешение
|
||||
await _requestPermission();
|
||||
|
||||
// Получаем токен
|
||||
await _getToken();
|
||||
|
||||
// Настраиваем обработчики
|
||||
_setupMessageHandling();
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка инициализации FCM: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _requestPermission() async {
|
||||
final status = await _messaging.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
// остальные по умолчанию false
|
||||
);
|
||||
|
||||
if (status.authorizationStatus == AuthorizationStatus.authorized ||
|
||||
status.authorizationStatus == AuthorizationStatus.provisional) {
|
||||
debugPrint('Разрешение на уведомления получено');
|
||||
} else {
|
||||
debugPrint('Разрешение отклонено');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getToken() async {
|
||||
try {
|
||||
final String? token = await _messaging.getToken();
|
||||
if (token != null) {
|
||||
_tokenController.add(token);
|
||||
print('FCM Token: $token');
|
||||
|
||||
// Отправляем токен на сервер — но только если ApiService уже инициализирован
|
||||
if (Get.isRegistered<ApiService>()) {
|
||||
Get.find<ApiService>().setFcmToken(token);
|
||||
} else {
|
||||
// Опционально: сохраните токен локально и отправьте позже
|
||||
print('ApiService не готов — токен временно не отправлен');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка получения токена FCM: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _setupMessageHandling() {
|
||||
// Уведомления, пришедшие при открытом приложении
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
debugPrint('Уведомление в foreground: ${message.notification?.title}');
|
||||
Get.find<MainController>().fetchTasks();
|
||||
_foregroundMessageController.add(message); // ← Передаём дальше
|
||||
});
|
||||
|
||||
// Пользователь нажал на уведомление из фонового режима
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||
debugPrint('Уведомление открыто из фона: ${message.notification?.title}');
|
||||
_handleMessageAction(message);
|
||||
});
|
||||
|
||||
// Фоновый обработчик (для закрытого приложения)
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||
}
|
||||
|
||||
// Обработка действия из уведомления
|
||||
void _handleMessageAction(RemoteMessage message) {
|
||||
final data = message.data;
|
||||
|
||||
// Пример: действие зависит от поля "action" в payload
|
||||
final action = data['action'];
|
||||
|
||||
switch (action) {
|
||||
case 'open_task':
|
||||
final taskId = data['task_id'];
|
||||
if (taskId != null) {
|
||||
// Здесь можно вызвать навигацию или обновить состояние
|
||||
// Например, через Get.toNamed('/task/$taskId');
|
||||
// Но лучше делегировать это MainController или другому слушателю
|
||||
Get.find<MainController>().openTaskFromNotification(taskId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'refresh_tasks':
|
||||
if (Get.isRegistered<MainController>()) {
|
||||
Get.find<MainController>().fetchTasks();
|
||||
}
|
||||
break;
|
||||
|
||||
// Добавьте другие действия по мере необходимости
|
||||
default:
|
||||
debugPrint('Неизвестное действие: $action');
|
||||
}
|
||||
}
|
||||
|
||||
// Статический фоновый обработчик (ограниченный функционал)
|
||||
static Future<void> _firebaseMessagingBackgroundHandler(
|
||||
RemoteMessage message,
|
||||
) async {
|
||||
// В фоне нельзя использовать GetX, Dio с авторизацией и т.п.
|
||||
// Только простые операции: логирование, запись в Hive и т.д.
|
||||
debugPrint('Фоновое уведомление: ${message.messageId}');
|
||||
// Пример: сохранить в локальную БД, чтобы обработать при запуске
|
||||
}
|
||||
}
|
||||
88
lib/services/api/socket_service.dart0
Normal file
88
lib/services/api/socket_service.dart0
Normal file
@@ -0,0 +1,88 @@
|
||||
// lib/services/api/socket_service.dart
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_stocky/utils/constants.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
class SocketService extends GetxService {
|
||||
final String _baseUrl = Constants.BASE_SOCKET_URL;
|
||||
WebSocketChannel? _channel;
|
||||
bool _connected = false;
|
||||
|
||||
// Поток для получения сообщений
|
||||
StreamController<Map<String, dynamic>> _messageController =
|
||||
StreamController.broadcast();
|
||||
|
||||
Stream<Map<String, dynamic>> get messages => _messageController.stream;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
connect();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_disconnect();
|
||||
_messageController.close();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void connect() async {
|
||||
if (_connected) return;
|
||||
|
||||
try {
|
||||
_channel = WebSocketChannel.connect(Uri.parse('$_baseUrl/ws'));
|
||||
_channel!.stream.listen(
|
||||
(message) {
|
||||
final data = json.decode(message);
|
||||
_messageController.add(data);
|
||||
},
|
||||
onDone: () {
|
||||
_connected = false;
|
||||
debugPrint('WebSocket отключен');
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('WebSocket ошибка: $error');
|
||||
_connected = false;
|
||||
// Автоподключение через 3 сек
|
||||
Future.delayed(const Duration(seconds: 3), connect);
|
||||
},
|
||||
);
|
||||
|
||||
_connected = true;
|
||||
debugPrint('WebSocket подключен');
|
||||
} catch (e) {
|
||||
debugPrint('Ошибка подключения к WebSocket: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _disconnect() {
|
||||
_channel?.close();
|
||||
_channel = null;
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
void sendMessage(Map<String, dynamic> message) {
|
||||
if (_connected && _channel != null) {
|
||||
_channel?.sink.add(json.encode(message));
|
||||
}
|
||||
}
|
||||
|
||||
// Пример: отправить обновление задачи
|
||||
void notifyTaskUpdate(String taskId) {
|
||||
sendMessage({'type': 'task_updated', 'task_id': taskId});
|
||||
}
|
||||
|
||||
// Пример: подписаться на изменения задач
|
||||
void subscribeToTasks() {
|
||||
sendMessage({'type': 'subscribe', 'entity': 'tasks'});
|
||||
}
|
||||
|
||||
// Пример: уведомить о создании задачи
|
||||
void notifyTaskCreated(Task task) {
|
||||
sendMessage({'type': 'task_created', 'data': task.toJson()});
|
||||
}
|
||||
|
||||
bool get isConnected => _connected;
|
||||
}
|
||||
106
lib/services/local/StorageService.dart
Normal file
106
lib/services/local/StorageService.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
// lib/services/local/local_storage_service.dart
|
||||
import 'package:Stocky/models/task_adapter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
class LocalStorageService extends GetxService {
|
||||
static final LocalStorageService _instance = LocalStorageService._internal();
|
||||
|
||||
late Box _settBox;
|
||||
late Box<Task> _tasksBox;
|
||||
late Box _docsBox;
|
||||
|
||||
LocalStorageService._internal();
|
||||
|
||||
factory LocalStorageService() => _instance;
|
||||
|
||||
Box get settingsBox => _settBox;
|
||||
Box<Task> get tasksBox => _tasksBox;
|
||||
Box get docsBox => _docsBox;
|
||||
|
||||
List<Task> getTasks() {
|
||||
return tasksBox.values.toList();
|
||||
}
|
||||
|
||||
Future<List<Task>> getCompletedTasks() async {
|
||||
return tasksBox.values.where((task) => !task.isCompleted).toList();
|
||||
}
|
||||
|
||||
Future<void> putNumber(String number) async {
|
||||
await settingsBox.put('number', number);
|
||||
}
|
||||
|
||||
Future<String> getNumber() async {
|
||||
return settingsBox.get('number');
|
||||
}
|
||||
|
||||
Future<void> putUsername(String name) async {
|
||||
await settingsBox.put('username', name);
|
||||
}
|
||||
|
||||
Future<String> getUsername() async {
|
||||
return settingsBox.get('username', defaultValue: 'john doe');
|
||||
}
|
||||
|
||||
Future<void> saveTasks(List<Task> tasks) async {
|
||||
await tasksBox.clear();
|
||||
for (final task in tasks) {
|
||||
await tasksBox.put(task.id.toString(), task);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addTask(Task task) async {
|
||||
await tasksBox.put(task.id.toString(), task);
|
||||
}
|
||||
|
||||
Future<void> updateTask(Task task) async {
|
||||
await tasksBox.put(task.id.toString(), task);
|
||||
}
|
||||
|
||||
Future<void> replaceTask(String oldId, Task newTask) async {
|
||||
await tasksBox.delete(oldId);
|
||||
await tasksBox.put(newTask.id.toString(), newTask);
|
||||
}
|
||||
|
||||
Future<void> removeTask(dynamic taskId) async {
|
||||
await tasksBox.delete(taskId.toString());
|
||||
}
|
||||
|
||||
// --- Токены ---
|
||||
Future<void> saveAccessToken(String token) async {
|
||||
await settingsBox.put('access_token', token);
|
||||
}
|
||||
|
||||
Future<void> clearAuth() async {
|
||||
await settingsBox.delete('access_token');
|
||||
}
|
||||
|
||||
Future<void> saveFcmToken(String token) async {
|
||||
await settingsBox.put('fcm_token', token);
|
||||
}
|
||||
|
||||
String? getAccessToken() => settingsBox.get('access_token');
|
||||
String? getFcmToken() => settingsBox.get('fcm_token');
|
||||
|
||||
static Future<LocalStorageService> init() async {
|
||||
await Hive.initFlutter();
|
||||
|
||||
Hive.registerAdapter(TaskAdapter());
|
||||
Hive.registerAdapter(SubtaskAdapter());
|
||||
|
||||
// Открываем боксы
|
||||
await Hive.openBox('settings');
|
||||
await Hive.openBox<Task>('tasks');
|
||||
await Hive.openBox('docs');
|
||||
|
||||
// Теперь можно безопасно получить ссылки
|
||||
final service = _instance;
|
||||
service._settBox = Hive.box('settings');
|
||||
service._tasksBox = Hive.box<Task>('tasks');
|
||||
service._docsBox = Hive.box('docs');
|
||||
|
||||
Get.put(service, permanent: true);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user