feat: make core pages local-first offline
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:miaoji_zhang/shared/api/api_client.dart';
|
||||
@@ -7,6 +9,7 @@ import 'package:miaoji_zhang/shared/theme/app_theme.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/app_icons.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/category_icon.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/app_controls.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/backend_status_icon.dart';
|
||||
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
|
||||
|
||||
class AddPage extends StatefulWidget {
|
||||
@@ -36,6 +39,7 @@ class _AddPageState extends State<AddPage> {
|
||||
DateTime _occurredAt = ShanghaiTime.now;
|
||||
bool _saving = false;
|
||||
bool _loadingCategories = true;
|
||||
int _loadRevision = 0;
|
||||
String get _categoryType => _tab == 'transfer'
|
||||
? _transferDirection == 'in'
|
||||
? 'income'
|
||||
@@ -60,7 +64,7 @@ class _AddPageState extends State<AddPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCategories();
|
||||
unawaited(_loadCategories());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -71,38 +75,60 @@ class _AddPageState extends State<AddPage> {
|
||||
}
|
||||
|
||||
Future<void> _loadCategories() async {
|
||||
setState(() => _loadingCategories = true);
|
||||
try {
|
||||
final results = await Future.wait([
|
||||
TxApi.categories('expense'),
|
||||
TxApi.categories('income'),
|
||||
]);
|
||||
if (!mounted) return;
|
||||
final revision = ++_loadRevision;
|
||||
final expense = TxApi.categoriesLocal('expense');
|
||||
final income = TxApi.categoriesLocal('income');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_categoriesByType['expense'] = results[0];
|
||||
_categoriesByType['income'] = results[1];
|
||||
if (_selectedByType['expense'] == null && results[0].isNotEmpty) {
|
||||
_selectedByType['expense'] = results[0].first;
|
||||
}
|
||||
if (_selectedByType['income'] == null && results[1].isNotEmpty) {
|
||||
_selectedByType['income'] = results[1].first;
|
||||
}
|
||||
_selectedByType['transfer_out'] ??= results[0].isEmpty
|
||||
? null
|
||||
: results[0].first;
|
||||
_selectedByType['transfer_in'] ??= results[1].isEmpty
|
||||
? null
|
||||
: results[1].first;
|
||||
_applyCategories(expense, income);
|
||||
_loadingCategories = false;
|
||||
});
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(apiErrorMessage(error))));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _loadingCategories = false);
|
||||
}
|
||||
await Future.wait([
|
||||
TxApi.categoriesRemote('expense').then<void>((values) {
|
||||
if (!mounted || revision != _loadRevision) return;
|
||||
setState(() {
|
||||
_applyCategories(values, _categoriesByType['income'] ?? const []);
|
||||
});
|
||||
}, onError: (_) {}),
|
||||
TxApi.categoriesRemote('income').then<void>((values) {
|
||||
if (!mounted || revision != _loadRevision) return;
|
||||
setState(() {
|
||||
_applyCategories(_categoriesByType['expense'] ?? const [], values);
|
||||
});
|
||||
}, onError: (_) {}),
|
||||
]);
|
||||
}
|
||||
|
||||
void _applyCategories(List<CategoryItem> expense, List<CategoryItem> income) {
|
||||
_categoriesByType['expense'] = expense;
|
||||
_categoriesByType['income'] = income;
|
||||
_selectedByType['expense'] = _preserveSelection(
|
||||
_selectedByType['expense'],
|
||||
expense,
|
||||
);
|
||||
_selectedByType['income'] = _preserveSelection(
|
||||
_selectedByType['income'],
|
||||
income,
|
||||
);
|
||||
_selectedByType['transfer_out'] = _preserveSelection(
|
||||
_selectedByType['transfer_out'],
|
||||
expense,
|
||||
);
|
||||
_selectedByType['transfer_in'] = _preserveSelection(
|
||||
_selectedByType['transfer_in'],
|
||||
income,
|
||||
);
|
||||
}
|
||||
|
||||
CategoryItem? _preserveSelection(
|
||||
CategoryItem? selected,
|
||||
List<CategoryItem> values,
|
||||
) {
|
||||
if (values.isEmpty) return null;
|
||||
if (selected == null) return values.first;
|
||||
return values.where((item) => item.id == selected.id).firstOrNull ??
|
||||
values.first;
|
||||
}
|
||||
|
||||
void _switchTab(String tab) {
|
||||
@@ -239,6 +265,7 @@ class _AddPageState extends State<AddPage> {
|
||||
),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
actions: [BackendStatusIcon(onRetry: _loadCategories)],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
|
||||
Reference in New Issue
Block a user