feat(learn,app): explanations wait behind a question mark beside their title
What explains a setting or a section, rather than what is the person's own, moves behind halkyon_ui's HelpButton, beside its title through the new ExplainedTitle (the developer's review of 3 Nov 2026): in Learn's settings (citations, the Pomodoro timer, grammar, the web search, Jellyfin, the device's dictionaries, counting one's learning), the study settings, export's progress, the Jellyfin tags and the study button's back first; in halkyon_app, setting up a password, adding SimpleLogin, own keys not allowed, the profile page's link back and the operator's grant sheet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
c984678c5b
commit
830aea10fe
24 changed files with 279 additions and 115 deletions
|
|
@ -33,9 +33,10 @@ class const AssistantSettingsCard({super.key}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l10n.settingsAskAssistant),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.settingsAskAssistantSubtitle),
|
l10n.settingsAskAssistant,
|
||||||
isThreeLine: true,
|
help: l10n.settingsAskAssistantSubtitle,
|
||||||
|
),
|
||||||
value: on,
|
value: on,
|
||||||
onChanged: (on) => context.read<DeviceSettingsBloc>().add(
|
onChanged: (on) => context.read<DeviceSettingsBloc>().add(
|
||||||
DeviceSettingsEventAssistantChosen(on),
|
DeviceSettingsEventAssistantChosen(on),
|
||||||
|
|
|
||||||
|
|
@ -186,15 +186,14 @@ class const _SettingsCard() extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
ExplainedTitle(
|
||||||
l10n.settingsDeviceDictionaryTitle,
|
l10n.settingsDeviceDictionaryTitle,
|
||||||
|
help: l10n.settingsDeviceDictionaryAbout,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 8),
|
||||||
Text(l10n.settingsDeviceDictionaryAbout),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
if (dictionaries.progress case final progress?)
|
if (dictionaries.progress case final progress?)
|
||||||
DeviceDictionaryProgressView(progress: progress)
|
DeviceDictionaryProgressView(progress: progress)
|
||||||
else if (installed != null)
|
else if (installed != null)
|
||||||
|
|
|
||||||
|
|
@ -256,8 +256,10 @@ class _ExportSheetState() extends State<_ExportSheet> {
|
||||||
value: _withProgress,
|
value: _withProgress,
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
setState(() => _withProgress = value ?? _withProgress),
|
setState(() => _withProgress = value ?? _withProgress),
|
||||||
title: Text(l10n.exportWithProgress),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.exportWithProgressSubtitle),
|
l10n.exportWithProgress,
|
||||||
|
help: l10n.exportWithProgressSubtitle,
|
||||||
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -159,15 +159,16 @@ class _JellyfinBrowserScreenState() extends State<JellyfinBrowserScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(l10n.jellyfinSubtitle),
|
ExplainedTitle(
|
||||||
const SizedBox(height: 4),
|
l10n.jellyfinTitle,
|
||||||
Text(
|
help:
|
||||||
l10n.jellyfinPrivacy,
|
'${l10n.jellyfinSubtitle}\n\n'
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
'${l10n.jellyfinPrivacy}',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 8),
|
||||||
const JellyfinConnectForm(),
|
const JellyfinConnectForm(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -14,22 +14,14 @@ class const JellyfinSettingsCard({super.key}) extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
ExplainedTitle(
|
||||||
l10n.jellyfinTitle,
|
l10n.jellyfinTitle,
|
||||||
|
help: '${l10n.jellyfinSubtitle}\n\n${l10n.jellyfinPrivacy}',
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 8),
|
||||||
Text(l10n.jellyfinSubtitle),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
l10n.jellyfinPrivacy,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
const JellyfinAccountView(),
|
const JellyfinAccountView(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -30,20 +30,14 @@ class const CitationSettingsCard({super.key}) extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
ExplainedTitle(
|
||||||
l10n.settingsCitations,
|
l10n.settingsCitations,
|
||||||
|
help: l10n.settingsCitationsDescription,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 8),
|
||||||
Text(
|
|
||||||
l10n.settingsCitationsDescription,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
DropdownButtonFormField<CitationStyle>(
|
DropdownButtonFormField<CitationStyle>(
|
||||||
initialValue: style,
|
initialValue: style,
|
||||||
// A style's name is cut short rather than overflowing a phone.
|
// A style's name is cut short rather than overflowing a phone.
|
||||||
|
|
|
||||||
|
|
@ -177,9 +177,10 @@ class const _PrivacyCard() extends StatelessWidget {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l10n.settingsCountLearning),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.settingsCountLearningSubtitle),
|
l10n.settingsCountLearning,
|
||||||
isThreeLine: true,
|
help: l10n.settingsCountLearningSubtitle,
|
||||||
|
),
|
||||||
value: counted ?? false,
|
value: counted ?? false,
|
||||||
onChanged: counted == null
|
onChanged: counted == null
|
||||||
? null
|
? null
|
||||||
|
|
@ -221,8 +222,10 @@ class const _StudyCard() extends StatelessWidget {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l10n.settingsStudyPomodoroTitle),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.settingsStudyPomodoroSubtitle),
|
l10n.settingsStudyPomodoroTitle,
|
||||||
|
help: l10n.settingsStudyPomodoroSubtitle,
|
||||||
|
),
|
||||||
value: context.select<DeviceSettingsBloc, bool>(
|
value: context.select<DeviceSettingsBloc, bool>(
|
||||||
(bloc) => bloc.state.pomodoroWhileStudying,
|
(bloc) => bloc.state.pomodoroWhileStudying,
|
||||||
),
|
),
|
||||||
|
|
@ -270,9 +273,10 @@ class _WritingCardState() extends State<_WritingCard> {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l10n.settingsGrammarTitle),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.settingsGrammarSubtitle),
|
l10n.settingsGrammarTitle,
|
||||||
isThreeLine: true,
|
help: l10n.settingsGrammarSubtitle,
|
||||||
|
),
|
||||||
value: context.select<DeviceSettingsBloc, bool>(
|
value: context.select<DeviceSettingsBloc, bool>(
|
||||||
(bloc) => bloc.state.checkGrammar,
|
(bloc) => bloc.state.checkGrammar,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -346,8 +346,10 @@ class _StudyButtonDialogState() extends State<_StudyButtonDialog> {
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l10n.studyButtonBackFirst),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.studyButtonBackFirstHelper),
|
l10n.studyButtonBackFirst,
|
||||||
|
help: l10n.studyButtonBackFirstHelper,
|
||||||
|
),
|
||||||
value: _backFirst,
|
value: _backFirst,
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
setState(() => _backFirst = value),
|
setState(() => _backFirst = value),
|
||||||
|
|
|
||||||
|
|
@ -240,14 +240,16 @@ class const _Options({required final StudyOptions options})
|
||||||
value: mode,
|
value: mode,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(switch (mode) {
|
title: switch (mode) {
|
||||||
LearnMode.spaced => l10n.studyModeSpaced,
|
LearnMode.spaced => ExplainedTitle(
|
||||||
LearnMode.cram => l10n.studyModeCram,
|
l10n.studyModeSpaced,
|
||||||
}),
|
help: l10n.studyModeSpacedHint,
|
||||||
subtitle: Text(switch (mode) {
|
),
|
||||||
LearnMode.spaced => l10n.studyModeSpacedHint,
|
LearnMode.cram => ExplainedTitle(
|
||||||
LearnMode.cram => l10n.studyModeCramHint,
|
l10n.studyModeCram,
|
||||||
}),
|
help: l10n.studyModeCramHint,
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -265,8 +267,12 @@ class const _Options({required final StudyOptions options})
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(l10n.studySettingsQuizMc),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.studySettingsQuizMcDescription),
|
l10n.studySettingsQuizMc,
|
||||||
|
help:
|
||||||
|
'${l10n.studySettingsQuizMcDescription}\n\n'
|
||||||
|
'${l10n.studySettingsAspectDescription}',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (options.quizVocabulary) ...[
|
if (options.quizVocabulary) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -289,13 +295,6 @@ class const _Options({required final StudyOptions options})
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
l10n.studySettingsAspectDescription,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: options.typeReadings,
|
value: options.typeReadings,
|
||||||
|
|
@ -307,8 +306,10 @@ class const _Options({required final StudyOptions options})
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(l10n.studySettingsTypeReading),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.studySettingsTypeReadingDescription),
|
l10n.studySettingsTypeReading,
|
||||||
|
help: l10n.studySettingsTypeReadingDescription,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(l10n.studySettingsCharacters, style: heading),
|
Text(l10n.studySettingsCharacters, style: heading),
|
||||||
|
|
@ -318,8 +319,10 @@ class const _Options({required final StudyOptions options})
|
||||||
onChanged: (write) => change(options.copyWith(writeByHand: write)),
|
onChanged: (write) => change(options.copyWith(writeByHand: write)),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(l10n.studySettingsWriteByHand),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.studySettingsWriteByHandDescription),
|
l10n.studySettingsWriteByHand,
|
||||||
|
help: l10n.studySettingsWriteByHandDescription,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,10 @@ class const _HideJellyfinTags() extends StatelessWidget {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
return SwitchListTile(
|
return SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l10n.tagManageHideJellyfin),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.tagManageHideJellyfinHint),
|
l10n.tagManageHideJellyfin,
|
||||||
|
help: l10n.tagManageHideJellyfinHint,
|
||||||
|
),
|
||||||
value: context.watch<JellyfinTagsBloc>().state ?? false,
|
value: context.watch<JellyfinTagsBloc>().state ?? false,
|
||||||
onChanged: (hidden) => context.read<JellyfinTagsBloc>().add(
|
onChanged: (hidden) => context.read<JellyfinTagsBloc>().add(
|
||||||
JellyfinTagsEventHidingChosen(hidden),
|
JellyfinTagsEventHidingChosen(hidden),
|
||||||
|
|
|
||||||
|
|
@ -419,6 +419,14 @@ void main() {
|
||||||
|
|
||||||
expect(copied, 'https://halkyon.eu/@raisondetredev');
|
expect(copied, 'https://halkyon.eu/@raisondetredev');
|
||||||
expect(find.text('Address copied.'), findsOneWidget);
|
expect(find.text('Address copied.'), findsOneWidget);
|
||||||
|
|
||||||
|
// What linking back does, behind its question mark (decided 3 Nov
|
||||||
|
// 2026).
|
||||||
|
final checkmark = find.textContaining('gets a checkmark');
|
||||||
|
expect(checkmark, findsNothing);
|
||||||
|
await tester.tap(find.byTooltip("What's this?").last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(checkmark, findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('a link saved is checked by itself, and gets a checkmark once '
|
testWidgets('a link saved is checked by itself, and gets a checkmark once '
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,38 @@ void main() {
|
||||||
expect(brightnessAt(tester, find.text('Appearance')), Brightness.light);
|
expect(brightnessAt(tester, find.text('Appearance')), Brightness.light);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("every setting's explanation waits behind the question mark "
|
||||||
|
"beside its title, its values in view (the developer's review of 3 Nov "
|
||||||
|
'2026)', (tester) async {
|
||||||
|
await pumpApp(tester, testDependencies(), location: RoutePaths.settings);
|
||||||
|
for (final (title, explanation) in [
|
||||||
|
('Citations', 'How sources and studysets'),
|
||||||
|
('Show the Pomodoro timer while studying', 'Show the focus / break'),
|
||||||
|
('Check grammar while writing', 'Underline possible spelling'),
|
||||||
|
('Search the web from the app', 'Off unless you turn it on.'),
|
||||||
|
('Jellyfin', 'Watch shows and films'),
|
||||||
|
('Count my learning in Explore', 'Explore shows how many people'),
|
||||||
|
]) {
|
||||||
|
final titled = find.widgetWithText(ExplainedTitle, title);
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
titled,
|
||||||
|
200,
|
||||||
|
scrollable: find.byType(Scrollable).first,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.textContaining(explanation), findsNothing, reason: title);
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(of: titled, matching: find.byType(HelpButton)),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.textContaining(explanation), findsOneWidget, reason: title);
|
||||||
|
await tester.tap(find.text('OK'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
expect(find.text('APA (7th edition)'), findsOneWidget, reason: 'a value');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('the citation style chosen shows on a sample at once and is '
|
testWidgets('the citation style chosen shows on a sample at once and is '
|
||||||
'kept', (tester) async {
|
'kept', (tester) async {
|
||||||
final store = MemoryDeviceSettingsStore();
|
final store = MemoryDeviceSettingsStore();
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,11 @@ class _GrantSheetState() extends State<_GrantSheet> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (grantee == null) ...[
|
if (grantee == null) ...[
|
||||||
Text(l10n.assistantPeopleTitle, style: heading),
|
ExplainedTitle(
|
||||||
|
l10n.assistantPeopleTitle,
|
||||||
|
help: l10n.assistantGrantNote,
|
||||||
|
style: heading,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (state.accounts.isNotEmpty) ...[
|
if (state.accounts.isNotEmpty) ...[
|
||||||
Wrap(
|
Wrap(
|
||||||
|
|
@ -505,7 +509,14 @@ class _GrantSheetState() extends State<_GrantSheet> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(l10n.assistantGrantBudget, style: heading),
|
ExplainedTitle(
|
||||||
|
l10n.assistantGrantBudget,
|
||||||
|
help: [
|
||||||
|
l10n.assistantGrantLimitHelper,
|
||||||
|
if (priced) l10n.assistantGrantMoneyNote,
|
||||||
|
].join(' '),
|
||||||
|
style: heading,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (priced)
|
if (priced)
|
||||||
_LimitRow(
|
_LimitRow(
|
||||||
|
|
@ -520,13 +531,6 @@ class _GrantSheetState() extends State<_GrantSheet> {
|
||||||
suffix: null,
|
suffix: null,
|
||||||
enabled: !state.busy,
|
enabled: !state.busy,
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
[
|
|
||||||
l10n.assistantGrantLimitHelper,
|
|
||||||
if (priced) l10n.assistantGrantMoneyNote,
|
|
||||||
].join(' '),
|
|
||||||
style: muted,
|
|
||||||
),
|
|
||||||
if (grantee == null && state.replaced.isNotEmpty) ...[
|
if (grantee == null && state.replaced.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_Replaced(state.replaced, currency: setUp.currency),
|
_Replaced(state.replaced, currency: setUp.currency),
|
||||||
|
|
@ -538,10 +542,6 @@ class _GrantSheetState() extends State<_GrantSheet> {
|
||||||
style: TextStyle(color: theme.colorScheme.error),
|
style: TextStyle(color: theme.colorScheme.error),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (grantee == null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(l10n.assistantGrantNote, style: muted),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
footer: FilledButton(
|
footer: FilledButton(
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class const _AssistantsView({
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
final offer = state.offer;
|
final offer = state.offer;
|
||||||
final muted = theme.textTheme.bodyMedium?.copyWith(
|
final subtitle = theme.textTheme.titleSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
);
|
);
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -141,19 +141,14 @@ class const _AssistantsView({
|
||||||
if (ownKept && (offer.ownKeys || state.own.isNotEmpty)) ...[
|
if (ownKept && (offer.ownKeys || state.own.isNotEmpty)) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Text(
|
// Why own keys go unasked, where they are not allowed,
|
||||||
|
// behind its question mark (decided 3 Nov 2026).
|
||||||
|
child: offer.ownKeys
|
||||||
|
? Text(l10n.assistantOwnTitle, style: subtitle)
|
||||||
|
: ExplainedTitle(
|
||||||
l10n.assistantOwnTitle,
|
l10n.assistantOwnTitle,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
help: l10n.assistantOwnNotAllowed(instance),
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
style: subtitle,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!offer.ownKeys)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
l10n.assistantOwnNotAllowed(instance),
|
|
||||||
style: muted,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (final own in state.own)
|
for (final own in state.own)
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,10 @@ class const _AccountSecurity() extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: const Icon(LucideIcons.keyRound),
|
leading: const Icon(LucideIcons.keyRound),
|
||||||
title: Text(l10n.passwordSectionSetUp),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.passwordSectionSetUpHint),
|
l10n.passwordSectionSetUp,
|
||||||
|
help: l10n.passwordSectionSetUpHint,
|
||||||
|
),
|
||||||
onTap: () => _setUp(context, security),
|
onTap: () => _setUp(context, security),
|
||||||
),
|
),
|
||||||
] else if (!security.finished)
|
] else if (!security.finished)
|
||||||
|
|
|
||||||
|
|
@ -731,9 +731,6 @@ class const _ProfilePage({required final String url}) extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = HalkyonAppLocalizations.of(context);
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final muted = theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
);
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -751,6 +748,9 @@ class const _ProfilePage({required final String url}) extends StatelessWidget {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// What linking back does, behind its question mark (decided
|
||||||
|
// 3 Nov 2026).
|
||||||
|
HelpButton(l10n.publicProfileVerifyHint),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: l10n.publicProfilePageCopy,
|
tooltip: l10n.publicProfilePageCopy,
|
||||||
icon: const Icon(LucideIcons.copy, size: 18),
|
icon: const Icon(LucideIcons.copy, size: 18),
|
||||||
|
|
@ -766,7 +766,6 @@ class const _ProfilePage({required final String url}) extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(l10n.publicProfileVerifyHint, style: muted),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
|
||||||
import 'package:halkyon_app/src/simplelogin/widget/keys_reset.dart';
|
import 'package:halkyon_app/src/simplelogin/widget/keys_reset.dart';
|
||||||
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart';
|
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart';
|
||||||
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_standing_scope.dart';
|
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_standing_scope.dart';
|
||||||
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
|
@ -64,8 +65,10 @@ class const _SimpleLoginSection({required final double gapAbove})
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: const Icon(LucideIcons.link),
|
leading: const Icon(LucideIcons.link),
|
||||||
title: Text(l10n.simpleLoginAddTitle),
|
title: ExplainedTitle(
|
||||||
subtitle: Text(l10n.simpleLoginAddHint),
|
l10n.simpleLoginAddTitle,
|
||||||
|
help: l10n.simpleLoginAddHint,
|
||||||
|
),
|
||||||
onTap: () => addSimpleLogin(context),
|
onTap: () => addSimpleLogin(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,37 @@ void main() {
|
||||||
await show(tester);
|
await show(tester);
|
||||||
await tap(tester, find.text('Grant assistants'));
|
await tap(tester, find.text('Grant assistants'));
|
||||||
expect(tester.widget<FilledButton>(saveButton()).onPressed, isNull);
|
expect(tester.widget<FilledButton>(saveButton()).onPressed, isNull);
|
||||||
|
// What a grant does and how its budget counts, behind the question
|
||||||
|
// marks beside People and Budget (decided 3 Nov 2026).
|
||||||
|
final note = find.textContaining('They agree once');
|
||||||
|
final counts = find.textContaining('Empty for none.');
|
||||||
|
expect(note, findsNothing);
|
||||||
|
expect(counts, findsNothing);
|
||||||
|
await tap(
|
||||||
|
tester,
|
||||||
|
find.descendant(
|
||||||
|
of: find.widgetWithText(ExplainedTitle, 'People'),
|
||||||
|
matching: find.byType(HelpButton),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(note, findsOneWidget);
|
||||||
|
await tap(tester, find.text('OK'));
|
||||||
|
await tester.ensureVisible(
|
||||||
|
find.widgetWithText(ExplainedTitle, 'Budget, for all of them together'),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tap(
|
||||||
|
tester,
|
||||||
|
find.descendant(
|
||||||
|
of: find.widgetWithText(
|
||||||
|
ExplainedTitle,
|
||||||
|
'Budget, for all of them together',
|
||||||
|
),
|
||||||
|
matching: find.byType(HelpButton),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(counts, findsOneWidget);
|
||||||
|
await tap(tester, find.text('OK'));
|
||||||
|
|
||||||
await addPerson(tester, 'anna');
|
await addPerson(tester, 'anna');
|
||||||
await addPerson(tester, 'ben');
|
await addPerson(tester, 'ben');
|
||||||
|
|
|
||||||
|
|
@ -406,14 +406,16 @@ void main() {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await show(tester);
|
await show(tester);
|
||||||
|
const why =
|
||||||
expect(
|
|
||||||
find.text(
|
|
||||||
"Whoever runs test.example.org doesn't allow own keys here, so the "
|
"Whoever runs test.example.org doesn't allow own keys here, so the "
|
||||||
"app doesn't ask these.",
|
"app doesn't ask these.";
|
||||||
),
|
|
||||||
findsOneWidget,
|
expect(find.text(why), findsNothing, reason: 'behind its question mark');
|
||||||
);
|
await tester.tap(find.byType(HelpButton));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(why), findsOneWidget);
|
||||||
|
await tester.tap(find.text('OK'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Add an assistant with your own key'), findsNothing);
|
expect(find.text('Add an assistant with your own key'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_app/testing.dart';
|
import 'package:halkyon_app/testing.dart';
|
||||||
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// A signed-in person's app as a test makes one: whose it is.
|
/// A signed-in person's app as a test makes one: whose it is.
|
||||||
|
|
@ -592,6 +593,20 @@ void main() {
|
||||||
|
|
||||||
expect(find.text('Set up a password'), findsOneWidget);
|
expect(find.text('Set up a password'), findsOneWidget);
|
||||||
expect(find.text('Change password'), findsNothing);
|
expect(find.text('Change password'), findsNothing);
|
||||||
|
|
||||||
|
// What it is, behind its question mark (decided 3 Nov 2026).
|
||||||
|
const what =
|
||||||
|
'Sign in with your email address, a password and an '
|
||||||
|
'authenticator app.';
|
||||||
|
expect(find.text(what), findsNothing);
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(
|
||||||
|
of: find.widgetWithText(ExplainedTitle, 'Set up a password'),
|
||||||
|
matching: find.byType(HelpButton),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(what), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('offers none to one without an address: a code mailed to '
|
testWidgets('offers none to one without an address: a code mailed to '
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue