feat(learn): search ranks titles first; privacy changes show progress

- The Library's search lists titles starting with what is typed first,
  then titles holding it as a word, then secondary titles, rather than
  by the shelves' recency.
- The offer to make every studyset private is gone: each studyset's
  lock already says and changes it, and new ones are private.
- Making a studyset private or not from its sheet flips the switch as
  soon as the person agrees and says that everything in it is being
  written anew, rather than standing still for a minute.
- Jellyfin's password can be shown while typed.

Whats-New: Searching the library lists title matches first; changing a studyset's privacy shows its progress; Jellyfin's password can be shown.
Whats-New-De: Die Suche in der Bibliothek zeigt Treffer im Titel zuerst; das Ändern der Privatsphäre eines Lernsets zeigt den Fortschritt; das Jellyfin-Passwort lässt sich anzeigen.

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-10 22:39:10 +01:00
commit 1cc27ef0d6
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
12 changed files with 276 additions and 235 deletions

View file

@ -2077,10 +2077,6 @@
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
"cardMovePrivateAction": "Privat machen und verschieben",
"privacyKeysMissing": "Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.",
"libraryOfferPrivateTitle": "Deine Lernsets privat machen?",
"libraryOfferPrivateBody": "Neue Lernsets sind privat: Ende-zu-Ende-verschlüsselt, sodass nur deine Geräte sie lesen können. Die, die du vorher gemacht hast, bleiben für die Instanz lesbar, bis du sie auch privat machst. Kopien in den Sicherungen der Instanz bleiben lesbar, bis diese ablaufen.",
"libraryOfferPrivateAction": "Alle privat machen",
"libraryOfferPrivateLater": "Jetzt nicht",
"exportReadableNote": "Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.",
"studysetVisibilityReadable": "Nicht privat, nicht veröffentlicht",
"studysetShareReadable": "Teilen: nicht privat",
@ -2202,5 +2198,7 @@
"markdownToShareToday": "heute bis {percent} %",
"markdownDeadlinesProgress": "{furthest} %/{target} % · {when}",
"readerShowSource": "Quelltext zeigen",
"readerShowFormatted": "Formatiert zeigen"
"readerShowFormatted": "Formatiert zeigen",
"studysetSheetMakingPrivate": "Alles darin wird verschlüsselt … Das kann eine Minute dauern; du kannst das hier solange schließen.",
"studysetSheetMakingReadable": "Alles darin wird lesbar gemacht … Das kann eine Minute dauern; du kannst das hier solange schließen."
}

View file

@ -2490,10 +2490,6 @@
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
"cardMovePrivateAction": "Make private and move",
"privacyKeysMissing": "Sign in with your password on this device: what you add or change here would be private.",
"libraryOfferPrivateTitle": "Make your studysets private?",
"libraryOfferPrivateBody": "New studysets are private: end-to-end encrypted, so only your devices can read them. Those you made before stay readable by the instance until you make them private too. Copies in the instance's backups stay readable until those expire.",
"libraryOfferPrivateAction": "Make all private",
"libraryOfferPrivateLater": "Not now",
"exportReadableNote": "The archive isn't encrypted: whoever has the file can read it.",
"studysetVisibilityReadable": "Not private, not published",
"studysetShareReadable": "Share: not private",
@ -2730,5 +2726,9 @@
"readerShowSource": "Show the source text",
"@readerShowSource": {"description": "Tooltip: shows a Markdown file as written, rather than formatted"},
"readerShowFormatted": "Show it formatted",
"@readerShowFormatted": {"description": "Tooltip: shows a Markdown file formatted again, after its source text"}
"@readerShowFormatted": {"description": "Tooltip: shows a Markdown file formatted again, after its source text"},
"studysetSheetMakingPrivate": "Encrypting everything in it… This can take a minute; you can close this meanwhile.",
"@studysetSheetMakingPrivate": {"description": "Below the Private switch of a studyset's sheet while it is being made private"},
"studysetSheetMakingReadable": "Making everything in it readable… This can take a minute; you can close this meanwhile.",
"@studysetSheetMakingReadable": {"description": "Below the Private switch of a studyset's sheet while it is being made readable by the instance"}
}

View file

@ -12242,30 +12242,6 @@ abstract class AppLocalizations {
/// **'Sign in with your password on this device: what you add or change here would be private.'**
String get privacyKeysMissing;
/// No description provided for @libraryOfferPrivateTitle.
///
/// In en, this message translates to:
/// **'Make your studysets private?'**
String get libraryOfferPrivateTitle;
/// No description provided for @libraryOfferPrivateBody.
///
/// In en, this message translates to:
/// **'New studysets are private: end-to-end encrypted, so only your devices can read them. Those you made before stay readable by the instance until you make them private too. Copies in the instance\'s backups stay readable until those expire.'**
String get libraryOfferPrivateBody;
/// No description provided for @libraryOfferPrivateAction.
///
/// In en, this message translates to:
/// **'Make all private'**
String get libraryOfferPrivateAction;
/// No description provided for @libraryOfferPrivateLater.
///
/// In en, this message translates to:
/// **'Not now'**
String get libraryOfferPrivateLater;
/// No description provided for @exportReadableNote.
///
/// In en, this message translates to:
@ -12997,6 +12973,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Show it formatted'**
String get readerShowFormatted;
/// Below the Private switch of a studyset's sheet while it is being made private
///
/// In en, this message translates to:
/// **'Encrypting everything in it… This can take a minute; you can close this meanwhile.'**
String get studysetSheetMakingPrivate;
/// Below the Private switch of a studyset's sheet while it is being made readable by the instance
///
/// In en, this message translates to:
/// **'Making everything in it readable… This can take a minute; you can close this meanwhile.'**
String get studysetSheetMakingReadable;
}
class _AppLocalizationsDelegate

View file

@ -7888,19 +7888,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get privacyKeysMissing =>
'Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.';
@override
String get libraryOfferPrivateTitle => 'Deine Lernsets privat machen?';
@override
String get libraryOfferPrivateBody =>
'Neue Lernsets sind privat: Ende-zu-Ende-verschlüsselt, sodass nur deine Geräte sie lesen können. Die, die du vorher gemacht hast, bleiben für die Instanz lesbar, bis du sie auch privat machst. Kopien in den Sicherungen der Instanz bleiben lesbar, bis diese ablaufen.';
@override
String get libraryOfferPrivateAction => 'Alle privat machen';
@override
String get libraryOfferPrivateLater => 'Jetzt nicht';
@override
String get exportReadableNote =>
'Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.';
@ -8323,4 +8310,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get readerShowFormatted => 'Formatiert zeigen';
@override
String get studysetSheetMakingPrivate =>
'Alles darin wird verschlüsselt … Das kann eine Minute dauern; du kannst das hier solange schließen.';
@override
String get studysetSheetMakingReadable =>
'Alles darin wird lesbar gemacht … Das kann eine Minute dauern; du kannst das hier solange schließen.';
}

View file

@ -7816,19 +7816,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get privacyKeysMissing =>
'Sign in with your password on this device: what you add or change here would be private.';
@override
String get libraryOfferPrivateTitle => 'Make your studysets private?';
@override
String get libraryOfferPrivateBody =>
'New studysets are private: end-to-end encrypted, so only your devices can read them. Those you made before stay readable by the instance until you make them private too. Copies in the instance\'s backups stay readable until those expire.';
@override
String get libraryOfferPrivateAction => 'Make all private';
@override
String get libraryOfferPrivateLater => 'Not now';
@override
String get exportReadableNote =>
'The archive isn\'t encrypted: whoever has the file can read it.';
@ -8249,6 +8236,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get readerShowFormatted => 'Show it formatted';
@override
String get studysetSheetMakingPrivate =>
'Encrypting everything in it… This can take a minute; you can close this meanwhile.';
@override
String get studysetSheetMakingReadable =>
'Making everything in it readable… This can take a minute; you can close this meanwhile.';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).

View file

@ -80,7 +80,6 @@ import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_reposit
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/sync_review_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/private_offer_store.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
@ -635,9 +634,6 @@ Future<PersonDependencies> _openPersonData(
File(p.join(folder.path, 'pomodoro.json')),
),
studysetTabs: studysetTabs,
privateOffer: FilePrivateOfferStore(
File(p.join(folder.path, 'private_offer')),
),
publishing: publishing,
explore: ExploreRepository(
ServerpodExploreGateway(client),

View file

@ -21,7 +21,6 @@ import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_sto
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/private_offer_store.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
@ -106,10 +105,6 @@ class PersonDependencies({
/// The tab each of the person's studysets was left on, on this device.
required final StudysetTabs studysetTabs,
/// Whether the library's offer to make every studyset private was
/// answered on this device (ADR 0019, D4).
required final PrivateOfferStore privateOffer,
/// Publishing the person's studysets, on the instance.
required final PublishRepository publishing,

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_gateway.dart';
@ -40,6 +41,9 @@ class _JellyfinConnectFormState() extends State<JellyfinConnectForm> {
final _address = TextEditingController();
final _user = TextEditingController();
final _password = TextEditingController();
/// Whether the password is shown as typed.
var _passwordShown = false;
final _passwordFocus = FocusNode();
JellyfinServer? _server;
List<JellyfinPublicUser> _publicUsers = const [];
@ -289,13 +293,25 @@ class _JellyfinConnectFormState() extends State<JellyfinConnectForm> {
focusNode: _passwordFocus,
maxLength: TextLimits.phrase,
buildCounter: nearLimitCounter,
obscureText: true,
obscureText: !_passwordShown,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
textInputAction: TextInputAction.go,
decoration: InputDecoration(
labelText: l10n.jellyfinPassword,
helperText: l10n.jellyfinPasswordNone,
border: const OutlineInputBorder(),
suffixIcon: IconButton(
tooltip: _passwordShown
? HalkyonAppLocalizations.of(context).passwordHide
: HalkyonAppLocalizations.of(context).passwordShow,
icon: Icon(
_passwordShown ? LucideIcons.eyeOff : LucideIcons.eye,
),
onPressed: () =>
setState(() => _passwordShown = !_passwordShown),
),
),
onSubmitted: _busy ? null : (_) => _signIn(),
),

View file

@ -20,7 +20,6 @@ import 'package:halkyon_learn_flutter/src/feature/study/widget/start_studying.da
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/library_shelves.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/subject.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
@ -507,8 +506,35 @@ class _GridState() extends State<_Grid> {
);
static bool _matches(Studyset studyset, String query) =>
studyset.content.title.toLowerCase().contains(query) ||
(studyset.content.secondaryTitle?.toLowerCase().contains(query) ?? false);
_rankOf(studyset, query) != null;
/// How well [studyset] matches [query], lower first: its title starting
/// with it, a word of its title starting with it, its title holding it,
/// then its secondary title alike; null for no match.
static int? _rankOf(Studyset studyset, String query) {
int? rankIn(String? text) {
final lower = text?.toLowerCase();
if (lower == null || !lower.contains(query)) {
return null;
}
if (lower.startsWith(query)) {
return 0;
}
return lower.contains(
RegExp(r'(^|[^\p{L}\p{N}])' + RegExp.escape(query), unicode: true),
)
? 1
: 2;
}
if (rankIn(studyset.content.title) case final rank?) {
return rank;
}
if (rankIn(studyset.content.secondaryTitle) case final rank?) {
return 3 + rank;
}
return null;
}
/// The studysets out on Explore, as last known.
var _published = const <String>{};
@ -540,6 +566,26 @@ class _GridState() extends State<_Grid> {
progress: widget.progress,
now: clock.now().toUtc(),
);
// A search lists its matches by how well they match, in one grid, the
// shelves' order within the same rank.
final ranked = searching
? ([
for (final (index, studyset) in [
for (final listed in shelves.values) ...listed,
].indexed)
(
rank: _rankOf(studyset, query)!,
index: index,
studyset: studyset,
),
]..sort(
(a, b) => a.rank != b.rank
? a.rank.compareTo(b.rank)
: a.index.compareTo(b.index),
))
.map((match) => match.studyset)
.toList()
: const <Studyset>[];
final archived = [
for (final studyset in widget.studysets)
if (studyset.isArchived && listed(studyset)) studyset,
@ -567,30 +613,42 @@ class _GridState() extends State<_Grid> {
hasScrollBody: false,
child: _Message(l10n.librarySearchNoMatches),
),
if (!searching)
SliverToBoxAdapter(
child: _PrivateOffer(studysets: widget.studysets),
),
SliverToBoxAdapter(
child: _SubjectDropdown(subjects: inUse, selected: subject),
),
for (final MapEntry(key: shelf, value: listed)
in shelves.entries) ...[
SliverToBoxAdapter(child: _ShelfHeading(shelf)),
if (searching && ranked.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.fromLTRB(
_spacing,
0,
_spacing,
_spacing,
_spacing,
),
sliver: SliverGrid.builder(
gridDelegate: _layout,
itemCount: listed.length,
itemBuilder: (context, index) => _card(context, listed[index]),
itemCount: ranked.length,
itemBuilder: (context, index) => _card(context, ranked[index]),
),
),
],
if (!searching)
for (final MapEntry(key: shelf, value: listed)
in shelves.entries) ...[
SliverToBoxAdapter(child: _ShelfHeading(shelf)),
SliverPadding(
padding: const EdgeInsets.fromLTRB(
_spacing,
0,
_spacing,
_spacing,
),
sliver: SliverGrid.builder(
gridDelegate: _layout,
itemCount: listed.length,
itemBuilder: (context, index) =>
_card(context, listed[index]),
),
),
],
if (archived.isNotEmpty) ...[
SliverToBoxAdapter(
child: _SectionHeader(
@ -742,142 +800,6 @@ class _GridState() extends State<_Grid> {
}
/// What Recently deleted keeps, and for how long.
/// Offers once, on this device, to make every studyset that isn't private
/// private (ADR 0019, D4): new ones are already, and those from before
/// stay readable until the person chooses. Shown only where this device
/// can encrypt and some studyset isn't private; gone once answered.
class const _PrivateOffer({required final List<Studyset> studysets})
extends StatefulWidget {
@override
State<_PrivateOffer> createState() => _PrivateOfferState();
}
class _PrivateOfferState() extends State<_PrivateOffer> {
/// Whether it was answered; null until read.
bool? _answered;
var _busy = false;
@override
void initState() {
super.initState();
unawaited(() async {
final person = PersonDependencies.of(context);
// Answered on this device, or on another of the person's (the
// reading of 22 Oct: the answer follows them).
final answered =
await person.privateOffer.answered() ||
(await person.settings.read()).privateOfferAnswered;
if (mounted) {
setState(() => _answered = answered);
}
}());
}
List<Studyset> get _readable => [
for (final studyset in widget.studysets)
if (studyset.parentId == null &&
!studyset.isPrivate &&
!studyset.isLocked)
studyset,
];
Future<void> _answer({required bool makePrivate}) async {
final person = PersonDependencies.of(context);
final messenger = ScaffoldMessenger.of(context);
final l10n = AppLocalizations.of(context);
setState(() => _busy = true);
try {
if (makePrivate) {
// Every top-level studyset, those waiting in Recently deleted and
// those whose parent is gone among them (the reading of 22 Oct).
var left = 0;
for (final studyset in topLevel(await person.studysets.withDeleted())) {
if (!studyset.isPrivate && !studyset.isLocked) {
left += await person.studysets.setPrivate(
studyset.id,
private: true,
);
}
}
if (left > 0) {
messenger.showSnackBar(
SnackBar(content: Text(l10n.studysetPrivateSomeLocked)),
);
}
}
await person.privateOffer.answer();
await person.settings.update(
(settings) => settings.copyWith(privateOfferAnswered: true),
);
} finally {
if (mounted) {
setState(() {
_busy = false;
_answered = true;
});
}
}
}
@override
Widget build(BuildContext context) {
final person = PersonDependencies.of(context);
if (_answered != false ||
!person.studysets.canMakePrivate ||
_readable.isEmpty) {
return const SizedBox.shrink();
}
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
child: Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(LucideIcons.lock),
const SizedBox(width: 12),
Expanded(
child: Text(
l10n.libraryOfferPrivateTitle,
style: theme.textTheme.titleMedium,
),
),
],
),
const SizedBox(height: 8),
Text(l10n.libraryOfferPrivateBody),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.end,
children: [
TextButton(
onPressed: _busy ? null : () => _answer(makePrivate: false),
child: Text(l10n.libraryOfferPrivateLater),
),
FilledButton(
onPressed: _busy ? null : () => _answer(makePrivate: true),
child: _busy
? const ButtonLoadingIndicator()
: Text(l10n.libraryOfferPrivateAction),
),
],
),
],
),
),
),
);
}
}
class const _DeletedNote() extends StatelessWidget {
@override
Widget build(BuildContext context) {

View file

@ -275,7 +275,12 @@ Future<void> editStudyset(
// at once, once the person agreed.
private: parentId == null ? studyset.isPrivate : null,
onPrivate: parentId == null
? (private) => setStudysetPrivacy(context, studyset, private: private)
? (private, agreed) => setStudysetPrivacy(
context,
studyset,
private: private,
onAgreed: agreed,
)
: null,
insideAnother: parentId != null,
inheritedColorName: parentId == null ? null : colorNameOf(parentId, all),

View file

@ -10,11 +10,13 @@ import 'package:material_ui/material_ui.dart';
/// readable by the instance again (ADR 0019), once the person agreed: the
/// dialog says what changes, and that the instance's backups keep readable
/// copies until they expire. Where this device can't encrypt (an account
/// whose keys aren't here), says how to. Whether it was changed.
/// whose keys aren't here), says how to. Calls [onAgreed] as the person
/// agrees, before the rewrite, which takes a while. Whether it was changed.
Future<bool> setStudysetPrivacy(
BuildContext context,
Studyset studyset, {
required bool private,
VoidCallback? onAgreed,
}) async {
final l10n = AppLocalizations.of(context);
if (private && !PersonDependencies.of(context).studysets.canMakePrivate) {
@ -54,6 +56,7 @@ Future<bool> setStudysetPrivacy(
if (confirmed != true || !context.mounted) {
return false;
}
onAgreed?.call();
await applyStudysetPrivacy(context, studyset, private: private);
return true;
}

View file

@ -44,8 +44,9 @@ Future<StudysetContent?> showStudysetSheet(
bool? private,
/// Makes it private or not at once, once the person agreed, rather than
/// on Save, which the content alone waits for; whether it was changed.
Future<bool> Function(bool private)? onPrivate,
/// on Save, which the content alone waits for, calling `agreed` as the
/// person agrees; whether it was changed.
Future<bool> Function(bool private, VoidCallback agreed)? onPrivate,
}) => showAdaptiveSheet<StudysetContent>(
context,
// Swiping would discard changes without asking.
@ -73,7 +74,7 @@ class const _StudysetSheet({
required final StudysetLicense inheritedLicense,
final String? studysetId,
final bool? private,
final Future<bool> Function(bool private)? onPrivate,
final Future<bool> Function(bool private, VoidCallback agreed)? onPrivate,
}) extends StatefulWidget {
@override
State<_StudysetSheet> createState() => _StudysetSheetState();
@ -129,13 +130,40 @@ class _StudysetSheetState() extends State<_StudysetSheet> {
late String? _frontLanguage = widget.initial?.frontLanguage;
late String? _backLanguage = widget.initial?.backLanguage;
/// Whether the studyset is private now, as last changed here.
/// Whether the studyset is private now, as last changed here: as soon
/// as the person agreed, while everything in it is written anew.
late var _private = widget.private;
/// Whether its privacy is being changed: every row of its tree written
/// anew, a minute for thousands of cards (the developer's review, 10 Nov
/// 2026: the switch stood still meanwhile, as if nothing happened).
var _changing = false;
Future<void> _setPrivate(bool private) async {
if (await widget.onPrivate?.call(private) ?? false) {
final before = _private;
try {
final changed =
await widget.onPrivate?.call(private, () {
if (mounted) {
setState(() {
_private = private;
_changing = true;
});
}
}) ??
false;
if (mounted && !changed) {
setState(() => _private = before);
}
} on Object {
// Cut short, as it stood: the rewrite said why.
if (mounted) {
setState(() => _private = private);
setState(() => _private = before);
}
rethrow;
} finally {
if (mounted) {
setState(() => _changing = false);
}
}
}
@ -573,8 +601,23 @@ class _StudysetSheetState() extends State<_StudysetSheet> {
),
title: Text(l10n.studysetSheetPrivate),
value: private,
onChanged: _setPrivate,
onChanged: _changing ? null : _setPrivate,
),
if (_changing)
Row(
children: [
const ButtonLoadingIndicator(),
const SizedBox(width: 12),
Expanded(
child: Text(
private
? l10n.studysetSheetMakingPrivate
: l10n.studysetSheetMakingReadable,
style: theme.textTheme.bodySmall,
),
),
],
),
],
const SizedBox(height: 16),
],