Files
rental_income_tracker/lib/screens/settings_screen.dart
T

245 lines
8.7 KiB
Dart

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:rental_income_tracker/state/rent_controller.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final TextEditingController _rentController = TextEditingController();
Future<bool> _confirmImport() async {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Import backup file?'),
content: const Text(
'This will overwrite current local settings and logged entries with the contents of the selected backup file.',
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Import'),
),
],
);
},
);
return result ?? false;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final controller = context.read<RentController>();
_rentController.text = controller.settings.rentUsd.toStringAsFixed(2);
}
@override
void dispose() {
_rentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<RentController>(
builder: (context, controller, _) {
final summary = controller.buildLifetimeSummary();
final usdCurrency = NumberFormat.currency(
symbol: r'$',
decimalDigits: 2,
);
final sekCurrency = NumberFormat.currency(
symbol: 'SEK ',
decimalDigits: 2,
);
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'All-time totals (since May 2024)',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'${usdCurrency.format(summary.totalUsd)} | ${sekCurrency.format(summary.totalSek)}',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
'Occupancy: ${summary.occupancyPercent.toStringAsFixed(1)}% (${summary.occupiedMonths}/${summary.totalTrackedMonths} months)',
),
],
),
),
),
const SizedBox(height: 12),
TextField(
controller: _rentController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Monthly rent (USD)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: () async {
final value = double.tryParse(_rentController.text.trim());
if (value == null || value <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Enter a valid rent amount.'),
),
);
return;
}
await controller.updateRentUsd(value);
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Rent updated.')),
);
},
child: const Text('Save rent amount'),
),
const SizedBox(height: 12),
SwitchListTile(
title: const Text('Unit occupied'),
subtitle: const Text(
'When turned off, current and future months are marked Not occupied and reminders stop.',
),
value: controller.settings.occupied,
onChanged: (value) async {
await controller.setOccupied(value);
},
),
const Divider(height: 24),
FilledButton.icon(
onPressed: () async {
await controller.backfillLastYear();
if (!context.mounted) {
return;
}
if (controller.errorMessage == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Backfill finished for last year.'),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(controller.errorMessage!)),
);
}
},
icon: const Icon(Icons.history),
label: const Text('Backfill last year (one click)'),
),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: () async {
final directoryPath = await FilePicker.platform
.getDirectoryPath(dialogTitle: 'Choose backup folder');
if (directoryPath == null || !context.mounted) {
return;
}
final path = await controller.exportBackupToDirectory(
directoryPath,
);
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Backup saved to $path')),
);
},
icon: const Icon(Icons.download),
label: const Text('Create backup'),
),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: () async {
final shouldImport = await _confirmImport();
if (!shouldImport || !context.mounted) {
return;
}
final result = await FilePicker.platform.pickFiles(
dialogTitle: 'Choose backup file',
type: FileType.custom,
allowedExtensions: <String>['json'],
withData: false,
);
final filePath = result?.files.single.path;
if (filePath == null || !context.mounted) {
return;
}
try {
final path = await controller.importBackupFromFile(
filePath,
);
if (!context.mounted) {
return;
}
_rentController.text = controller.settings.rentUsd
.toStringAsFixed(2);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Imported backup from $path')),
);
} catch (_) {
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
controller.errorMessage ?? 'Restore failed.',
),
),
);
}
},
icon: const Icon(Icons.restore),
label: const Text('Import backup file'),
),
const SizedBox(height: 16),
const Text(
'Using Frankfurter exchange rates (no API key required).',
),
],
),
);
},
);
}
}