562 lines
17 KiB
Dart
562 lines
17 KiB
Dart
// lib/screens/config_screen.dart
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:cashumit/widgets/custom_text_config_dialog.dart';
|
|
import 'package:cashumit/services/firefly_api_service.dart';
|
|
import 'package:bluetooth_print/bluetooth_print.dart';
|
|
import 'package:bluetooth_print/bluetooth_print_model.dart';
|
|
|
|
class ConfigScreen extends StatefulWidget {
|
|
const ConfigScreen({super.key});
|
|
|
|
@override
|
|
State<ConfigScreen> createState() => _ConfigScreenState();
|
|
}
|
|
|
|
class _ConfigScreenState extends State<ConfigScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _urlController = TextEditingController();
|
|
final _tokenController = TextEditingController();
|
|
bool _isTestingConnection = false;
|
|
bool _isTestingAuth = false;
|
|
|
|
// Bluetooth printer variables
|
|
BluetoothPrint bluetoothPrint = BluetoothPrint.instance;
|
|
bool _isScanning = false;
|
|
List<BluetoothDevice> _devices = [];
|
|
BluetoothDevice? _selectedDevice;
|
|
bool _connected = false;
|
|
|
|
static const String _prefsKeyUrl = 'firefly_url';
|
|
static const String _prefsKeyToken = 'firefly_token';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadConfig();
|
|
_initBluetooth();
|
|
}
|
|
|
|
/// Memuat konfigurasi yang tersimpan dari shared preferences.
|
|
///
|
|
/// Metode ini dipanggil saat ConfigScreen pertama kali dibuat (initState)
|
|
/// untuk mengisi field input dengan konfigurasi yang telah disimpan sebelumnya.
|
|
Future<void> _loadConfig() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final savedUrl = prefs.getString(_prefsKeyUrl) ?? '';
|
|
final savedToken = prefs.getString(_prefsKeyToken) ?? '';
|
|
|
|
setState(() {
|
|
_urlController.text = savedUrl;
|
|
_tokenController.text = savedToken;
|
|
});
|
|
}
|
|
|
|
/// Inisialisasi Bluetooth printer
|
|
Future<void> _initBluetooth() async {
|
|
// Periksa status koneksi
|
|
final isConnected = await bluetoothPrint.isConnected ?? false;
|
|
if (mounted) {
|
|
setState(() {
|
|
_connected = isConnected;
|
|
});
|
|
}
|
|
|
|
// Listen to bluetooth state changes
|
|
bluetoothPrint.state.listen((state) {
|
|
if (mounted) {
|
|
switch (state) {
|
|
case BluetoothPrint.connected:
|
|
setState(() {
|
|
_connected = true;
|
|
});
|
|
break;
|
|
case BluetoothPrint.disconnected:
|
|
setState(() {
|
|
_connected = false;
|
|
_selectedDevice = null;
|
|
});
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Load saved device
|
|
await _loadSavedBluetoothDevice();
|
|
}
|
|
|
|
/// Memuat device bluetooth yang tersimpan
|
|
Future<void> _loadSavedBluetoothDevice() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final deviceAddress = prefs.getString('bluetooth_device_address');
|
|
final deviceName = prefs.getString('bluetooth_device_name');
|
|
|
|
if (deviceAddress != null && deviceName != null) {
|
|
final device = BluetoothDevice();
|
|
device.name = deviceName;
|
|
device.address = deviceAddress;
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_selectedDevice = device;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Menyimpan device bluetooth yang terhubung
|
|
Future<void> _saveBluetoothDevice(BluetoothDevice device) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('bluetooth_device_address', device.address ?? '');
|
|
await prefs.setString('bluetooth_device_name', device.name ?? '');
|
|
}
|
|
|
|
/// Menyimpan konfigurasi ke shared preferences.
|
|
///
|
|
/// Setelah menyimpan, metode ini akan kembali ke ReceiptScreen dengan hasil `true`
|
|
/// untuk memberi tahu ReceiptScreen bahwa kredensial telah diperbarui dan perlu
|
|
/// memuat ulang daftar akun.
|
|
Future<void> _saveConfig() async {
|
|
if (_formKey.currentState!.validate()) {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_prefsKeyUrl, _urlController.text);
|
|
await prefs.setString(_prefsKeyToken, _tokenController.text);
|
|
|
|
// Menampilkan snackbar konfirmasi
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Konfigurasi tersimpan!')),
|
|
);
|
|
|
|
// Kembali ke ReceiptScreen dan memberi tahu untuk memuat ulang data
|
|
Navigator.pop(context, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Menguji koneksi ke Firefly III.
|
|
///
|
|
/// Metode ini menguji apakah URL Firefly III yang dimasukkan pengguna
|
|
/// dapat dijangkau dan merespons dengan benar.
|
|
Future<void> _testConnection() async {
|
|
if (_urlController.text.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content:
|
|
Text('Silakan masukkan URL Firefly III terlebih dahulu')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isTestingConnection = true;
|
|
});
|
|
|
|
try {
|
|
final isConnected =
|
|
await FireflyApiService.testConnection(baseUrl: _urlController.text);
|
|
|
|
if (mounted) {
|
|
if (isConnected) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Koneksi berhasil!')),
|
|
);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Koneksi gagal. Periksa URL dan koneksi internet Anda.')),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Terjadi kesalahan saat menguji koneksi')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isTestingConnection = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Menguji autentikasi dengan token.
|
|
///
|
|
/// Metode ini menguji apakah Personal Access Token yang dimasukkan pengguna
|
|
/// valid dan dapat digunakan untuk mengakses API Firefly III.
|
|
Future<void> _testAuthentication() async {
|
|
if (_urlController.text.isEmpty || _tokenController.text.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Silakan masukkan URL dan token Firefly III terlebih dahulu')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isTestingAuth = true;
|
|
});
|
|
|
|
try {
|
|
final isAuthenticated = await FireflyApiService.testAuthentication(
|
|
baseUrl: _urlController.text,
|
|
accessToken: _tokenController.text,
|
|
);
|
|
|
|
if (mounted) {
|
|
if (isAuthenticated) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Autentikasi berhasil!')),
|
|
);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Autentikasi gagal. Periksa token Anda.')),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Terjadi kesalahan saat menguji autentikasi')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isTestingAuth = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Mulai scan perangkat Bluetooth
|
|
Future<void> _startScan() async {
|
|
setState(() {
|
|
_isScanning = true;
|
|
_devices.clear();
|
|
});
|
|
|
|
try {
|
|
await bluetoothPrint.startScan(timeout: const Duration(seconds: 4));
|
|
|
|
// Listen to scan results
|
|
bluetoothPrint.scanResults.listen((devices) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_devices = devices;
|
|
});
|
|
}
|
|
});
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Gagal memindai perangkat: $e')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isScanning = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Menghubungkan ke perangkat Bluetooth
|
|
Future<void> _connectToDevice(BluetoothDevice device) async {
|
|
try {
|
|
await bluetoothPrint.connect(device);
|
|
|
|
// Simpan device yang terhubung
|
|
await _saveBluetoothDevice(device);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_selectedDevice = device;
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Terhubung ke ${device.name}')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Gagal terhubung ke ${device.name}: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Memutuskan koneksi dari printer bluetooth
|
|
Future<void> _disconnectPrinter() async {
|
|
try {
|
|
await bluetoothPrint.disconnect();
|
|
|
|
// Hapus device yang tersimpan
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('bluetooth_device_address');
|
|
await prefs.remove('bluetooth_device_name');
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_selectedDevice = null;
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Printer terputus')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Gagal memutuskan koneksi: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Membuka dialog konfigurasi teks kustom
|
|
Future<void> _openCustomTextConfig() async {
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => const CustomTextConfigDialog(),
|
|
);
|
|
|
|
// Jika teks berhasil disimpan, tampilkan snackbar konfirmasi
|
|
if (result == true && mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Teks kustom tersimpan!')),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_urlController.dispose();
|
|
_tokenController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Konfigurasi'),
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Firefly III Configuration Section
|
|
const Text(
|
|
'Konfigurasi Firefly III',
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 10),
|
|
const Text(
|
|
'Masukkan detail koneksi ke instance Firefly III Anda:',
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// TextField untuk URL
|
|
TextFormField(
|
|
controller: _urlController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Firefly III URL',
|
|
hintText: 'https://firefly.yourdomain.com',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'URL tidak boleh kosong';
|
|
}
|
|
// Validasi sederhana untuk URL
|
|
if (!value.startsWith('http')) {
|
|
return 'URL harus dimulai dengan http:// atau https://';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// TextField untuk Token
|
|
TextFormField(
|
|
controller: _tokenController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Personal Access Token',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
obscureText: true, // Sembunyikan token
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Token tidak boleh kosong';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Tombol Simpan
|
|
ElevatedButton.icon(
|
|
onPressed: _saveConfig,
|
|
icon: const Icon(Icons.save),
|
|
label: const Text('Simpan Konfigurasi'),
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// Tombol Test Connection
|
|
ElevatedButton.icon(
|
|
onPressed: _isTestingConnection ? null : _testConnection,
|
|
icon: _isTestingConnection
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.link),
|
|
label: Text(_isTestingConnection
|
|
? 'Menguji Koneksi...'
|
|
: 'Test Koneksi'),
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// Tombol Test Authentication
|
|
ElevatedButton.icon(
|
|
onPressed: _isTestingAuth ? null : _testAuthentication,
|
|
icon: _isTestingAuth
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.verified_user),
|
|
label: Text(_isTestingAuth
|
|
? 'Menguji Autentikasi...'
|
|
: 'Test Autentikasi'),
|
|
),
|
|
const SizedBox(height: 30),
|
|
|
|
// Bluetooth Printer Configuration Section
|
|
const Text(
|
|
'Pengaturan Printer Bluetooth',
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 10),
|
|
const Text(
|
|
'Hubungkan printer thermal Anda melalui Bluetooth untuk mencetak struk.',
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Status koneksi printer
|
|
Container(
|
|
padding: const EdgeInsets.all(16.0),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey),
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
_connected
|
|
? 'Printer Terhubung: ${_selectedDevice?.name ?? 'Unknown'}'
|
|
: 'Printer Tidak Terhubung',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: _connected ? Colors.green : Colors.red,
|
|
),
|
|
),
|
|
Icon(
|
|
_connected
|
|
? Icons.bluetooth_connected
|
|
: Icons.bluetooth_disabled,
|
|
color: _connected ? Colors.green : Colors.red,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Tombol Scan Bluetooth
|
|
ElevatedButton.icon(
|
|
onPressed: _isScanning ? null : _startScan,
|
|
icon: _isScanning
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.bluetooth_searching),
|
|
label: Text(_isScanning ? 'Memindai...' : 'Scan Perangkat'),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Daftar perangkat yang ditemukan
|
|
if (_devices.isNotEmpty) ...[
|
|
const Text(
|
|
'Perangkat yang Ditemukan:',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 10),
|
|
ListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: _devices.length,
|
|
itemBuilder: (context, index) {
|
|
final device = _devices[index];
|
|
return Card(
|
|
child: ListTile(
|
|
title: Text(device.name ?? 'Unknown Device'),
|
|
subtitle: Text(device.address ?? ''),
|
|
trailing: ElevatedButton(
|
|
onPressed: () => _connectToDevice(device),
|
|
child: const Text('Hubungkan'),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
|
|
// Tombol Putuskan Koneksi
|
|
if (_connected)
|
|
ElevatedButton.icon(
|
|
onPressed: _disconnectPrinter,
|
|
icon: const Icon(Icons.bluetooth_disabled),
|
|
label: const Text('Putuskan Koneksi'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|