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">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="coffee_iot_flutter"
android:label="Iot Kopi"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,6 @@
<?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">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
</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"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
</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
the Flutter engine draws its first frame -->
<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>
<!-- Theme applied to the Android Window as soon as the process has started.
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"?>
<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">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<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>
<!-- 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">
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</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:provider/provider.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 'screens/login_screen.dart';
import 'screens/register_screen.dart';
@ -10,6 +16,7 @@ import 'screens/analytics_screen.dart';
import 'screens/image_result_screen.dart';
import 'screens/change_password_screen.dart';
import 'screens/forgot_password_screen.dart';
import 'screens/splash_screen.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -61,13 +68,72 @@ class MyApp extends StatelessWidget {
navigatorKey: navigatorKey,
title: 'Coffee IoT',
debugShowCheckedModeBanner: false,
// --- KONFIGURASI TEMA LIGHT MODE (TERPUSAT) ---
theme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.green,
scaffoldBackgroundColor: Colors.black,
useMaterial3: true,
brightness: Brightness.light,
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: {
'/': (context) => const SplashScreen(),
'/login': (context) => const LoginScreen(),
'/register': (context) => const RegisterScreen(),
'/home': (context) => const HomeScreen(),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class AnalyticsScreen extends StatefulWidget {
const AnalyticsScreen({super.key});
@ -36,21 +37,27 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
Widget build(BuildContext context) {
final args = (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?) ?? {};
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';
return Scaffold(
backgroundColor: Colors.black,
// --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: Colors.black,
title: Text(title, style: const TextStyle(color: Colors.white)),
// --- BERUBAH: Dibuat transparan agar menyatu dengan background ---
backgroundColor: Colors.transparent,
elevation: 0,
// --- BERUBAH: Warna Teks Judul ---
title: Text(title, style: const TextStyle(color: AppColors.textMain)),
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),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.green))
// --- BERUBAH: Warna Loading Indicator ---
? const Center(child: CircularProgressIndicator(color: AppColors.primary))
: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
@ -60,21 +67,27 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Riwayat Data', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
IconButton(icon: const Icon(Icons.refresh, color: Colors.green), onPressed: _fetchLogs),
// --- BERUBAH: Warna Teks Judul Riwayat ---
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),
Expanded(
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Warna Latar Daftar Riwayat ---
color: AppColors.cardBg,
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(
padding: const EdgeInsets.all(15),
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) {
final log = _logs[index];
return Padding(
@ -85,10 +98,13 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(log['timestamp'] ?? '', style: const TextStyle(color: Colors.grey, fontSize: 10)),
Text('Fans: ${log['kipas1']}/${log['kipas2']}', style: const TextStyle(color: Colors.green, fontSize: 10)),
// --- BERUBAH: Warna Teks Tanggal/Waktu ---
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)),
],
),
@ -116,14 +132,15 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
height: 250,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Warna Latar Kartu Grafik ---
color: AppColors.cardBg,
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Expanded(
child: LineChart(
LineChartData(
@ -134,10 +151,12 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
LineChartBarData(
spots: spots.isEmpty ? [const FlSpot(0, 0)] : spots,
isCurved: true,
// Warna garis grafik tetap mengikuti `color` dinamis
color: color,
barWidth: 4,
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,
child: Text(
'Terakhir: ${logs.isNotEmpty ? logs.first[key] : "-"}',
// Warna teks terakhir tetap mengikuti `color` dinamis
style: TextStyle(color: color, fontSize: 12)
),
),
@ -154,4 +174,4 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
),
);
}
}
}

View File

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

View File

@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/mqtt_service.dart';
import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class ControlSettingsScreen extends StatefulWidget {
const ControlSettingsScreen({super.key});
@ -60,9 +61,10 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
_humMinController.text.isEmpty ||
_humMaxController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Semua kolom batas harus diisi!'),
backgroundColor: Colors.orange,
SnackBar(
content: const Text('Semua kolom batas harus diisi!'),
// --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
),
);
return;
@ -76,18 +78,20 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
// Validasi logika
if (suhuMin >= suhuMax) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Suhu min harus lebih kecil dari suhu max!'),
backgroundColor: Colors.orange,
SnackBar(
content: const Text('Suhu min harus lebih kecil dari suhu max!'),
// --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
),
);
return;
}
if (rhMin >= rhMax) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('RH min harus lebih kecil dari RH max!'),
backgroundColor: Colors.orange,
SnackBar(
content: const Text('RH min harus lebih kecil dari RH max!'),
// --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
),
);
return;
@ -113,9 +117,10 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('✅ Pengaturan Tersimpan & Terkirim ke ESP32'),
backgroundColor: Colors.green,
SnackBar(
content: const Text('✅ Pengaturan Tersimpan & Terkirim ke ESP32'),
// --- BERUBAH: Warna Notifikasi Sukses ---
backgroundColor: AppColors.success,
),
);
}
@ -124,7 +129,8 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('❌ Gagal: $e'),
backgroundColor: Colors.red,
// --- BERUBAH: Warna Notifikasi Error ---
backgroundColor: AppColors.error,
),
);
}
@ -136,11 +142,13 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
// --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
body: SafeArea(
child: _isLoading
? const Center(
child: CircularProgressIndicator(color: Colors.green))
// --- BERUBAH: Warna Loading Indicator ---
child: CircularProgressIndicator(color: AppColors.primary))
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20.0),
@ -150,13 +158,15 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
const Text(
'Pengaturan',
style: TextStyle(
color: Colors.white,
// --- BERUBAH: Warna Judul Utama ---
color: AppColors.textMain,
fontSize: 32,
fontWeight: FontWeight.bold),
),
const Text(
'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),
@ -188,15 +198,19 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Background Kartu Mode Operasi ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Menambahkan border halus agar serasi ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Mode Operasi',
style: TextStyle(
color: Colors.white,
// --- BERUBAH: Warna Judul Kartu Mode ---
color: AppColors.textMain,
fontWeight: FontWeight.bold,
fontSize: 16)),
const SizedBox(height: 10),
@ -206,12 +220,15 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
onChanged: (val) =>
setState(() => _operatingMode = val!),
title: const Text('Mode Otomatis',
style: TextStyle(color: Colors.white)),
activeColor: Colors.green,
// --- BERUBAH: Warna Teks Radio Button ---
style: TextStyle(color: AppColors.textMain)),
// --- BERUBAH: Warna Lingkaran Radio Button (Aktif) ---
activeColor: AppColors.primary,
subtitle: const Text(
'Kipas bekerja otomatis berdasarkan sensor',
// --- BERUBAH: Warna Sub-teks Radio Button ---
style: TextStyle(
color: Colors.grey, fontSize: 10),
color: AppColors.textSecondary, fontSize: 10),
),
),
RadioListTile<int>(
@ -220,12 +237,15 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
onChanged: (val) =>
setState(() => _operatingMode = val!),
title: const Text('Mode Manual',
style: TextStyle(color: Colors.white)),
activeColor: Colors.green,
// --- BERUBAH: Warna Teks Radio Button ---
style: TextStyle(color: AppColors.textMain)),
// --- BERUBAH: Warna Lingkaran Radio Button (Aktif) ---
activeColor: AppColors.primary,
subtitle: const Text(
'Kontrol penuh kipas melalui dashboard HP',
// --- BERUBAH: Warna Sub-teks Radio Button ---
style: TextStyle(
color: Colors.grey, fontSize: 10),
color: AppColors.textSecondary, fontSize: 10),
),
),
],
@ -240,7 +260,8 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
child: ElevatedButton(
onPressed: _saveSettings,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
// --- BERUBAH: Warna Latar Tombol Simpan ---
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
@ -250,7 +271,8 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white),
// Tetap putih agar tulisan dalam tombol kontras
color: Colors.white),
),
),
),
@ -268,31 +290,44 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Latar Belakang Kartu Input Sensor ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Menambahkan border halus agar serasi ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
color: Colors.white,
// --- BERUBAH: Warna Judul Input Sensor ---
color: AppColors.textMain,
fontSize: 12,
fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: controller,
keyboardType: TextInputType.number,
style: const TextStyle(color: Colors.white),
// --- BERUBAH: Warna Angka/Nilai Input Sensor ---
style: const TextStyle(color: AppColors.textMain),
decoration: InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Colors.grey),
// --- BERUBAH: Warna Teks Placeholder/Hint ---
hintStyle: const TextStyle(color: AppColors.textSecondary),
filled: true,
fillColor: Colors.black,
// --- BERUBAH: Warna Kolom Input Sensor ---
fillColor: AppColors.background,
isDense: true,
border: OutlineInputBorder(
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 '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@ -104,7 +105,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
content: Row(children: [
Icon(
isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white,
color: Colors.white, // Tetap putih untuk kontras
),
const SizedBox(width: 10),
Expanded(
@ -113,8 +114,9 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
color: Colors.white, fontWeight: FontWeight.bold)),
),
]),
// --- BERUBAH: Menggunakan warna notifikasi dari AppColors ---
backgroundColor:
isError ? Colors.red.shade700 : Colors.green.shade700,
isError ? AppColors.error : AppColors.success,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.all(16),
@ -126,12 +128,14 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
// --- BERUBAH: Background Utama ---
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
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),
),
),
@ -152,12 +156,14 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
const Text(
'Lupa Password?',
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 Text(
'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),
@ -165,18 +171,23 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Latar Belakang Form ---
color: AppColors.cardBg,
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(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white),
// --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email, color: Colors.green),
// --- BERUBAH: Warna Ikon ---
prefixIcon: Icon(Icons.email, color: AppColors.primary),
hintText: 'Email',
hintStyle: TextStyle(color: Colors.grey),
// --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none,
),
),
@ -190,8 +201,10 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
child: ElevatedButton(
onPressed: _isLoading ? null : _handleKirimOTP,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
disabledBackgroundColor: Colors.green.withOpacity(0.3),
// --- BERUBAH: Warna Tombol ---
backgroundColor: AppColors.primary,
// --- BERUBAH: Warna Tombol saat Loading ---
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
),
@ -211,7 +224,8 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
child: TextButton(
onPressed: () => Navigator.pop(context),
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: [
const CircleAvatar(
radius: 40,
backgroundColor: Color(0xFF1A1A1A),
child: Icon(Icons.mark_email_read, size: 40, color: Colors.green),
// --- BERUBAH: Latar Belakang Ikon ---
backgroundColor: AppColors.cardBg,
// --- BERUBAH: Warna Ikon ---
child: Icon(Icons.mark_email_read, size: 40, color: AppColors.primary),
),
const SizedBox(height: 24),
const Text(
'Cek Email Kamu',
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),
Text(
'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),
@ -245,9 +263,11 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Latar Belakang Kotak OTP ---
color: AppColors.cardBg,
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(
controller: _otpController,
@ -255,11 +275,14 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
maxLength: 6,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white, fontSize: 28, letterSpacing: 12),
// --- BERUBAH: Warna Teks Inputan ---
color: AppColors.textMain, fontSize: 28, letterSpacing: 12),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock_open, color: Colors.green),
// --- BERUBAH: Warna Ikon ---
prefixIcon: Icon(Icons.lock_open, color: AppColors.primary),
hintText: '000000',
hintStyle: TextStyle(color: Colors.grey, letterSpacing: 8),
// --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary, letterSpacing: 8),
border: InputBorder.none,
counterText: '', // sembunyikan counter maxLength
),
@ -274,8 +297,10 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
child: ElevatedButton(
onPressed: _isLoading ? null : _handleVerifikasiOTP,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
disabledBackgroundColor: Colors.green.withOpacity(0.3),
// --- BERUBAH: Warna Tombol ---
backgroundColor: AppColors.primary,
// --- BERUBAH: Warna Tombol saat Loading ---
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
),
@ -303,7 +328,8 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
}),
child: const Text(
'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:provider/provider.dart';
import '../services/mqtt_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
import 'monitoring_screen.dart';
import 'control_settings_screen.dart';
import 'recording_screen.dart';
@ -26,8 +27,15 @@ class _HomeScreenState extends State<HomeScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isSuccess ? Colors.green.withOpacity(0.8) : Colors.redAccent,
content: Text(
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),
),
);
@ -51,11 +59,15 @@ class _HomeScreenState extends State<HomeScreen> {
currentIndex: _currentIndex,
onTap: (index) => setState(() => _currentIndex = index),
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.black,
selectedItemColor: Colors.green,
unselectedItemColor: Colors.grey,
// --- BERUBAH: Latar Belakang Menu Navigasi Bawah ---
backgroundColor: AppColors.cardBg,
// --- BERUBAH: Warna Ikon yang Sedang Dipilih ---
selectedItemColor: AppColors.primary,
// --- BERUBAH: Warna Ikon yang Tidak Dipilih ---
unselectedItemColor: AppColors.textSecondary,
showSelectedLabels: false,
showUnselectedLabels: false,
elevation: 10, // Menambahkan sedikit bayangan agar terpisah dari body
items: const [
BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'Monitoring'),
BottomNavigationBarItem(icon: Icon(Icons.tune), label: 'Control'),
@ -65,4 +77,4 @@ class _HomeScreenState extends State<HomeScreen> {
),
);
}
}
}

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@ -64,20 +65,21 @@ class _LoginScreenState extends State<LoginScreen> {
SnackBar(
content: Row(
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),
Expanded(
child: Text(
pesan,
style: const TextStyle(
color: Colors.white,
color: Colors.white, // Tetap putih
fontWeight: FontWeight.bold,
),
),
),
],
),
backgroundColor: Colors.red.shade700,
// --- BERUBAH: Background SnackBar Error ---
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -91,70 +93,93 @@ class _LoginScreenState extends State<LoginScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
// --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 80.0),
child: Column(
children: [
const CircleAvatar(
CircleAvatar(
radius: 60,
backgroundColor: Color(0xFF1A1A1A),
child: Icon(Icons.coffee, size: 60, color: Colors.orange),
// --- BERUBAH: Latar belakang logo ---
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 Text(
'Selamat Datang',
style: TextStyle(
color: Colors.green,
// --- BERUBAH: Warna Judul ---
color: AppColors.primary,
fontSize: 32,
fontWeight: FontWeight.bold),
),
const Text(
'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),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Warna Kotak Form ---
color: AppColors.cardBg,
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(
children: [
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white),
// --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email, color: Colors.green),
// --- BERUBAH: Warna Ikon ---
prefixIcon: Icon(Icons.email, color: AppColors.primary),
hintText: 'Email',
hintStyle: TextStyle(color: Colors.grey),
// --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none,
),
),
const Divider(color: Colors.grey),
// --- BERUBAH: Garis Pemisah (Divider) ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
style: const TextStyle(color: Colors.white),
// --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: InputDecoration(
prefixIcon:
const Icon(Icons.lock, color: Colors.green),
// --- BERUBAH: Warna Ikon ---
prefixIcon: const Icon(Icons.lock, color: AppColors.primary),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
// --- BERUBAH: Warna Ikon Mata ---
color: AppColors.textSecondary,
),
onPressed: () => setState(
() => _obscurePassword = !_obscurePassword),
),
hintText: 'Password',
hintStyle: const TextStyle(color: Colors.grey),
// --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: const TextStyle(color: AppColors.textSecondary),
border: InputBorder.none,
),
),
@ -165,42 +190,64 @@ class _LoginScreenState extends State<LoginScreen> {
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
// --- BERUBAH: Latar Belakang Tombol ---
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: _isLoading
? const CircularProgressIndicator(
color: Colors.white)
color: Colors.white) // Tetap putih untuk kontras
: const Text(
'Masuk ke Akun',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white),
color: Colors.white), // Tetap putih
),
),
),
],
),
),
// Lupa Password dan Daftar rapat tanpa jarak besar, warna sama hijau
const SizedBox(height: 10),
const SizedBox(height: 16),
// 1. TOMBOL LUPA PASSWORD
TextButton(
onPressed: () =>
Navigator.pushNamed(context, '/forgot_password'),
onPressed: () => Navigator.pushNamed(context, '/forgot_password'),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
'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(
onPressed: () => Navigator.pushNamed(context, '/register'),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
'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 '../services/mqtt_service.dart';
import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class MonitoringScreen extends StatefulWidget {
final VoidCallback? onProfileTap;
@ -60,19 +61,21 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
});
return Scaffold(
backgroundColor: Colors.black,
// --- BERUBAH: Background Utama ---
backgroundColor: AppColors.background,
body: SafeArea(
child: _isInitialLoading
? const Center(child: CircularProgressIndicator(color: Colors.green))
// --- BERUBAH: Warna Loading Indicator ---
? const Center(child: CircularProgressIndicator(color: AppColors.primary))
: RefreshIndicator(
onRefresh: _initData,
color: Colors.green,
// --- BERUBAH: Warna Refresh Indicator ---
color: AppColors.primary,
child: LayoutBuilder(
builder: (context, constraints) {
// Tinggi tersedia = tinggi layar dikurangi header
const headerHeight = 175.0; // header + timestamp + mode badge + padding
const gaps = 15.0 * 2; // 2 gap antar 3 baris
const padding = 20.0 * 2; // padding atas bawah
const headerHeight = 175.0;
const gaps = 15.0 * 2;
const padding = 20.0 * 2;
final available = constraints.maxHeight - headerHeight - gaps - padding;
final cardH = (available / 3).clamp(100.0, 180.0);
@ -94,11 +97,13 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [
const Text('Monitoring',
style: TextStyle(
color: Colors.white,
// --- BERUBAH: Warna Judul ---
color: AppColors.textMain,
fontSize: 32,
fontWeight: FontWeight.bold)),
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),
Row(
children: [
@ -106,10 +111,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
width: 8, height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
// --- BERUBAH: Indikator Titik Koneksi ---
color: mqtt.client?.connectionStatus?.state ==
MqttConnectionState.connected
? Colors.green
: Colors.red,
? AppColors.success
: AppColors.error,
),
),
const SizedBox(width: 8),
@ -119,11 +125,13 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
? 'Connected'
: 'Disconnected',
style: TextStyle(
// --- BERUBAH: Warna Teks Koneksi ---
color: mqtt.client?.connectionStatus?.state ==
MqttConnectionState.connected
? Colors.green
: Colors.red,
? AppColors.success
: AppColors.error,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
@ -133,9 +141,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
GestureDetector(
onTap: widget.onProfileTap,
child: const CircleAvatar(
backgroundColor: Color(0xFF1A1A1A),
// --- BERUBAH: Latar Belakang Avatar Profil ---
backgroundColor: AppColors.cardBg,
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
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),
// Mode Badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
// --- BERUBAH: Latar Belakang Transparan Badge ---
color: mqtt.mode == 'manual'
? Colors.orange.withOpacity(0.2)
: Colors.green.withOpacity(0.2),
? AppColors.warning.withValues(alpha: 0.2)
: AppColors.success.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
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(
'Mode: ${mqtt.mode.toUpperCase()}',
style: TextStyle(
color: mqtt.mode == 'manual' ? Colors.orange : Colors.green,
// --- BERUBAH: Warna Teks Badge ---
color: mqtt.mode == 'manual' ? AppColors.warning : AppColors.success,
fontSize: 12,
fontWeight: FontWeight.bold,
),
@ -183,11 +197,12 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
suffix: '°C',
dec: 1,
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,
isLow: curSuhu < mqtt.minSuhu,
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),
@ -199,11 +214,12 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
suffix: '%',
dec: 1,
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,
isLow: curHum < mqtt.minRh,
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',
dec: 1,
icon: Icons.wb_sunny,
color: Colors.orange,
// --- BERUBAH: Warna Tema Kartu Cahaya ---
color: AppColors.warning,
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),
@ -261,19 +278,23 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
margin: const EdgeInsets.only(top: 15),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.2),
// --- BERUBAH: Background Peringatan ---
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.red),
// --- BERUBAH: Garis Pinggir Peringatan ---
border: Border.all(color: AppColors.error),
),
child: const Row(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.red),
// --- BERUBAH: Ikon Peringatan ---
Icon(Icons.warning_amber_rounded, color: AppColors.error),
SizedBox(width: 10),
Expanded(
child: Text(
'Peringatan: Parameter pengeringan melewati batas aman!',
style: TextStyle(
color: Colors.red,
// --- BERUBAH: Teks Peringatan ---
color: AppColors.error,
fontWeight: FontWeight.bold,
fontSize: 12),
),
@ -286,19 +307,23 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
margin: const EdgeInsets.only(top: 15),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.2),
// --- BERUBAH: Background Info/Low ---
color: AppColors.info.withValues(alpha: 0.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(
children: [
Icon(Icons.info_outline, color: Colors.blue),
// --- BERUBAH: Ikon Info/Low ---
Icon(Icons.info_outline, color: AppColors.info),
SizedBox(width: 10),
Expanded(
child: Text(
'Info: Parameter pengeringan di bawah batas minimum!',
style: TextStyle(
color: Colors.blue,
// --- BERUBAH: Teks Info/Low ---
color: AppColors.info,
fontWeight: FontWeight.bold,
fontSize: 12),
),
@ -337,16 +362,23 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
height: double.infinity,
padding: const EdgeInsets.all(12),
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),
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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
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),
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: prev, end: cur),
@ -357,7 +389,8 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
child: Text(
'${value.toStringAsFixed(dec)}$suffix',
style: const TextStyle(
color: Colors.white,
// --- BERUBAH: Warna Angka/Nilai Sensor ---
color: AppColors.textMain,
fontSize: 22,
fontWeight: FontWeight.bold),
),
@ -380,8 +413,10 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
height: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Kartu Latar Putih Bersih & Garis Halus ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -389,13 +424,15 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [
Row(
children: [
// --- BERUBAH: Warna Ikon Kontroler ---
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),
Expanded(
// --- BERUBAH: Warna Judul Kartu Kontrol ---
child: Text(title,
style: const TextStyle(
color: Colors.white,
color: AppColors.textMain,
fontSize: 11,
fontWeight: FontWeight.bold)),
),
@ -404,16 +441,18 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- BERUBAH: Warna Status Mode ---
Text('Mode: ${mqtt.mode.toUpperCase()}',
style: const TextStyle(
color: Colors.green, fontSize: 9, fontWeight: FontWeight.bold)),
color: AppColors.primary, fontSize: 9, fontWeight: FontWeight.bold)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isOn ? 'ON' : 'OFF',
style: TextStyle(
color: isOn ? Colors.green : Colors.grey,
// --- BERUBAH: Warna Status ON/OFF ---
color: isOn ? AppColors.primary : AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold),
),
@ -422,7 +461,8 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
child: Switch(
value: isOn,
onChanged: mqtt.mode == 'manual' ? onChanged : null,
activeColor: Colors.green,
// --- BERUBAH: Warna Switch ---
activeColor: AppColors.primary,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../services/supabase_service.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@ -89,7 +90,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
children: [
Icon(
isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white,
color: Colors.white, // Tetap putih agar kontras dengan background
),
const SizedBox(width: 10),
Expanded(
@ -101,8 +102,9 @@ class _RegisterScreenState extends State<RegisterScreen> {
),
],
),
// --- BERUBAH: Menggunakan warna notifikasi dari AppColors ---
backgroundColor:
isError ? Colors.red.shade700 : Colors.green.shade700,
isError ? AppColors.error : AppColors.success,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -116,12 +118,14 @@ class _RegisterScreenState extends State<RegisterScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
// --- BERUBAH: Background Scaffold Utama ---
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
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),
),
),
@ -134,75 +138,91 @@ class _RegisterScreenState extends State<RegisterScreen> {
const Text(
'Buat Akun Baru',
style: TextStyle(
color: Colors.green,
// --- BERUBAH: Warna Judul ---
color: AppColors.primary,
fontSize: 32,
fontWeight: FontWeight.bold),
),
const Text(
'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),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Latar Belakang Form ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(25),
// --- BERUBAH: Garis Pinggir Form ---
border:
Border.all(color: Colors.grey.withOpacity(0.3)),
Border.all(color: AppColors.textSecondary.withValues(alpha: 0.3)),
),
child: Column(
children: [
// Nama Lengkap
TextField(
controller: _nameController,
style: const TextStyle(color: Colors.white),
// --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
// --- BERUBAH: Warna Ikon ---
prefixIcon:
Icon(Icons.person, color: Colors.green),
Icon(Icons.person, color: AppColors.primary),
hintText: 'Nama Lengkap',
hintStyle: TextStyle(color: Colors.grey),
// --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none,
),
),
const Divider(color: Colors.grey),
// --- BERUBAH: Garis Pemisah (Divider) ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
// Email
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white),
// --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: const InputDecoration(
// --- BERUBAH: Warna Ikon ---
prefixIcon:
Icon(Icons.email, color: Colors.green),
Icon(Icons.email, color: AppColors.primary),
hintText: 'Email',
hintStyle: TextStyle(color: Colors.grey),
// --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: TextStyle(color: AppColors.textSecondary),
border: InputBorder.none,
),
),
const Divider(color: Colors.grey),
// --- BERUBAH: Garis Pemisah (Divider) ---
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
// Password
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
style: const TextStyle(color: Colors.white),
// --- BERUBAH: Warna Teks Inputan ---
style: const TextStyle(color: AppColors.textMain),
decoration: InputDecoration(
// --- BERUBAH: Warna Ikon Kunci ---
prefixIcon:
const Icon(Icons.lock, color: Colors.green),
const Icon(Icons.lock, color: AppColors.primary),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
color: Colors.grey,
// --- BERUBAH: Warna Ikon Mata ---
color: AppColors.textSecondary,
),
onPressed: () => setState(
() => _obscurePassword = !_obscurePassword),
),
hintText: 'Password (min. 6 karakter)',
hintStyle: const TextStyle(color: Colors.grey),
// --- BERUBAH: Warna Placeholder/Hint ---
hintStyle: const TextStyle(color: AppColors.textSecondary),
border: InputBorder.none,
),
),
@ -215,9 +235,11 @@ class _RegisterScreenState extends State<RegisterScreen> {
child: ElevatedButton(
onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
// --- BERUBAH: Warna Tombol ---
backgroundColor: AppColors.primary,
// --- BERUBAH: Warna Tombol saat Nonaktif (Loading) ---
disabledBackgroundColor:
Colors.green.withOpacity(0.3),
AppColors.primary.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
@ -243,7 +265,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
onPressed: () => Navigator.pop(context),
child: const Text(
'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 'package:flutter/material.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:path_provider/path_provider.dart';
import '../services/supabase_service.dart';
import '../services/mqtt_service.dart';
import 'package:mqtt_client/mqtt_client.dart';
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
class SettingsScreen extends StatefulWidget {
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 {
setState(() => _isExporting = true);
@ -51,9 +52,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (data.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Belum ada data dataset untuk diexport!'),
backgroundColor: Colors.orange,
SnackBar(
content: const Text('Belum ada data dataset untuk diexport!'),
// --- BERUBAH: Warna Notifikasi Peringatan ---
backgroundColor: AppColors.warning,
),
);
}
@ -71,12 +73,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
'Suhu (°C)',
'Kelembapan (%)',
'Intensitas Cahaya (Lux)',
'Kipas 1 (Exhaust)',
'Kipas 2 (Intake)',
'URL Foto',
'Kipas 1 (Intake)',
'Kipas 2 (Exhaust)',
];
// Style header
// Style header (Dibiarkan tetap hijau standar Excel)
for (int i = 0; i < headers.length; i++) {
final cell = sheet.cell(
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++) {
final row = data[i];
final rowIndex = i + 1;
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 0, rowIndex: rowIndex))
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: rowIndex))
.value = IntCellValue(i + 1);
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 1, rowIndex: rowIndex))
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: rowIndex))
.value = TextCellValue(row['timestamp']?.toString() ?? '-');
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 2, rowIndex: rowIndex))
.value = DoubleCellValue(
double.tryParse(row['suhu'].toString()) ?? 0);
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: rowIndex))
.value = DoubleCellValue(double.tryParse(row['suhu'].toString()) ?? 0);
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 3, rowIndex: rowIndex))
.value = DoubleCellValue(
double.tryParse(row['kelembapan'].toString()) ?? 0);
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: rowIndex))
.value = DoubleCellValue(double.tryParse(row['kelembapan'].toString()) ?? 0);
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 4, rowIndex: rowIndex))
.value = DoubleCellValue(
double.tryParse(row['intensitas'].toString()) ?? 0);
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: rowIndex))
.value = DoubleCellValue(double.tryParse(row['intensitas'].toString()) ?? 0);
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 5, rowIndex: rowIndex))
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: rowIndex))
.value = TextCellValue(row['kipas1']?.toString() ?? '-');
sheet
.cell(CellIndex.indexByColumnRow(
columnIndex: 6, rowIndex: rowIndex))
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 6, rowIndex: rowIndex))
.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(1, 22);
sheet.setColumnWidth(2, 12);
@ -145,7 +124,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
sheet.setColumnWidth(4, 24);
sheet.setColumnWidth(5, 18);
sheet.setColumnWidth(6, 18);
sheet.setColumnWidth(7, 50);
// Simpan file
final dir = await getApplicationDocumentsDirectory();
@ -170,7 +148,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
SnackBar(
content: Text(
'✓ 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(
SnackBar(
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;
return Scaffold(
backgroundColor: Colors.black,
// --- BERUBAH: Background UI Utama ---
backgroundColor: AppColors.background,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
@ -206,14 +187,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Background Kartu Profil ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Garis pinggir halus ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
),
child: Row(
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.green,
// --- BERUBAH: Warna Avatar ---
backgroundColor: AppColors.primary,
child: Icon(Icons.person,
color: Colors.white, size: 40),
),
@ -226,8 +211,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
width: 100,
height: 16,
child: LinearProgressIndicator(
color: Colors.green,
backgroundColor: Colors.grey,
// --- BERUBAH: Warna Loading ---
color: AppColors.primary,
backgroundColor: AppColors.background,
),
)
: Text(
@ -235,7 +221,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
? _namaLengkap
: 'Admin Kopi',
style: const TextStyle(
color: Colors.white,
// --- BERUBAH: Warna Nama Profil ---
color: AppColors.textMain,
fontSize: 18,
fontWeight: FontWeight.bold,
),
@ -244,12 +231,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
Text(
user?.email ?? 'admin@coffee.io',
style: const TextStyle(
color: Colors.grey, fontSize: 12),
// --- BERUBAH: Warna Email Profil ---
color: AppColors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Text(
'Status: Active User',
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([
_buildConnectionStatus(
'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(
'MQTT Broker (EMQX)',
mqtt.client?.connectionStatus?.state ==
@ -284,12 +280,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
width: 22,
height: 22,
child: CircularProgressIndicator(
color: Colors.green, strokeWidth: 2),
// --- BERUBAH: Warna Loading ---
color: AppColors.primary, strokeWidth: 2),
),
title: Text(
'Menyiapkan file Excel...',
style: TextStyle(
color: Colors.grey, fontSize: 14),
// --- BERUBAH: Warna Teks Loading ---
color: AppColors.textSecondary, fontSize: 14),
),
)
: _buildSettingItem(
@ -303,14 +301,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Keamanan
_buildSectionTitle('Keamanan'),
_buildSettingItem(
Icons.lock_reset, 'Ganti Password Akun', () {
Navigator.pushNamed(
context,
'/change_password',
arguments: {'dari_reset': false},
);
}),
_buildSettingsCard([
_buildSettingItem(
Icons.lock_reset, 'Ganti Password Akun', () {
Navigator.pushNamed(
context,
'/change_password',
arguments: {'dari_reset': false},
);
}),
]),
const SizedBox(height: 30),
@ -319,25 +319,70 @@ class _SettingsScreenState extends State<SettingsScreen> {
width: double.infinity,
height: 55,
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();
if (context.mounted) {
Navigator.pushReplacementNamed(context, '/login');
}
},
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent.withOpacity(0.8),
// --- BERUBAH: Warna Latar Tombol Keluar ---
backgroundColor: AppColors.error.withValues(alpha: 0.8),
shape: RoundedRectangleBorder(
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(
'Keluar dari Akun',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
color: Colors.white), // Tetap putih
),
),
),
@ -356,7 +401,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Text(
title,
style: const TextStyle(
color: Colors.grey,
// --- BERUBAH: Warna Judul Seksi/Kategori ---
color: AppColors.textSecondary,
fontSize: 13,
fontWeight: FontWeight.bold),
),
@ -366,8 +412,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _buildSettingsCard(List<Widget> children) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
// --- BERUBAH: Latar Belakang Kartu Pengaturan ---
color: AppColors.cardBg,
borderRadius: BorderRadius.circular(20),
// --- BERUBAH: Garis pinggir halus ---
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
),
child: Column(children: children),
);
@ -376,13 +425,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _buildSettingItem(IconData icon, String title, VoidCallback? onTap,
{String? trailing}) {
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,
style: const TextStyle(color: Colors.white, fontSize: 14)),
// --- BERUBAH: Warna Teks Menu ---
style: const TextStyle(color: AppColors.textMain, fontSize: 14)),
trailing: trailing != null
? Text(trailing,
style: const TextStyle(color: Colors.grey, fontSize: 12))
: const Icon(Icons.chevron_right, color: Colors.grey, size: 18),
// --- BERUBAH: Warna Teks Trailing ---
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,
);
}
@ -390,10 +443,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _buildConnectionStatus(
String title, bool isConnected, IconData icon) {
return ListTile(
// --- BERUBAH: Warna Ikon Status Database/MQTT ---
leading: Icon(icon,
color: isConnected ? Colors.blue : Colors.red, size: 22),
color: isConnected ? AppColors.info : AppColors.error, size: 22),
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(
mainAxisSize: MainAxisSize.min,
children: [
@ -402,14 +457,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isConnected ? Colors.green : Colors.red,
// --- BERUBAH: Warna Titik Indikator ---
color: isConnected ? AppColors.success : AppColors.error,
),
),
const SizedBox(width: 8),
Text(
isConnected ? 'Online' : 'Offline',
style: TextStyle(
color: isConnected ? Colors.green : Colors.red,
// --- BERUBAH: Warna Teks Online/Offline ---
color: isConnected ? AppColors.success : AppColors.error,
fontSize: 12,
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
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
app_links:
dependency: transitive
description:
@ -89,6 +97,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -137,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@ -230,6 +262,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct dev"
description:
@ -238,6 +278,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct dev"
description: flutter
@ -264,6 +312,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -288,6 +344,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.3"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http:
dependency: "direct main"
description:
@ -336,6 +400,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -853,6 +925,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

@ -43,6 +43,7 @@ dependencies:
excel: ^4.0.3
open_file: ^3.3.2
path_provider: ^2.1.2
google_fonts: ^6.2.1
dependency_overrides:
win32: ^6.1.0
@ -51,27 +52,38 @@ dependency_overrides:
dev_dependencies:
flutter_test:
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
# 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
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
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
# 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_ham.jpeg

View File

@ -1,6 +1,4 @@
<!DOCTYPE html>
<html>
<head>
<!DOCTYPE html><html><head>
<!--
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.
@ -27,12 +25,90 @@
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="icon" type="image/png" href="favicon.png">
<title>coffee_iot_flutter</title>
<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>
<body>
<!--
You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader
@ -41,6 +117,7 @@
For more details:
* https://docs.flutter.dev/platform-integration/web/initialization
-->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
<script src="flutter_bootstrap.js" async=""></script>
</body></html>