Update fitur, assets, dan perbaikan tampilan
|
|
@ -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
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 69 B |
|
|
@ -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>
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 69 B |
|
|
@ -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>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 41 KiB |
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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> {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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> {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
80
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
26
pubspec.yaml
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||