chore: normalize line endings (CRLF -> LF)
No content changes: git diff --ignore-all-space over these files is empty. The churn came from editing on Windows against a repo checked out with LF.
This commit is contained in:
1 parent
15566a6951
commit
caf8e98378
315 files changed
+86950
-86950
No files matched your search
+231
-231
@@ -1,231 +1,231 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class AppConfig {
|
||||
// ─── API Configuration ────────────────────────────────────────────────────
|
||||
//
|
||||
// Dual-Mode Configuration, tried in this order at every app startup:
|
||||
// 1. Local/LAN backend (fast, low-latency - the common case when the phone
|
||||
// and backend are on the same Wi-Fi).
|
||||
// 2. Public Ngrok tunnel (fallback for when the phone is off that network,
|
||||
// or the LAN URL below is stale).
|
||||
// Whichever responds first with a real backend (not a dead route/tunnel
|
||||
// error page) wins; if neither does, LAN is used as the default so error
|
||||
// messages point at the expected local backend rather than a dead tunnel.
|
||||
//
|
||||
|
||||
// LAN / WiFi / Emulator URL (port 8000 = nginx gateway)
|
||||
// - Physical Device on same Wi-Fi: use the backend machine's current LAN IP
|
||||
// - 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.80.84: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: tries the LAN backend first, then the public
|
||||
/// Ngrok tunnel, and picks whichever responds with a real backend.
|
||||
static Future<void> initializeApiBaseUrl() async {
|
||||
debugPrint('AppConfig: Checking local/LAN backend at $_lanBaseUrl...');
|
||||
if (await _isBackendReachable(_lanBaseUrl)) {
|
||||
apiBaseUrl = _lanBaseUrl;
|
||||
debugPrint('AppConfig: LAN backend is ACTIVE. API Base URL: $apiBaseUrl');
|
||||
return;
|
||||
}
|
||||
debugPrint('AppConfig: LAN backend not reachable, trying public Ngrok tunnel at $_ngrokBaseUrl...');
|
||||
|
||||
if (await _isBackendReachable(_ngrokBaseUrl)) {
|
||||
apiBaseUrl = _ngrokBaseUrl;
|
||||
debugPrint('AppConfig: Ngrok tunnel is ACTIVE. API Base URL: $apiBaseUrl');
|
||||
return;
|
||||
}
|
||||
|
||||
apiBaseUrl = _lanBaseUrl;
|
||||
debugPrint('AppConfig: Neither LAN nor Ngrok reachable. Defaulting to LAN API Base URL: $apiBaseUrl');
|
||||
}
|
||||
|
||||
/// Probes [baseUrl]/auth/login with a throwaway request to check whether a
|
||||
/// real backend answers behind it - not a timed-out connection, a dead
|
||||
/// tunnel, or a router/error page pretending to be a response.
|
||||
static Future<bool> _isBackendReachable(String baseUrl) async {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 2);
|
||||
try {
|
||||
final uri = Uri.parse('$baseUrl/auth/login');
|
||||
final request = await client.postUrl(uri);
|
||||
|
||||
// Bypasses the Ngrok browser-warning interstitial; harmless for LAN.
|
||||
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: Probe $baseUrl -> status ${response.statusCode}, '
|
||||
'Content-Type: $contentType, Ngrok-Error-Code: $ngrokErrorCode');
|
||||
|
||||
// A real backend behind this URL:
|
||||
// 1. Should not return an ngrok-error-code header (dead/offline tunnel).
|
||||
// 2. The response content-type should be JSON (our login API returns JSON).
|
||||
// 3. Status code should not be 502, 503, or 504 (gateway/proxy errors).
|
||||
return ngrokErrorCode == null &&
|
||||
contentType.contains('json') &&
|
||||
response.statusCode != 502 &&
|
||||
response.statusCode != 503 &&
|
||||
response.statusCode != 504;
|
||||
} catch (e) {
|
||||
debugPrint('AppConfig: Probe $baseUrl failed: $e');
|
||||
return false;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
static const String loginEndpoint = '/auth/login';
|
||||
static const String uploadDocumentEndpoint = '/documents/upload';
|
||||
static const String fetchDocumentsEndpoint = '/documents';
|
||||
static const String masterSkusEndpoint = '/master/skus';
|
||||
static const String scanProductEndpoint = '/scan-product';
|
||||
|
||||
|
||||
// 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);
|
||||
// DO mode color — orange cue used throughout for the DO Scan mode:
|
||||
// document_card.dart top-label, documents_tab_switcher.dart active DO tab,
|
||||
// camera_drawer_mode_toggle.dart DO segment decoration. Formalized as a
|
||||
// shared constant so all three call sites pull from one place (was an
|
||||
// ad-hoc Colors.orange.shade700 inline in document_card.dart).
|
||||
static const Color doModeColor = Color(0xFFF57C00); // == Colors.orange.shade700
|
||||
|
||||
// Text Colors
|
||||
static const Color textPrimary = Color(0xFF212121);
|
||||
static const Color textSecondary = Color(0xFF757575);
|
||||
static const Color textOnPrimary = Colors.white;
|
||||
|
||||
// Spacing System (Margin & Padding)
|
||||
static const double spaceXS = 4.0;
|
||||
static const double spaceSmall = 8.0;
|
||||
static const double spaceMedium = 12.0;
|
||||
static const double spaceLarge = 16.0;
|
||||
static const double spaceXL = 24.0;
|
||||
static const double spaceXXL = 32.0;
|
||||
|
||||
static const EdgeInsets paddingXS = EdgeInsets.all(spaceXS);
|
||||
static const EdgeInsets paddingSmall = EdgeInsets.all(spaceSmall);
|
||||
static const EdgeInsets paddingMedium = EdgeInsets.all(spaceMedium);
|
||||
static const EdgeInsets paddingLarge = EdgeInsets.all(spaceLarge);
|
||||
static const EdgeInsets paddingXL = EdgeInsets.all(spaceXL);
|
||||
static const EdgeInsets screenPadding = EdgeInsets.symmetric(horizontal: spaceXL, vertical: spaceLarge);
|
||||
|
||||
// Border Radii
|
||||
static const double borderRadiusSmall = 8.0;
|
||||
static const double borderRadiusMedium = 12.0;
|
||||
static const double borderRadiusLarge = 16.0;
|
||||
static const double borderRadiusXL = 24.0;
|
||||
|
||||
// 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class AppConfig {
|
||||
// ─── API Configuration ────────────────────────────────────────────────────
|
||||
//
|
||||
// Dual-Mode Configuration, tried in this order at every app startup:
|
||||
// 1. Local/LAN backend (fast, low-latency - the common case when the phone
|
||||
// and backend are on the same Wi-Fi).
|
||||
// 2. Public Ngrok tunnel (fallback for when the phone is off that network,
|
||||
// or the LAN URL below is stale).
|
||||
// Whichever responds first with a real backend (not a dead route/tunnel
|
||||
// error page) wins; if neither does, LAN is used as the default so error
|
||||
// messages point at the expected local backend rather than a dead tunnel.
|
||||
//
|
||||
|
||||
// LAN / WiFi / Emulator URL (port 8000 = nginx gateway)
|
||||
// - Physical Device on same Wi-Fi: use the backend machine's current LAN IP
|
||||
// - 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.80.84: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: tries the LAN backend first, then the public
|
||||
/// Ngrok tunnel, and picks whichever responds with a real backend.
|
||||
static Future<void> initializeApiBaseUrl() async {
|
||||
debugPrint('AppConfig: Checking local/LAN backend at $_lanBaseUrl...');
|
||||
if (await _isBackendReachable(_lanBaseUrl)) {
|
||||
apiBaseUrl = _lanBaseUrl;
|
||||
debugPrint('AppConfig: LAN backend is ACTIVE. API Base URL: $apiBaseUrl');
|
||||
return;
|
||||
}
|
||||
debugPrint('AppConfig: LAN backend not reachable, trying public Ngrok tunnel at $_ngrokBaseUrl...');
|
||||
|
||||
if (await _isBackendReachable(_ngrokBaseUrl)) {
|
||||
apiBaseUrl = _ngrokBaseUrl;
|
||||
debugPrint('AppConfig: Ngrok tunnel is ACTIVE. API Base URL: $apiBaseUrl');
|
||||
return;
|
||||
}
|
||||
|
||||
apiBaseUrl = _lanBaseUrl;
|
||||
debugPrint('AppConfig: Neither LAN nor Ngrok reachable. Defaulting to LAN API Base URL: $apiBaseUrl');
|
||||
}
|
||||
|
||||
/// Probes [baseUrl]/auth/login with a throwaway request to check whether a
|
||||
/// real backend answers behind it - not a timed-out connection, a dead
|
||||
/// tunnel, or a router/error page pretending to be a response.
|
||||
static Future<bool> _isBackendReachable(String baseUrl) async {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 2);
|
||||
try {
|
||||
final uri = Uri.parse('$baseUrl/auth/login');
|
||||
final request = await client.postUrl(uri);
|
||||
|
||||
// Bypasses the Ngrok browser-warning interstitial; harmless for LAN.
|
||||
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: Probe $baseUrl -> status ${response.statusCode}, '
|
||||
'Content-Type: $contentType, Ngrok-Error-Code: $ngrokErrorCode');
|
||||
|
||||
// A real backend behind this URL:
|
||||
// 1. Should not return an ngrok-error-code header (dead/offline tunnel).
|
||||
// 2. The response content-type should be JSON (our login API returns JSON).
|
||||
// 3. Status code should not be 502, 503, or 504 (gateway/proxy errors).
|
||||
return ngrokErrorCode == null &&
|
||||
contentType.contains('json') &&
|
||||
response.statusCode != 502 &&
|
||||
response.statusCode != 503 &&
|
||||
response.statusCode != 504;
|
||||
} catch (e) {
|
||||
debugPrint('AppConfig: Probe $baseUrl failed: $e');
|
||||
return false;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
static const String loginEndpoint = '/auth/login';
|
||||
static const String uploadDocumentEndpoint = '/documents/upload';
|
||||
static const String fetchDocumentsEndpoint = '/documents';
|
||||
static const String masterSkusEndpoint = '/master/skus';
|
||||
static const String scanProductEndpoint = '/scan-product';
|
||||
|
||||
|
||||
// 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);
|
||||
// DO mode color — orange cue used throughout for the DO Scan mode:
|
||||
// document_card.dart top-label, documents_tab_switcher.dart active DO tab,
|
||||
// camera_drawer_mode_toggle.dart DO segment decoration. Formalized as a
|
||||
// shared constant so all three call sites pull from one place (was an
|
||||
// ad-hoc Colors.orange.shade700 inline in document_card.dart).
|
||||
static const Color doModeColor = Color(0xFFF57C00); // == Colors.orange.shade700
|
||||
|
||||
// Text Colors
|
||||
static const Color textPrimary = Color(0xFF212121);
|
||||
static const Color textSecondary = Color(0xFF757575);
|
||||
static const Color textOnPrimary = Colors.white;
|
||||
|
||||
// Spacing System (Margin & Padding)
|
||||
static const double spaceXS = 4.0;
|
||||
static const double spaceSmall = 8.0;
|
||||
static const double spaceMedium = 12.0;
|
||||
static const double spaceLarge = 16.0;
|
||||
static const double spaceXL = 24.0;
|
||||
static const double spaceXXL = 32.0;
|
||||
|
||||
static const EdgeInsets paddingXS = EdgeInsets.all(spaceXS);
|
||||
static const EdgeInsets paddingSmall = EdgeInsets.all(spaceSmall);
|
||||
static const EdgeInsets paddingMedium = EdgeInsets.all(spaceMedium);
|
||||
static const EdgeInsets paddingLarge = EdgeInsets.all(spaceLarge);
|
||||
static const EdgeInsets paddingXL = EdgeInsets.all(spaceXL);
|
||||
static const EdgeInsets screenPadding = EdgeInsets.symmetric(horizontal: spaceXL, vertical: spaceLarge);
|
||||
|
||||
// Border Radii
|
||||
static const double borderRadiusSmall = 8.0;
|
||||
static const double borderRadiusMedium = 12.0;
|
||||
static const double borderRadiusLarge = 16.0;
|
||||
static const double borderRadiusXL = 24.0;
|
||||
|
||||
// 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user