new UI + app renamed to SIAB
This commit is contained in:
1 parent
363f113121
commit
b67e8da37b
9 files changed
+913
-270
No files matched your search
@@ -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,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.0'
|
||||
ext.kotlin_version = '2.0.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user