feat(tag): a tag can be hidden on cards being studied
A tag's eye in Manage tags hides it on cards being studied, or shows it again: for the many tags naming where a word was met (an episode, a song), which would crowd a common word's card. A hidden tag still filters, narrows a study session and shows where tags are edited. Tag schema 2 adds 'hidden', written only for a hidden tag, so older apps go on reading every other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
747d73b867
commit
68945b2224
14 changed files with 179 additions and 18 deletions
|
|
@ -597,6 +597,8 @@
|
||||||
"tagManageAdd": "Tag hinzufügen",
|
"tagManageAdd": "Tag hinzufügen",
|
||||||
"tagManageRow": "{name} ({count})",
|
"tagManageRow": "{name} ({count})",
|
||||||
"tagManageEdit": "Tag bearbeiten",
|
"tagManageEdit": "Tag bearbeiten",
|
||||||
|
"tagManageHide": "Auf Karten ausblenden",
|
||||||
|
"tagManageShow": "Auf Karten zeigen",
|
||||||
"tagManageDelete": "Tag löschen",
|
"tagManageDelete": "Tag löschen",
|
||||||
"tagDeleteTitle": "Tag „{name}“ löschen?",
|
"tagDeleteTitle": "Tag „{name}“ löschen?",
|
||||||
"tagDeleteMessage": "{count, plural, =0{Er ist an keiner Karteikarte.} =1{Er wird auf all deinen Geräten von 1 Karteikarte entfernt.} other{Er wird auf all deinen Geräten von {count} Karteikarten entfernt.}}",
|
"tagDeleteMessage": "{count, plural, =0{Er ist an keiner Karteikarte.} =1{Er wird auf all deinen Geräten von 1 Karteikarte entfernt.} other{Er wird auf all deinen Geräten von {count} Karteikarten entfernt.}}",
|
||||||
|
|
|
||||||
|
|
@ -674,6 +674,8 @@
|
||||||
"tagManageRow": "{name} ({count})",
|
"tagManageRow": "{name} ({count})",
|
||||||
"@tagManageRow": {"description": "A tag in the list of tags, with how many flashcards have it.", "placeholders": {"name": {"type": "String"}, "count": {"type": "int"}}},
|
"@tagManageRow": {"description": "A tag in the list of tags, with how many flashcards have it.", "placeholders": {"name": {"type": "String"}, "count": {"type": "int"}}},
|
||||||
"tagManageEdit": "Edit tag",
|
"tagManageEdit": "Edit tag",
|
||||||
|
"tagManageHide": "Hide on cards",
|
||||||
|
"tagManageShow": "Show on cards",
|
||||||
"tagManageDelete": "Delete tag",
|
"tagManageDelete": "Delete tag",
|
||||||
"tagDeleteTitle": "Delete the tag '{name}'?",
|
"tagDeleteTitle": "Delete the tag '{name}'?",
|
||||||
"@tagDeleteTitle": {"placeholders": {"name": {"type": "String"}}},
|
"@tagDeleteTitle": {"placeholders": {"name": {"type": "String"}}},
|
||||||
|
|
|
||||||
|
|
@ -3568,6 +3568,18 @@ abstract class AppLocalizations {
|
||||||
/// **'Edit tag'**
|
/// **'Edit tag'**
|
||||||
String get tagManageEdit;
|
String get tagManageEdit;
|
||||||
|
|
||||||
|
/// No description provided for @tagManageHide.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Hide on cards'**
|
||||||
|
String get tagManageHide;
|
||||||
|
|
||||||
|
/// No description provided for @tagManageShow.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show on cards'**
|
||||||
|
String get tagManageShow;
|
||||||
|
|
||||||
/// No description provided for @tagManageDelete.
|
/// No description provided for @tagManageDelete.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -2159,6 +2159,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get tagManageEdit => 'Tag bearbeiten';
|
String get tagManageEdit => 'Tag bearbeiten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tagManageHide => 'Auf Karten ausblenden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tagManageShow => 'Auf Karten zeigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tagManageDelete => 'Tag löschen';
|
String get tagManageDelete => 'Tag löschen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2142,6 +2142,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get tagManageEdit => 'Edit tag';
|
String get tagManageEdit => 'Edit tag';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tagManageHide => 'Hide on cards';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tagManageShow => 'Show on cards';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tagManageDelete => 'Delete tag';
|
String get tagManageDelete => 'Delete tag';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,17 +35,21 @@ class DictionaryAdder(final CardRepository _cards, final TagRepository _tags) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Adds [words] to [studysetId], at its end, in their order; each card
|
/// Adds [words] to [studysetId], at its end, in their order; each card
|
||||||
/// also tagged [tagNames] (the show a word was met in).
|
/// also tagged [tagNames] (the show a word was met in) and
|
||||||
|
/// [hiddenTagNames], tags made hidden on cards if they are new (the
|
||||||
|
/// episode or song).
|
||||||
Future<DictionaryAdded> add(
|
Future<DictionaryAdded> add(
|
||||||
String studysetId,
|
String studysetId,
|
||||||
List<DictionaryItem> words, {
|
List<DictionaryItem> words, {
|
||||||
List<String> tagNames = const [],
|
List<String> tagNames = const [],
|
||||||
}) => _queue.run(() => _add(studysetId, words, tagNames));
|
List<String> hiddenTagNames = const [],
|
||||||
|
}) => _queue.run(() => _add(studysetId, words, tagNames, hiddenTagNames));
|
||||||
|
|
||||||
Future<DictionaryAdded> _add(
|
Future<DictionaryAdded> _add(
|
||||||
String studysetId,
|
String studysetId,
|
||||||
List<DictionaryItem> words,
|
List<DictionaryItem> words,
|
||||||
List<String> extraTagNames,
|
List<String> extraTagNames,
|
||||||
|
List<String> hiddenTagNames,
|
||||||
) async {
|
) async {
|
||||||
final there = await _entriesIn(studysetId);
|
final there = await _entriesIn(studysetId);
|
||||||
final fresh = [
|
final fresh = [
|
||||||
|
|
@ -71,6 +75,10 @@ class DictionaryAdder(final CardRepository _cards, final TagRepository _tags) {
|
||||||
final tagId = tagIdOf[name] ??= await _tags.create(name);
|
final tagId = tagIdOf[name] ??= await _tags.create(name);
|
||||||
(tagsByCard[ids[index]] ??= {}).add(tagId);
|
(tagsByCard[ids[index]] ??= {}).add(tagId);
|
||||||
}
|
}
|
||||||
|
for (final name in hiddenTagNames) {
|
||||||
|
final tagId = tagIdOf[name] ??= await _tags.create(name, hidden: true);
|
||||||
|
(tagsByCard[ids[index]] ??= {}).add(tagId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await _tags.addTags(tagsByCard);
|
await _tags.addTags(tagsByCard);
|
||||||
return (added: fresh.length, skipped: words.length - fresh.length);
|
return (added: fresh.length, skipped: words.length - fresh.length);
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,14 @@ class const StudyCardHeader({
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
] else
|
] else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OverflowingTagChipsRow(tags: tags, onPressed: onTags),
|
// Tags hidden on cards (where a word was met) stay out.
|
||||||
|
child: OverflowingTagChipsRow(
|
||||||
|
tags: [
|
||||||
|
for (final tag in tags)
|
||||||
|
if (!tag.hidden) tag,
|
||||||
|
],
|
||||||
|
onPressed: onTags,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
PopupMenuButton<_CardAction>(
|
PopupMenuButton<_CardAction>(
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ class TagsBloc(final TagRepository _tags) extends Bloc<TagsEvent, TagsState> {
|
||||||
colorName: event.content.colorName,
|
colorName: event.content.colorName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
on<TagsEventHiddenRequested>(
|
||||||
|
(event, emit) => _tags.setHidden(event.id, hidden: event.hidden),
|
||||||
|
);
|
||||||
on<TagsEventDeleteRequested>((event, emit) async {
|
on<TagsEventDeleteRequested>((event, emit) async {
|
||||||
_deleting = _tags.delete(event.id);
|
_deleting = _tags.delete(event.id);
|
||||||
await _deleting;
|
await _deleting;
|
||||||
|
|
@ -75,6 +78,18 @@ final class const TagsEventUpdateRequested(
|
||||||
List<Object?> get props => [id, content];
|
List<Object?> get props => [id, content];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hide the tag [id] on cards being studied, or show it again.
|
||||||
|
final class const TagsEventHiddenRequested(
|
||||||
|
/// The tag's id.
|
||||||
|
final String id, {
|
||||||
|
|
||||||
|
/// Whether it is hidden.
|
||||||
|
required final bool hidden,
|
||||||
|
}) extends TagsEvent {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, hidden];
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete the tag [id] from every card and then itself.
|
/// Delete the tag [id] from every card and then itself.
|
||||||
final class const TagsEventDeleteRequested(
|
final class const TagsEventDeleteRequested(
|
||||||
/// The tag's id.
|
/// The tag's id.
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,37 @@ import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.dart';
|
||||||
|
|
||||||
// A tag's content as its synced row stores it (see core/content/payload).
|
// A tag's content as its synced row stores it (see core/content/payload).
|
||||||
//
|
//
|
||||||
// Schema 1 holds the name and the colour.
|
// Schema 1 holds the name and the colour; schema 2 adds `hidden` (decided
|
||||||
|
// 2026-10-13), written only for a hidden tag, so older apps still read the
|
||||||
|
// others.
|
||||||
|
|
||||||
/// The schema this app writes.
|
/// The newest schema this app writes.
|
||||||
const tagSchemaVersion = 1;
|
const tagSchemaVersion = 2;
|
||||||
|
|
||||||
/// A tag's content: its name and colour.
|
/// A tag's content: its name and colour.
|
||||||
typedef TagContent = ({String name, String colorName});
|
typedef TagContent = ({String name, String colorName});
|
||||||
|
|
||||||
/// Encodes a tag's [content] into its payload, the name as a [tidyTagName].
|
/// Encodes a tag's [content] into its payload, the name as a [tidyTagName].
|
||||||
ByteData encodeTagPayload(TagContent content) =>
|
///
|
||||||
encodePayload(tagSchemaVersion, {
|
/// A [hidden] tag is written in schema 2, any other in schema 1.
|
||||||
|
ByteData encodeTagPayload(TagContent content, {bool hidden = false}) =>
|
||||||
|
encodePayload(hidden ? 2 : 1, {
|
||||||
'name': tidyTagName(content.name),
|
'name': tidyTagName(content.name),
|
||||||
'color': content.colorName,
|
'color': content.colorName,
|
||||||
|
if (hidden) 'hidden': true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Whether the tag in [payload] is hidden on cards being studied; false for
|
||||||
|
/// anything it can't read.
|
||||||
|
bool tagPayloadHidden(ByteData payload) {
|
||||||
|
try {
|
||||||
|
return decodePayload(payload, newestSchema: tagSchemaVersion)['hidden'] ==
|
||||||
|
true;
|
||||||
|
} on FormatException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Decodes a tag's payload, the name as a [tidyTagName] whoever wrote it.
|
/// Decodes a tag's payload, the name as a [tidyTagName] whoever wrote it.
|
||||||
/// Names from before tags kept their case are in lowercase.
|
/// Names from before tags kept their case are in lowercase.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -70,15 +70,17 @@ class TagRepository(
|
||||||
Future<Tagging> read() => _serially(_read);
|
Future<Tagging> read() => _serially(_read);
|
||||||
|
|
||||||
/// The id of the tag named [name] in any case: an existing one, keeping
|
/// The id of the tag named [name] in any case: an existing one, keeping
|
||||||
/// its case, or a new one in [colorName]. Throws an [ArgumentError] for a blank name.
|
/// its case, or a new one in [colorName], [hidden] on cards if asked.
|
||||||
|
/// Throws an [ArgumentError] for a blank name.
|
||||||
Future<String> create(
|
Future<String> create(
|
||||||
String name, {
|
String name, {
|
||||||
String colorName = defaultTagColorName,
|
String colorName = defaultTagColorName,
|
||||||
|
bool hidden = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (tidyTagName(name).isEmpty) {
|
if (tidyTagName(name).isEmpty) {
|
||||||
throw ArgumentError.value(name, 'name', 'is blank');
|
throw ArgumentError.value(name, 'name', 'is blank');
|
||||||
}
|
}
|
||||||
final id = await _serially(() => _idOf(name, colorName));
|
final id = await _serially(() => _idOf(name, colorName, hidden: hidden));
|
||||||
await _refresh();
|
await _refresh();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
@ -94,14 +96,33 @@ class TagRepository(
|
||||||
throw ArgumentError.value(name, 'name', 'is blank');
|
throw ArgumentError.value(name, 'name', 'is blank');
|
||||||
}
|
}
|
||||||
await _serially(
|
await _serially(
|
||||||
() => _gateway.updateTagPayload(
|
() async => await _gateway.updateTagPayload(
|
||||||
id,
|
id,
|
||||||
encodeTagPayload((name: name, colorName: colorName)),
|
encodeTagPayload(
|
||||||
|
(name: name, colorName: colorName),
|
||||||
|
hidden: (await _read()).byId(id)?.hidden ?? false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _refresh();
|
await _refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hides the tag [id] on cards being studied, or shows it again.
|
||||||
|
Future<void> setHidden(String id, {required bool hidden}) async {
|
||||||
|
await _serially(() async {
|
||||||
|
if ((await _read()).byId(id) case final tag?) {
|
||||||
|
await _gateway.updateTagPayload(
|
||||||
|
id,
|
||||||
|
encodeTagPayload(
|
||||||
|
(name: tag.name, colorName: tag.colorName),
|
||||||
|
hidden: hidden,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await _refresh();
|
||||||
|
}
|
||||||
|
|
||||||
/// Gives the card [cardId] exactly the tags [tagIds]: the missing ones
|
/// Gives the card [cardId] exactly the tags [tagIds]: the missing ones
|
||||||
/// are added, the others taken away. Tags gone in the meantime are left
|
/// are added, the others taken away. Tags gone in the meantime are left
|
||||||
/// out.
|
/// out.
|
||||||
|
|
@ -389,13 +410,17 @@ class TagRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The id of the tag named [name]: an existing one, or a new one in
|
/// The id of the tag named [name]: an existing one, or a new one in
|
||||||
/// [colorName]; call it only in [_serially].
|
/// [colorName], [hidden] if asked; call it only in [_serially].
|
||||||
Future<String> _idOf(String name, String colorName) async {
|
Future<String> _idOf(
|
||||||
|
String name,
|
||||||
|
String colorName, {
|
||||||
|
bool hidden = false,
|
||||||
|
}) async {
|
||||||
if ((await _read()).named(name) case final existing?) {
|
if ((await _read()).named(name) case final existing?) {
|
||||||
return existing.id;
|
return existing.id;
|
||||||
}
|
}
|
||||||
return await _gateway.insertTag(
|
return await _gateway.insertTag(
|
||||||
encodeTagPayload((name: name, colorName: colorName)),
|
encodeTagPayload((name: name, colorName: colorName), hidden: hidden),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -410,7 +435,12 @@ class TagRepository(
|
||||||
Tag? _decode(TagRow row) {
|
Tag? _decode(TagRow row) {
|
||||||
try {
|
try {
|
||||||
final (:name, :colorName) = decodeTagPayload(row.payload);
|
final (:name, :colorName) = decodeTagPayload(row.payload);
|
||||||
return Tag(id: row.id, name: name, colorName: colorName);
|
return Tag(
|
||||||
|
id: row.id,
|
||||||
|
name: name,
|
||||||
|
colorName: colorName,
|
||||||
|
hidden: tagPayloadHidden(row.payload),
|
||||||
|
);
|
||||||
} on FormatException catch (error) {
|
} on FormatException catch (error) {
|
||||||
log('Skipped tag ${row.id}: ${error.message}', name: 'Tags');
|
log('Skipped tag ${row.id}: ${error.message}', name: 'Tags');
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,18 @@ final class const Tag({
|
||||||
|
|
||||||
/// The colour, a `PaletteColor`'s name.
|
/// The colour, a `PaletteColor`'s name.
|
||||||
required final String colorName,
|
required final String colorName,
|
||||||
|
|
||||||
|
/// Whether cards being studied leave it out of their tags (decided
|
||||||
|
/// 2026-10-13): for the many tags naming where a word was met, an
|
||||||
|
/// episode or a song, which would crowd a common word's card. It still
|
||||||
|
/// filters, narrows a study session and shows where tags are edited.
|
||||||
|
final bool hidden = false,
|
||||||
}) with Equatable {
|
}) with Equatable {
|
||||||
/// This tag as something else refers to it.
|
/// This tag as something else refers to it.
|
||||||
TagRef get ref => (id: id, name: name);
|
TagRef get ref => (id: id, name: name);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [id, name, colorName];
|
List<Object?> get props => [id, name, colorName, hidden];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A tag as something else refers to it, settings say: by its `id`, and by
|
/// A tag as something else refers to it, settings say: by its `id`, and by
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,16 @@ class const _TagRow({required final Tag tag, required final Tagging tagging})
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: tag.hidden ? l10n.tagManageShow : l10n.tagManageHide,
|
||||||
|
onPressed: () => context.read<TagsBloc>().add(
|
||||||
|
TagsEventHiddenRequested(tag.id, hidden: !tag.hidden),
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
tag.hidden ? LucideIcons.eyeOff : LucideIcons.eye,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: l10n.tagManageEdit,
|
tooltip: l10n.tagManageEdit,
|
||||||
onPressed: () => _edit(context),
|
onPressed: () => _edit(context),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,29 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a hidden tag is schema 2 and says so; any other stays schema 1, '
|
||||||
|
'which older apps read', () {
|
||||||
|
final hidden = encodeTagPayload((
|
||||||
|
name: 'あかね噺 S1 E1',
|
||||||
|
colorName: 'fern',
|
||||||
|
), hidden: true);
|
||||||
|
final shown = encodeTagPayload((name: 'あかね噺', colorName: 'fern'));
|
||||||
|
|
||||||
|
expect(decodePayload(hidden, newestSchema: tagSchemaVersion), {
|
||||||
|
'schemaVersion': 2,
|
||||||
|
'name': 'あかね噺 S1 E1',
|
||||||
|
'color': 'fern',
|
||||||
|
'hidden': true,
|
||||||
|
});
|
||||||
|
expect(tagPayloadHidden(hidden), isTrue);
|
||||||
|
expect(decodeTagPayload(hidden).name, 'あかね噺 S1 E1');
|
||||||
|
expect(
|
||||||
|
decodePayload(shown, newestSchema: tagSchemaVersion)['schemaVersion'],
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
expect(tagPayloadHidden(shown), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('reads a name as another device wrote it, tidied', () {
|
test('reads a name as another device wrote it, tidied', () {
|
||||||
final payload = encodePayload(1, {
|
final payload = encodePayload(1, {
|
||||||
'name': ' Verb form',
|
'name': ' Verb form',
|
||||||
|
|
@ -32,7 +55,7 @@ void main() {
|
||||||
test('refuses a blank name, a newer schema and a blanked tag', () {
|
test('refuses a blank name, a newer schema and a blanked tag', () {
|
||||||
for (final payload in [
|
for (final payload in [
|
||||||
encodePayload(1, {'name': ' ', 'color': 'fern'}),
|
encodePayload(1, {'name': ' ', 'color': 'fern'}),
|
||||||
encodePayload(2, {'name': 'verb', 'color': 'fern'}),
|
encodePayload(3, {'name': 'verb', 'color': 'fern'}),
|
||||||
redactedPayload,
|
redactedPayload,
|
||||||
]) {
|
]) {
|
||||||
expect(() => decodeTagPayload(payload), throwsFormatException);
|
expect(() => decodeTagPayload(payload), throwsFormatException);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,24 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a tag hidden on cards stays hidden when renamed, and shows again '
|
||||||
|
'when asked; one is made hidden from the start if asked', () async {
|
||||||
|
final id = await repository.create('Verb', colorName: 'fern');
|
||||||
|
|
||||||
|
await repository.setHidden(id, hidden: true);
|
||||||
|
expect((await repository.read()).byId(id)?.hidden, isTrue);
|
||||||
|
await repository.update(id, name: 'Verbs', colorName: 'plum');
|
||||||
|
expect(
|
||||||
|
(await repository.read()).byId(id),
|
||||||
|
Tag(id: id, name: 'Verbs', colorName: 'plum', hidden: true),
|
||||||
|
);
|
||||||
|
await repository.setHidden(id, hidden: false);
|
||||||
|
expect((await repository.read()).byId(id)?.hidden, isFalse);
|
||||||
|
|
||||||
|
final episode = await repository.create('あかね噺 S1 E1', hidden: true);
|
||||||
|
expect((await repository.read()).byId(episode)?.hidden, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
test('lists the tags by name, whatever their case', () async {
|
test('lists the tags by name, whatever their case', () async {
|
||||||
await repository.create('verb');
|
await repository.create('verb');
|
||||||
await repository.create('adjective');
|
await repository.create('adjective');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue