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:
Tizian 「ティツィアーン」 2026-11-03 22:24:39 +01:00
commit 830aea10fe
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
10 changed files with 279 additions and 115 deletions

View file

@ -33,9 +33,10 @@ class const AssistantSettingsCard({super.key}) extends StatelessWidget {
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsAskAssistant),
subtitle: Text(l10n.settingsAskAssistantSubtitle),
isThreeLine: true,
title: ExplainedTitle(
l10n.settingsAskAssistant,
help: l10n.settingsAskAssistantSubtitle,
),
value: on,
onChanged: (on) => context.read<DeviceSettingsBloc>().add(
DeviceSettingsEventAssistantChosen(on),

View file

@ -186,15 +186,14 @@ class const _SettingsCard() extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
ExplainedTitle(
l10n.settingsDeviceDictionaryTitle,
help: l10n.settingsDeviceDictionaryAbout,
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(l10n.settingsDeviceDictionaryAbout),
const SizedBox(height: 12),
const SizedBox(height: 8),
if (dictionaries.progress case final progress?)
DeviceDictionaryProgressView(progress: progress)
else if (installed != null)

View file

@ -256,8 +256,10 @@ class _ExportSheetState() extends State<_ExportSheet> {
value: _withProgress,
onChanged: (value) =>
setState(() => _withProgress = value ?? _withProgress),
title: Text(l10n.exportWithProgress),
subtitle: Text(l10n.exportWithProgressSubtitle),
title: ExplainedTitle(
l10n.exportWithProgress,
help: l10n.exportWithProgressSubtitle,
),
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
),

View file

@ -159,15 +159,16 @@ class _JellyfinBrowserScreenState() extends State<JellyfinBrowserScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(l10n.jellyfinSubtitle),
const SizedBox(height: 4),
Text(
l10n.jellyfinPrivacy,
style: theme.textTheme.bodySmall?.copyWith(
ExplainedTitle(
l10n.jellyfinTitle,
help:
'${l10n.jellyfinSubtitle}\n\n'
'${l10n.jellyfinPrivacy}',
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
const SizedBox(height: 8),
const JellyfinConnectForm(),
],
),

View file

@ -14,22 +14,14 @@ class const JellyfinSettingsCard({super.key}) extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
ExplainedTitle(
l10n.jellyfinTitle,
help: '${l10n.jellyfinSubtitle}\n\n${l10n.jellyfinPrivacy}',
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(l10n.jellyfinSubtitle),
const SizedBox(height: 4),
Text(
l10n.jellyfinPrivacy,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
const SizedBox(height: 8),
const JellyfinAccountView(),
],
),

View file

@ -30,20 +30,14 @@ class const CitationSettingsCard({super.key}) extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
ExplainedTitle(
l10n.settingsCitations,
help: l10n.settingsCitationsDescription,
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
l10n.settingsCitationsDescription,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
const SizedBox(height: 8),
DropdownButtonFormField<CitationStyle>(
initialValue: style,
// A style's name is cut short rather than overflowing a phone.

View file

@ -177,9 +177,10 @@ class const _PrivacyCard() extends StatelessWidget {
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsCountLearning),
subtitle: Text(l10n.settingsCountLearningSubtitle),
isThreeLine: true,
title: ExplainedTitle(
l10n.settingsCountLearning,
help: l10n.settingsCountLearningSubtitle,
),
value: counted ?? false,
onChanged: counted == null
? null
@ -221,8 +222,10 @@ class const _StudyCard() extends StatelessWidget {
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsStudyPomodoroTitle),
subtitle: Text(l10n.settingsStudyPomodoroSubtitle),
title: ExplainedTitle(
l10n.settingsStudyPomodoroTitle,
help: l10n.settingsStudyPomodoroSubtitle,
),
value: context.select<DeviceSettingsBloc, bool>(
(bloc) => bloc.state.pomodoroWhileStudying,
),
@ -270,9 +273,10 @@ class _WritingCardState() extends State<_WritingCard> {
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsGrammarTitle),
subtitle: Text(l10n.settingsGrammarSubtitle),
isThreeLine: true,
title: ExplainedTitle(
l10n.settingsGrammarTitle,
help: l10n.settingsGrammarSubtitle,
),
value: context.select<DeviceSettingsBloc, bool>(
(bloc) => bloc.state.checkGrammar,
),

View file

@ -346,8 +346,10 @@ class _StudyButtonDialogState() extends State<_StudyButtonDialog> {
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.studyButtonBackFirst),
subtitle: Text(l10n.studyButtonBackFirstHelper),
title: ExplainedTitle(
l10n.studyButtonBackFirst,
help: l10n.studyButtonBackFirstHelper,
),
value: _backFirst,
onChanged: (value) =>
setState(() => _backFirst = value),

View file

@ -240,14 +240,16 @@ class const _Options({required final StudyOptions options})
value: mode,
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(switch (mode) {
LearnMode.spaced => l10n.studyModeSpaced,
LearnMode.cram => l10n.studyModeCram,
}),
subtitle: Text(switch (mode) {
LearnMode.spaced => l10n.studyModeSpacedHint,
LearnMode.cram => l10n.studyModeCramHint,
}),
title: switch (mode) {
LearnMode.spaced => ExplainedTitle(
l10n.studyModeSpaced,
help: l10n.studyModeSpacedHint,
),
LearnMode.cram => ExplainedTitle(
l10n.studyModeCram,
help: l10n.studyModeCramHint,
),
},
),
],
),
@ -265,8 +267,12 @@ class const _Options({required final StudyOptions options})
),
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(l10n.studySettingsQuizMc),
subtitle: Text(l10n.studySettingsQuizMcDescription),
title: ExplainedTitle(
l10n.studySettingsQuizMc,
help:
'${l10n.studySettingsQuizMcDescription}\n\n'
'${l10n.studySettingsAspectDescription}',
),
),
if (options.quizVocabulary) ...[
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(
value: options.typeReadings,
@ -307,8 +306,10 @@ class const _Options({required final StudyOptions options})
),
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(l10n.studySettingsTypeReading),
subtitle: Text(l10n.studySettingsTypeReadingDescription),
title: ExplainedTitle(
l10n.studySettingsTypeReading,
help: l10n.studySettingsTypeReadingDescription,
),
),
const SizedBox(height: 20),
Text(l10n.studySettingsCharacters, style: heading),
@ -318,8 +319,10 @@ class const _Options({required final StudyOptions options})
onChanged: (write) => change(options.copyWith(writeByHand: write)),
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(l10n.studySettingsWriteByHand),
subtitle: Text(l10n.studySettingsWriteByHandDescription),
title: ExplainedTitle(
l10n.studySettingsWriteByHand,
help: l10n.studySettingsWriteByHandDescription,
),
),
],
);

View file

@ -106,8 +106,10 @@ class const _HideJellyfinTags() extends StatelessWidget {
final l10n = AppLocalizations.of(context);
return SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.tagManageHideJellyfin),
subtitle: Text(l10n.tagManageHideJellyfinHint),
title: ExplainedTitle(
l10n.tagManageHideJellyfin,
help: l10n.tagManageHideJellyfinHint,
),
value: context.watch<JellyfinTagsBloc>().state ?? false,
onChanged: (hidden) => context.read<JellyfinTagsBloc>().add(
JellyfinTagsEventHidingChosen(hidden),