From b67e8da37b2adb34c29db045cf6a87e00296b5f1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 29 Jun 2026 09:25:52 +0700 Subject: [PATCH] new UI + app renamed to SIAB --- android/app/src/main/AndroidManifest.xml | 2 +- android/build.gradle | 2 +- android/gradle.properties | 4 + ios/Runner/Info.plist | 2 +- lib/main.dart | 73 +- lib/screens/devices_screen.dart | 2 +- lib/screens/main_shell.dart | 177 ++++- lib/screens/timbang_screen.dart | 901 ++++++++++++++++------- pubspec.lock | 20 +- 9 files changed, 913 insertions(+), 270 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 000bf80..e28ce59 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Siab Bluetooth App + SIAB CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/lib/main.dart b/lib/main.dart index a6d5bb8..2a838e0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,14 +12,83 @@ class SIABApp extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = ColorScheme.fromSeed( + seedColor: const Color(0xFF2563EB), + brightness: Brightness.light, + ); + return ChangeNotifierProvider( create: (_) => BLEService(), child: MaterialApp( - title: 'SIAB Bluetooth', + title: 'SIAB', theme: ThemeData( - primarySwatch: Colors.blue, + colorScheme: colorScheme, + scaffoldBackgroundColor: const Color(0xFFF4F8FF), useMaterial3: true, + appBarTheme: const AppBarTheme( + centerTitle: false, + backgroundColor: Color(0xFFF8FBFF), + foregroundColor: Color(0xFF0F172A), + elevation: 0, + surfaceTintColor: Colors.transparent, + ), + cardTheme: CardThemeData( + color: Colors.white, + elevation: 0, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFF2563EB), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2563EB), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF2563EB), + side: const BorderSide(color: Color(0x663B82F6)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF2563EB), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + ), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: const Color(0xFF0F172A), + contentTextStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), ), + themeMode: ThemeMode.light, home: const MainShell(), debugShowCheckedModeBanner: false, ), diff --git a/lib/screens/devices_screen.dart b/lib/screens/devices_screen.dart index dfb8538..3760126 100644 --- a/lib/screens/devices_screen.dart +++ b/lib/screens/devices_screen.dart @@ -117,7 +117,7 @@ class _DevicesScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Timbangan'), + title: const Text('TIMBANGAN SIAB'), actions: [ IconButton( onPressed: _loading ? null : _loadSaved, diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index a632a19..7bdfc6a 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -1,4 +1,7 @@ +import 'dart:ui' show ImageFilter; + import 'package:flutter/material.dart'; + import 'devices_screen.dart'; import 'timbang_screen.dart'; @@ -21,6 +24,7 @@ class _MainShellState extends State { @override Widget build(BuildContext context) { return Scaffold( + extendBody: true, body: IndexedStack( index: _index, children: const [ @@ -28,15 +32,174 @@ class _MainShellState extends State { TimbangScreen(), ], ), - bottomNavigationBar: BottomNavigationBar( + bottomNavigationBar: _FloatingGlassNavigationBar( currentIndex: _index, + tabs: _tabs, onTap: (i) => setState(() => _index = i), - items: _tabs - .map((t) => BottomNavigationBarItem( - icon: Icon(t.icon), - label: t.label, - )) - .toList(), + ), + ); + } +} + +class _FloatingGlassNavigationBar extends StatelessWidget { + const _FloatingGlassNavigationBar({ + required this.currentIndex, + required this.tabs, + required this.onTap, + }); + + final int currentIndex; + final List<({String label, IconData icon})> tabs; + final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + return SafeArea( + minimum: const EdgeInsets.fromLTRB(20, 0, 20, 18), + child: ClipRRect( + borderRadius: BorderRadius.circular(32), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), + child: Container( + height: 72, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withValues(alpha: 0.84), + Colors.white.withValues(alpha: 0.62), + ], + ), + borderRadius: BorderRadius.circular(32), + border: Border.all(color: Colors.white.withValues(alpha: 0.9)), + boxShadow: [ + BoxShadow( + color: const Color(0xFF0F172A).withValues(alpha: 0.13), + blurRadius: 34, + offset: const Offset(0, 18), + ), + BoxShadow( + color: const Color(0xFF38BDF8).withValues(alpha: 0.1), + blurRadius: 24, + offset: const Offset(0, -6), + ), + ], + ), + child: Row( + children: [ + for (var i = 0; i < tabs.length; i++) + Expanded( + child: _GlassNavigationItem( + icon: tabs[i].icon, + label: tabs[i].label, + isSelected: i == currentIndex, + onTap: () => onTap(i), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _GlassNavigationItem extends StatelessWidget { + const _GlassNavigationItem({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); + + final IconData icon; + final String label; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + const activeColor = Color(0xFF2563EB); + const inactiveColor = Color(0xFF64748B); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(26), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + alignment: Alignment.center, + margin: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.symmetric(horizontal: 10), + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withValues(alpha: 0.95), + activeColor.withValues(alpha: 0.12), + ], + ) + : null, + color: isSelected ? null : Colors.transparent, + borderRadius: BorderRadius.circular(26), + border: Border.all( + color: isSelected + ? Colors.white.withValues(alpha: 0.96) + : Colors.transparent, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: activeColor.withValues(alpha: 0.16), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ] + : null, + ), + child: AnimatedScale( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + scale: isSelected ? 1 : 0.96, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: isSelected ? activeColor : inactiveColor, + size: 22, + ), + AnimatedSize( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + child: isSelected + ? Row( + children: [ + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: Color(0xFF2563EB), + fontSize: 13, + fontWeight: FontWeight.w900, + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), ), ); } diff --git a/lib/screens/timbang_screen.dart b/lib/screens/timbang_screen.dart index 00a5e57..8d5eea1 100644 --- a/lib/screens/timbang_screen.dart +++ b/lib/screens/timbang_screen.dart @@ -1,263 +1,85 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:path/path.dart' as p; + import '../database/siab_database.dart'; import '../services/ble_service.dart'; -/// Screen that shows the current timbang (weight) value on a scale-style -/// display matching the SIAB scale design (dark frame, glowing red digits). +/// Screen that shows the current timbang (weight) value on a light, +/// futuristic dashboard. /// When connected, title shows "TIMBANGAN [label] SIAB" from registered device. class TimbangScreen extends StatelessWidget { const TimbangScreen({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - body: SafeArea( - child: Consumer( + final timbangTheme = Theme.of(context).copyWith( + brightness: Brightness.light, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF2563EB), + brightness: Brightness.light, + ), + scaffoldBackgroundColor: const Color(0xFFF4F8FF), + ); + + return Theme( + data: timbangTheme, + child: Scaffold( + backgroundColor: const Color(0xFFF4F8FF), + body: Consumer( builder: (context, ble, _) { final weightKg = ble.lastWeightKg; - final displayValue = weightKg != null - ? weightKg.toStringAsFixed(1) - : '0.0'; + final displayValue = + weightKg != null ? weightKg.toStringAsFixed(1) : '0.0'; final isConnected = ble.isConnected; final address = ble.deviceAddress; - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Outer frame with decorative corners - Stack( + return _TimbangBackground( + child: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 136), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height - + MediaQuery.paddingOf(context).vertical - + 160, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Outer dark grey/blue frame - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: const Color(0xFF2A2A3A), // Dark grey/blue - borderRadius: BorderRadius.circular(16), - ), - child: Stack( - children: [ - // Inner border (lighter grey/blue line) - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFF4A4A5A), - width: 2, - ), - ), - child: // Deep black display area - Container( - padding: const EdgeInsets.symmetric( - vertical: 40, - horizontal: 24, - ), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(10), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Large glowing red weight value - Text( - displayValue, - style: TextStyle( - fontSize: 72, - fontWeight: FontWeight.w400, - color: Colors.red.shade400, - fontFamily: 'monospace', - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - shadows: [ - Shadow( - color: Colors.red.shade400 - .withOpacity(0.8), - blurRadius: 20, - ), - Shadow( - color: Colors.red.shade400 - .withOpacity(0.5), - blurRadius: 40, - ), - ], - ), - ), - const SizedBox(height: 8), - // Smaller glowing red "kg" unit - Text( - 'kg', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Colors.red.shade400, - fontFamily: 'monospace', - shadows: [ - Shadow( - color: Colors.red.shade400 - .withOpacity(0.8), - blurRadius: 15, - ), - Shadow( - color: Colors.red.shade400 - .withOpacity(0.5), - blurRadius: 30, - ), - ], - ), - ), - ], - ), - ), - ), - // Corner decorative elements (rivets/screws) - Positioned( - top: 2, - left: 2, - child: _CornerRivet(), - ), - Positioned( - top: 2, - right: 2, - child: _CornerRivet(), - ), - Positioned( - bottom: 2, - left: 2, - child: _CornerRivet(), - ), - Positioned( - bottom: 2, - right: 2, - child: _CornerRivet(), - ), - ], - ), + FutureBuilder( + future: address != null + ? SiabDatabase() + .getByUuid(address) + .then((d) => d?.label ?? '') + : Future.value(''), + builder: (context, snapshot) { + final label = snapshot.data?.trim() ?? ''; + final title = isConnected && label.isNotEmpty + ? 'TIMBANGAN $label SIAB' + : 'TIMBANGAN SIAB'; + + return _HeaderSection( + title: title, + isConnected: isConnected, + ); + }, + ), + const SizedBox(height: 24), + _WeightDisplayCard( + displayValue: displayValue, + isConnected: isConnected, + ), + const SizedBox(height: 20), + _ActionPanel( + ble: ble, + isConnected: isConnected, ), ], ), - const SizedBox(height: 24), - // "TIMBANGAN [label] SIAB" when connected, else "TIMBANGAN SIAB" - FutureBuilder( - future: address != null - ? SiabDatabase().getByUuid(address).then((d) => d?.label ?? '') - : Future.value(''), - builder: (context, snapshot) { - final label = snapshot.data?.trim() ?? ''; - final title = isConnected && label.isNotEmpty - ? 'TIMBANGAN $label SIAB' - : 'TIMBANGAN SIAB'; - return Text( - title, - style: TextStyle( - fontSize: 16, - color: Colors.grey.shade400, - fontWeight: FontWeight.w300, - letterSpacing: 1.2, - ), - ); - }, - ), - const SizedBox(height: 16), - // Connection status + actions - if (!isConnected) - Text( - 'Connect scale in Devices tab', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ) - else ...[ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.bluetooth_connected, - size: 20, - color: Colors.green.shade300, - ), - const SizedBox(width: 12), - TextButton.icon( - onPressed: () async { - await ble.disconnect(); - }, - icon: const Icon(Icons.power_settings_new, size: 18), - label: const Text('Disconnect'), - style: TextButton.styleFrom( - foregroundColor: Colors.red.shade300, - ), - ), - ], - ), - const SizedBox(height: 12), - ElevatedButton.icon( - onPressed: ble.hasStableWeight - ? () async { - final ok = await ble.saveStableMeasurement(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(ok - ? 'Stable weight saved to file' - : 'No stable value to save yet'), - ), - ); - } - : null, - icon: const Icon(Icons.save), - label: const Text('Save stable value'), - ), - const SizedBox(height: 8), - OutlinedButton.icon( - onPressed: () async { - try { - final file = await ble.getTodayMeasurementFile(); - final exists = await file.exists(); - if (!exists || await file.length() == 0) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'No measurements file for today yet. Save a value first.', - ), - ), - ); - return; - } - - await Share.shareXFiles( - [ - XFile( - file.path, - mimeType: 'text/csv', - name: p.basename(file.path), - ), - ], - subject: 'SIAB Laporan hasil Timbang ', - text: - 'SIAB Laporan hasil Timbang harian.', - ); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('gagal mengirim file: $e'), - ), - ); - } - }, - icon: const Icon(Icons.email), - label: const Text('email terkirim'), - ), - ], - ], + ), ), ), ); @@ -268,24 +90,609 @@ class TimbangScreen extends StatelessWidget { } } -/// Decorative corner rivet/screw element -class _CornerRivet extends StatelessWidget { +class _TimbangBackground extends StatelessWidget { + const _TimbangBackground({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFFFFF), + Color(0xFFEFF8FF), + Color(0xFFF7FBFF), + Color(0xFFEAF1FF), + ], + ), + ), + child: Stack( + children: [ + const Positioned( + top: -90, + right: -80, + child: _GlowOrb( + size: 220, + color: Color(0x663AB7FF), + ), + ), + const Positioned( + bottom: 80, + left: -100, + child: _GlowOrb( + size: 240, + color: Color(0x553D5AFE), + ), + ), + const Positioned( + top: 120, + left: 28, + right: 28, + child: _LightBeam(), + ), + const Positioned( + bottom: 188, + right: 32, + child: _SignalDash(), + ), + Positioned.fill(child: child), + ], + ), + ); + } +} + +class _LightBeam extends StatelessWidget { + const _LightBeam(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999), + gradient: LinearGradient( + colors: [ + Colors.transparent, + const Color(0xFF38BDF8).withValues(alpha: 0.18), + const Color(0xFF6366F1).withValues(alpha: 0.12), + Colors.transparent, + ], + ), + ), + child: const SizedBox(height: 2), + ); + } +} + +class _SignalDash extends StatelessWidget { + const _SignalDash(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.58), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: Colors.white.withValues(alpha: 0.82)), + boxShadow: [ + BoxShadow( + color: const Color(0xFF2563EB).withValues(alpha: 0.12), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: const SizedBox(width: 72, height: 8), + ); + } +} + +class _GlowOrb extends StatelessWidget { + const _GlowOrb({ + required this.size, + required this.color, + }); + + final double size; + final Color color; + @override Widget build(BuildContext context) { return Container( - width: 8, - height: 8, + width: size, + height: size, decoration: BoxDecoration( - color: Colors.grey.shade500, shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 2, - offset: const Offset(0, 1), + color: color, + blurRadius: 80, + spreadRadius: 40, ), ], ), ); } } + +class _HeaderSection extends StatelessWidget { + const _HeaderSection({ + required this.title, + required this.isConnected, + }); + + final String title; + final bool isConnected; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.68), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: Colors.white.withValues(alpha: 0.88)), + ), + child: const Text( + 'LIVE WEIGHING CONSOLE', + style: TextStyle( + color: Color(0xFF2563EB), + fontSize: 11, + fontWeight: FontWeight.w800, + letterSpacing: 1.2, + ), + ), + ), + const SizedBox(height: 12), + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + color: Color(0xFF1B2B4A), + fontSize: 17, + fontWeight: FontWeight.w800, + letterSpacing: 1.4, + ), + ), + const SizedBox(height: 12), + _StatusBadge(isConnected: isConnected), + ], + ); + } +} + +class _StatusBadge extends StatelessWidget { + const _StatusBadge({required this.isConnected}); + + final bool isConnected; + + @override + Widget build(BuildContext context) { + final color = + isConnected ? const Color(0xFF00A878) : const Color(0xFFE45555); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.78), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.28)), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.12), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isConnected ? Icons.bluetooth_connected : Icons.bluetooth_disabled, + color: color, + size: 18, + ), + const SizedBox(width: 8), + Text( + isConnected ? 'Scale connected' : 'Connect scale in Devices tab', + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ); + } +} + +class _WeightDisplayCard extends StatelessWidget { + const _WeightDisplayCard({ + required this.displayValue, + required this.isConnected, + }); + + final String displayValue; + final bool isConnected; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFFFFF), + Color(0xFFE2F4FF), + Color(0xFFF8FBFF), + ], + ), + borderRadius: BorderRadius.circular(34), + boxShadow: [ + BoxShadow( + color: const Color(0xFF2563EB).withValues(alpha: 0.16), + blurRadius: 42, + offset: const Offset(0, 20), + ), + BoxShadow( + color: const Color(0xFF22D3EE).withValues(alpha: 0.12), + blurRadius: 34, + offset: const Offset(0, -8), + ), + ], + ), + child: LayoutBuilder( + builder: (context, constraints) { + final isCompact = constraints.maxWidth < 340; + + return Container( + padding: EdgeInsets.symmetric( + horizontal: isCompact ? 18 : 24, + vertical: isCompact ? 28 : 36, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withValues(alpha: 0.9)), + ), + child: Column( + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.spaceBetween, + children: [ + const _MetricChip( + icon: Icons.speed, + label: 'Live load', + color: Color(0xFF2563EB), + ), + _MetricChip( + icon: isConnected ? Icons.sensors : Icons.sensors_off, + label: isConnected ? 'Streaming' : 'Idle', + color: isConnected + ? const Color(0xFF00A878) + : const Color(0xFFE45555), + ), + ], + ), + SizedBox(height: isCompact ? 22 : 30), + const Text( + 'CURRENT WEIGHT', + style: TextStyle( + color: Color(0xFF64748B), + fontSize: 11, + fontWeight: FontWeight.w900, + letterSpacing: 2.0, + ), + ), + const SizedBox(height: 8), + FittedBox( + fit: BoxFit.scaleDown, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + displayValue, + style: TextStyle( + color: const Color(0xFF0F1B33), + fontSize: isCompact ? 78 : 94, + height: 0.9, + fontWeight: FontWeight.w900, + letterSpacing: -4, + fontFeatures: const [FontFeature.tabularFigures()], + shadows: const [ + Shadow( + color: Color(0x5522D3EE), + blurRadius: 28, + ), + ], + ), + ), + const SizedBox(width: 10), + Padding( + padding: EdgeInsets.only(bottom: isCompact ? 7 : 10), + child: const Text( + 'kg', + style: TextStyle( + color: Color(0xFF2563EB), + fontSize: 28, + fontWeight: FontWeight.w900, + letterSpacing: -0.5, + ), + ), + ), + ], + ), + ), + SizedBox(height: isCompact ? 22 : 28), + Container( + height: 8, + decoration: BoxDecoration( + color: const Color(0xFFE0F2FE), + borderRadius: BorderRadius.circular(999), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: isConnected ? 0.78 : 0.34, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999), + gradient: LinearGradient( + colors: [ + const Color(0xFF22D3EE).withValues(alpha: 0.72), + const Color(0xFF2563EB).withValues( + alpha: isConnected ? 0.9 : 0.48, + ), + const Color(0xFF6366F1).withValues( + alpha: isConnected ? 0.78 : 0.36, + ), + ], + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF22D3EE).withValues( + alpha: isConnected ? 0.28 : 0.12, + ), + blurRadius: 16, + ), + ], + ), + ), + ), + ), + ], + ), + ); + }, + ), + ); + } +} + +class _MetricChip extends StatelessWidget { + const _MetricChip({ + required this.icon, + required this.label, + required this.color, + }); + + final IconData icon; + final String label; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.16)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + color: color, + fontSize: 11, + fontWeight: FontWeight.w800, + letterSpacing: 0.2, + ), + ), + ], + ), + ); + } +} + +class _ActionPanel extends StatelessWidget { + const _ActionPanel({ + required this.ble, + required this.isConnected, + }); + + final BLEService ble; + final bool isConnected; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Colors.white.withValues(alpha: 0.92)), + boxShadow: [ + BoxShadow( + color: const Color(0xFF1D4ED8).withValues(alpha: 0.1), + blurRadius: 30, + offset: const Offset(0, 16), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isConnected) ...[ + Wrap( + spacing: 12, + runSpacing: 8, + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const Text( + 'Ready to capture stable measurements', + style: TextStyle( + color: Color(0xFF334155), + fontSize: 13, + fontWeight: FontWeight.w800, + ), + ), + TextButton.icon( + onPressed: () async { + await ble.disconnect(); + }, + icon: const Icon(Icons.power_settings_new, size: 18), + label: const Text('Disconnect'), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFE45555), + ), + ), + ], + ), + const SizedBox(height: 14), + ElevatedButton.icon( + onPressed: ble.hasStableWeight + ? () async { + final ok = await ble.saveStableMeasurement(); + if (!context.mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + ok + ? 'Stable weight saved to file' + : 'No stable value to save yet', + ), + ), + ); + } + : null, + icon: const Icon(Icons.save_rounded), + label: const Text('Save stable value'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2563EB), + foregroundColor: Colors.white, + disabledBackgroundColor: const Color(0xFFE2E8F0), + disabledForegroundColor: const Color(0xFF94A3B8), + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + ), + const SizedBox(height: 10), + OutlinedButton.icon( + onPressed: () async { + try { + final file = await ble.getTodayMeasurementFile(); + final exists = await file.exists(); + if (!context.mounted) return; + + if (!exists || await file.length() == 0) { + if (!context.mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'No measurements file for today yet. Save a value first.', + ), + ), + ); + return; + } + + await Share.shareXFiles( + [ + XFile( + file.path, + mimeType: 'text/csv', + name: p.basename(file.path), + ), + ], + subject: 'SIAB Laporan hasil Timbang ', + text: 'SIAB Laporan hasil Timbang harian.', + ); + } catch (e) { + if (!context.mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('gagal mengirim file: $e'), + ), + ); + } + }, + icon: const Icon(Icons.ios_share_rounded), + label: const Text('Share today CSV'), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF2563EB), + side: const BorderSide(color: Color(0x663B82F6)), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + ), + ] else + const _DisconnectedHint(), + ], + ), + ); + } +} + +class _DisconnectedHint extends StatelessWidget { + const _DisconnectedHint(); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + Icon( + Icons.info_outline_rounded, + color: Color(0xFF64748B), + ), + SizedBox(width: 12), + Expanded( + child: Text( + 'Open the Devices tab, scan nearby scales, then connect to start live weighing.', + style: TextStyle( + color: Color(0xFF475569), + fontSize: 13, + height: 1.35, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c107896..41077ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -260,26 +260,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -577,10 +577,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.11" typed_data: dependency: transitive description: