Line data Source code
1 : // Copyright 2026 The terCAD team. All rights reserved.
2 : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
3 :
4 : import 'dart:math';
5 :
6 : import 'package:app_finance/_classes/herald/app_locale.dart';
7 : import 'package:app_finance/_classes/storage/transaction_log.dart';
8 : import 'package:app_finance/_classes/structure/navigation/app_route.dart';
9 : import 'package:app_finance/_configs/theme_helper.dart';
10 : import 'package:app_finance/design/generic/loading_widget.dart';
11 : import 'package:app_finance/design/wrapper/background_wrapper.dart';
12 : import 'package:app_finance/design/wrapper/confirmation_wrapper.dart';
13 : import 'package:app_finance/pages/_interfaces/abstract_page_state.dart';
14 : import 'package:app_finance/pages/manager/widgets/log_entry.dart';
15 : import 'package:app_finance/pages/manager/widgets/log_entry_widget.dart';
16 : import 'package:material_ui/material_ui.dart';
17 :
18 : class ManagerPage extends StatefulWidget {
19 2 : const ManagerPage({super.key});
20 :
21 0 : @override
22 0 : ManagerPageState createState() => ManagerPageState();
23 : }
24 :
25 : class ManagerPageState extends AbstractPageState<ManagerPage> {
26 : static const batch = 25;
27 : final ScrollController scrollController = ScrollController();
28 : final List<LogEntry> entries = [];
29 : int visible = batch;
30 : bool isLoading = true;
31 : bool isChanged = false;
32 :
33 0 : @override
34 : void initState() {
35 0 : super.initState();
36 0 : scrollController.addListener(_scrollListener);
37 0 : _load();
38 : }
39 :
40 0 : @override
41 : void dispose() {
42 0 : scrollController.removeListener(_scrollListener);
43 0 : scrollController.dispose();
44 0 : super.dispose();
45 : }
46 :
47 0 : Future<void> _load() async {
48 0 : final lines = <LogEntry>[];
49 : int index = 0;
50 0 : await for (final line in TransactionLog.read()) {
51 0 : if (line.trim().isNotEmpty) {
52 0 : lines.add(LogEntry(index, line));
53 : }
54 0 : index++;
55 : }
56 0 : if (!mounted) {
57 : return;
58 : }
59 0 : setState(() {
60 0 : entries.addAll(lines.reversed);
61 0 : isLoading = false;
62 : });
63 : }
64 :
65 0 : void _scrollListener() {
66 0 : if (scrollController.position.extentAfter < 200 && visible < entries.length) {
67 0 : setState(() => visible += batch);
68 : }
69 : }
70 :
71 0 : void _reorder(int oldIndex, int newIndex) {
72 0 : if (oldIndex == newIndex) {
73 : return;
74 : }
75 0 : setState(() {
76 0 : entries.insert(newIndex, entries.removeAt(oldIndex));
77 0 : isChanged = true;
78 : });
79 : }
80 :
81 0 : void _delete(LogEntry entry) {
82 0 : setState(() {
83 0 : entries.remove(entry);
84 0 : isChanged = true;
85 : });
86 : }
87 :
88 0 : Future<void> _save(NavigatorState nav) async {
89 0 : setState(() => isLoading = true);
90 0 : TransactionLog.rewrite(entries.reversed.map((e) => e.raw).toList());
91 0 : await super.state.flush();
92 0 : nav.pushNamedAndRemoveUntil(AppRoute.homeRoute, (_) => false);
93 : }
94 :
95 0 : @override
96 0 : String getTitle() => AppLocale.labels.managerHeadline;
97 :
98 0 : @override
99 0 : String getButtonName() => isChanged ? AppLocale.labels.saveSettingsTooltip : '';
100 :
101 0 : @override
102 : Widget buildButton(BuildContext context, BoxConstraints constraints) {
103 0 : if (!isChanged || isLoading) {
104 : return ThemeHelper.emptyBox;
105 : }
106 0 : final nav = Navigator.of(context);
107 0 : return FloatingActionButton(
108 : heroTag: 'manager_page',
109 0 : onPressed: () => ConfirmationWrapper.show(context, () => _save(nav)),
110 0 : tooltip: getButtonName(),
111 : child: const Icon(Icons.save),
112 : );
113 : }
114 :
115 0 : @override
116 : Widget buildContent(BuildContext context, BoxConstraints constraints) {
117 0 : if (isLoading) {
118 0 : return Center(child: LoadingWidget(isLoading: isLoading));
119 : }
120 0 : final count = min(visible, entries.length);
121 0 : return ReorderableListView.builder(
122 0 : scrollController: scrollController,
123 : buildDefaultDragHandles: false,
124 : itemCount: count,
125 0 : onReorderItem: _reorder,
126 0 : footer: visible < entries.length ? const Center(child: CircularProgressIndicator()) : ThemeHelper.formEndBox,
127 0 : itemBuilder: (context, index) {
128 0 : final entry = entries[index];
129 0 : return BackgroundWrapper(
130 0 : key: ValueKey(entry.index),
131 : index: index,
132 0 : child: LogEntryWidget(
133 : entry: entry,
134 : position: index,
135 0 : onToggle: () => setState(() => entry.isExpanded = !entry.isExpanded),
136 0 : onDelete: () => _delete(entry),
137 : ),
138 : );
139 : },
140 : );
141 : }
142 : }
|