Flutter App

This commit is contained in:
2025-12-14 21:53:46 -05:00
parent 383e2e07bd
commit 7ed7a2470d
108 changed files with 7077 additions and 130 deletions

View File

@@ -0,0 +1,76 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/price_data.dart';
class ApiService {
final String baseUrl;
ApiService() : baseUrl = dotenv.env['API_URL'] ?? 'http://localhost:3000' {
// Debug: Print the actual URL being used
if (kDebugMode) {
print('ApiService initialized with baseUrl: $baseUrl');
print('Available env vars: ${dotenv.env.keys.toList()}');
}
}
Future<LatestPrice?> fetchLatestPrice() async {
try {
final url = '$baseUrl/prices/latest';
if (kDebugMode) {
print('Fetching latest price from: $url');
}
final response = await http.get(Uri.parse(url));
if (kDebugMode) {
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
}
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
return LatestPrice.fromJson(data);
} else {
if (kDebugMode) {
print('HTTP Error ${response.statusCode}: ${response.body}');
}
}
} catch (e) {
if (kDebugMode) {
print('Error fetching latest price: $e');
}
}
return null;
}
Future<HistoryData?> fetchHistory(String range) async {
try {
final url = '$baseUrl/index/history?range=$range';
if (kDebugMode) {
print('Fetching history from: $url');
}
final response = await http.get(Uri.parse(url));
if (kDebugMode) {
print('History response status: ${response.statusCode}');
}
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
return HistoryData.fromJson(data);
} else {
if (kDebugMode) {
print('HTTP Error ${response.statusCode}: ${response.body}');
}
}
} catch (e) {
if (kDebugMode) {
print('Error fetching history: $e');
}
}
return null;
}
}

View File

@@ -0,0 +1,150 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/foundation.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
bool _initialized = false;
Future<void> initialize() async {
if (_initialized) return;
try {
// Android initialization
const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
// iOS initialization
const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
// Linux/Windows initialization
const LinuxInitializationSettings linuxSettings = LinuxInitializationSettings(
defaultActionName: 'Open rmtPocketWatcher',
);
const InitializationSettings settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
linux: linuxSettings,
);
await _notifications.initialize(
settings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
_initialized = true;
if (kDebugMode) {
print('NotificationService initialized successfully');
}
} catch (e) {
if (kDebugMode) {
print('Failed to initialize notifications: $e');
}
// Don't throw - allow app to continue without notifications
}
}
void _onNotificationTapped(NotificationResponse response) {
// Handle notification tap if needed
if (kDebugMode) {
print('Notification tapped: ${response.payload}');
}
}
Future<void> showPriceAlert({
required String title,
required String body,
required double auecAmount,
required double price,
required String seller,
}) async {
if (!_initialized) await initialize();
if (!_initialized) return; // Skip if initialization failed
try {
// Android notification details
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
'price_alerts',
'Price Alerts',
channelDescription: 'Notifications for AUEC price alerts from rmtPocketWatcher',
importance: Importance.high,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound('notifcation'),
icon: '@mipmap/ic_launcher',
largeIcon: DrawableResourceAndroidBitmap('@mipmap/ic_launcher'),
enableVibration: true,
enableLights: true,
showWhen: true,
);
// iOS notification details
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
sound: 'notifcation.mp3', // iOS looks in main bundle, not assets
presentAlert: true,
presentBadge: true,
presentSound: true,
badgeNumber: 1,
subtitle: 'Lambda Banking Conglomerate',
threadIdentifier: 'price_alerts',
);
// Linux/Windows notification details
final LinuxNotificationDetails linuxDetails = LinuxNotificationDetails(
icon: AssetsLinuxIcon('assets/logo.png'),
sound: AssetsLinuxSound('assets/notifcation.mp3'),
category: LinuxNotificationCategory.imReceived,
urgency: LinuxNotificationUrgency.critical,
);
final NotificationDetails details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
linux: linuxDetails,
);
final int notificationId = DateTime.now().millisecondsSinceEpoch.remainder(100000);
await _notifications.show(
notificationId,
title,
body,
details,
payload: 'price_alert:$seller:$auecAmount:$price',
);
if (kDebugMode) {
print('Price alert notification sent: $title - $body');
}
} catch (e) {
if (kDebugMode) {
print('Failed to show notification: $e');
}
}
}
Future<void> requestPermissions() async {
if (!_initialized) await initialize();
// Request permissions for iOS
await _notifications
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
// Request permissions for Android 13+
await _notifications
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
}
}

View File

@@ -0,0 +1,54 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/price_data.dart';
class StorageService {
static const String _alertsKey = 'price_alerts';
static const String _customAuecKey = 'custom_auec_amount';
Future<List<PriceAlert>> getAlerts() async {
final prefs = await SharedPreferences.getInstance();
final alertsJson = prefs.getString(_alertsKey);
if (alertsJson == null) return [];
final List<dynamic> decoded = jsonDecode(alertsJson);
return decoded.map((e) => PriceAlert.fromJson(e as Map<String, dynamic>)).toList();
}
Future<void> saveAlerts(List<PriceAlert> alerts) async {
final prefs = await SharedPreferences.getInstance();
final alertsJson = jsonEncode(alerts.map((e) => e.toJson()).toList());
await prefs.setString(_alertsKey, alertsJson);
}
Future<void> addAlert(PriceAlert alert) async {
final alerts = await getAlerts();
alerts.add(alert);
await saveAlerts(alerts);
}
Future<void> updateAlert(PriceAlert alert) async {
final alerts = await getAlerts();
final index = alerts.indexWhere((a) => a.id == alert.id);
if (index != -1) {
alerts[index] = alert;
await saveAlerts(alerts);
}
}
Future<void> deleteAlert(String id) async {
final alerts = await getAlerts();
alerts.removeWhere((a) => a.id == id);
await saveAlerts(alerts);
}
Future<double?> getCustomAuecAmount() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getDouble(_customAuecKey);
}
Future<void> setCustomAuecAmount(double amount) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_customAuecKey, amount);
}
}

View File

@@ -0,0 +1,127 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/price_data.dart';
class WebSocketService {
WebSocketChannel? _channel;
final _latestPriceController = StreamController<LatestPrice>.broadcast();
final _connectionStatusController = StreamController<String>.broadcast();
Timer? _reconnectTimer;
bool _isConnecting = false;
Stream<LatestPrice> get latestPriceStream => _latestPriceController.stream;
Stream<String> get connectionStatusStream => _connectionStatusController.stream;
void connect() {
if (_isConnecting) return;
_isConnecting = true;
try {
final wsUrl = dotenv.env['WS_URL'] ?? 'ws://localhost:3000/ws/index';
if (kDebugMode) {
print('WebSocket connecting to: $wsUrl');
print('Available env vars: ${dotenv.env.keys.toList()}');
}
_channel = WebSocketChannel.connect(Uri.parse(wsUrl));
// Send subscription message after connection
_channel!.sink.add(jsonEncode({
'type': 'subscribe',
'data': {'channel': 'price_updates'}
}));
_connectionStatusController.add('Connected');
_isConnecting = false;
_channel!.stream.listen(
(message) {
try {
final data = jsonDecode(message as String) as Map<String, dynamic>;
if (kDebugMode) {
print('WebSocket message received: ${data['type']}');
}
switch (data['type']) {
case 'price_update':
final latestPrice = LatestPrice.fromJson({'data': data['data']});
_latestPriceController.add(latestPrice);
break;
case 'history_data':
// Handle history data if needed
break;
case 'connection_status':
if (kDebugMode) {
print('Connection status: ${data['data']}');
}
break;
case 'error':
if (kDebugMode) {
print('WebSocket error message: ${data['data']}');
}
break;
default:
if (kDebugMode) {
print('Unknown message type: ${data['type']}');
}
}
} catch (e) {
if (kDebugMode) {
print('Error parsing WebSocket message: $e');
}
}
},
onError: (error) {
if (kDebugMode) {
print('WebSocket error: $error');
}
_connectionStatusController.add('Error');
_scheduleReconnect();
},
onDone: () {
if (kDebugMode) {
print('WebSocket connection closed');
}
_connectionStatusController.add('Disconnected');
_scheduleReconnect();
},
);
} catch (e) {
if (kDebugMode) {
print('Failed to connect: $e');
}
_connectionStatusController.add('Error');
_isConnecting = false;
_scheduleReconnect();
}
}
void _scheduleReconnect() {
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(seconds: 5), () {
if (kDebugMode) {
print('Attempting to reconnect...');
}
connect();
});
}
void requestHistory(String range) {
if (_channel != null) {
_channel!.sink.add(jsonEncode({
'type': 'request_history',
'range': range,
}));
}
}
void dispose() {
_reconnectTimer?.cancel();
_channel?.sink.close();
_latestPriceController.close();
_connectionStatusController.close();
}
}