184 lines
6.8 KiB
Dart
184 lines
6.8 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
class AppConfig {
|
|
// ─── API Configuration ────────────────────────────────────────────────────
|
|
//
|
|
// Dual-Mode Configuration:
|
|
// - If public ngrok is running and active, the app will use it automatically.
|
|
// - If ngrok is down/inactive or unreachable, it falls back to the LAN/Local URL.
|
|
//
|
|
|
|
// LAN / WiFi / Emulator URL (port 8000 = nginx gateway)
|
|
// - Physical Device on same Wi-Fi: use 'http://192.168.70.7:8000/api/v1'
|
|
// - Android Emulator: use 'http://10.0.2.2:8000/api/v1'
|
|
// - iOS Simulator: use 'http://localhost:8000/api/v1'
|
|
static const String _lanBaseUrl = 'http://192.168.70.7:8000/api/v1';
|
|
|
|
// Ngrok public tunnel URL (update ini setiap ngrok di-restart)
|
|
static const String _ngrokBaseUrl = 'https://unlocated-waylon-potently.ngrok-free.dev/api/v1';
|
|
|
|
// Dynamic Base URL determined at startup
|
|
static String apiBaseUrl = _lanBaseUrl;
|
|
|
|
/// Dynamic URL resolver: Checks if the Ngrok tunnel is active.
|
|
/// If it returns a valid response (not 502/503/504), we use Ngrok.
|
|
/// Otherwise, we fall back to the LAN/Local base URL.
|
|
static Future<void> initializeApiBaseUrl() async {
|
|
final client = HttpClient();
|
|
client.connectionTimeout = const Duration(seconds: 2);
|
|
try {
|
|
debugPrint('AppConfig: Checking public Ngrok tunnel at $_ngrokBaseUrl/auth/login...');
|
|
final uri = Uri.parse('$_ngrokBaseUrl/auth/login');
|
|
final request = await client.postUrl(uri);
|
|
|
|
// Add headers to bypass the ngrok browser warning page
|
|
request.headers.add('ngrok-skip-browser-warning', 'true');
|
|
request.headers.contentType = ContentType.json;
|
|
request.write('{}');
|
|
|
|
final response = await request.close();
|
|
|
|
final contentType = response.headers.contentType?.mimeType ?? '';
|
|
final ngrokErrorCode = response.headers.value('ngrok-error-code');
|
|
|
|
debugPrint('AppConfig: Ngrok response status: ${response.statusCode}, Content-Type: $contentType, Ngrok-Error-Code: $ngrokErrorCode');
|
|
|
|
// An active tunnel connected to our backend:
|
|
// 1. Should not return an ngrok-error-code header.
|
|
// 2. The response content-type should be JSON (our login API returns JSON).
|
|
// 3. Status code should not be 502, 503, or 504.
|
|
if (ngrokErrorCode == null &&
|
|
contentType.contains('json') &&
|
|
response.statusCode != 502 &&
|
|
response.statusCode != 503 &&
|
|
response.statusCode != 504) {
|
|
apiBaseUrl = _ngrokBaseUrl;
|
|
debugPrint('AppConfig: Ngrok tunnel is ACTIVE and backend responded. API Base URL: $apiBaseUrl');
|
|
return;
|
|
} else {
|
|
debugPrint('AppConfig: Ngrok is offline, inactive, or returned an error page.');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('AppConfig: Ngrok connection failed or timed out: $e');
|
|
} finally {
|
|
client.close();
|
|
}
|
|
|
|
apiBaseUrl = _lanBaseUrl;
|
|
debugPrint('AppConfig: Using Local/LAN API Base URL: $apiBaseUrl');
|
|
}
|
|
|
|
static const String loginEndpoint = '/auth/login';
|
|
static const String uploadDocumentEndpoint = '/documents/upload';
|
|
static const String fetchDocumentsEndpoint = '/documents';
|
|
|
|
|
|
// App Constants
|
|
static const String appName = 'Prima Mart Scanner';
|
|
static const String appVersion = '1.0.0';
|
|
|
|
// Colors - Prima Fresh Mart Theme
|
|
static const Color primaryColor = Color(0xFF6EBE4A); // Prima Green
|
|
static const Color secondaryColor = Color(0xFF005DAA); // Prima Blue
|
|
static const Color accentColor = Color(0xFFFFCC00); // Prima Yellow
|
|
static const Color backgroundColor = Color(0xFFF8F9FA); // Off-white
|
|
static const Color surfaceColor = Colors.white;
|
|
static const Color errorColor = Color(0xFFD32F2F);
|
|
static const Color successColor = Color(0xFF388E3C);
|
|
|
|
// Text Colors
|
|
static const Color textPrimary = Color(0xFF212121);
|
|
static const Color textSecondary = Color(0xFF757575);
|
|
static const Color textOnPrimary = Colors.white;
|
|
|
|
// Typography Config
|
|
// Using Google Fonts - Inter for clean, modern look
|
|
static TextStyle get h1 => GoogleFonts.inter(
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.bold,
|
|
color: textPrimary,
|
|
);
|
|
|
|
static TextStyle get h2 => GoogleFonts.inter(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w600,
|
|
color: textPrimary,
|
|
);
|
|
|
|
static TextStyle get h3 => GoogleFonts.inter(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: textPrimary,
|
|
);
|
|
|
|
static TextStyle get bodyLarge => GoogleFonts.inter(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.normal,
|
|
color: textPrimary,
|
|
);
|
|
|
|
static TextStyle get bodyMedium => GoogleFonts.inter(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.normal,
|
|
color: textPrimary,
|
|
);
|
|
|
|
static TextStyle get bodySmall => GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.normal,
|
|
color: textSecondary,
|
|
);
|
|
|
|
// Theme Data Builder
|
|
static ThemeData get lightTheme {
|
|
return ThemeData(
|
|
primaryColor: primaryColor,
|
|
scaffoldBackgroundColor: backgroundColor,
|
|
colorScheme: const ColorScheme.light(
|
|
primary: primaryColor,
|
|
secondary: secondaryColor,
|
|
surface: surfaceColor,
|
|
error: errorColor,
|
|
),
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: surfaceColor,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
iconTheme: const IconThemeData(color: textPrimary),
|
|
titleTextStyle: h2.copyWith(color: textPrimary),
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: primaryColor,
|
|
foregroundColor: textOnPrimary,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
|
textStyle: bodyLarge.copyWith(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: surfaceColor,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: primaryColor, width: 2),
|
|
),
|
|
labelStyle: bodyMedium.copyWith(color: textSecondary),
|
|
),
|
|
);
|
|
}
|
|
}
|