Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ What changed in each version of the app, newest first. The format is
[Semantic Versioning](https://semver.org/), and the file is kept by
[cider](https://pub.dev/packages/cider): see "Versions" in docs/runbook.md.

## [4.1.0] - 2026-10-16
### Added
- Search expenses and recorded payments from the group toolbar, across currencies, using descriptions and notes saved on this device.

### Changed
- Groups use connected rows with clearer, softer Material surfaces and more visible artwork in light mode. Neutral cards share the same surface treatment, and expense sections use outlined cards.

### Fixed
- Expense and payment editors ask before discarding changes. The whole export settings row opens the format picker, which remains scrollable with large text.
- Clearing Insights search or filters also clears the visible search text, and restored searches display their saved query.

## [4.0.0] - 2026-10-15
### Changed
- When a numbered API version is retired, saved data stays readable and pending uploads pause. Update OpenSplit through Play or restart into the new web release to edit again.
Expand Down Expand Up @@ -106,6 +117,7 @@ What changed in each version of the app, newest first. The format is
### Added
- The first build, to Play internal testing.

[4.1.0]: https://github.com/eigeninteractive/opensplit/compare/v4.0.0...v4.1.0
[4.0.0]: https://github.com/eigeninteractive/opensplit/compare/v3.1.0...v4.0.0
[3.1.0]: https://github.com/eigeninteractive/opensplit/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/eigeninteractive/opensplit/compare/v2.0.0...v3.0.0
Expand Down
5 changes: 5 additions & 0 deletions lib/application/ledger_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ Stream<List<Member>> members(Ref ref, String groupId) =>
Stream<List<Entry>> entries(Ref ref, String groupId) =>
ref.watch(entryRepositoryProvider).watchEntries(groupId);

/// Local group search, independent of the currency and filters in Insights.
@riverpod
Stream<List<Entry>> groupSearchResults(Ref ref, String groupId, String query) =>
ref.watch(entryRepositoryProvider).search(groupId, query);

/// One historical entry, including records removed from current balances.
@riverpod
Stream<Entry?> entryRecord(Ref ref, String entryId) =>
Expand Down
11 changes: 11 additions & 0 deletions lib/data/local/entry_search.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Quotes user terms for SQLite FTS5, matching each term as a word prefix.
///
/// Shared by group search and Insights so both interpret typed text the same
/// way. The returned expression must still be passed as a bound SQL variable.
String? entrySearchMatch(String query) {
final terms = query
.replaceAll('"', ' ')
.split(RegExp(r'\s+'))
.where((term) => term.isNotEmpty);
return terms.isEmpty ? null : terms.map((term) => '"$term"*').join(' ');
}
14 changes: 2 additions & 12 deletions lib/data/repositories/drift_analytics_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '../../domain/calendar_date.dart';
import '../../domain/models/entry.dart';
import '../../domain/analytics/analytics_query.dart';
import '../local/database.dart';
import '../local/entry_search.dart';
import 'drift_entry_repository.dart';

/// Local analytics.
Expand Down Expand Up @@ -50,7 +51,7 @@ final class DriftAnalyticsRepository {
vars.add(Variable<String>(filter.memberId!));
}

final match = _ftsMatch(filter.query);
final match = entrySearchMatch(filter.query);
if (match != null) {
clauses.add(
'$alias.rowid IN '
Expand All @@ -62,17 +63,6 @@ final class DriftAnalyticsRepository {
return (sql: clauses.join(' AND '), vars: vars);
}

/// Turns user input into an FTS5 query.
static String? _ftsMatch(String query) {
final terms = query
.replaceAll('"', ' ')
.split(RegExp(r'\s+'))
.where((t) => t.isNotEmpty)
.toList();
if (terms.isEmpty) return null;
return terms.map((t) => '"$t"*').join(' ');
}

Stream<List<Entry>> search(AnalyticsFilter filter) {
final where = _where(filter);
return _db
Expand Down
25 changes: 25 additions & 0 deletions lib/data/repositories/drift_entry_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../domain/models/entry.dart';
import '../../domain/models/entry_snapshot.dart';
import '../../domain/split/splitter.dart';
import '../local/database.dart';
import '../local/entry_search.dart';
import '../local/entry_writer.dart';
import '../local/tables.dart';
import '../sync/outbox_queue.dart';
Expand Down Expand Up @@ -98,6 +99,30 @@ final class DriftEntryRepository {
return [for (final id in ids) ?byId[id]];
}

/// Matches descriptions and notes across a group's currencies and entry kinds.
///
/// Empty input yields no results. Deleted entries are excluded, and local
/// edits or incoming sync changes update the stream automatically.
Stream<List<Entry>> search(String groupId, String query) {
final match = entrySearchMatch(query);
if (match == null) return Stream.value(const []);
return _db
.customSelect(
'SELECT e.id FROM entries e '
'WHERE e.group_id = ? AND e.deleted_at IS NULL '
'AND e.rowid IN '
'(SELECT rowid FROM entries_fts WHERE entries_fts MATCH ?) '
'ORDER BY e.entry_date DESC, e.occurred_at DESC NULLS LAST, '
'e.created_at DESC',
variables: [Variable<String>(groupId), Variable<String>(match)],
readsFrom: {_db.entries, _db.entryPayers, _db.entryShares},
)
.watch()
.asyncMap(
(rows) => getByIds([for (final row in rows) row.read<String>('id')]),
);
}

/// Every entry in a group, most recent first.
Stream<List<Entry>> watchEntries(
String groupId, {
Expand Down
1 change: 1 addition & 0 deletions lib/presentation/screens/account_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ class _Identity extends ConsumerWidget {
final named = name?.trim() ?? '';

return Card.filled(
color: scheme.surfaceContainerHigh,
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(12),
Expand Down
Loading