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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ What changed in each version of the app, newest first. The format is
[cider](https://pub.dev/packages/cider): see "Versions" in docs/runbook.md.

## [Unreleased]
### Changed
- Groups have larger names and avatars with quieter cover art. Forms use consistent filled fields, and app wording is shorter.

### Fixed
- Improved browser database transaction reliability.

Expand Down
23 changes: 6 additions & 17 deletions lib/presentation/screens/about_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,27 @@ class AboutScreen extends StatelessWidget {
children: [
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: BrandHeader(
title: 'OpenSplit',
subtitle: 'Shared expenses, without the extraction.',
),
child: BrandHeader(title: 'OpenSplit', subtitle: brandTagline),
),
const _Version(),
const Divider(height: 40),

_Link(
icon: Icons.code,
title: 'Source code',
subtitle: 'Every line of this app, and the server behind it.',
subtitle: 'App and server.',
url: repositoryUrl,
),
_Link(
icon: Icons.bug_report_outlined,
title: 'Report an issue',
subtitle: 'Bugs, and things that work but should not.',
subtitle: 'Bugs and suggestions.',
url: issuesUrl,
),
_Link(
icon: Icons.balance_outlined,
title: 'Licence — AGPL-3.0',
subtitle:
'Anyone can use, change and share the code, and what is '
'built from it stays open source too.',
subtitle: 'Use, modify and share. Keep changes open source.',
url: licenseUrl,
),

Expand All @@ -70,10 +65,7 @@ class AboutScreen extends StatelessWidget {
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.inventory_outlined),
title: const Text('Open-source licences'),
subtitle: const Text(
'The packages and typefaces this app is built on, and their '
'terms.',
),
subtitle: const Text('Packages and fonts.'),
trailing: const Icon(Icons.chevron_right),
onTap: () => showLicensePage(
context: context,
Expand All @@ -84,10 +76,7 @@ class AboutScreen extends StatelessWidget {

const Divider(height: 40),
Text(
'Free for everyone, open source, with no ads, no analytics SDK '
'and no venture funding. Those are commitments rather than '
'features — they are written down in the repository so they can '
'be held against us.',
'Open source. No ads, analytics SDKs or venture funding.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
Expand Down
4 changes: 1 addition & 3 deletions lib/presentation/screens/activity_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,6 @@ class _Empty extends StatelessWidget {
Widget build(BuildContext context) => const EmptyState(
icon: Icons.history,
title: 'Nothing has happened here yet',
message:
'Every expense added, edited or deleted in this group shows up '
'here, with who did it.',
message: 'Expense changes and who made them appear here.',
);
}
10 changes: 2 additions & 8 deletions lib/presentation/screens/edit_profile_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,7 @@ class _EditProfileScreenState extends ConsumerState<EditProfileScreen> {
autofillHints: const [AutofillHints.name],
decoration: const InputDecoration(
labelText: 'Name',
helperText:
'What everybody in your groups sees. Changing '
'it here changes it everywhere, including for '
'them.',
helperText: 'Your name in all groups.',
helperMaxLines: 3,
),
// A blank name would look saved while the member
Expand All @@ -193,10 +190,7 @@ class _EditProfileScreenState extends ConsumerState<EditProfileScreen> {
decoration: const InputDecoration(
labelText: 'UPI ID (optional)',
hintText: 'you@bank',
helperText:
'Lets people in your groups open their UPI app '
'to pay you. OpenSplit never handles the '
'money.',
helperText: 'Receive payments in your UPI app.',
helperMaxLines: 3,
),
validator: (value) {
Expand Down
3 changes: 1 addition & 2 deletions lib/presentation/screens/entry_editor_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,7 @@ class _Editor extends ConsumerWidget {
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Stored in ${currency.code}. Balances in a group are '
'always kept per currency, never converted.',
'${currency.code} balances stay separate.',
style: textTheme.bodySmall,
),
),
Expand Down
118 changes: 69 additions & 49 deletions lib/presentation/screens/group_list_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,11 @@ abstract final class _GroupList {
// be folding balances.
sliver: SliverList.builder(
itemCount: groups.length,
itemBuilder: (context, index) => Segment(
index: index,
count: groups.length,
child: _GroupTile(
group: groups[index],
borderRadius: Segment.radiusAt(
index: index,
count: groups.length,
),
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.only(
bottom: index == groups.length - 1 ? 0 : 12,
),
child: _GroupTile(group: groups[index]),
),
),
),
Expand Down Expand Up @@ -163,55 +158,82 @@ class _ArchivedRow extends StatelessWidget {
}

class _GroupTile extends ConsumerWidget {
const _GroupTile({required this.group, required this.borderRadius});
const _GroupTile({required this.group});

final Group group;
final BorderRadius borderRadius;

@override
Widget build(BuildContext context, WidgetRef ref) {
final ledger = ref.watch(groupLedgerProvider(group.id));
final currencies = ref.watch(currenciesProvider).value ?? const {};
final theme = Theme.of(context);
const corners = BorderRadius.all(Radius.circular(20));

// A real ListTile, for its touch target, density and large-font growth.
return Stack(
children: [
if (ledger != null)
Positioned.fill(
child: GroupCoverHero(
ledger: ledger,
borderRadius: borderRadius,
patternFade: 1,
),
),
Material(
type: MaterialType.transparency,
child: ListTile(
onTap: () => context.push('/g/${group.id}'),
contentPadding: const EdgeInsetsDirectional.fromSTEB(
16,
12,
24,
12,
),
leading: CircleAvatar(
radius: 24,
backgroundColor: Theme.of(context).colorScheme.surface,
child: GroupAvatarHero(group: group, radius: 22),
// Stock cards and tiles retain Material's ink, focus and touch behaviour.
return Card.filled(
color: theme.colorScheme.surfaceContainerLow,
shape: const RoundedRectangleBorder(borderRadius: corners),
child: Stack(
children: [
if (ledger != null)
Positioned.fill(
child: GroupCoverHero(
ledger: ledger,
borderRadius: corners,
patternFade: 1,
),
),
title: SharedText(
tag: ('group-name', group.id),
text: group.name,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
Material(
type: MaterialType.transparency,
child: ListTile(
onTap: () => context.push('/g/${group.id}'),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
minLeadingWidth: 56,
titleAlignment: ListTileTitleAlignment.top,
leading: GroupAvatarHero(group: group, radius: 28),
title: SharedText(
tag: ('group-name', group.id),
text: group.name,
maxLines: null,
style: theme.textTheme.titleLarge!.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
subtitle: ledger == null
? const LaunchPlaceholder(child: SizedBox(height: 20))
: Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (ledger.me != null) ...[
Text(
'${ledger.members.length} '
'${ledger.members.length == 1 ? 'member' : 'members'}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
],
GroupStanding(
ledger: ledger,
currencies: currencies,
style: theme.textTheme.bodyLarge,
overflow: TextOverflow.clip,
),
],
),
),
),
subtitle: ledger == null
? const LaunchPlaceholder(child: SizedBox(height: 20))
: GroupStanding(ledger: ledger, currencies: currencies),
),
),
],
],
),
);
}
}
Expand All @@ -223,9 +245,7 @@ class _EmptyState extends StatelessWidget {
Widget build(BuildContext context) => const EmptyState(
icon: Icons.groups_outlined,
title: 'No groups yet',
message:
'Make one for a trip, a flat, or a single dinner. '
'You can add people who do not have the app.',
message: 'Create a group for a trip, flat or dinner.',
);
}

Expand Down
24 changes: 7 additions & 17 deletions lib/presentation/screens/group_settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,8 @@ class _GroupSettingsScreenState extends ConsumerState<GroupSettingsScreen> {
),
title: const Text('Suggest the fewest payments'),
subtitle: const Text(
'On: as few payments as settle the group, which can mean '
'paying someone you never shared an expense with. Off: '
'each person settles with each person they owe. Either '
'way, everyone ends up square.',
'Fewer payments, same balances. You may pay someone '
'you did not directly owe.',
),
),

Expand All @@ -238,8 +236,7 @@ class _GroupSettingsScreenState extends ConsumerState<GroupSettingsScreen> {
: (value) => _setArchived(ledger, archived: value),
title: const Text('Archive'),
subtitle: const Text(
'Hides it from your list. Nothing is deleted, everyone stays '
'in it, and un-archiving brings it straight back.',
'Hide this group without deleting its expenses.',
),
),
],
Expand All @@ -251,8 +248,7 @@ class _GroupSettingsScreenState extends ConsumerState<GroupSettingsScreen> {
leading: const Icon(Icons.ios_share),
title: const Text('Export this group'),
subtitle: const Text(
'A spreadsheet to read, or a full backup that keeps every '
'split, rate and change.',
'Download a spreadsheet or full backup.',
),
isThreeLine: true,
trailing: ExportButton(groupId: widget.groupId),
Expand All @@ -263,8 +259,7 @@ class _GroupSettingsScreenState extends ConsumerState<GroupSettingsScreen> {

if (me == null)
Text(
'You are not a member of this group, so there is nothing here '
'to leave.',
'You are not a member of this group.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: scheme.onSurfaceVariant,
),
Expand All @@ -278,19 +273,14 @@ class _GroupSettingsScreenState extends ConsumerState<GroupSettingsScreen> {
'Leave group',
style: TextStyle(color: scheme.error),
),
subtitle: const Text(
'Your past expenses stay in the group. You stop appearing in '
'new ones.',
),
subtitle: const Text('Past expenses stay in the group.'),
onTap: _busy ? null : () => _leave(ledger, me),
),
],
),
const SizedBox(height: 8),
Text(
'A group cannot be deleted once it has expenses in it, by '
'design — somebody else\'s record of who paid for what is not '
'yours to remove. Archive it instead.',
'Groups with expenses cannot be deleted. Archive them instead.',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant),
Expand Down
17 changes: 2 additions & 15 deletions lib/presentation/screens/join_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,7 @@ class _JoinScreenState extends ConsumerState<JoinScreen> {
children: [
BrandHeader(
title: 'Rejoin ${preview.groupName}',
subtitle:
'You were in this group before. Rejoining brings back your place '
'as it was, with everything you paid and owed, and you get '
'updates again.',
subtitle: 'Restore your membership, expenses and balances.',
),
const SizedBox(height: 32),
..._errorIfAny(theme),
Expand Down Expand Up @@ -345,10 +342,6 @@ class _JoinScreenState extends ConsumerState<JoinScreen> {
// Not joined yet, deliberately.
if (mounted) setState(() {});
},
guestNote:
'You will join straight away and the group will synchronize. '
'This device is the only way back into the guest account '
'until you add an email address or Google account.',
)
else
_ChoosePlace(
Expand Down Expand Up @@ -529,13 +522,7 @@ class _Claim extends ConsumerWidget {
// Nobody yet. The three ways to become somebody, then the claim happens
// against whichever one they picked.
if (account == null) {
return IdentityChoices(
onSignedIn: onJoin,
guestNote:
'You will join straight away and the group will synchronize. This '
'device is the only way back into the guest account until you add '
'an email address or Google account.',
);
return IdentityChoices(onSignedIn: onJoin);
}

final me = ref.watch(myProfileProvider).value;
Expand Down
6 changes: 2 additions & 4 deletions lib/presentation/screens/save_account_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,8 @@ class _Unsaved extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Your groups are synchronized, but this device is the only way back '
'into this guest account, and only while you open OpenSplit at '
'least once a year. Adding an email address lets you recover it and '
'use OpenSplit on more than one device.',
'Guest access depends on this device and yearly use. Add an email '
'to recover your account and sign in on other devices.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
Expand Down
Loading