new UI + app renamed to SIAB

This commit is contained in:
Andrew committed 2026-06-29 09:25:52 +07:00
1 parent 363f113121
commit b67e8da37b
9 files changed
+913 -270

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<application
android:label="SIAB Bluetooth"
android:label="SIAB"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
+1 -1
View File
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.9.0'
ext.kotlin_version = '2.0.0'
repositories {
google()
mavenCentral()
+4
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+1 -1
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Siab Bluetooth App</string>
<string>SIAB</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
+71 -2
View File
@@ -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,
),
+1 -1
View File
@@ -117,7 +117,7 @@ class _DevicesScreenState extends State<DevicesScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Timbangan'),
title: const Text('TIMBANGAN SIAB'),
actions: [
IconButton(
onPressed: _loading ? null : _loadSaved,
+170 -7
View File
@@ -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<MainShell> {
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: IndexedStack(
index: _index,
children: const [
@@ -28,15 +32,174 @@ class _MainShellState extends State<MainShell> {
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<int> 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(),
),
],
),
),
),
),
);
}
+654 -247
View File
@@ -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<BLEService>(
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<BLEService>(
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<String>(
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<String>(
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,
),
),
),
],
);
}
}
+10 -10
View File
@@ -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: