Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a93dbbe30d | ||
|
|
22a535a6fd |
@@ -521,10 +521,15 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Replace labels on every transaction in [txs] (per-id overrides).
|
/// Replace labels on every transaction in [txs] (per-id overrides).
|
||||||
|
///
|
||||||
|
/// When [matchPattern] is set (apply-to-matched), also refreshes the
|
||||||
|
/// merchant rule so future transactions matching the pattern inherit the
|
||||||
|
/// full label set (parent + sub-labels).
|
||||||
Future<void> setLabelsForMany(
|
Future<void> setLabelsForMany(
|
||||||
List<BankTransaction> txs,
|
List<BankTransaction> txs,
|
||||||
List<String> labelIds,
|
List<String> labelIds, {
|
||||||
) async {
|
String? matchPattern,
|
||||||
|
}) async {
|
||||||
if (txs.isEmpty) return;
|
if (txs.isEmpty) return;
|
||||||
|
|
||||||
final expanded = expandWithAncestors([
|
final expanded = expandWithAncestors([
|
||||||
@@ -545,6 +550,22 @@ class TxMetaController extends ChangeNotifier {
|
|||||||
: current.copyWith(labelIds: unique);
|
: current.copyWith(labelIds: unique);
|
||||||
}
|
}
|
||||||
_byId = nextById;
|
_byId = nextById;
|
||||||
|
|
||||||
|
final pattern = (matchPattern ?? '').trim().toLowerCase();
|
||||||
|
if (pattern.isNotEmpty) {
|
||||||
|
final nextRules = Map<String, List<String>>.of(_labelsByMerchant);
|
||||||
|
if (unique.isEmpty) {
|
||||||
|
nextRules.remove(pattern);
|
||||||
|
for (final tx in txs) {
|
||||||
|
nextRules.remove(tx.merchantKey);
|
||||||
|
nextRules.remove(tx.descriptionKey);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextRules[pattern] = unique;
|
||||||
|
}
|
||||||
|
_labelsByMerchant = nextRules;
|
||||||
|
}
|
||||||
|
|
||||||
_schedulePersist();
|
_schedulePersist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:pocketbase/pocketbase.dart';
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
email: _emailController.text,
|
email: _emailController.text,
|
||||||
password: _passwordController.text,
|
password: _passwordController.text,
|
||||||
);
|
);
|
||||||
|
// Lets iOS/Android password managers offer to save / confirm the login.
|
||||||
|
TextInput.finishAutofillContext();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (!widget.asRoot) {
|
if (!widget.asRoot) {
|
||||||
Navigator.of(context).pop(true);
|
Navigator.of(context).pop(true);
|
||||||
@@ -96,68 +99,77 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Form(
|
AutofillGroup(
|
||||||
key: _formKey,
|
child: Form(
|
||||||
child: Column(
|
key: _formKey,
|
||||||
children: [
|
child: Column(
|
||||||
AppTextField(
|
children: [
|
||||||
controller: _emailController,
|
AppTextField(
|
||||||
label: 'Email',
|
controller: _emailController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
label: 'Email',
|
||||||
textInputAction: TextInputAction.next,
|
keyboardType: TextInputType.emailAddress,
|
||||||
autofocus: true,
|
textInputAction: TextInputAction.next,
|
||||||
validator: (value) {
|
autofocus: true,
|
||||||
final text = value?.trim() ?? '';
|
autofillHints: const [
|
||||||
if (text.isEmpty) return 'Enter your email';
|
AutofillHints.email,
|
||||||
if (!text.contains('@')) return 'Enter a valid email';
|
AutofillHints.username,
|
||||||
return null;
|
],
|
||||||
},
|
validator: (value) {
|
||||||
),
|
final text = value?.trim() ?? '';
|
||||||
const SizedBox(height: 16),
|
if (text.isEmpty) return 'Enter your email';
|
||||||
TextFormField(
|
if (!text.contains('@')) return 'Enter a valid email';
|
||||||
controller: _passwordController,
|
return null;
|
||||||
obscureText: _obscurePassword,
|
},
|
||||||
textInputAction: TextInputAction.done,
|
),
|
||||||
onFieldSubmitted: (_) => _submit(),
|
const SizedBox(height: 16),
|
||||||
validator: (value) {
|
TextFormField(
|
||||||
if (value == null || value.isEmpty) {
|
controller: _passwordController,
|
||||||
return 'Enter your password';
|
obscureText: _obscurePassword,
|
||||||
}
|
textInputAction: TextInputAction.done,
|
||||||
return null;
|
autofillHints: const [AutofillHints.password],
|
||||||
},
|
enableSuggestions: false,
|
||||||
decoration: InputDecoration(
|
autocorrect: false,
|
||||||
labelText: 'Password',
|
onFieldSubmitted: (_) => _submit(),
|
||||||
suffixIcon: IconButton(
|
validator: (value) {
|
||||||
tooltip: _obscurePassword
|
if (value == null || value.isEmpty) {
|
||||||
? 'Show password'
|
return 'Enter your password';
|
||||||
: 'Hide password',
|
}
|
||||||
onPressed: () => setState(
|
return null;
|
||||||
() => _obscurePassword = !_obscurePassword,
|
},
|
||||||
),
|
decoration: InputDecoration(
|
||||||
icon: Icon(
|
labelText: 'Password',
|
||||||
_obscurePassword
|
suffixIcon: IconButton(
|
||||||
? Icons.visibility_outlined
|
tooltip: _obscurePassword
|
||||||
: Icons.visibility_off_outlined,
|
? 'Show password'
|
||||||
|
: 'Hide password',
|
||||||
|
onPressed: () => setState(
|
||||||
|
() => _obscurePassword = !_obscurePassword,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
_obscurePassword
|
||||||
|
? Icons.visibility_outlined
|
||||||
|
: Icons.visibility_off_outlined,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 24),
|
FilledButton.icon(
|
||||||
FilledButton.icon(
|
onPressed: _busy ? null : _submit,
|
||||||
onPressed: _busy ? null : _submit,
|
icon: _busy
|
||||||
icon: _busy
|
? SizedBox(
|
||||||
? SizedBox(
|
width: 18,
|
||||||
width: 18,
|
height: 18,
|
||||||
height: 18,
|
child: CircularProgressIndicator(
|
||||||
child: CircularProgressIndicator(
|
strokeWidth: 2,
|
||||||
strokeWidth: 2,
|
color: theme.colorScheme.onPrimary,
|
||||||
color: theme.colorScheme.onPrimary,
|
),
|
||||||
),
|
)
|
||||||
)
|
: const Icon(Icons.login),
|
||||||
: const Icon(Icons.login),
|
label: Text(_busy ? 'Signing in…' : 'Sign in'),
|
||||||
label: Text(_busy ? 'Signing in…' : 'Sign in'),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
|
|||||||
await meta.setLabelsForMany(
|
await meta.setLabelsForMany(
|
||||||
targets,
|
targets,
|
||||||
_draftLabelIds.toList(growable: false),
|
_draftLabelIds.toList(growable: false),
|
||||||
|
matchPattern: toMatched ? _pattern : null,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ class AppTextField extends StatelessWidget {
|
|||||||
this.keyboardType,
|
this.keyboardType,
|
||||||
this.validator,
|
this.validator,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
this.autofillHints,
|
||||||
|
this.obscureText = false,
|
||||||
|
this.enableSuggestions,
|
||||||
|
this.autocorrect,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@@ -24,6 +28,10 @@ class AppTextField extends StatelessWidget {
|
|||||||
final TextInputType? keyboardType;
|
final TextInputType? keyboardType;
|
||||||
final String? Function(String?)? validator;
|
final String? Function(String?)? validator;
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
final Iterable<String>? autofillHints;
|
||||||
|
final bool obscureText;
|
||||||
|
final bool? enableSuggestions;
|
||||||
|
final bool? autocorrect;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -31,8 +39,12 @@ class AppTextField extends StatelessWidget {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
|
obscureText: obscureText,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
|
autofillHints: autofillHints,
|
||||||
|
enableSuggestions: enableSuggestions ?? !obscureText,
|
||||||
|
autocorrect: autocorrect ?? !obscureText,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
|
|||||||
Reference in New Issue
Block a user