Update fitur, assets, dan perbaikan tampilan

This commit is contained in:
M.Nizar Fahrurrozi 2026-07-07 20:53:08 +07:00
parent 318f572f4a
commit 3d4741b535
34 changed files with 1417 additions and 552 deletions

View File

@ -2,7 +2,7 @@
package="com.example.coffee_iot_flutter"> package="com.example.coffee_iot_flutter">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="coffee_iot_flutter" android:label="Iot Kopi"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,18 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item> <item name="android:windowBackground">?android:colorBackground</item>
</style> </style>
</resources> </resources>

BIN
assets/icon/kopibaru.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class AppColors {
// Latar Belakang (Mint Ice yang lembut)
static const Color background = Color(0xFFF4F7F6);
static const Color cardBg = Colors.white;
// Warna Utama (Earth Tone Premium - Olive Sage)
static const Color primary = Color(0xFF627254);
static const Color primaryLight = Color(0xFF76885B);
// Teks (Menggunakan pilihan barumu: Charcoal & Cool Grey)
static const Color textMain = Color(0xFF3D3D3B); // Charcoal Dark Grey (Tegas)
static const Color textSecondary = Color(0xFF8B949E); // Cool Grey (Elegan)
// Warna Status & Peringatan
static const Color success = Color(0xFF7DA78C);
static const Color error = Color(0xFFE74C3C);
static const Color warning = Color(0xFFE67E22);
static const Color info = Color(0xFF3498DB);
static const Color light = Color(0xFFF1C40F);
// Latar belakang transparan untuk elemen status (Update .withValues untuk Flutter terbaru)
static Color get successBg => success.withValues(alpha: 0.15);
static Color get errorBg => error.withValues(alpha: 0.15);
static Color get warningBg => warning.withValues(alpha: 0.15);
static Color get infoBg => info.withValues(alpha: 0.15);
}

View File

@ -2,6 +2,12 @@ import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:google_fonts/google_fonts.dart';
// --- IMPORT APP COLORS ---
import 'constants/app_colors.dart';
// -------------------------
import 'services/mqtt_service.dart'; import 'services/mqtt_service.dart';
import 'screens/login_screen.dart'; import 'screens/login_screen.dart';
import 'screens/register_screen.dart'; import 'screens/register_screen.dart';
@ -10,6 +16,7 @@ import 'screens/analytics_screen.dart';
import 'screens/image_result_screen.dart'; import 'screens/image_result_screen.dart';
import 'screens/change_password_screen.dart'; import 'screens/change_password_screen.dart';
import 'screens/forgot_password_screen.dart'; import 'screens/forgot_password_screen.dart';
import 'screens/splash_screen.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -61,13 +68,72 @@ class MyApp extends StatelessWidget {
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
title: 'Coffee IoT', title: 'Coffee IoT',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
// --- KONFIGURASI TEMA LIGHT MODE (TERPUSAT) ---
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark, useMaterial3: true,
primarySwatch: Colors.green, brightness: Brightness.light,
scaffoldBackgroundColor: Colors.black, scaffoldBackgroundColor: AppColors.background,
primaryColor: AppColors.primary,
// ColorScheme untuk warna dasar komponen Material (Tombol, Dialog, dll)
colorScheme: const ColorScheme.light(
primary: AppColors.primary,
secondary: AppColors.primary,
surface: AppColors.cardBg,
error: AppColors.error,
onPrimary: Colors.white,
onSurface: AppColors.textMain,
),
// Menerapkan Google Fonts (Poppins) dengan pewarnaan dari AppColors
textTheme: GoogleFonts.poppinsTextTheme(
ThemeData.light().textTheme,
).copyWith(
displayLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.bold),
displayMedium: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.bold),
headlineLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.bold),
headlineMedium: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.w600),
titleLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.w600),
bodyLarge: GoogleFonts.poppins(color: AppColors.textMain),
bodyMedium: GoogleFonts.poppins(color: AppColors.textMain),
bodySmall: GoogleFonts.poppins(color: AppColors.textSecondary),
labelLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.w500),
),
// Tema AppBar Global (Biar transparan dan tulisannya otomatis ikut tema)
appBarTheme: AppBarTheme(
backgroundColor: AppColors.background,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: AppColors.textMain),
titleTextStyle: GoogleFonts.poppins(
color: AppColors.textMain,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
// Tema Card Global (Otomatis membulat dan punya bayangan halus)
cardTheme: CardThemeData(
color: AppColors.cardBg,
elevation: 2,
shadowColor: Colors.black.withValues(alpha: 0.05),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
// Tema Ikon Global
iconTheme: const IconThemeData(
color: AppColors.textMain,
),
), ),
initialRoute: '/login', // ----------------------------------------------
initialRoute: '/',
routes: { routes: {
'/': (context) => const SplashScreen(),
'/login': (context) => const LoginScreen(), '/login': (context) => const LoginScreen(),
'/register': (context) => const RegisterScreen(), '/register': (context) => const RegisterScreen(),
'/home': (context) => const HomeScreen(), '/home': (context) => const HomeScreen(),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class AnalyticsScreen extends StatefulWidget { class AnalyticsScreen extends StatefulWidget {
const AnalyticsScreen({super.key}); const AnalyticsScreen({super.key});
@ -36,21 +37,27 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final args = (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?) ?? {}; final args = (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?) ?? {};
final String title = args['title'] ?? 'Grafik'; final String title = args['title'] ?? 'Grafik';
final Color color = args['color'] ?? Colors.green; final Color color = args['color'] ?? AppColors.primary; // --- BERUBAH: Fallback color ---
final String sensorKey = title.contains('Suhu') ? 'suhu' : title.contains('Kelembaban') ? 'kelembapan' : 'intensitas'; final String sensorKey = title.contains('Suhu') ? 'suhu' : title.contains('Kelembaban') ? 'kelembapan' : 'intensitas';
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.black, // --- BERUBAH: Dibuat transparan agar menyatu dengan background ---
title: Text(title, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.transparent,
elevation: 0,
// --- BERUBAH: Warna Teks Judul ---
title: Text(title, style: const TextStyle(color: AppColors.textMain)),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.grey), // --- BERUBAH: Warna Ikon Back ---
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
), ),
body: _isLoading body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.green)) // --- BERUBAH: Warna Loading Indicator ---
? const Center(child: CircularProgressIndicator(color: AppColors.primary))
: Padding( : Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
@ -60,21 +67,27 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text('Riwayat Data', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), // --- BERUBAH: Warna Teks Judul Riwayat ---
IconButton(icon: const Icon(Icons.refresh, color: Colors.green), onPressed: _fetchLogs), const Text('Riwayat Data', style: TextStyle(color: AppColors.textMain, fontSize: 18, fontWeight: FontWeight.bold)),
// --- BERUBAH: Warna Ikon Refresh ---
IconButton(icon: const Icon(Icons.refresh, color: AppColors.primary), onPressed: _fetchLogs),
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Expanded( Expanded(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Warna Latar Daftar Riwayat ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Menambahkan border halus agar serasi dengan kartu lain ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: ListView.separated( child: ListView.separated(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
itemCount: _logs.length, itemCount: _logs.length,
separatorBuilder: (context, index) => const Divider(color: Colors.grey), // --- BERUBAH: Warna Garis Pemisah (Divider) antar item ---
separatorBuilder: (context, index) => Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final log = _logs[index]; final log = _logs[index];
return Padding( return Padding(
@ -85,10 +98,13 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(log['timestamp'] ?? '', style: const TextStyle(color: Colors.grey, fontSize: 10)), // --- BERUBAH: Warna Teks Tanggal/Waktu ---
Text('Fans: ${log['kipas1']}/${log['kipas2']}', style: const TextStyle(color: Colors.green, fontSize: 10)), Text(log['timestamp'] ?? '', style: const TextStyle(color: AppColors.textSecondary, fontSize: 10)),
// --- BERUBAH: Warna Teks Status Kipas ---
Text('Kipas 1: ${log['kipas1']} | Kipas 2: ${log['kipas2']}', style: const TextStyle(color: AppColors.primary, fontSize: 10)),
], ],
), ),
// Warna nilai sensor tetap mengikuti `color` dinamis (Merah/Cyan/Orange)
Text('${log[sensorKey]}', style: TextStyle(color: color, fontWeight: FontWeight.bold)), Text('${log[sensorKey]}', style: TextStyle(color: color, fontWeight: FontWeight.bold)),
], ],
), ),
@ -116,14 +132,15 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
height: 250, height: 250,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Warna Latar Kartu Grafik ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Menambahkan border halus agar serasi dengan kartu lain ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Expanded( Expanded(
child: LineChart( child: LineChart(
LineChartData( LineChartData(
@ -134,10 +151,12 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
LineChartBarData( LineChartBarData(
spots: spots.isEmpty ? [const FlSpot(0, 0)] : spots, spots: spots.isEmpty ? [const FlSpot(0, 0)] : spots,
isCurved: true, isCurved: true,
// Warna garis grafik tetap mengikuti `color` dinamis
color: color, color: color,
barWidth: 4, barWidth: 4,
dotData: FlDotData(show: false), dotData: FlDotData(show: false),
belowBarData: BarAreaData(show: true, color: color.withOpacity(0.1)), // --- BERUBAH: Gunakan withValues untuk transparansi area bawah grafik ---
belowBarData: BarAreaData(show: true, color: color.withValues(alpha: 0.1)),
), ),
], ],
), ),
@ -147,6 +166,7 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: Text( child: Text(
'Terakhir: ${logs.isNotEmpty ? logs.first[key] : "-"}', 'Terakhir: ${logs.isNotEmpty ? logs.first[key] : "-"}',
// Warna teks terakhir tetap mengikuti `color` dinamis
style: TextStyle(color: color, fontSize: 12) style: TextStyle(color: color, fontSize: 12)
), ),
), ),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class ChangePasswordScreen extends StatefulWidget { class ChangePasswordScreen extends StatefulWidget {
const ChangePasswordScreen({super.key}); const ChangePasswordScreen({super.key});
@ -118,17 +119,18 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
content: Row( content: Row(
children: [ children: [
Icon(isError ? Icons.error_outline : Icons.check_circle_outline, Icon(isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white), color: Colors.white), // Tetap putih agar kontras
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text(pesan, child: Text(pesan,
style: const TextStyle( style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)), color: Colors.white, fontWeight: FontWeight.bold)), // Tetap putih
), ),
], ],
), ),
// --- BERUBAH: Warna Notifikasi ---
backgroundColor: backgroundColor:
isError ? Colors.red.shade700 : Colors.green.shade700, isError ? AppColors.error : AppColors.success,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
@ -140,11 +142,15 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background Utama ---
backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.black, // --- BERUBAH: AppBar Transparan ---
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white), // --- BERUBAH: Warna Ikon Kembali ---
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
onPressed: () => _dariResetLink onPressed: () => _dariResetLink
? Navigator.pushReplacementNamed(context, '/login') ? Navigator.pushReplacementNamed(context, '/login')
: Navigator.pop(context), : Navigator.pop(context),
@ -152,7 +158,8 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
title: Text( title: Text(
_dariResetLink ? 'Reset Password' : 'Ganti Password', _dariResetLink ? 'Reset Password' : 'Ganti Password',
style: const TextStyle( style: const TextStyle(
color: Colors.white, // --- BERUBAH: Warna Judul ---
color: AppColors.textMain,
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
@ -167,16 +174,19 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
_dariResetLink _dariResetLink
? 'Masukkan password baru untuk akun kamu.' ? 'Masukkan password baru untuk akun kamu.'
: 'Ubah password akun kamu.\nPastikan password baru minimal 6 karakter.', : 'Ubah password akun kamu.\nPastikan password baru minimal 6 karakter.',
style: const TextStyle(color: Colors.grey, fontSize: 14), // --- BERUBAH: Warna Sub-judul ---
style: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Kotak Form ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey.withOpacity(0.3)), // --- BERUBAH: Garis Pinggir Halus ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
children: [ children: [
@ -189,7 +199,8 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
onToggle: () => setState( onToggle: () => setState(
() => _obscurePassLama = !_obscurePassLama), () => _obscurePassLama = !_obscurePassLama),
), ),
const Divider(color: Colors.grey), // --- BERUBAH: Warna Divider ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
], ],
_buildPasswordField( _buildPasswordField(
@ -199,7 +210,8 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
onToggle: () => setState( onToggle: () => setState(
() => _obscurePassBaru = !_obscurePassBaru), () => _obscurePassBaru = !_obscurePassBaru),
), ),
const Divider(color: Colors.grey), // --- BERUBAH: Warna Divider ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
_buildPasswordField( _buildPasswordField(
controller: _konfirmasiController, controller: _konfirmasiController,
@ -220,20 +232,22 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _isLoading ? null : _handleGantiPassword, onPressed: _isLoading ? null : _handleGantiPassword,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, // --- BERUBAH: Warna Tombol ---
disabledBackgroundColor: Colors.green.withOpacity(0.3), backgroundColor: AppColors.primary,
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
), ),
child: _isLoading child: _isLoading
// Tetap putih agar kontras dengan tombol primer
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(color: Colors.white)
: const Text( : const Text(
'Simpan Password Baru', 'Simpan Password Baru',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white), color: Colors.white), // Tetap putih
), ),
), ),
), ),
@ -253,16 +267,20 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
return TextField( return TextField(
controller: controller, controller: controller,
obscureText: obscure, obscureText: obscure,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock, color: Colors.green), // --- BERUBAH: Warna Ikon Kunci ---
prefixIcon: const Icon(Icons.lock, color: AppColors.primary),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon(obscure ? Icons.visibility : Icons.visibility_off, icon: Icon(obscure ? Icons.visibility : Icons.visibility_off,
color: Colors.grey), // --- BERUBAH: Warna Ikon Mata ---
color: AppColors.textSecondary),
onPressed: onToggle, onPressed: onToggle,
), ),
hintText: label, hintText: label,
hintStyle: const TextStyle(color: Colors.grey), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: const TextStyle(color: AppColors.textSecondary),
border: InputBorder.none, border: InputBorder.none,
), ),
); );

View File

@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../services/mqtt_service.dart'; import '../services/mqtt_service.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class ControlSettingsScreen extends StatefulWidget { class ControlSettingsScreen extends StatefulWidget {
const ControlSettingsScreen({super.key}); const ControlSettingsScreen({super.key});
@ -60,9 +61,10 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
_humMinController.text.isEmpty || _humMinController.text.isEmpty ||
_humMaxController.text.isEmpty) { _humMaxController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Semua kolom batas harus diisi!'), content: const Text('Semua kolom batas harus diisi!'),
backgroundColor: Colors.orange, // --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
), ),
); );
return; return;
@ -76,18 +78,20 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
// Validasi logika // Validasi logika
if (suhuMin >= suhuMax) { if (suhuMin >= suhuMax) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Suhu min harus lebih kecil dari suhu max!'), content: const Text('Suhu min harus lebih kecil dari suhu max!'),
backgroundColor: Colors.orange, // --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
), ),
); );
return; return;
} }
if (rhMin >= rhMax) { if (rhMin >= rhMax) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('RH min harus lebih kecil dari RH max!'), content: const Text('RH min harus lebih kecil dari RH max!'),
backgroundColor: Colors.orange, // --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
), ),
); );
return; return;
@ -113,9 +117,10 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('✅ Pengaturan Tersimpan & Terkirim ke ESP32'), content: const Text('✅ Pengaturan Tersimpan & Terkirim ke ESP32'),
backgroundColor: Colors.green, // --- BERUBAH: Warna Notifikasi Sukses ---
backgroundColor: AppColors.success,
), ),
); );
} }
@ -124,7 +129,8 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('❌ Gagal: $e'), content: Text('❌ Gagal: $e'),
backgroundColor: Colors.red, // --- BERUBAH: Warna Notifikasi Error ---
backgroundColor: AppColors.error,
), ),
); );
} }
@ -136,11 +142,13 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: _isLoading child: _isLoading
? const Center( ? const Center(
child: CircularProgressIndicator(color: Colors.green)) // --- BERUBAH: Warna Loading Indicator ---
child: CircularProgressIndicator(color: AppColors.primary))
: SingleChildScrollView( : SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@ -150,13 +158,15 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
const Text( const Text(
'Pengaturan', 'Pengaturan',
style: TextStyle( style: TextStyle(
color: Colors.white, // --- BERUBAH: Warna Judul Utama ---
color: AppColors.textMain,
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
const Text( const Text(
'Batas Sensor & Mode Operasi', 'Batas Sensor & Mode Operasi',
style: TextStyle(color: Colors.grey, fontSize: 16), // --- BERUBAH: Warna Sub-judul ---
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
@ -188,15 +198,19 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Background Kartu Mode Operasi ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Menambahkan border halus agar serasi ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text('Mode Operasi', const Text('Mode Operasi',
style: TextStyle( style: TextStyle(
color: Colors.white, // --- BERUBAH: Warna Judul Kartu Mode ---
color: AppColors.textMain,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16)), fontSize: 16)),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -206,12 +220,15 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
onChanged: (val) => onChanged: (val) =>
setState(() => _operatingMode = val!), setState(() => _operatingMode = val!),
title: const Text('Mode Otomatis', title: const Text('Mode Otomatis',
style: TextStyle(color: Colors.white)), // --- BERUBAH: Warna Teks Radio Button ---
activeColor: Colors.green, style: TextStyle(color: AppColors.textMain)),
// --- BERUBAH: Warna Lingkaran Radio Button (Aktif) ---
activeColor: AppColors.primary,
subtitle: const Text( subtitle: const Text(
'Kipas bekerja otomatis berdasarkan sensor', 'Kipas bekerja otomatis berdasarkan sensor',
// --- BERUBAH: Warna Sub-teks Radio Button ---
style: TextStyle( style: TextStyle(
color: Colors.grey, fontSize: 10), color: AppColors.textSecondary, fontSize: 10),
), ),
), ),
RadioListTile<int>( RadioListTile<int>(
@ -220,12 +237,15 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
onChanged: (val) => onChanged: (val) =>
setState(() => _operatingMode = val!), setState(() => _operatingMode = val!),
title: const Text('Mode Manual', title: const Text('Mode Manual',
style: TextStyle(color: Colors.white)), // --- BERUBAH: Warna Teks Radio Button ---
activeColor: Colors.green, style: TextStyle(color: AppColors.textMain)),
// --- BERUBAH: Warna Lingkaran Radio Button (Aktif) ---
activeColor: AppColors.primary,
subtitle: const Text( subtitle: const Text(
'Kontrol penuh kipas melalui dashboard HP', 'Kontrol penuh kipas melalui dashboard HP',
// --- BERUBAH: Warna Sub-teks Radio Button ---
style: TextStyle( style: TextStyle(
color: Colors.grey, fontSize: 10), color: AppColors.textSecondary, fontSize: 10),
), ),
), ),
], ],
@ -240,7 +260,8 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _saveSettings, onPressed: _saveSettings,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, // --- BERUBAH: Warna Latar Tombol Simpan ---
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
@ -250,6 +271,7 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
// Tetap putih agar tulisan dalam tombol kontras
color: Colors.white), color: Colors.white),
), ),
), ),
@ -268,31 +290,44 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
return Container( return Container(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Kartu Input Sensor ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Menambahkan border halus agar serasi ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, Text(title,
style: const TextStyle( style: const TextStyle(
color: Colors.white, // --- BERUBAH: Warna Judul Input Sensor ---
color: AppColors.textMain,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(
controller: controller, controller: controller,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Angka/Nilai Input Sensor ---
style: const TextStyle(color: AppColors.textMain),
decoration: InputDecoration( decoration: InputDecoration(
hintText: hint, hintText: hint,
hintStyle: const TextStyle(color: Colors.grey), // --- BERUBAH: Warna Teks Placeholder/Hint ---
hintStyle: const TextStyle(color: AppColors.textSecondary),
filled: true, filled: true,
fillColor: Colors.black, // --- BERUBAH: Warna Kolom Input Sensor ---
fillColor: AppColors.background,
isDense: true, isDense: true,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: Colors.grey), // --- BERUBAH: Warna Garis Pinggir Input Sensor ---
borderSide: BorderSide(color: AppColors.textSecondary.withValues(alpha: 0.3)),
),
// Menambahkan fokus border agar saat diklik terlihat jelas warnanya
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: AppColors.primary),
), ),
), ),
), ),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class ForgotPasswordScreen extends StatefulWidget { class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key}); const ForgotPasswordScreen({super.key});
@ -104,7 +105,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
content: Row(children: [ content: Row(children: [
Icon( Icon(
isError ? Icons.error_outline : Icons.check_circle_outline, isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white, color: Colors.white, // Tetap putih untuk kontras
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
@ -113,8 +114,9 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
color: Colors.white, fontWeight: FontWeight.bold)), color: Colors.white, fontWeight: FontWeight.bold)),
), ),
]), ]),
// --- BERUBAH: Menggunakan warna notifikasi dari AppColors ---
backgroundColor: backgroundColor:
isError ? Colors.red.shade700 : Colors.green.shade700, isError ? AppColors.error : AppColors.success,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
@ -126,12 +128,14 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background Utama ---
backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.green), // --- BERUBAH: Warna Ikon Back ---
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
), ),
@ -152,12 +156,14 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
const Text( const Text(
'Lupa Password?', 'Lupa Password?',
style: TextStyle( style: TextStyle(
color: Colors.green, fontSize: 32, fontWeight: FontWeight.bold), // --- BERUBAH: Warna Judul ---
color: AppColors.primary, fontSize: 32, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'Masukkan email akun kamu. Kami akan kirim kode verifikasi 6 angka ke inbox kamu.', 'Masukkan email akun kamu. Kami akan kirim kode verifikasi 6 angka ke inbox kamu.',
style: TextStyle(color: Colors.grey, fontSize: 14), // --- BERUBAH: Warna Sub-judul ---
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
), ),
const SizedBox(height: 50), const SizedBox(height: 50),
@ -165,18 +171,23 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Form ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.grey.withOpacity(0.3)), // --- BERUBAH: Garis Pinggir Form ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.3)),
), ),
child: TextField( child: TextField(
controller: _emailController, controller: _emailController,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: const InputDecoration( decoration: const InputDecoration(
prefixIcon: Icon(Icons.email, color: Colors.green), // --- BERUBAH: Warna Ikon ---
prefixIcon: Icon(Icons.email, color: AppColors.primary),
hintText: 'Email', hintText: 'Email',
hintStyle: TextStyle(color: Colors.grey), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none, border: InputBorder.none,
), ),
), ),
@ -190,8 +201,10 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _isLoading ? null : _handleKirimOTP, onPressed: _isLoading ? null : _handleKirimOTP,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, // --- BERUBAH: Warna Tombol ---
disabledBackgroundColor: Colors.green.withOpacity(0.3), backgroundColor: AppColors.primary,
// --- BERUBAH: Warna Tombol saat Loading ---
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)), borderRadius: BorderRadius.circular(15)),
), ),
@ -211,7 +224,8 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
child: TextButton( child: TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Kembali ke Login', child: const Text('Kembali ke Login',
style: TextStyle(color: Colors.green)), // --- BERUBAH: Warna Teks Bawah ---
style: TextStyle(color: AppColors.primary)),
), ),
), ),
], ],
@ -225,19 +239,23 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
children: [ children: [
const CircleAvatar( const CircleAvatar(
radius: 40, radius: 40,
backgroundColor: Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Ikon ---
child: Icon(Icons.mark_email_read, size: 40, color: Colors.green), backgroundColor: AppColors.cardBg,
// --- BERUBAH: Warna Ikon ---
child: Icon(Icons.mark_email_read, size: 40, color: AppColors.primary),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
const Text( const Text(
'Cek Email Kamu', 'Cek Email Kamu',
style: TextStyle( style: TextStyle(
color: Colors.green, fontSize: 28, fontWeight: FontWeight.bold), // --- BERUBAH: Warna Judul ---
color: AppColors.primary, fontSize: 28, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Kode verifikasi 6 angka sudah dikirim ke:\n$_email\n\nMasukkan kode tersebut di bawah ini.', 'Kode verifikasi 6 angka sudah dikirim ke:\n$_email\n\nMasukkan kode tersebut di bawah ini.',
style: const TextStyle(color: Colors.grey, fontSize: 14), // --- BERUBAH: Warna Subteks ---
style: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
@ -245,9 +263,11 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Kotak OTP ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.green.withOpacity(0.5)), // --- BERUBAH: Garis Pinggir Kotak OTP ---
border: Border.all(color: AppColors.primary.withValues(alpha: 0.5)),
), ),
child: TextField( child: TextField(
controller: _otpController, controller: _otpController,
@ -255,11 +275,14 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
maxLength: 6, maxLength: 6,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
color: Colors.white, fontSize: 28, letterSpacing: 12), // --- BERUBAH: Warna Teks Inputan ---
color: AppColors.textMain, fontSize: 28, letterSpacing: 12),
decoration: const InputDecoration( decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock_open, color: Colors.green), // --- BERUBAH: Warna Ikon ---
prefixIcon: Icon(Icons.lock_open, color: AppColors.primary),
hintText: '000000', hintText: '000000',
hintStyle: TextStyle(color: Colors.grey, letterSpacing: 8), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary, letterSpacing: 8),
border: InputBorder.none, border: InputBorder.none,
counterText: '', // sembunyikan counter maxLength counterText: '', // sembunyikan counter maxLength
), ),
@ -274,8 +297,10 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _isLoading ? null : _handleVerifikasiOTP, onPressed: _isLoading ? null : _handleVerifikasiOTP,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, // --- BERUBAH: Warna Tombol ---
disabledBackgroundColor: Colors.green.withOpacity(0.3), backgroundColor: AppColors.primary,
// --- BERUBAH: Warna Tombol saat Loading ---
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)), borderRadius: BorderRadius.circular(15)),
), ),
@ -303,7 +328,8 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
}), }),
child: const Text( child: const Text(
'Kirim ulang kode', 'Kirim ulang kode',
style: TextStyle(color: Colors.green), // --- BERUBAH: Warna Teks Bawah ---
style: TextStyle(color: AppColors.primary),
), ),
), ),
), ),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../services/mqtt_service.dart'; import '../services/mqtt_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
import 'monitoring_screen.dart'; import 'monitoring_screen.dart';
import 'control_settings_screen.dart'; import 'control_settings_screen.dart';
import 'recording_screen.dart'; import 'recording_screen.dart';
@ -26,8 +27,15 @@ class _HomeScreenState extends State<HomeScreen> {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(message), content: Text(
backgroundColor: isSuccess ? Colors.green.withOpacity(0.8) : Colors.redAccent, message,
// Opsional: Teks putih agar selalu kontras dengan warna notifikasi
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)
),
// --- BERUBAH: Warna Notifikasi Berhasil / Gagal ---
backgroundColor: isSuccess
? AppColors.success.withValues(alpha: 0.9)
: AppColors.error,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
); );
@ -51,11 +59,15 @@ class _HomeScreenState extends State<HomeScreen> {
currentIndex: _currentIndex, currentIndex: _currentIndex,
onTap: (index) => setState(() => _currentIndex = index), onTap: (index) => setState(() => _currentIndex = index),
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
backgroundColor: Colors.black, // --- BERUBAH: Latar Belakang Menu Navigasi Bawah ---
selectedItemColor: Colors.green, backgroundColor: AppColors.cardBg,
unselectedItemColor: Colors.grey, // --- BERUBAH: Warna Ikon yang Sedang Dipilih ---
selectedItemColor: AppColors.primary,
// --- BERUBAH: Warna Ikon yang Tidak Dipilih ---
unselectedItemColor: AppColors.textSecondary,
showSelectedLabels: false, showSelectedLabels: false,
showUnselectedLabels: false, showUnselectedLabels: false,
elevation: 10, // Menambahkan sedikit bayangan agar terpisah dari body
items: const [ items: const [
BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'Monitoring'), BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'Monitoring'),
BottomNavigationBarItem(icon: Icon(Icons.tune), label: 'Control'), BottomNavigationBarItem(icon: Icon(Icons.tune), label: 'Control'),

View File

@ -4,6 +4,7 @@ import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../constants/app_colors.dart';
class ImageResultScreen extends StatelessWidget { class ImageResultScreen extends StatelessWidget {
const ImageResultScreen({super.key}); const ImageResultScreen({super.key});
@ -14,19 +15,17 @@ class ImageResultScreen extends StatelessWidget {
Future<void> _exportPDF( Future<void> _exportPDF(
BuildContext context, Map<String, dynamic> args) async { BuildContext context, Map<String, dynamic> args) async {
// Tampilkan loading
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (_) => const Center( builder: (_) => const Center(
child: CircularProgressIndicator(color: Colors.green), child: CircularProgressIndicator(color: AppColors.primary),
), ),
); );
try { try {
final pdf = pw.Document(); final pdf = pw.Document();
// Download foto dari URL untuk dimasukkan ke PDF
pw.MemoryImage? fotoImage; pw.MemoryImage? fotoImage;
final urlFoto = args['url_foto']?.toString() ?? ''; final urlFoto = args['url_foto']?.toString() ?? '';
if (urlFoto.isNotEmpty) { if (urlFoto.isNotEmpty) {
@ -40,7 +39,6 @@ class ImageResultScreen extends StatelessWidget {
} }
} }
// Buat halaman PDF
pdf.addPage( pdf.addPage(
pw.Page( pw.Page(
pageFormat: PdfPageFormat.a4, pageFormat: PdfPageFormat.a4,
@ -49,7 +47,6 @@ class ImageResultScreen extends StatelessWidget {
return pw.Column( return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
// Header
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
padding: const pw.EdgeInsets.all(16), padding: const pw.EdgeInsets.all(16),
@ -82,7 +79,6 @@ class ImageResultScreen extends StatelessWidget {
pw.SizedBox(height: 20), pw.SizedBox(height: 20),
// Info Waktu
pw.Text( pw.Text(
'Tanggal & Waktu: ${args['date']} - ${args['time']}', 'Tanggal & Waktu: ${args['date']} - ${args['time']}',
style: pw.TextStyle( style: pw.TextStyle(
@ -95,7 +91,6 @@ class ImageResultScreen extends StatelessWidget {
pw.Divider(), pw.Divider(),
pw.SizedBox(height: 16), pw.SizedBox(height: 16),
// Foto
if (fotoImage != null) ...[ if (fotoImage != null) ...[
pw.Center( pw.Center(
child: pw.Container( child: pw.Container(
@ -122,7 +117,6 @@ class ImageResultScreen extends StatelessWidget {
pw.SizedBox(height: 16), pw.SizedBox(height: 16),
], ],
// Data Sensor
pw.Text( pw.Text(
'Data Sensor', 'Data Sensor',
style: pw.TextStyle( style: pw.TextStyle(
@ -151,7 +145,6 @@ class ImageResultScreen extends StatelessWidget {
pw.Divider(), pw.Divider(),
pw.SizedBox(height: 8), pw.SizedBox(height: 8),
// Footer
pw.Text( pw.Text(
'Diekspor dari Aplikasi Monitoring Kopi', 'Diekspor dari Aplikasi Monitoring Kopi',
style: const pw.TextStyle( style: const pw.TextStyle(
@ -165,10 +158,8 @@ class ImageResultScreen extends StatelessWidget {
), ),
); );
// Tutup loading
if (context.mounted) Navigator.pop(context); if (context.mounted) Navigator.pop(context);
// Tampilkan preview PDF + opsi simpan/share
await Printing.layoutPdf( await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async => pdf.save(), onLayout: (PdfPageFormat format) async => pdf.save(),
name: name:
@ -176,11 +167,11 @@ class ImageResultScreen extends StatelessWidget {
); );
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); // tutup loading Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Gagal membuat PDF: $e'), content: Text('Gagal membuat PDF: $e'),
backgroundColor: Colors.red.shade700, backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -231,18 +222,18 @@ class ImageResultScreen extends StatelessWidget {
}; };
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.black, backgroundColor: Colors.transparent,
elevation: 0,
title: const Text( title: const Text(
'Detail Data Citra', 'Detail Data Citra',
style: TextStyle(color: Colors.white), style: TextStyle(color: AppColors.textMain),
), ),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.grey), icon: const Icon(Icons.arrow_back, color: AppColors.primary),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
// Icon download dihapus
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@ -253,8 +244,9 @@ class ImageResultScreen extends StatelessWidget {
height: 250, height: 250,
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: args['url_foto'] != null && child: args['url_foto'] != null &&
@ -265,26 +257,26 @@ class ImageResultScreen extends StatelessWidget {
loadingBuilder: (context, child, progress) { loadingBuilder: (context, child, progress) {
if (progress == null) return child; if (progress == null) return child;
return const Center( return const Center(
child: child: CircularProgressIndicator(color: AppColors.primary),
CircularProgressIndicator(color: Colors.green),
); );
}, },
errorBuilder: (context, error, stack) => Column( errorBuilder: (context, error, stack) => Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.broken_image, const Icon(Icons.broken_image,
size: 80, color: Colors.grey), size: 80, color: AppColors.textSecondary),
Text('Citra #${args['id']}', Text('Citra #${args['id']}',
style: const TextStyle(color: Colors.grey)), style: const TextStyle(color: AppColors.textSecondary)),
], ],
), ),
) )
: Column( : Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.image, size: 80, color: Colors.grey), const Icon(Icons.image,
size: 80, color: AppColors.textSecondary),
Text('Citra #${args['id']}', Text('Citra #${args['id']}',
style: const TextStyle(color: Colors.grey)), style: const TextStyle(color: AppColors.textSecondary)),
], ],
), ),
), ),
@ -294,41 +286,43 @@ class ImageResultScreen extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
children: [ children: [
// Tanggal & waktu
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.calendar_today, const Icon(Icons.calendar_today,
color: Colors.green, size: 16), color: AppColors.primary, size: 16),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'${args['date']} - ${args['time']}', '${args['date']} - ${args['time']}',
style: const TextStyle( style: const TextStyle(
color: Colors.green, color: AppColors.primary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16), fontSize: 16),
), ),
], ],
), ),
// Lokasi dihapus Divider(color: AppColors.textSecondary.withValues(alpha: 0.3), height: 30),
const Divider(color: Colors.grey, height: 30),
_buildDataRow( _buildDataRow(
'Suhu', args['temp'], 'Kelembaban', args['humidity']), 'Suhu', args['temp'], 'Kelembaban', args['humidity']),
const SizedBox(height: 15), const SizedBox(height: 15),
// --- BERUBAH: Mengirimkan string kosong ('') untuk menghapus kolom Mode ---
_buildDataRow( _buildDataRow(
'Cahaya', args['light'], 'Mode', 'Terjadwal'), 'Cahaya', args['light'], '', ''),
const Divider(color: Colors.grey, height: 30),
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3), height: 30),
_buildDataRow( _buildDataRow(
'Kipas 1 (Intake)', args['intake'], 'Kipas 1 (Intake)', args['intake'],
'Kipas 2 (Exhaust)', args['exhaust'], 'Kipas 2 (Exhaust)', args['exhaust'],
valueColor: Colors.green, valueColor: AppColors.primary,
), ),
], ],
), ),
@ -342,7 +336,7 @@ class ImageResultScreen extends StatelessWidget {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => _exportPDF(context, args), onPressed: () => _exportPDF(context, args),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
@ -361,12 +355,13 @@ class ImageResultScreen extends StatelessWidget {
); );
} }
// --- BERUBAH: Menambahkan logika untuk menyembunyikan kolom kanan jika string kosong ---
Widget _buildDataRow( Widget _buildDataRow(
String label1, String label1,
String value1, String value1,
String label2, String label2,
String value2, { String value2, {
Color valueColor = Colors.white, Color? valueColor,
}) { }) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -376,30 +371,32 @@ class ImageResultScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label1, Text(label1,
style: const TextStyle(color: Colors.grey, fontSize: 12)), style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(': $value1', Text(': $value1',
style: TextStyle( style: TextStyle(
color: valueColor, color: valueColor ?? AppColors.textMain,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 15)), fontSize: 15)),
], ],
), ),
), ),
Expanded( Expanded(
child: Column( child: label2.isEmpty
crossAxisAlignment: CrossAxisAlignment.start, ? const SizedBox.shrink() // Jika kosong, sembunyikan sepenuhnya
children: [ : Column(
Text(label2, crossAxisAlignment: CrossAxisAlignment.start,
style: const TextStyle(color: Colors.grey, fontSize: 12)), children: [
const SizedBox(height: 4), Text(label2,
Text(': $value2', style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
style: TextStyle( const SizedBox(height: 4),
color: valueColor, Text(': $value2',
fontWeight: FontWeight.bold, style: TextStyle(
fontSize: 15)), color: valueColor ?? AppColors.textMain,
], fontWeight: FontWeight.bold,
), fontSize: 15)),
],
),
), ),
], ],
); );

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@ -64,20 +65,21 @@ class _LoginScreenState extends State<LoginScreen> {
SnackBar( SnackBar(
content: Row( content: Row(
children: [ children: [
const Icon(Icons.error_outline, color: Colors.white), const Icon(Icons.error_outline, color: Colors.white), // Tetap putih agar kontras di atas merah
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
pesan, pesan,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white, // Tetap putih
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
], ],
), ),
backgroundColor: Colors.red.shade700, // --- BERUBAH: Background SnackBar Error ---
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -91,70 +93,93 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 80.0), padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 80.0),
child: Column( child: Column(
children: [ children: [
const CircleAvatar( CircleAvatar(
radius: 60, radius: 60,
backgroundColor: Color(0xFF1A1A1A), // --- BERUBAH: Latar belakang logo ---
child: Icon(Icons.coffee, size: 60, color: Colors.orange), backgroundColor: AppColors.primary,
child: ClipOval(
child: Padding(
padding: const EdgeInsets.all(8),
child: Image.asset(
'assets/icon/kopibaru_removebg.png',
width: 120,
height: 120,
fit: BoxFit.contain,
),
),
),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
const Text( const Text(
'Selamat Datang', 'Selamat Datang',
style: TextStyle( style: TextStyle(
color: Colors.green, // --- BERUBAH: Warna Judul ---
color: AppColors.primary,
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
const Text( const Text(
'Sistem Kendali Pengering Kopi', 'Sistem Kendali Pengering Kopi',
style: TextStyle(color: Colors.grey, fontSize: 16), // --- BERUBAH: Warna Sub-judul ---
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
), ),
const SizedBox(height: 50), const SizedBox(height: 50),
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Warna Kotak Form ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.grey.withOpacity(0.3)), // --- BERUBAH: Warna Garis Pinggir Form ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.3)),
), ),
child: Column( child: Column(
children: [ children: [
TextField( TextField(
controller: _emailController, controller: _emailController,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: const InputDecoration( decoration: const InputDecoration(
prefixIcon: Icon(Icons.email, color: Colors.green), // --- BERUBAH: Warna Ikon ---
prefixIcon: Icon(Icons.email, color: AppColors.primary),
hintText: 'Email', hintText: 'Email',
hintStyle: TextStyle(color: Colors.grey), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none, border: InputBorder.none,
), ),
), ),
const Divider(color: Colors.grey), // --- BERUBAH: Garis Pemisah (Divider) ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
TextField( TextField(
controller: _passwordController, controller: _passwordController,
obscureText: _obscurePassword, obscureText: _obscurePassword,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: // --- BERUBAH: Warna Ikon ---
const Icon(Icons.lock, color: Colors.green), prefixIcon: const Icon(Icons.lock, color: AppColors.primary),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
_obscurePassword _obscurePassword
? Icons.visibility ? Icons.visibility
: Icons.visibility_off, : Icons.visibility_off,
color: Colors.grey, // --- BERUBAH: Warna Ikon Mata ---
color: AppColors.textSecondary,
), ),
onPressed: () => setState( onPressed: () => setState(
() => _obscurePassword = !_obscurePassword), () => _obscurePassword = !_obscurePassword),
), ),
hintText: 'Password', hintText: 'Password',
hintStyle: const TextStyle(color: Colors.grey), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: const TextStyle(color: AppColors.textSecondary),
border: InputBorder.none, border: InputBorder.none,
), ),
), ),
@ -165,42 +190,64 @@ class _LoginScreenState extends State<LoginScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin, onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, // --- BERUBAH: Latar Belakang Tombol ---
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
), ),
child: _isLoading child: _isLoading
? const CircularProgressIndicator( ? const CircularProgressIndicator(
color: Colors.white) color: Colors.white) // Tetap putih untuk kontras
: const Text( : const Text(
'Masuk ke Akun', 'Masuk ke Akun',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white), color: Colors.white), // Tetap putih
), ),
), ),
), ),
], ],
), ),
), ),
const SizedBox(height: 16),
// Lupa Password dan Daftar rapat tanpa jarak besar, warna sama hijau // 1. TOMBOL LUPA PASSWORD
const SizedBox(height: 10),
TextButton( TextButton(
onPressed: () => onPressed: () => Navigator.pushNamed(context, '/forgot_password'),
Navigator.pushNamed(context, '/forgot_password'), style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text( child: const Text(
'Lupa Password?', 'Lupa Password?',
style: TextStyle(color: Colors.green), style: TextStyle(
// --- BERUBAH: Warna Teks ---
color: AppColors.primary,
fontSize: 14,
),
), ),
), ),
const SizedBox(height: 12),
// 2. TOMBOL DAFTAR AKUN
TextButton( TextButton(
onPressed: () => Navigator.pushNamed(context, '/register'), onPressed: () => Navigator.pushNamed(context, '/register'),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text( child: const Text(
'Belum punya akun? Daftar Sekarang', 'Belum punya akun? Daftar Sekarang',
style: TextStyle(color: Colors.green), style: TextStyle(
// --- BERUBAH: Warna Teks ---
color: AppColors.primary,
fontSize: 14,
),
), ),
), ),
], ],

View File

@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_client.dart';
import '../services/mqtt_service.dart'; import '../services/mqtt_service.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class MonitoringScreen extends StatefulWidget { class MonitoringScreen extends StatefulWidget {
final VoidCallback? onProfileTap; final VoidCallback? onProfileTap;
@ -60,19 +61,21 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
}); });
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background Utama ---
backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: _isInitialLoading child: _isInitialLoading
? const Center(child: CircularProgressIndicator(color: Colors.green)) // --- BERUBAH: Warna Loading Indicator ---
? const Center(child: CircularProgressIndicator(color: AppColors.primary))
: RefreshIndicator( : RefreshIndicator(
onRefresh: _initData, onRefresh: _initData,
color: Colors.green, // --- BERUBAH: Warna Refresh Indicator ---
color: AppColors.primary,
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
// Tinggi tersedia = tinggi layar dikurangi header const headerHeight = 175.0;
const headerHeight = 175.0; // header + timestamp + mode badge + padding const gaps = 15.0 * 2;
const gaps = 15.0 * 2; // 2 gap antar 3 baris const padding = 20.0 * 2;
const padding = 20.0 * 2; // padding atas bawah
final available = constraints.maxHeight - headerHeight - gaps - padding; final available = constraints.maxHeight - headerHeight - gaps - padding;
final cardH = (available / 3).clamp(100.0, 180.0); final cardH = (available / 3).clamp(100.0, 180.0);
@ -94,11 +97,13 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [ children: [
const Text('Monitoring', const Text('Monitoring',
style: TextStyle( style: TextStyle(
color: Colors.white, // --- BERUBAH: Warna Judul ---
color: AppColors.textMain,
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
const Text('Sistem Pengeringan Kopi', const Text('Sistem Pengeringan Kopi',
style: TextStyle(color: Colors.grey, fontSize: 16)), // --- BERUBAH: Warna Sub-judul ---
style: TextStyle(color: AppColors.textSecondary, fontSize: 16)),
const SizedBox(height: 5), const SizedBox(height: 5),
Row( Row(
children: [ children: [
@ -106,10 +111,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
width: 8, height: 8, width: 8, height: 8,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
// --- BERUBAH: Indikator Titik Koneksi ---
color: mqtt.client?.connectionStatus?.state == color: mqtt.client?.connectionStatus?.state ==
MqttConnectionState.connected MqttConnectionState.connected
? Colors.green ? AppColors.success
: Colors.red, : AppColors.error,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -119,11 +125,13 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
? 'Connected' ? 'Connected'
: 'Disconnected', : 'Disconnected',
style: TextStyle( style: TextStyle(
// --- BERUBAH: Warna Teks Koneksi ---
color: mqtt.client?.connectionStatus?.state == color: mqtt.client?.connectionStatus?.state ==
MqttConnectionState.connected MqttConnectionState.connected
? Colors.green ? AppColors.success
: Colors.red, : AppColors.error,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold,
), ),
), ),
], ],
@ -133,9 +141,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
GestureDetector( GestureDetector(
onTap: widget.onProfileTap, onTap: widget.onProfileTap,
child: const CircleAvatar( child: const CircleAvatar(
backgroundColor: Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Avatar Profil ---
backgroundColor: AppColors.cardBg,
radius: 25, radius: 25,
child: Icon(Icons.person, color: Colors.green), // --- BERUBAH: Warna Ikon Avatar ---
child: Icon(Icons.person, color: AppColors.primary),
), ),
), ),
], ],
@ -144,25 +154,29 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
// Timestamp // Timestamp
Text('Update: ${mqtt.timestamp}', Text('Update: ${mqtt.timestamp}',
style: const TextStyle(color: Colors.grey, fontSize: 11)), // --- BERUBAH: Warna Teks Timestamp ---
style: const TextStyle(color: AppColors.textSecondary, fontSize: 11)),
const SizedBox(height: 10), const SizedBox(height: 10),
// Mode Badge // Mode Badge
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
// --- BERUBAH: Latar Belakang Transparan Badge ---
color: mqtt.mode == 'manual' color: mqtt.mode == 'manual'
? Colors.orange.withOpacity(0.2) ? AppColors.warning.withValues(alpha: 0.2)
: Colors.green.withOpacity(0.2), : AppColors.success.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: mqtt.mode == 'manual' ? Colors.orange : Colors.green, // --- BERUBAH: Warna Garis Pinggir Badge ---
color: mqtt.mode == 'manual' ? AppColors.warning : AppColors.success,
), ),
), ),
child: Text( child: Text(
'Mode: ${mqtt.mode.toUpperCase()}', 'Mode: ${mqtt.mode.toUpperCase()}',
style: TextStyle( style: TextStyle(
color: mqtt.mode == 'manual' ? Colors.orange : Colors.green, // --- BERUBAH: Warna Teks Badge ---
color: mqtt.mode == 'manual' ? AppColors.warning : AppColors.success,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -183,11 +197,12 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
suffix: '°C', suffix: '°C',
dec: 1, dec: 1,
icon: Icons.thermostat, icon: Icons.thermostat,
color: curSuhu > mqtt.maxSuhu ? Colors.red : Colors.redAccent, // --- BERUBAH: Warna Tema Kartu Suhu ---
color: curSuhu > mqtt.maxSuhu ? AppColors.error : AppColors.warning,
isAlert: curSuhu > mqtt.maxSuhu, isAlert: curSuhu > mqtt.maxSuhu,
isLow: curSuhu < mqtt.minSuhu, isLow: curSuhu < mqtt.minSuhu,
onTap: () => Navigator.pushNamed(context, '/analytics', onTap: () => Navigator.pushNamed(context, '/analytics',
arguments: {'title': 'Grafik Suhu (°C)', 'color': Colors.redAccent, 'value': '${mqtt.suhu}°C'}), arguments: {'title': 'Grafik Suhu (°C)', 'color': AppColors.error, 'value': '${mqtt.suhu}°C'}),
), ),
), ),
const SizedBox(width: 15), const SizedBox(width: 15),
@ -199,11 +214,12 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
suffix: '%', suffix: '%',
dec: 1, dec: 1,
icon: Icons.water_drop, icon: Icons.water_drop,
color: curHum > mqtt.maxRh ? Colors.red : Colors.cyan, // --- BERUBAH: Warna Tema Kartu Kelembaban ---
color: curHum > mqtt.maxRh ? AppColors.error : AppColors.info,
isAlert: curHum > mqtt.maxRh, isAlert: curHum > mqtt.maxRh,
isLow: curHum < mqtt.minRh, isLow: curHum < mqtt.minRh,
onTap: () => Navigator.pushNamed(context, '/analytics', onTap: () => Navigator.pushNamed(context, '/analytics',
arguments: {'title': 'Grafik Kelembaban (%)', 'color': Colors.cyan, 'value': '${mqtt.kelembapan}%'}), arguments: {'title': 'Grafik Kelembaban (%)', 'color': AppColors.info, 'value': '${mqtt.kelembapan}%'}),
), ),
), ),
], ],
@ -222,9 +238,10 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
suffix: ' Lux', suffix: ' Lux',
dec: 1, dec: 1,
icon: Icons.wb_sunny, icon: Icons.wb_sunny,
color: Colors.orange, // --- BERUBAH: Warna Tema Kartu Cahaya ---
color: AppColors.warning,
onTap: () => Navigator.pushNamed(context, '/analytics', onTap: () => Navigator.pushNamed(context, '/analytics',
arguments: {'title': 'Grafik Cahaya (Lux)', 'color': Colors.orange, 'value': '${mqtt.intensitas} Lux'}), arguments: {'title': 'Grafik Cahaya (Lux)', 'color': AppColors.light, 'value': '${mqtt.intensitas} Lux'}),
), ),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
@ -261,19 +278,23 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
margin: const EdgeInsets.only(top: 15), margin: const EdgeInsets.only(top: 15),
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.withOpacity(0.2), // --- BERUBAH: Background Peringatan ---
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.red), // --- BERUBAH: Garis Pinggir Peringatan ---
border: Border.all(color: AppColors.error),
), ),
child: const Row( child: const Row(
children: [ children: [
Icon(Icons.warning_amber_rounded, color: Colors.red), // --- BERUBAH: Ikon Peringatan ---
Icon(Icons.warning_amber_rounded, color: AppColors.error),
SizedBox(width: 10), SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
'Peringatan: Parameter pengeringan melewati batas aman!', 'Peringatan: Parameter pengeringan melewati batas aman!',
style: TextStyle( style: TextStyle(
color: Colors.red, // --- BERUBAH: Teks Peringatan ---
color: AppColors.error,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12), fontSize: 12),
), ),
@ -286,19 +307,23 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
margin: const EdgeInsets.only(top: 15), margin: const EdgeInsets.only(top: 15),
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.2), // --- BERUBAH: Background Info/Low ---
color: AppColors.info.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.blue), // --- BERUBAH: Garis Pinggir Info/Low ---
border: Border.all(color: AppColors.info),
), ),
child: const Row( child: const Row(
children: [ children: [
Icon(Icons.info_outline, color: Colors.blue), // --- BERUBAH: Ikon Info/Low ---
Icon(Icons.info_outline, color: AppColors.info),
SizedBox(width: 10), SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
'Info: Parameter pengeringan di bawah batas minimum!', 'Info: Parameter pengeringan di bawah batas minimum!',
style: TextStyle( style: TextStyle(
color: Colors.blue, // --- BERUBAH: Teks Info/Low ---
color: AppColors.info,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12), fontSize: 12),
), ),
@ -337,16 +362,23 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
height: double.infinity, height: double.infinity,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isAlert ? Colors.red.withOpacity(0.1) : isLow ? Colors.blue.withOpacity(0.1) : const Color(0xFF1A1A1A), // --- BERUBAH: Kartu Latar Putih Bersih atau Transparan Merah/Biru ---
color: isAlert ? AppColors.error.withValues(alpha: 0.1) : isLow ? AppColors.info.withValues(alpha: 0.1) : AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: isAlert ? Border.all(color: Colors.red, width: 2) : isLow ? Border.all(color: Colors.blue, width: 2) : null, // --- BERUBAH: Memberi garis halus agar kartu terlihat pop up di latar terang ---
border: isAlert
? Border.all(color: AppColors.error, width: 2)
: isLow
? Border.all(color: AppColors.info, width: 2)
: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(icon, color: color, size: 32), Icon(icon, color: color, size: 32),
const SizedBox(height: 6), const SizedBox(height: 6),
Text(title, style: const TextStyle(color: Colors.grey, fontSize: 12)), // --- BERUBAH: Warna Judul Sensor ---
Text(title, style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
const SizedBox(height: 4), const SizedBox(height: 4),
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween<double>(begin: prev, end: cur), tween: Tween<double>(begin: prev, end: cur),
@ -357,7 +389,8 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
child: Text( child: Text(
'${value.toStringAsFixed(dec)}$suffix', '${value.toStringAsFixed(dec)}$suffix',
style: const TextStyle( style: const TextStyle(
color: Colors.white, // --- BERUBAH: Warna Angka/Nilai Sensor ---
color: AppColors.textMain,
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
@ -380,8 +413,10 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
height: double.infinity, height: double.infinity,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Kartu Latar Putih Bersih & Garis Halus ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -389,13 +424,15 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [ children: [
Row( Row(
children: [ children: [
// --- BERUBAH: Warna Ikon Kontroler ---
Icon(Icons.settings_input_component, Icon(Icons.settings_input_component,
color: isOn ? Colors.green : Colors.grey, size: 16), color: isOn ? AppColors.primary : AppColors.textSecondary, size: 16),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
// --- BERUBAH: Warna Judul Kartu Kontrol ---
child: Text(title, child: Text(title,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: AppColors.textMain,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
), ),
@ -404,16 +441,18 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// --- BERUBAH: Warna Status Mode ---
Text('Mode: ${mqtt.mode.toUpperCase()}', Text('Mode: ${mqtt.mode.toUpperCase()}',
style: const TextStyle( style: const TextStyle(
color: Colors.green, fontSize: 9, fontWeight: FontWeight.bold)), color: AppColors.primary, fontSize: 9, fontWeight: FontWeight.bold)),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
isOn ? 'ON' : 'OFF', isOn ? 'ON' : 'OFF',
style: TextStyle( style: TextStyle(
color: isOn ? Colors.green : Colors.grey, // --- BERUBAH: Warna Status ON/OFF ---
color: isOn ? AppColors.primary : AppColors.textSecondary,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
@ -422,7 +461,8 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
child: Switch( child: Switch(
value: isOn, value: isOn,
onChanged: mqtt.mode == 'manual' ? onChanged : null, onChanged: mqtt.mode == 'manual' ? onChanged : null,
activeColor: Colors.green, // --- BERUBAH: Warna Switch ---
activeColor: AppColors.primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
), ),

View File

@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
import '../services/mqtt_service.dart'; import '../services/mqtt_service.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../constants/app_colors.dart';
import '../services/recording_service.dart'; // Memori Abadi Timer Tetap Dipakai
class RecordingScreen extends StatefulWidget { class RecordingScreen extends StatefulWidget {
const RecordingScreen({super.key}); const RecordingScreen({super.key});
@ -15,8 +17,8 @@ class _RecordingScreenState extends State<RecordingScreen> {
final TextEditingController _intervalController = TextEditingController(); final TextEditingController _intervalController = TextEditingController();
List<Map<String, dynamic>> _gallery = []; List<Map<String, dynamic>> _gallery = [];
bool _isLoading = true; bool _isLoading = true;
bool _isRecording = false;
final recordingService = RecordingService();
late final RealtimeChannel _channel; late final RealtimeChannel _channel;
@override @override
@ -24,7 +26,6 @@ class _RecordingScreenState extends State<RecordingScreen> {
super.initState(); super.initState();
_loadData(); _loadData();
// 🔥 REALTIME LISTENER SUPABASE (AUTO REFRESH GALERI)
_channel = Supabase.instance.client _channel = Supabase.instance.client
.channel('realtime-foto') .channel('realtime-foto')
.onPostgresChanges( .onPostgresChanges(
@ -32,7 +33,7 @@ class _RecordingScreenState extends State<RecordingScreen> {
schema: 'public', schema: 'public',
table: 'foto_dataset', table: 'foto_dataset',
callback: (payload) { callback: (payload) {
_loadData(); // reload gallery otomatis saat foto baru masuk _loadData();
}, },
) )
.subscribe(); .subscribe();
@ -41,10 +42,7 @@ class _RecordingScreenState extends State<RecordingScreen> {
@override @override
void dispose() { void dispose() {
_intervalController.dispose(); _intervalController.dispose();
// 🔥 MATIKAN REALTIME LISTENER
Supabase.instance.client.removeChannel(_channel); Supabase.instance.client.removeChannel(_channel);
super.dispose(); super.dispose();
} }
@ -74,31 +72,31 @@ class _RecordingScreenState extends State<RecordingScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Interval harus lebih dari 0 menit!'), content: Text('Interval harus lebih dari 0 menit!'),
backgroundColor: Colors.orange, backgroundColor: AppColors.warning,
), ),
); );
return; return;
} }
if (start) { if (start) {
// Simpan interval ke Supabase
await SupabaseService().updateIntervalSetting(intervalVal); await SupabaseService().updateIntervalSetting(intervalVal);
// Kirim interval ke ESP32 via MQTT
mqtt.setIntervalFoto(intervalVal); mqtt.setIntervalFoto(intervalVal);
setState(() => _isRecording = true); recordingService.setRecordingStatus(true);
} else { } else {
// Kirim stop ke ESP32 Dev via MQTT
mqtt.setIntervalFoto(0); mqtt.setIntervalFoto(0);
setState(() => _isRecording = false); recordingService.setRecordingStatus(false);
} }
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(start content: Text(
? '📸 Perekaman dimulai — interval ${intervalVal} menit' start
: '⏹ Perekaman dihentikan'), ? 'Perekaman dimulai — interval $intervalVal menit'
backgroundColor: start ? Colors.green : Colors.red, : 'Perekaman dihentikan',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
backgroundColor: start ? AppColors.success : AppColors.error,
), ),
); );
} }
@ -107,173 +105,140 @@ class _RecordingScreenState extends State<RecordingScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: _isLoading child: _isLoading
? const Center( ? const Center(child: CircularProgressIndicator(color: AppColors.primary))
child: CircularProgressIndicator(color: Colors.green))
: Padding( : Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header const Text('Perekaman',
const Text( style: TextStyle(color: AppColors.textMain, fontSize: 32, fontWeight: FontWeight.bold),
'Perekaman',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold),
), ),
const Text( const Text('Dataset Citra Kopi',
'Dataset Citra Kopi', style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
style: TextStyle(color: Colors.grey, fontSize: 16),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Panel Kontrol // LISTENABLE BUILDER: Membuat timer kebal pindah halaman
Container( ListenableBuilder(
padding: const EdgeInsets.all(20), listenable: recordingService,
decoration: BoxDecoration( builder: (context, child) {
color: const Color(0xFF1A1A1A), final isRecording = recordingService.isRecording;
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Interval Waktu (menit)',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextField(
controller: _intervalController,
keyboardType: TextInputType.number,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: '30',
hintStyle:
const TextStyle(color: Colors.grey),
filled: true,
fillColor: Colors.black,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide:
const BorderSide(color: Colors.grey),
),
),
),
const SizedBox(height: 15),
// Status perekaman return Container(
if (_isRecording) padding: const EdgeInsets.all(20),
Container( decoration: BoxDecoration(
padding: const EdgeInsets.all(10), color: AppColors.cardBg,
decoration: BoxDecoration( borderRadius: BorderRadius.circular(20),
color: Colors.green.withOpacity(0.1), border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
borderRadius: BorderRadius.circular(10), ),
border: Border.all(color: Colors.green), child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
child: const Row(
children: [
Icon(Icons.circle,
color: Colors.green, size: 10),
SizedBox(width: 8),
Text('Perekaman berjalan...',
style: TextStyle(
color: Colors.green,
fontSize: 12)),
],
),
),
const SizedBox(height: 10),
Row(
children: [ children: [
Expanded( const Text('Interval Waktu (menit)',
child: ElevatedButton.icon( style: TextStyle(color: AppColors.textMain, fontWeight: FontWeight.bold),
onPressed: _isRecording ),
? null const SizedBox(height: 10),
: () => _controlRecording(true), TextField(
icon: const Icon(Icons.play_arrow, controller: _intervalController,
color: Colors.white), keyboardType: TextInputType.number,
label: const Text('Mulai', style: const TextStyle(color: AppColors.textMain),
style: decoration: InputDecoration(
TextStyle(color: Colors.white)), hintText: '30',
style: ElevatedButton.styleFrom( hintStyle: const TextStyle(color: AppColors.textSecondary),
backgroundColor: Colors.green, filled: true,
disabledBackgroundColor: fillColor: AppColors.background,
Colors.green.withOpacity(0.3), border: OutlineInputBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15),
borderRadius: borderSide: BorderSide(color: AppColors.textSecondary.withValues(alpha: 0.3)),
BorderRadius.circular(12), ),
), focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: AppColors.primary),
), ),
), ),
), ),
const SizedBox(width: 10), const SizedBox(height: 15),
Expanded(
child: ElevatedButton.icon( if (isRecording)
onPressed: !_isRecording Container(
? null padding: const EdgeInsets.all(10),
: () => _controlRecording(false), decoration: BoxDecoration(
icon: const Icon(Icons.stop, color: AppColors.success.withValues(alpha: 0.15),
color: Colors.white), borderRadius: BorderRadius.circular(10),
label: const Text('Berhenti', border: Border.all(color: AppColors.success),
style: ),
TextStyle(color: Colors.white)), child: const Row(
style: ElevatedButton.styleFrom( children: [
backgroundColor: Colors.redAccent, Icon(Icons.circle, color: AppColors.success, size: 10),
disabledBackgroundColor: SizedBox(width: 8),
Colors.redAccent.withOpacity(0.3), Text('Perekaman berjalan...',
shape: RoundedRectangleBorder( style: TextStyle(color: AppColors.success, fontSize: 12, fontWeight: FontWeight.bold)),
borderRadius: ],
BorderRadius.circular(12),
),
), ),
), ),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: isRecording ? null : () => _controlRecording(true),
icon: const Icon(Icons.play_arrow, color: Colors.white),
label: const Text('Mulai', style: TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton.icon(
onPressed: !isRecording ? null : () => _controlRecording(false),
icon: const Icon(Icons.stop, color: Colors.white),
label: const Text('Berhenti', style: TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.error,
disabledBackgroundColor: AppColors.error.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
), ),
], ],
), ),
], );
), },
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Header Galeri
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text('Galeri Citra Dataset (${_gallery.length} foto)',
'Galeri Citra Dataset (${_gallery.length} foto)', style: const TextStyle(color: AppColors.textMain, fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold),
), ),
IconButton( IconButton(
icon: const Icon(Icons.refresh, icon: const Icon(Icons.refresh, color: AppColors.primary),
color: Colors.green),
onPressed: _loadData, onPressed: _loadData,
), ),
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Grid Galeri
Expanded( Expanded(
child: _gallery.isEmpty child: _gallery.isEmpty
? const Center( ? const Center(
child: Text( child: Text('Belum ada data citra', style: TextStyle(color: AppColors.textSecondary)),
'Belum ada data citra',
style: TextStyle(color: Colors.grey),
),
) )
: GridView.builder( : GridView.builder(
gridDelegate: gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisCount: 2,
crossAxisSpacing: 10, crossAxisSpacing: 10,
mainAxisSpacing: 10, mainAxisSpacing: 10,
@ -282,9 +247,13 @@ class _RecordingScreenState extends State<RecordingScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = _gallery[index]; final item = _gallery[index];
final nomorFoto = index + 1; final nomorFoto = index + 1;
final timestamp = item['timestamp'] ?? '';
final tanggal = timestamp.contains(' ') ? timestamp.split(' ')[0] : timestamp; // PENYARING WAKTU: Ubah 'T' jadi Spasi agar kodingan lamamu aman
final jam = timestamp.contains(' ') ? timestamp.split(' ')[1] : ''; String rawTime = item['timestamp'] ?? item['created_at'] ?? '';
String cleanTime = rawTime.replaceAll('T', ' ');
final tanggal = cleanTime.contains(' ') ? cleanTime.split(' ')[0] : cleanTime;
final jam = cleanTime.contains(' ') ? cleanTime.split(' ')[1] : '';
final jamSingkat = jam.length >= 5 ? jam.substring(0, 5) : jam; final jamSingkat = jam.length >= 5 ? jam.substring(0, 5) : jam;
return GestureDetector( return GestureDetector(
@ -294,10 +263,11 @@ class _RecordingScreenState extends State<RecordingScreen> {
arguments: { arguments: {
'id': item['id'].toString(), 'id': item['id'].toString(),
'date': tanggal, 'date': tanggal,
'time': jam, 'time': jamSingkat,
'temp': '${item['suhu']} °C', // PENYARING SENSOR 0: Jika 0, ubah jadi strip '-'
'humidity': '${item['kelembapan']} %', 'temp': (item['suhu'] == 0 || item['suhu'] == null) ? '-' : '${item['suhu']} °C',
'light': '${item['intensitas']} Lux', 'humidity': (item['kelembapan'] == 0 || item['kelembapan'] == null) ? '-' : '${item['kelembapan']} %',
'light': (item['intensitas'] == 0 || item['intensitas'] == null) ? '-' : '${item['intensitas']} Lux',
'location': 'Gudang Pengering', 'location': 'Gudang Pengering',
'intake': item['kipas1'] ?? 'OFF', 'intake': item['kipas1'] ?? 'OFF',
'exhaust': item['kipas2'] ?? 'OFF', 'exhaust': item['kipas2'] ?? 'OFF',
@ -306,71 +276,61 @@ class _RecordingScreenState extends State<RecordingScreen> {
), ),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), color: AppColors.cardBg,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Foto 75% tinggi card
Expanded( Expanded(
flex: 3, flex: 3,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
child: item['url_foto'] != null child: item['url_foto'] != null
? Image.network( ? Image.network(
item['url_foto'], item['url_foto'],
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center( errorBuilder: (_, __, ___) => const Center(
child: Icon(Icons.broken_image, color: Colors.grey, size: 40), child: Icon(Icons.broken_image, color: AppColors.textSecondary, size: 40),
), ),
loadingBuilder: (_, child, progress) { loadingBuilder: (_, child, progress) {
if (progress == null) return child; if (progress == null) return child;
return const Center( return const Center(
child: CircularProgressIndicator(color: Colors.green, strokeWidth: 2), child: CircularProgressIndicator(color: AppColors.primary, strokeWidth: 2),
); );
}, },
) )
: const Center( : const Center(
child: Icon(Icons.image, color: Colors.grey, size: 40), child: Icon(Icons.image, color: AppColors.textSecondary, size: 40),
), ),
), ),
), ),
// Caption bawah foto
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xFF1A1A1A), color: Colors.transparent,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(15)), borderRadius: BorderRadius.vertical(bottom: Radius.circular(15)),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// Badge nomor foto
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.green.withOpacity(0.2), color: AppColors.primary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.green.withOpacity(0.5)), border: Border.all(color: AppColors.primary.withValues(alpha: 0.5)),
), ),
child: Text( child: Text('#$nomorFoto',
'#$nomorFoto', style: const TextStyle(color: AppColors.primary, fontSize: 10, fontWeight: FontWeight.bold),
style: const TextStyle(
color: Colors.green, fontSize: 10, fontWeight: FontWeight.bold),
), ),
), ),
// Tanggal dan jam
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text(tanggal, Text(tanggal, style: const TextStyle(color: AppColors.textSecondary, fontSize: 8)),
style: const TextStyle(color: Colors.grey, fontSize: 8)), Text(jamSingkat, style: const TextStyle(color: AppColors.textMain, fontSize: 10, fontWeight: FontWeight.bold)),
Text(jamSingkat,
style: const TextStyle(
color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
], ],
), ),
], ],

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class RegisterScreen extends StatefulWidget { class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key}); const RegisterScreen({super.key});
@ -89,7 +90,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
children: [ children: [
Icon( Icon(
isError ? Icons.error_outline : Icons.check_circle_outline, isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white, color: Colors.white, // Tetap putih agar kontras dengan background
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
@ -101,8 +102,9 @@ class _RegisterScreenState extends State<RegisterScreen> {
), ),
], ],
), ),
// --- BERUBAH: Menggunakan warna notifikasi dari AppColors ---
backgroundColor: backgroundColor:
isError ? Colors.red.shade700 : Colors.green.shade700, isError ? AppColors.error : AppColors.success,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -116,12 +118,14 @@ class _RegisterScreenState extends State<RegisterScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.green), // --- BERUBAH: Warna Ikon Back ---
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
), ),
@ -134,75 +138,91 @@ class _RegisterScreenState extends State<RegisterScreen> {
const Text( const Text(
'Buat Akun Baru', 'Buat Akun Baru',
style: TextStyle( style: TextStyle(
color: Colors.green, // --- BERUBAH: Warna Judul ---
color: AppColors.primary,
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
const Text( const Text(
'Lengkapi data diri Anda untuk memulai', 'Lengkapi data diri Anda untuk memulai',
style: TextStyle(color: Colors.grey, fontSize: 16), // --- BERUBAH: Warna Sub-judul ---
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
), ),
const SizedBox(height: 50), const SizedBox(height: 50),
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Form ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
// --- BERUBAH: Garis Pinggir Form ---
border: border:
Border.all(color: Colors.grey.withOpacity(0.3)), Border.all(color: AppColors.textSecondary.withValues(alpha: 0.3)),
), ),
child: Column( child: Column(
children: [ children: [
// Nama Lengkap // Nama Lengkap
TextField( TextField(
controller: _nameController, controller: _nameController,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
textCapitalization: TextCapitalization.words, textCapitalization: TextCapitalization.words,
decoration: const InputDecoration( decoration: const InputDecoration(
// --- BERUBAH: Warna Ikon ---
prefixIcon: prefixIcon:
Icon(Icons.person, color: Colors.green), Icon(Icons.person, color: AppColors.primary),
hintText: 'Nama Lengkap', hintText: 'Nama Lengkap',
hintStyle: TextStyle(color: Colors.grey), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none, border: InputBorder.none,
), ),
), ),
const Divider(color: Colors.grey), // --- BERUBAH: Garis Pemisah (Divider) ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
// Email // Email
TextField( TextField(
controller: _emailController, controller: _emailController,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: const InputDecoration( decoration: const InputDecoration(
// --- BERUBAH: Warna Ikon ---
prefixIcon: prefixIcon:
Icon(Icons.email, color: Colors.green), Icon(Icons.email, color: AppColors.primary),
hintText: 'Email', hintText: 'Email',
hintStyle: TextStyle(color: Colors.grey), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none, border: InputBorder.none,
), ),
), ),
const Divider(color: Colors.grey), // --- BERUBAH: Garis Pemisah (Divider) ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
// Password // Password
TextField( TextField(
controller: _passwordController, controller: _passwordController,
obscureText: _obscurePassword, obscureText: _obscurePassword,
style: const TextStyle(color: Colors.white), // --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: InputDecoration( decoration: InputDecoration(
// --- BERUBAH: Warna Ikon Kunci ---
prefixIcon: prefixIcon:
const Icon(Icons.lock, color: Colors.green), const Icon(Icons.lock, color: AppColors.primary),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
_obscurePassword _obscurePassword
? Icons.visibility ? Icons.visibility
: Icons.visibility_off, : Icons.visibility_off,
color: Colors.grey, // --- BERUBAH: Warna Ikon Mata ---
color: AppColors.textSecondary,
), ),
onPressed: () => setState( onPressed: () => setState(
() => _obscurePassword = !_obscurePassword), () => _obscurePassword = !_obscurePassword),
), ),
hintText: 'Password (min. 6 karakter)', hintText: 'Password (min. 6 karakter)',
hintStyle: const TextStyle(color: Colors.grey), // --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: const TextStyle(color: AppColors.textSecondary),
border: InputBorder.none, border: InputBorder.none,
), ),
), ),
@ -215,9 +235,11 @@ class _RegisterScreenState extends State<RegisterScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _isLoading ? null : _handleRegister, onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, // --- BERUBAH: Warna Tombol ---
backgroundColor: AppColors.primary,
// --- BERUBAH: Warna Tombol saat Nonaktif (Loading) ---
disabledBackgroundColor: disabledBackgroundColor:
Colors.green.withOpacity(0.3), AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
@ -243,7 +265,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text( child: const Text(
'Sudah punya akun? Masuk', 'Sudah punya akun? Masuk',
style: TextStyle(color: Colors.green), // --- BERUBAH: Warna Teks Bawah ---
style: TextStyle(color: AppColors.primary),
), ),
), ),
), ),

View File

@ -1,12 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:excel/excel.dart'; import 'package:excel/excel.dart' hide Border;
import 'package:open_file/open_file.dart'; import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../services/supabase_service.dart'; import '../services/supabase_service.dart';
import '../services/mqtt_service.dart'; import '../services/mqtt_service.dart';
import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_client.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@ -40,7 +41,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
} }
// Export foto_dataset ke Excel // Export foto_dataset ke Excel (Warna Excel Tetap Dipertahankan)
Future<void> _exportExcel() async { Future<void> _exportExcel() async {
setState(() => _isExporting = true); setState(() => _isExporting = true);
@ -51,9 +52,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (data.isEmpty) { if (data.isEmpty) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Belum ada data dataset untuk diexport!'), content: const Text('Belum ada data dataset untuk diexport!'),
backgroundColor: Colors.orange, // --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
), ),
); );
} }
@ -71,12 +73,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
'Suhu (°C)', 'Suhu (°C)',
'Kelembapan (%)', 'Kelembapan (%)',
'Intensitas Cahaya (Lux)', 'Intensitas Cahaya (Lux)',
'Kipas 1 (Exhaust)', 'Kipas 1 (Intake)',
'Kipas 2 (Intake)', 'Kipas 2 (Exhaust)',
'URL Foto',
]; ];
// Style header // Style header (Dibiarkan tetap hijau standar Excel)
for (int i = 0; i < headers.length; i++) { for (int i = 0; i < headers.length; i++) {
final cell = sheet.cell( final cell = sheet.cell(
CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)); CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0));
@ -88,56 +89,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
// Isi data // Isi data tanpa URL Foto
for (int i = 0; i < data.length; i++) { for (int i = 0; i < data.length; i++) {
final row = data[i]; final row = data[i];
final rowIndex = i + 1; final rowIndex = i + 1;
sheet sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: rowIndex))
.cell(CellIndex.indexByColumnRow(
columnIndex: 0, rowIndex: rowIndex))
.value = IntCellValue(i + 1); .value = IntCellValue(i + 1);
sheet sheet.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: rowIndex))
.cell(CellIndex.indexByColumnRow(
columnIndex: 1, rowIndex: rowIndex))
.value = TextCellValue(row['timestamp']?.toString() ?? '-'); .value = TextCellValue(row['timestamp']?.toString() ?? '-');
sheet sheet.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: rowIndex))
.cell(CellIndex.indexByColumnRow( .value = DoubleCellValue(double.tryParse(row['suhu'].toString()) ?? 0);
columnIndex: 2, rowIndex: rowIndex))
.value = DoubleCellValue(
double.tryParse(row['suhu'].toString()) ?? 0);
sheet sheet.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: rowIndex))
.cell(CellIndex.indexByColumnRow( .value = DoubleCellValue(double.tryParse(row['kelembapan'].toString()) ?? 0);
columnIndex: 3, rowIndex: rowIndex))
.value = DoubleCellValue(
double.tryParse(row['kelembapan'].toString()) ?? 0);
sheet sheet.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: rowIndex))
.cell(CellIndex.indexByColumnRow( .value = DoubleCellValue(double.tryParse(row['intensitas'].toString()) ?? 0);
columnIndex: 4, rowIndex: rowIndex))
.value = DoubleCellValue(
double.tryParse(row['intensitas'].toString()) ?? 0);
sheet sheet.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: rowIndex))
.cell(CellIndex.indexByColumnRow(
columnIndex: 5, rowIndex: rowIndex))
.value = TextCellValue(row['kipas1']?.toString() ?? '-'); .value = TextCellValue(row['kipas1']?.toString() ?? '-');
sheet sheet.cell(CellIndex.indexByColumnRow(columnIndex: 6, rowIndex: rowIndex))
.cell(CellIndex.indexByColumnRow(
columnIndex: 6, rowIndex: rowIndex))
.value = TextCellValue(row['kipas2']?.toString() ?? '-'); .value = TextCellValue(row['kipas2']?.toString() ?? '-');
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 7, rowIndex: rowIndex))
.value = TextCellValue(row['url_foto']?.toString() ?? '-');
} }
// Set lebar kolom // Set lebar kolom tanpa kolom URL
sheet.setColumnWidth(0, 5); sheet.setColumnWidth(0, 5);
sheet.setColumnWidth(1, 22); sheet.setColumnWidth(1, 22);
sheet.setColumnWidth(2, 12); sheet.setColumnWidth(2, 12);
@ -145,7 +124,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
sheet.setColumnWidth(4, 24); sheet.setColumnWidth(4, 24);
sheet.setColumnWidth(5, 18); sheet.setColumnWidth(5, 18);
sheet.setColumnWidth(6, 18); sheet.setColumnWidth(6, 18);
sheet.setColumnWidth(7, 50);
// Simpan file // Simpan file
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationDocumentsDirectory();
@ -170,7 +148,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar( SnackBar(
content: Text( content: Text(
'✓ Export berhasil — ${data.length} data tersimpan'), '✓ Export berhasil — ${data.length} data tersimpan'),
backgroundColor: Colors.green, // --- BERUBAH: Warna Notifikasi Sukses ---
backgroundColor: AppColors.success,
), ),
); );
} }
@ -179,7 +158,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Gagal export: $e'), content: Text('Gagal export: $e'),
backgroundColor: Colors.red, // --- BERUBAH: Warna Notifikasi Error ---
backgroundColor: AppColors.error,
), ),
); );
} }
@ -194,7 +174,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
final user = SupabaseService().currentUser; final user = SupabaseService().currentUser;
return Scaffold( return Scaffold(
backgroundColor: Colors.black, // --- BERUBAH: Background UI Utama ---
backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
@ -206,14 +187,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
Container( Container(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Background Kartu Profil ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Garis pinggir halus ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Row( child: Row(
children: [ children: [
const CircleAvatar( const CircleAvatar(
radius: 30, radius: 30,
backgroundColor: Colors.green, // --- BERUBAH: Warna Avatar ---
backgroundColor: AppColors.primary,
child: Icon(Icons.person, child: Icon(Icons.person,
color: Colors.white, size: 40), color: Colors.white, size: 40),
), ),
@ -226,8 +211,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
width: 100, width: 100,
height: 16, height: 16,
child: LinearProgressIndicator( child: LinearProgressIndicator(
color: Colors.green, // --- BERUBAH: Warna Loading ---
backgroundColor: Colors.grey, color: AppColors.primary,
backgroundColor: AppColors.background,
), ),
) )
: Text( : Text(
@ -235,7 +221,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
? _namaLengkap ? _namaLengkap
: 'Admin Kopi', : 'Admin Kopi',
style: const TextStyle( style: const TextStyle(
color: Colors.white, // --- BERUBAH: Warna Nama Profil ---
color: AppColors.textMain,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -244,12 +231,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
Text( Text(
user?.email ?? 'admin@coffee.io', user?.email ?? 'admin@coffee.io',
style: const TextStyle( style: const TextStyle(
color: Colors.grey, fontSize: 12), // --- BERUBAH: Warna Email Profil ---
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
), ),
const Text( const Text(
'Status: Active User', 'Status: Active User',
style: TextStyle( style: TextStyle(
color: Colors.green, fontSize: 12), // --- BERUBAH: Warna Status Profil ---
color: AppColors.success,
fontSize: 12,
fontWeight: FontWeight.bold,
),
), ),
], ],
), ),
@ -264,7 +259,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildSettingsCard([ _buildSettingsCard([
_buildConnectionStatus( _buildConnectionStatus(
'Database Supabase', true, Icons.storage), 'Database Supabase', true, Icons.storage),
const Divider(color: Colors.grey, height: 1), // --- BERUBAH: Warna Garis Pemisah Menu ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.2), height: 1),
_buildConnectionStatus( _buildConnectionStatus(
'MQTT Broker (EMQX)', 'MQTT Broker (EMQX)',
mqtt.client?.connectionStatus?.state == mqtt.client?.connectionStatus?.state ==
@ -284,12 +280,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
width: 22, width: 22,
height: 22, height: 22,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.green, strokeWidth: 2), // --- BERUBAH: Warna Loading ---
color: AppColors.primary, strokeWidth: 2),
), ),
title: Text( title: Text(
'Menyiapkan file Excel...', 'Menyiapkan file Excel...',
style: TextStyle( style: TextStyle(
color: Colors.grey, fontSize: 14), // --- BERUBAH: Warna Teks Loading ---
color: AppColors.textSecondary, fontSize: 14),
), ),
) )
: _buildSettingItem( : _buildSettingItem(
@ -303,14 +301,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Keamanan // Keamanan
_buildSectionTitle('Keamanan'), _buildSectionTitle('Keamanan'),
_buildSettingItem( _buildSettingsCard([
Icons.lock_reset, 'Ganti Password Akun', () { _buildSettingItem(
Navigator.pushNamed( Icons.lock_reset, 'Ganti Password Akun', () {
context, Navigator.pushNamed(
'/change_password', context,
arguments: {'dari_reset': false}, '/change_password',
); arguments: {'dari_reset': false},
}), );
}),
]),
const SizedBox(height: 30), const SizedBox(height: 30),
@ -319,25 +319,70 @@ class _SettingsScreenState extends State<SettingsScreen> {
width: double.infinity, width: double.infinity,
height: 55, height: 55,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () async { onPressed: () async {
final konfirmasi = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
// --- BERUBAH: Latar Belakang Dialog ---
backgroundColor: AppColors.cardBg,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Row(
children: [
Icon(Icons.logout, color: AppColors.error),
SizedBox(width: 10),
Text('Keluar dari Akun',
// --- BERUBAH: Warna Judul Dialog ---
style: TextStyle(color: AppColors.textMain, fontSize: 18)),
],
),
content: const Text(
'Apakah kamu yakin ingin keluar dari akun ini?',
// --- BERUBAH: Warna Teks Konten Dialog ---
style: TextStyle(color: AppColors.textSecondary),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Batal',
// --- BERUBAH: Warna Tombol Batal ---
style: TextStyle(color: AppColors.primary)),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(
// --- BERUBAH: Warna Tombol Keluar di Dialog ---
backgroundColor: AppColors.error,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: const Text('Keluar',
style: TextStyle(color: Colors.white)),
),
],
),
);
if (konfirmasi == true) {
await SupabaseService().signOut(); await SupabaseService().signOut();
if (context.mounted) { if (context.mounted) {
Navigator.pushReplacementNamed(context, '/login'); Navigator.pushReplacementNamed(context, '/login');
} }
}, }
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent.withOpacity(0.8), // --- BERUBAH: Warna Latar Tombol Keluar ---
backgroundColor: AppColors.error.withValues(alpha: 0.8),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
), ),
icon: const Icon(Icons.logout, color: Colors.white), icon: const Icon(Icons.logout, color: Colors.white), // Tetap putih
label: const Text( label: const Text(
'Keluar dari Akun', 'Keluar dari Akun',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white), color: Colors.white), // Tetap putih
), ),
), ),
), ),
@ -356,7 +401,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Text( child: Text(
title, title,
style: const TextStyle( style: const TextStyle(
color: Colors.grey, // --- BERUBAH: Warna Judul Seksi/Kategori ---
color: AppColors.textSecondary,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
@ -366,8 +412,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _buildSettingsCard(List<Widget> children) { Widget _buildSettingsCard(List<Widget> children) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A1A), // --- BERUBAH: Latar Belakang Kartu Pengaturan ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Garis pinggir halus ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
), ),
child: Column(children: children), child: Column(children: children),
); );
@ -376,13 +425,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _buildSettingItem(IconData icon, String title, VoidCallback? onTap, Widget _buildSettingItem(IconData icon, String title, VoidCallback? onTap,
{String? trailing}) { {String? trailing}) {
return ListTile( return ListTile(
leading: Icon(icon, color: Colors.green, size: 22), // --- BERUBAH: Warna Ikon Leading ---
leading: Icon(icon, color: AppColors.primary, size: 22),
title: Text(title, title: Text(title,
style: const TextStyle(color: Colors.white, fontSize: 14)), // --- BERUBAH: Warna Teks Menu ---
style: const TextStyle(color: AppColors.textMain, fontSize: 14)),
trailing: trailing != null trailing: trailing != null
? Text(trailing, ? Text(trailing,
style: const TextStyle(color: Colors.grey, fontSize: 12)) // --- BERUBAH: Warna Teks Trailing ---
: const Icon(Icons.chevron_right, color: Colors.grey, size: 18), style: const TextStyle(color: AppColors.textSecondary, fontSize: 12))
// --- BERUBAH: Warna Ikon Panah Kanan ---
: const Icon(Icons.chevron_right, color: AppColors.textSecondary, size: 18),
onTap: onTap, onTap: onTap,
); );
} }
@ -390,10 +443,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _buildConnectionStatus( Widget _buildConnectionStatus(
String title, bool isConnected, IconData icon) { String title, bool isConnected, IconData icon) {
return ListTile( return ListTile(
// --- BERUBAH: Warna Ikon Status Database/MQTT ---
leading: Icon(icon, leading: Icon(icon,
color: isConnected ? Colors.blue : Colors.red, size: 22), color: isConnected ? AppColors.info : AppColors.error, size: 22),
title: Text(title, title: Text(title,
style: const TextStyle(color: Colors.white, fontSize: 14)), // --- BERUBAH: Warna Teks Nama Layanan ---
style: const TextStyle(color: AppColors.textMain, fontSize: 14)),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -402,14 +457,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
height: 8, height: 8,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: isConnected ? Colors.green : Colors.red, // --- BERUBAH: Warna Titik Indikator ---
color: isConnected ? AppColors.success : AppColors.error,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
isConnected ? 'Online' : 'Offline', isConnected ? 'Online' : 'Offline',
style: TextStyle( style: TextStyle(
color: isConnected ? Colors.green : Colors.red, // --- BERUBAH: Warna Teks Online/Offline ---
color: isConnected ? AppColors.success : AppColors.error,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),

View File

@ -0,0 +1,308 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; // <-- IMPORT SUPABASE DITAMBAHKAN
import '../constants/app_colors.dart';
import 'login_screen.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with TickerProviderStateMixin {
// Controllers
late AnimationController _mainController;
late AnimationController _slideController;
late AnimationController _textController;
late AnimationController _revealController;
// Animations
late Animation<double> _logoY;
late Animation<double> _shadowScale;
late Animation<double> _shadowOpacity;
late Animation<double> _logoSlideX;
late Animation<double> _textOpacity;
late Animation<double> _revealProgress;
// Typewriter State
final String _fullText = 'IoT Kopi';
String _displayText = '';
int _charIndex = 0;
@override
void initState() {
super.initState();
_setupAnimations();
_mulaiAnimasi();
}
void _setupAnimations() {
// Total durasi sekuens utama = 3.6 detik
_mainController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3600),
);
// KOREOGRAFI LOGO DENGAN EFEK GRAVITASI
_logoY = TweenSequence<double>([
TweenSequenceItem(tween: ConstantTween<double>(120.0), weight: 25),
TweenSequenceItem(
tween: Tween(begin: 120.0, end: -180.0).chain(CurveTween(curve: Curves.easeOut)),
weight: 14,
),
TweenSequenceItem(
tween: TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: -180.0, end: -188.0).chain(CurveTween(curve: Curves.easeInOut)), weight: 50),
TweenSequenceItem(tween: Tween(begin: -188.0, end: -180.0).chain(CurveTween(curve: Curves.easeInOut)), weight: 50),
]),
weight: 25,
),
TweenSequenceItem(
tween: Tween(begin: -180.0, end: 0.0).chain(CurveTween(curve: Curves.easeIn)),
weight: 11,
),
TweenSequenceItem(tween: Tween(begin: 0.0, end: -45.0).chain(CurveTween(curve: Curves.easeOut)), weight: 5),
TweenSequenceItem(tween: Tween(begin: -45.0, end: 0.0).chain(CurveTween(curve: Curves.easeIn)), weight: 5),
TweenSequenceItem(tween: Tween(begin: 0.0, end: -15.0).chain(CurveTween(curve: Curves.easeOut)), weight: 4),
TweenSequenceItem(tween: Tween(begin: -15.0, end: 0.0).chain(CurveTween(curve: Curves.easeIn)), weight: 4),
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 7),
]).animate(_mainController);
// KOREOGRAFI UKURAN BAYANGAN
_shadowScale = TweenSequence<double>([
TweenSequenceItem(tween: ConstantTween<double>(2.5), weight: 25),
TweenSequenceItem(tween: Tween(begin: 2.5, end: 0.1).chain(CurveTween(curve: Curves.easeOut)), weight: 14),
TweenSequenceItem(tween: ConstantTween<double>(0.1), weight: 61),
]).animate(_mainController);
// KOREOGRAFI TRANSPARANSI BAYANGAN
_shadowOpacity = TweenSequence<double>([
TweenSequenceItem(tween: ConstantTween<double>(0.6), weight: 25),
TweenSequenceItem(tween: Tween(begin: 0.6, end: 0.0).chain(CurveTween(curve: Curves.easeOut)), weight: 14),
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 61),
]).animate(_mainController);
// SLIDE LOGO KE KIRI
_slideController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_logoSlideX = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeInOutCubic),
);
// FADE IN OPACITY TEKS
_textController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_textOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(_textController);
// KONTROL TRANSISI MENGECIL
_revealController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 750),
);
_revealProgress = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _revealController, curve: Curves.easeInOutQuad),
);
}
// --- FUNGSI ANIMASI YANG SUDAH DIUPDATE DENGAN PENGECEKAN LOGIN ---
Future<void> _mulaiAnimasi() async {
await _mainController.forward();
await Future.delayed(const Duration(milliseconds: 200));
_slideController.forward();
await Future.delayed(const Duration(milliseconds: 300));
_textController.forward();
await Future.delayed(const Duration(milliseconds: 500));
// Typewriter
_charIndex = 0;
_displayText = '';
for (int i = 0; i < _fullText.length; i++) {
await Future.delayed(const Duration(milliseconds: 120));
if (mounted) {
setState(() {
_charIndex++;
_displayText = _fullText.substring(0, _charIndex);
});
}
}
await Future.delayed(const Duration(milliseconds: 100));
if (mounted) {
// 1. Jalankan animasi lingkaran mengecil internal sampai habis
await _revealController.forward();
// 2. CEK SESI LOGIN DI SINI
if (mounted) {
final session = Supabase.instance.client.auth.currentSession;
if (session != null) {
// JIKA SUDAH LOGIN: Lempar ke Monitoring / Home
Navigator.of(context).pushReplacementNamed('/home');
} else {
// JIKA BELUM LOGIN: Lempar ke LoginScreen
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (_, __, ___) => const LoginScreen(),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
}
}
}
}
@override
void dispose() {
_mainController.dispose();
_slideController.dispose();
_textController.dispose();
_revealController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
const logoSize = 80.0;
const textEstimatedWidth = 140.0;
const gap = 16.0;
const totalWidth = logoSize + gap + textEstimatedWidth;
final startX = (size.width - totalWidth) / 2;
final logoFinalX = startX;
final logoCenterX = size.width / 2 - logoSize / 2;
final centerY = size.height / 2;
return AnimatedBuilder(
animation: _revealProgress,
builder: (context, child) {
return ClipPath(
clipper: CircularRevealClipper(progress: _revealProgress.value),
child: Scaffold(
backgroundColor: AppColors.primary,
body: AnimatedBuilder(
animation: Listenable.merge([_mainController, _slideController, _textController]),
builder: (context, child) {
final currentLogoX = logoCenterX - (_logoSlideX.value * (logoCenterX - logoFinalX));
final currentLogoY = centerY - logoSize / 2 + _logoY.value;
final shadowW = logoSize * 0.75 * _shadowScale.value;
final shadowH = 14.0 * _shadowScale.value;
final shadowX = currentLogoX + (logoSize - shadowW) / 2;
final shadowY = centerY + logoSize / 2 + 6;
final textX = currentLogoX + logoSize + gap;
final textY = centerY - 20;
return Stack(
children: [
// 1. BAYANGAN ELIPS
Positioned(
left: shadowX,
top: shadowY,
child: Opacity(
opacity: _shadowOpacity.value.clamp(0.0, 1.0),
child: Container(
width: shadowW.clamp(0.0, logoSize * 2.5),
height: shadowH.clamp(0.0, 20.0 * 2.5),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(50),
),
),
),
),
// 2. LOGO
Positioned(
left: currentLogoX,
top: currentLogoY,
child: Opacity(
opacity: _mainController.value > 0.25 ? 1.0 : 0.0,
child: Container(
width: logoSize,
height: logoSize,
decoration: BoxDecoration(
color: AppColors.cardBg,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: ClipOval(
child: Padding(
padding: const EdgeInsets.all(12),
child: Image.asset(
'assets/icon/kopibaru.png',
fit: BoxFit.contain,
),
),
),
),
),
),
// 3. TEKS POPPINS
Positioned(
left: textX,
top: textY,
child: Opacity(
opacity: _textOpacity.value,
child: Text(
_displayText,
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 30,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
),
],
);
},
),
),
);
},
);
}
}
// CUSTOM CLIPPER UNTUK EFEK LINGKARAN MENGECIL TEPAT DI TENGAH LAYAR
class CircularRevealClipper extends CustomClipper<Path> {
final double progress;
CircularRevealClipper({required this.progress});
@override
Path getClip(Size size) {
final center = Offset(size.width / 2, size.height / 2);
final maxRadius = sqrt(size.width * size.width + size.height * size.height);
final radius = maxRadius * progress;
final path = Path();
path.addOval(Rect.fromCircle(center: center, radius: radius));
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class RecordingService extends ChangeNotifier {
// Membuat Singleton
static final RecordingService _instance = RecordingService._internal();
factory RecordingService() => _instance;
RecordingService._internal() {
// Saat file ini pertama kali dipanggil (saat app dibuka),
// langsung cek ke dalam memori hardisk HP!
_loadStatusDariMemoriHP();
}
bool isRecording = false; // Status bawaan
// FUNGSI BARU: Membaca status terakhir dari dalam HP
Future<void> _loadStatusDariMemoriHP() async {
final prefs = await SharedPreferences.getInstance();
// Jika ada catatan sebelumnya, pakai itu. Jika tidak, anggap false.
isRecording = prefs.getBool('status_perekaman') ?? false;
notifyListeners(); // Beritahu layar untuk update UI
}
// FUNGSI DIUPDATE: Mengubah status DAN menyimpannya ke memori HP
Future<void> setRecordingStatus(bool status) async {
isRecording = status;
notifyListeners(); // Beritahu layar untuk update tombol
// Simpan ke hardisk HP agar tahan banting walau app di-close paksa!
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('status_perekaman', status);
}
}

View File

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
app_links: app_links:
dependency: transitive dependency: transitive
description: description:
@ -89,6 +97,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -137,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -230,6 +262,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -238,6 +278,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_native_splash:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -264,6 +312,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "6.3.3"
gotrue: gotrue:
dependency: transitive dependency: transitive
description: description:
@ -288,6 +344,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.3" version: "1.0.3"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -336,6 +400,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
jwt_decode: jwt_decode:
dependency: transitive dependency: transitive
description: description:
@ -853,6 +925,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
url_launcher: url_launcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -43,6 +43,7 @@ dependencies:
excel: ^4.0.3 excel: ^4.0.3
open_file: ^3.3.2 open_file: ^3.3.2
path_provider: ^2.1.2 path_provider: ^2.1.2
google_fonts: ^6.2.1
dependency_overrides: dependency_overrides:
win32: ^6.1.0 win32: ^6.1.0
@ -51,27 +52,38 @@ dependency_overrides:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.13.1
flutter_native_splash: ^2.3.9
flutter_native_splash:
color: "#627254" # background hitam sesuai tema
#image: assets/icon/kopibaru.png
android: true
ios: false
flutter_launcher_icons:
android: true
ios: false
image_path: "assets/icon/kopibaru.png"
min_sdk_android: 21
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: assets:
# assets: - assets/icon/kopibaru.png
- assets/icon/kopibaru_removebg.png
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg

View File

@ -1,6 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html><html><head>
<html>
<head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
@ -27,12 +25,90 @@
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png">
<title>coffee_iot_flutter</title> <title>coffee_iot_flutter</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<style id="splash-screen-style">
html {
height: 100%
}
body {
margin: 0;
min-height: 100%;
background-color: #627254;
background-size: 100% 100%;
}
.center {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.contain {
display:block;
width:100%; height:100%;
object-fit: contain;
}
.stretch {
display:block;
width:100%; height:100%;
}
.cover {
display:block;
width:100%; height:100%;
object-fit: cover;
}
.bottom {
position: absolute;
bottom: 0;
left: 50%;
-ms-transform: translate(-50%, 0);
transform: translate(-50%, 0);
}
.bottomLeft {
position: absolute;
bottom: 0;
left: 0;
}
.bottomRight {
position: absolute;
bottom: 0;
right: 0;
}
</style>
<script id="splash-screen-script">
function removeSplashFromWeb() {
document.getElementById("splash")?.remove();
document.getElementById("splash-branding")?.remove();
document.body.style.background = "transparent";
}
</script>
</head> </head>
<body> <body>
<!-- <!--
You can customize the "flutter_bootstrap.js" script. You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader This is useful to provide a custom configuration to the Flutter loader
@ -41,6 +117,7 @@
For more details: For more details:
* https://docs.flutter.dev/platform-integration/web/initialization * https://docs.flutter.dev/platform-integration/web/initialization
--> -->
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async=""></script>
</body>
</html>
</body></html>