feat(studyset): a studyset's marker says what the instance sees of it (ADR 0019)

The developer's decision of 21 Oct, on the library's cards and the
studyset's Share in its action row alike:
- a lock: private, end-to-end encrypted;
- an open lock: readable by the instance, not published;
- a globe: published.

The lock and the open lock open one dialog saying what that means and
changing it (Make non-private; Make private, or Publish…, which opens the
Publish sheet, while it isn't private); a topic's dialog says its privacy
is its tree's. The globe opens the Publish sheet, whose status for a
studyset not published now says so ("Not published") with an open lock,
as "Private" means something else now. Texts in en and de.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 23:07:31 +02:00
commit 9fb1ab2f6c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
12 changed files with 425 additions and 48 deletions

View file

@ -1600,7 +1600,7 @@
"studysetSharePrivate": "Teilen: privat",
"studysetSharePublished": "Teilen: veröffentlicht",
"publishTitle": "Veröffentlichen",
"publishPrivate": "Privat · nur du siehst es",
"publishPrivate": "Nicht veröffentlicht · nur du siehst es in Learn",
"publishPublished": "Veröffentlicht · v{version} · {date}",
"publishIntro": "Eine Veröffentlichung ist dieses Lernset mit allem darin, so wie es jetzt ist. Andere können es finden, damit lernen und es kopieren. Jede Veröffentlichung bleibt, wie sie war; spätere Änderungen kommen in die nächste.",
"publishAnonymous": "Melde dich an, um ein öffentliches Profil zu haben und zu veröffentlichen.",
@ -2079,5 +2079,13 @@
"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."
"exportReadableNote": "Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.",
"studysetVisibilityReadable": "Nicht privat, nicht veröffentlicht",
"studysetShareReadable": "Teilen: nicht privat",
"studysetPrivacyPrivateTitle": "„{title}“ ist privat",
"studysetPrivacyPrivateBody": "Alles darin ist Ende-zu-Ende-verschlüsselt: Nur deine Geräte können es lesen. Die Instanz kann es weder veröffentlichen noch Claude zeigen. Nicht mehr privat, wird es entschlüsselt und neu synchronisiert, sodass die Instanz es lesen kann.",
"studysetPrivacyReadableTitle": "„{title}“ ist nicht privat",
"studysetPrivacyReadableBody": "Die Instanz kann es lesen: Du kannst es veröffentlichen, und Claude kann es über den Connector lesen. Privat wird es auf deinen Geräten Ende-zu-Ende-verschlüsselt; Kopien in den Sicherungen der Instanz bleiben lesbar, bis diese ablaufen.",
"studysetPrivacyTopic": "Seine Privatsphäre ist die des Lernsets, in dem es liegt.",
"studysetPrivacyPublish": "Veröffentlichen …"
}

View file

@ -1883,7 +1883,7 @@
"studysetSharePrivate": "Share: private",
"studysetSharePublished": "Share: published",
"publishTitle": "Publish",
"publishPrivate": "Private · only you can see it",
"publishPrivate": "Not published · only you see it in Learn",
"publishPublished": "Published · v{version} · {date}",
"@publishPublished": {"placeholders": {"version": {"type": "int"}, "date": {"type": "String"}}},
"publishIntro": "A release is this studyset with everything inside it, as it is now. Others can find it, learn from it and copy it. Each release stays as it was; later changes go into the next one.",
@ -2445,5 +2445,15 @@
"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."
"exportReadableNote": "The archive isn't encrypted: whoever has the file can read it.",
"studysetVisibilityReadable": "Not private, not published",
"studysetShareReadable": "Share: not private",
"studysetPrivacyPrivateTitle": "“{title}” is private",
"@studysetPrivacyPrivateTitle": {"placeholders": {"title": {"type": "String"}}},
"studysetPrivacyPrivateBody": "Everything in it is end-to-end encrypted: only your devices can read it. The instance can't publish it, nor show it to Claude. Making it non-private decrypts it and syncs it again, so the instance can read it.",
"studysetPrivacyReadableTitle": "“{title}” isn't private",
"@studysetPrivacyReadableTitle": {"placeholders": {"title": {"type": "String"}}},
"studysetPrivacyReadableBody": "The instance can read it: you can publish it, and Claude can read it through the connector. Making it private encrypts it end to end on your devices; copies in the instance's backups stay readable until those expire.",
"studysetPrivacyTopic": "Its privacy is that of the studyset it's in.",
"studysetPrivacyPublish": "Publish…"
}

View file

@ -9468,7 +9468,7 @@ abstract class AppLocalizations {
/// No description provided for @publishPrivate.
///
/// In en, this message translates to:
/// **'Private · only you can see it'**
/// **'Not published · only you see it in Learn'**
String get publishPrivate;
/// No description provided for @publishPublished.
@ -12266,6 +12266,54 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'The archive isn\'t encrypted: whoever has the file can read it.'**
String get exportReadableNote;
/// No description provided for @studysetVisibilityReadable.
///
/// In en, this message translates to:
/// **'Not private, not published'**
String get studysetVisibilityReadable;
/// No description provided for @studysetShareReadable.
///
/// In en, this message translates to:
/// **'Share: not private'**
String get studysetShareReadable;
/// No description provided for @studysetPrivacyPrivateTitle.
///
/// In en, this message translates to:
/// **'“{title}” is private'**
String studysetPrivacyPrivateTitle(String title);
/// No description provided for @studysetPrivacyPrivateBody.
///
/// In en, this message translates to:
/// **'Everything in it is end-to-end encrypted: only your devices can read it. The instance can\'t publish it, nor show it to Claude. Making it non-private decrypts it and syncs it again, so the instance can read it.'**
String get studysetPrivacyPrivateBody;
/// No description provided for @studysetPrivacyReadableTitle.
///
/// In en, this message translates to:
/// **'“{title}” isn\'t private'**
String studysetPrivacyReadableTitle(String title);
/// No description provided for @studysetPrivacyReadableBody.
///
/// In en, this message translates to:
/// **'The instance can read it: you can publish it, and Claude can read it through the connector. Making it private encrypts it end to end on your devices; copies in the instance\'s backups stay readable until those expire.'**
String get studysetPrivacyReadableBody;
/// No description provided for @studysetPrivacyTopic.
///
/// In en, this message translates to:
/// **'Its privacy is that of the studyset it\'s in.'**
String get studysetPrivacyTopic;
/// No description provided for @studysetPrivacyPublish.
///
/// In en, this message translates to:
/// **'Publish…'**
String get studysetPrivacyPublish;
}
class _AppLocalizationsDelegate

View file

@ -5951,7 +5951,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get publishTitle => 'Veröffentlichen';
@override
String get publishPrivate => 'Privat · nur du siehst es';
String get publishPrivate =>
'Nicht veröffentlicht · nur du siehst es in Learn';
@override
String publishPublished(int version, String date) {
@ -7772,4 +7773,35 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get exportReadableNote =>
'Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.';
@override
String get studysetVisibilityReadable => 'Nicht privat, nicht veröffentlicht';
@override
String get studysetShareReadable => 'Teilen: nicht privat';
@override
String studysetPrivacyPrivateTitle(String title) {
return '„$title“ ist privat';
}
@override
String get studysetPrivacyPrivateBody =>
'Alles darin ist Ende-zu-Ende-verschlüsselt: Nur deine Geräte können es lesen. Die Instanz kann es weder veröffentlichen noch Claude zeigen. Nicht mehr privat, wird es entschlüsselt und neu synchronisiert, sodass die Instanz es lesen kann.';
@override
String studysetPrivacyReadableTitle(String title) {
return '„$title“ ist nicht privat';
}
@override
String get studysetPrivacyReadableBody =>
'Die Instanz kann es lesen: Du kannst es veröffentlichen, und Claude kann es über den Connector lesen. Privat wird es auf deinen Geräten Ende-zu-Ende-verschlüsselt; Kopien in den Sicherungen der Instanz bleiben lesbar, bis diese ablaufen.';
@override
String get studysetPrivacyTopic =>
'Seine Privatsphäre ist die des Lernsets, in dem es liegt.';
@override
String get studysetPrivacyPublish => 'Veröffentlichen …';
}

View file

@ -5898,7 +5898,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get publishTitle => 'Publish';
@override
String get publishPrivate => 'Private · only you can see it';
String get publishPrivate => 'Not published · only you see it in Learn';
@override
String publishPublished(int version, String date) {
@ -7700,6 +7700,37 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get exportReadableNote =>
'The archive isn\'t encrypted: whoever has the file can read it.';
@override
String get studysetVisibilityReadable => 'Not private, not published';
@override
String get studysetShareReadable => 'Share: not private';
@override
String studysetPrivacyPrivateTitle(String title) {
return '“$title” is private';
}
@override
String get studysetPrivacyPrivateBody =>
'Everything in it is end-to-end encrypted: only your devices can read it. The instance can\'t publish it, nor show it to Claude. Making it non-private decrypts it and syncs it again, so the instance can read it.';
@override
String studysetPrivacyReadableTitle(String title) {
return '“$title” isn\'t private';
}
@override
String get studysetPrivacyReadableBody =>
'The instance can read it: you can publish it, and Claude can read it through the connector. Making it private encrypts it end to end on your devices; copies in the instance\'s backups stay readable until those expire.';
@override
String get studysetPrivacyTopic =>
'Its privacy is that of the studyset it\'s in.';
@override
String get studysetPrivacyPublish => 'Publish…';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).

View file

@ -425,7 +425,7 @@ class const _Status({required final Publication? publication})
contentPadding: EdgeInsets.zero,
leading: Icon(switch (publication) {
Publication(withdrawn: true) => LucideIcons.eyeOff,
_ when latest == null => LucideIcons.lock,
_ when latest == null => LucideIcons.lockOpen,
_ => LucideIcons.globe,
}),
title: Text(switch (latest) {

View file

@ -5,14 +5,16 @@ import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.
import 'package:halkyon_learn_flutter/src/feature/publish/bloc/publish_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/widget/publish_sheet.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_privacy.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Share, in a studyset's action row beside Study and Add material: its
/// icon says whether the studyset is published (a globe) or private (a
/// lock), and it opens the Publish sheet. Only an icon below medium
/// windows, so the row fits a phone's app bar.
/// icon says what the instance sees of the studyset, as the library's cards
/// do ([visibilityIcon]), and opens what explains and changes it
/// ([openVisibility]). Only an icon below medium windows, so the row fits a
/// phone's app bar.
///
/// Only a person with an identity can publish; for anyone else the sheet
/// says how to have one, and the instance isn't asked.
@ -48,6 +50,45 @@ class const ShareButton({
!state.publication!.withdrawn;
}
/// What a studyset's marker shows (decided 21 Oct 2026): a globe once it
/// is published, a lock while it is private (end-to-end encrypted, ADR
/// 0019), an open lock while the instance can read it and it isn't
/// published.
IconData visibilityIcon(Studyset studyset, {required bool published}) =>
published
? LucideIcons.globe
: studyset.isPrivate
? LucideIcons.lock
: LucideIcons.lockOpen;
/// What a studyset's marker says of it, as [visibilityIcon] shows it.
String visibilityLabel(
AppLocalizations l10n,
Studyset studyset, {
required bool published,
}) => published
? l10n.studysetVisibilityPublic
: studyset.isPrivate
? l10n.studysetVisibilityPrivate
: l10n.studysetVisibilityReadable;
/// Opens what explains [studyset]'s marker and changes it: the Publish
/// sheet once it is [published]; else the privacy dialog, which makes it
/// private or not and, while it isn't private, leads to publishing it
/// ([openPublish], the Publish sheet).
Future<void> openVisibility(
BuildContext context, {
required Studyset studyset,
required bool published,
required VoidCallback openPublish,
}) async {
if (published) {
openPublish();
return;
}
await showStudysetPrivacy(context, studyset, onPublish: openPublish);
}
/// Opens the Publish sheet of [studyset] where no Share button is, as from
/// a card in the library: with a [PublishBloc] of its own while it is open.
Future<void> showShareSheet(
@ -99,13 +140,21 @@ class const _Button({
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final icon = published ? LucideIcons.globe : LucideIcons.lock;
void open() => showPublishSheet(context, studyset: studyset, all: all);
final icon = visibilityIcon(studyset, published: published);
void open() => openVisibility(
context,
studyset: studyset,
published: published,
openPublish: () =>
showPublishSheet(context, studyset: studyset, all: all),
);
if (!context.windowSize.isAtLeastMedium) {
return IconButton(
tooltip: published
? l10n.studysetSharePublished
: l10n.studysetSharePrivate,
: studyset.isPrivate
? l10n.studysetSharePrivate
: l10n.studysetShareReadable,
icon: Icon(icon),
onPressed: open,
);

View file

@ -584,12 +584,21 @@ class _GridState() extends State<_Grid> {
studyset: studyset,
progress: widget.progress[studyset.id],
published: _published.contains(studyset.id),
onShare: () async {
final all = await PersonDependencies.of(this.context).studysets.all();
if (mounted) {
await showShareSheet(this.context, studyset: studyset, all: all);
}
},
// A globe opens the Publish sheet; a lock, open or not, what its
// privacy means, which leads there too while it isn't private.
onShare: () => openVisibility(
this.context,
studyset: studyset,
published: _published.contains(studyset.id),
openPublish: () async {
final all = await PersonDependencies.of(
this.context,
).studysets.all();
if (mounted) {
await showShareSheet(this.context, studyset: studyset, all: all);
}
},
),
// Within the library tab, so its back button returns here.
onOpen: () => context.go(RoutePaths.studyset(studyset.id)),
onStudy: () async {

View file

@ -2,6 +2,7 @@ import 'package:flutter/services.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_colors.dart';
import 'package:halkyon_learn_flutter/src/core/widget/content_counts_line.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/widget/share_button.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_actions.dart';
@ -347,10 +348,8 @@ class const _BottomBar({
),
),
IconButton(
icon: Icon(published ? LucideIcons.globe : LucideIcons.lock),
tooltip: published
? l10n.studysetVisibilityPublic
: l10n.studysetVisibilityPrivate,
icon: Icon(visibilityIcon(studyset, published: published)),
tooltip: visibilityLabel(l10n, studyset, published: published),
onPressed: onShare,
),
StudysetOptionsButton(

View file

@ -1,6 +1,7 @@
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Makes the top-level [studyset] private, with everything inside it, or
@ -14,10 +15,8 @@ Future<void> setStudysetPrivacy(
required bool private,
}) async {
final l10n = AppLocalizations.of(context);
final studysets = PersonDependencies.of(context).studysets;
final messenger = ScaffoldMessenger.of(context);
if (private && !studysets.canMakePrivate) {
messenger.showSnackBar(
if (private && !PersonDependencies.of(context).studysets.canMakePrivate) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.studysetPrivateNoKeys)),
);
return;
@ -50,7 +49,27 @@ Future<void> setStudysetPrivacy(
],
),
);
if (confirmed != true) {
if (confirmed != true || !context.mounted) {
return;
}
await applyStudysetPrivacy(context, studyset, private: private);
}
/// Makes the top-level [studyset] private or not at once, the person
/// having agreed where it was explained; says when some of it couldn't be
/// opened to do so.
Future<void> applyStudysetPrivacy(
BuildContext context,
Studyset studyset, {
required bool private,
}) async {
final l10n = AppLocalizations.of(context);
final studysets = PersonDependencies.of(context).studysets;
final messenger = ScaffoldMessenger.of(context);
if (private && !studysets.canMakePrivate) {
messenger.showSnackBar(
SnackBar(content: Text(l10n.studysetPrivateNoKeys)),
);
return;
}
final locked = await studysets.setPrivate(studyset.id, private: private);
@ -60,3 +79,86 @@ Future<void> setStudysetPrivacy(
);
}
}
/// What the person may do from [showStudysetPrivacy].
enum PrivacyChoice() {
makePrivate,
makeReadable,
publish,
}
/// Says whether [studyset] is private (ADR 0019) and what that means, as
/// the lock beside it opens it: a private one can be made non-private, one
/// that isn't private made private or published ([PrivacyChoice.publish],
/// for the caller to open the Publish sheet). A topic's privacy is its
/// tree's, which it only says.
Future<void> showStudysetPrivacy(
BuildContext context,
Studyset studyset, {
required VoidCallback onPublish,
}) async {
final l10n = AppLocalizations.of(context);
final title = studyset.content.title;
final topLevel = studyset.parentId == null;
final choice = await showDialog<PrivacyChoice>(
context: context,
builder: (context) => AlertDialog(
icon: Icon(
studyset.isPrivate ? LucideIcons.lock : LucideIcons.lockOpen,
),
title: Text(
studyset.isPrivate
? l10n.studysetPrivacyPrivateTitle(title)
: l10n.studysetPrivacyReadableTitle(title),
),
content: Text(
[
if (studyset.isPrivate)
l10n.studysetPrivacyPrivateBody
else
l10n.studysetPrivacyReadableBody,
if (!topLevel) l10n.studysetPrivacyTopic,
].join('\n\n'),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
),
if (!studyset.isPrivate)
TextButton(
onPressed: () => Navigator.of(context).pop(PrivacyChoice.publish),
child: Text(l10n.studysetPrivacyPublish),
),
if (topLevel)
FilledButton(
onPressed: () => Navigator.of(context).pop(
studyset.isPrivate
? PrivacyChoice.makeReadable
: PrivacyChoice.makePrivate,
),
child: Text(
studyset.isPrivate
? l10n.studysetMenuMakeReadable
: l10n.studysetMenuMakePrivate,
),
),
],
),
);
if (!context.mounted) {
return;
}
switch (choice) {
case PrivacyChoice.publish:
onPublish();
case PrivacyChoice.makePrivate || PrivacyChoice.makeReadable:
await applyStudysetPrivacy(
context,
studyset,
private: choice == PrivacyChoice.makePrivate,
);
case null:
break;
}
}

View file

@ -90,19 +90,30 @@ void main() {
await tester.pumpAndSettle();
}
/// Opens the Publish sheet from Share: at once once published, else
/// through what its privacy means, which leads there while it isn't
/// private (ADR 0019).
Future<void> openSheet(WidgetTester tester) async {
await tester.tap(find.text('Share'));
await tester.pumpAndSettle();
if (find.text('Publish…').evaluate().isNotEmpty) {
await tester.tap(find.text('Publish…'));
await tester.pumpAndSettle();
}
}
testWidgets('a private studyset is published under a name made from its '
"title, at its author's handle", (tester) async {
testWidgets('a studyset not published is published under a name made '
"from its title, at its author's handle", (tester) async {
stored('japanese', 'Japanese', license: 'CC-BY-SA-4.0');
await showStudyset(tester, 'japanese');
expect(find.byIcon(LucideIcons.lock), findsOneWidget);
// Readable by the instance and not published: an open lock.
expect(find.byIcon(LucideIcons.lockOpen), findsOneWidget);
await openSheet(tester);
expect(find.text('Private · only you can see it'), findsOneWidget);
expect(
find.text('Not published · only you see it in Learn'),
findsOneWidget,
);
expect(
find.text('Found at halkyon.eu/@raisondetredev/japanese'),
findsOneWidget,
@ -362,16 +373,20 @@ void main() {
expect(publishing.publishes, hasLength(1));
});
testWidgets('on a phone it is an icon that says the studyset is private', (
tester,
) async {
testWidgets('on a phone it is an icon that says the studyset is not '
'private, leading to publishing it', (tester) async {
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
await showStudyset(tester, 'japanese', width: 400);
expect(find.byTooltip('Share: private'), findsOneWidget);
await tester.tap(find.byTooltip('Share: private'));
expect(find.byTooltip('Share: not private'), findsOneWidget);
await tester.tap(find.byTooltip('Share: not private'));
await tester.pumpAndSettle();
expect(find.text('Private · only you can see it'), findsOneWidget);
await tester.tap(find.text('Publish…'));
await tester.pumpAndSettle();
expect(
find.text('Not published · only you see it in Learn'),
findsOneWidget,
);
});
testWidgets('an anonymous person is told how to publish, and the instance '

View file

@ -16,6 +16,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_conten
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_card.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
import '../../../support/fake_card_gateway.dart';
@ -85,6 +86,7 @@ void main() {
DateTime? archivedAt,
DateTime? starredAt,
String? publishedAt,
bool private = false,
}) => gateway.receiveRemoteChange(
id,
encodeStudysetPayload(
@ -100,6 +102,7 @@ void main() {
),
archivedAt: archivedAt,
starredAt: starredAt,
isPrivate: private,
);
/// [finder] within the card of the studyset titled [title].
@ -796,11 +799,13 @@ void main() {
expect(onCard('Kanji', find.text('By @alice')), findsOneWidget);
});
testWidgets('a lock while private, a globe once out on Explore, as the '
'instance says', (tester) async {
testWidgets('a lock while private, an open lock while the instance can '
'read it and it is not published, a globe once out on Explore, as '
'the instance says', (tester) async {
stored('japanese', 'Japanese');
stored('kanji', 'Kanji');
stored('kana', 'Kana');
stored('diary', 'Diary', private: true);
final store = MemoryPublishedStore();
await showLibrary(
@ -815,9 +820,25 @@ void main() {
publishedStore: store,
);
expect(
onCard('Japanese', find.byIcon(LucideIcons.globe)),
findsOneWidget,
);
expect(onCard('Japanese', find.byTooltip('Public')), findsOneWidget);
expect(onCard('Kanji', find.byTooltip('Private')), findsOneWidget);
expect(onCard('Kana', find.byTooltip('Private')), findsOneWidget);
expect(
onCard('Kanji', find.byIcon(LucideIcons.lockOpen)),
findsOneWidget,
);
expect(
onCard('Kanji', find.byTooltip('Not private, not published')),
findsOneWidget,
);
expect(
onCard('Kana', find.byTooltip('Not private, not published')),
findsOneWidget,
);
expect(onCard('Diary', find.byIcon(LucideIcons.lock)), findsOneWidget);
expect(onCard('Diary', find.byTooltip('Private')), findsOneWidget);
// Remembered for when the instance can't be reached.
expect(store.kept, {'japanese'});
});
@ -835,21 +856,74 @@ void main() {
);
expect(onCard('Japanese', find.byTooltip('Public')), findsOneWidget);
expect(onCard('Kanji', find.byTooltip('Private')), findsOneWidget);
expect(
onCard('Kanji', find.byTooltip('Not private, not published')),
findsOneWidget,
);
});
testWidgets('the lock opens the Publish sheet', (tester) async {
testWidgets('the open lock says the instance can read it, and leads to '
'the Publish sheet or makes it private', (tester) async {
stored('japanese', 'Japanese');
gateway.canMakePrivate = true;
await showLibrary(tester);
await tester.tap(onCard('Japanese', find.byTooltip('Private')));
await tester.tap(
onCard('Japanese', find.byTooltip('Not private, not published')),
);
await tester.pumpAndSettle();
expect(find.text("“Japanese” isn't private"), findsOneWidget);
await tester.tap(find.text('Publish…'));
await tester.pumpAndSettle();
expect(find.text('Publish'), findsOneWidget);
expect(
find.text('Sign in to have a public profile and publish.'),
findsOneWidget,
);
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
await tester.tap(
onCard('Japanese', find.byTooltip('Not private, not published')),
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Make private'));
await tester.pump();
await tester.pump();
expect(gateway.privacyChanges, [('japanese', true)]);
});
testWidgets('the lock says it is private, offers no publishing, and makes '
'it non-private', (tester) async {
stored('diary', 'Diary', private: true);
await showLibrary(tester);
await tester.tap(onCard('Diary', find.byTooltip('Private')));
await tester.pumpAndSettle();
expect(find.text('“Diary” is private'), findsOneWidget);
expect(find.text('Publish…'), findsNothing);
await tester.tap(find.widgetWithText(FilledButton, 'Make non-private'));
await tester.pump();
await tester.pump();
expect(gateway.privacyChanges, [('diary', false)]);
});
testWidgets('the globe opens the Publish sheet', (tester) async {
stored('japanese', 'Japanese');
await showLibrary(
tester,
identified: true,
publishing: FakePublishGateway(
publications: {'japanese': publication()},
),
publishedStore: MemoryPublishedStore({'japanese'}),
);
await tester.tap(onCard('Japanese', find.byTooltip('Public')));
await tester.pumpAndSettle();
expect(find.text('Publish'), findsOneWidget);
expect(find.textContaining('halkyon.eu/@raisondetredev'), findsWidgets);
});
});
}