feat(learn): a Viewer opens a shared studyset's material read-only
Every route of a studyset says what the person may do there (StudysetAccess, set in _inStudyset): whether it is shared with people, and whether they only view and study it (Halkyon's ADR 0044, D35, D36). A Viewer opens cards and notes on the same screens, read-only: the rich fields show their content (LearnContentEditor shows LearnContentView), the composer's plain fields are read-only and its changes go nowhere, the note screen reads as a conflict leaves it, each with the line that the studyset is theirs to view and study. The lists, the card browser and a study session offer them nothing that writes: no new card, import, edit, move, rename, reading deadline, reorder or delete. Their tags, progress and reading stay theirs to change, being their own rows. Burying and reporting a card are written on its row, which every member shares, so they are hidden in shared studysets for everyone until they become each person's own rows (D36). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7f1f07883a
commit
8dd3ec0522
17 changed files with 503 additions and 194 deletions
|
|
@ -7,6 +7,7 @@ import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.
|
|||
import 'package:halkyon_learn_flutter/src/core/router/app_shell.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/tab_visibility.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/auth/widget/profile_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_composer_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_browser_screen.dart';
|
||||
|
|
@ -427,7 +428,13 @@ Widget _inStudyset(
|
|||
color:
|
||||
PaletteColor.byName(colorNameOf(studyset.id, all))?.background ??
|
||||
Brand.primary,
|
||||
child: builder(studyset, all),
|
||||
// What the person may do there, shared with them as a Viewer or
|
||||
// not (Halkyon's ADR 0044, D30, D35, D36).
|
||||
child: StudysetAccess(
|
||||
shared: studyset.isShared,
|
||||
viewOnly: studyset.viewOnly,
|
||||
child: builder(studyset, all),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import 'package:halkyon_editor/halkyon_editor.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/core_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_host.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// halkyon_editor's [RichContentEditor] with Learn's blocks ([blocksOf]) and,
|
||||
/// with [grammar], spelling and grammar checked as the text is written, in
|
||||
/// that language ([GrammarHost], ADR 0024).
|
||||
/// that language ([GrammarHost], ADR 0024). In a studyset the person only
|
||||
/// views ([StudysetAccess]), the content shown, not editable.
|
||||
class const LearnContentEditor({
|
||||
/// The content the editor opens with; later changes to it are ignored.
|
||||
required final RichContent initialContent,
|
||||
|
|
@ -32,16 +34,23 @@ class const LearnContentEditor({
|
|||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => RichContentEditor(
|
||||
initialContent: initialContent,
|
||||
onChanged: onChanged,
|
||||
autofocus: autofocus,
|
||||
focusNode: focusNode,
|
||||
formatBar: formatBar,
|
||||
padding: padding,
|
||||
blocks: blocksOf(context),
|
||||
wrapEditor: grammar == null ? null : _checked,
|
||||
);
|
||||
Widget build(BuildContext context) => StudysetAccess.of(context).viewOnly
|
||||
// A Viewer reads what the editor would hold, unchanged (Halkyon's ADR
|
||||
// 0044, D35); a long text scrolls where the editor would have.
|
||||
? SingleChildScrollView(
|
||||
padding: padding,
|
||||
child: LearnContentView(content: initialContent),
|
||||
)
|
||||
: RichContentEditor(
|
||||
initialContent: initialContent,
|
||||
onChanged: onChanged,
|
||||
autofocus: autofocus,
|
||||
focusNode: focusNode,
|
||||
formatBar: formatBar,
|
||||
padding: padding,
|
||||
blocks: blocksOf(context),
|
||||
wrapEditor: grammar == null ? null : _checked,
|
||||
);
|
||||
|
||||
Widget _checked(
|
||||
BuildContext context,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// What the person may do in the studyset whose screens are below
|
||||
/// (Halkyon's ADR 0044, D30, D35, D36), which every route of a studyset
|
||||
/// sets: whether it is [shared] with people, and whether the person only
|
||||
/// views and studies it there, a Viewer ([viewOnly]). Screens and lists
|
||||
/// offer a Viewer nothing that writes, and nobody, in a shared studyset,
|
||||
/// what would be written on rows every member shares for one person alone
|
||||
/// (burying, reporting a card).
|
||||
class const StudysetAccess({
|
||||
required final bool shared,
|
||||
required final bool viewOnly,
|
||||
required super.child,
|
||||
super.key,
|
||||
}) extends InheritedWidget {
|
||||
/// The access above [context]; a studyset of the person's own where none
|
||||
/// is set.
|
||||
static ({bool shared, bool viewOnly}) of(BuildContext context) {
|
||||
final access = context.dependOnInheritedWidgetOfExactType<StudysetAccess>();
|
||||
return (
|
||||
shared: access?.shared ?? false,
|
||||
viewOnly: access?.viewOnly ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the person only views the studyset above [context], read
|
||||
/// without depending on it: for callbacks, outside a build.
|
||||
static bool viewOnlyNow(BuildContext context) =>
|
||||
context.getInheritedWidgetOfExactType<StudysetAccess>()?.viewOnly ??
|
||||
false;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(StudysetAccess oldWidget) =>
|
||||
shared != oldWidget.shared || viewOnly != oldWidget.viewOnly;
|
||||
}
|
||||
|
||||
/// The line saying the studyset is the person's to view and study, not to
|
||||
/// change, above what a Viewer opens (Halkyon's ADR 0044, D35).
|
||||
class const ViewOnlyNote({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.eye,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).studysetViewOnly,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/editor/ruby.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_browser_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_browser.dart';
|
||||
|
|
@ -353,6 +354,8 @@ class _BrowserState() extends State<_Browser> {
|
|||
);
|
||||
void study() => context.push(RoutePaths.study(widget.studyset.id));
|
||||
void write() => context.push(RoutePaths.newCard(widget.studyset.id));
|
||||
// A Viewer adds, moves and deletes nothing (Halkyon's ADR 0044, D35).
|
||||
final viewOnly = StudysetAccess.of(context).viewOnly;
|
||||
// Cards moved show where they go at once; a move that fails puts them
|
||||
// back, and says so in place of the message that they moved.
|
||||
return BlocListener<CardBrowserBloc, CardBrowserState>(
|
||||
|
|
@ -370,8 +373,9 @@ class _BrowserState() extends State<_Browser> {
|
|||
child: AppLoadingIndicator(),
|
||||
),
|
||||
CardBrowserState(cards: []) => _Empty(
|
||||
onCreate: () =>
|
||||
context.push(RoutePaths.newCard(widget.studyset.id)),
|
||||
onCreate: viewOnly
|
||||
? null
|
||||
: () => context.push(RoutePaths.newCard(widget.studyset.id)),
|
||||
),
|
||||
_ => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
@ -486,11 +490,12 @@ class _BrowserState() extends State<_Browser> {
|
|||
label: Text(l10n.cardBrowserStudy),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: write,
|
||||
icon: const Icon(LucideIcons.squarePen, size: 18),
|
||||
label: Text(l10n.cardBrowserEditor),
|
||||
),
|
||||
if (!viewOnly)
|
||||
TextButton.icon(
|
||||
onPressed: write,
|
||||
icon: const Icon(LucideIcons.squarePen, size: 18),
|
||||
label: Text(l10n.cardBrowserEditor),
|
||||
),
|
||||
] else ...[
|
||||
IconButton.filled(
|
||||
style: filledIconButtonStyle(context),
|
||||
|
|
@ -498,11 +503,12 @@ class _BrowserState() extends State<_Browser> {
|
|||
icon: const Icon(LucideIcons.play),
|
||||
tooltip: l10n.cardBrowserStudy,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: write,
|
||||
icon: const Icon(LucideIcons.squarePen),
|
||||
tooltip: l10n.cardBrowserEditor,
|
||||
),
|
||||
if (!viewOnly)
|
||||
IconButton(
|
||||
onPressed: write,
|
||||
icon: const Icon(LucideIcons.squarePen),
|
||||
tooltip: l10n.cardBrowserEditor,
|
||||
),
|
||||
],
|
||||
PopupMenuButton<_Menu>(
|
||||
icon: const Icon(LucideIcons.ellipsisVertical),
|
||||
|
|
@ -512,15 +518,16 @@ class _BrowserState() extends State<_Browser> {
|
|||
_Menu.tags => unawaited(showManageTagsSheet(context)),
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _Menu.import,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.fileInput),
|
||||
title: Text(l10n.cardBrowserImport),
|
||||
if (!viewOnly)
|
||||
PopupMenuItem(
|
||||
value: _Menu.import,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.fileInput),
|
||||
title: Text(l10n.cardBrowserImport),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _Menu.tags,
|
||||
child: ListTile(
|
||||
|
|
@ -549,12 +556,15 @@ class _BrowserState() extends State<_Browser> {
|
|||
|
||||
_Actions _actions(CardBrowserState state) {
|
||||
final some = state.selected.isNotEmpty;
|
||||
// Tags and progress are the person's own; the cards a Viewer's to
|
||||
// view (Halkyon's ADR 0044, D35).
|
||||
final writes = some && !StudysetAccess.of(context).viewOnly;
|
||||
return (
|
||||
move: some ? () => _move(state) : null,
|
||||
move: writes ? () => _move(state) : null,
|
||||
addTags: some ? () => _addTags(state) : null,
|
||||
removeTags: some ? () => _removeTags(state) : null,
|
||||
reset: some ? () => _resetProgress(state) : null,
|
||||
delete: some ? () => _delete(state) : null,
|
||||
delete: writes ? () => _delete(state) : null,
|
||||
done: () => _bloc.add(const CardBrowserEventSelectingToggled()),
|
||||
);
|
||||
}
|
||||
|
|
@ -1693,7 +1703,7 @@ class const _BarButton({
|
|||
}
|
||||
|
||||
/// A studyset without cards: none to browse, and a way to make some.
|
||||
class const _Empty({required final VoidCallback onCreate})
|
||||
class const _Empty({required final VoidCallback? onCreate})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -1715,12 +1725,14 @@ class const _Empty({required final VoidCallback onCreate})
|
|||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: onCreate,
|
||||
icon: const Icon(LucideIcons.plus, size: 18),
|
||||
label: Text(l10n.cardBrowserCreate),
|
||||
),
|
||||
if (onCreate != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: onCreate,
|
||||
icon: const Icon(LucideIcons.plus, size: 18),
|
||||
label: Text(l10n.cardBrowserCreate),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/file_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/report_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.dart';
|
||||
|
|
@ -49,6 +50,10 @@ class const CardTile({
|
|||
final report = card.content.report;
|
||||
final hasMeta = tags.isNotEmpty;
|
||||
final hasBadges = report != null || buried;
|
||||
// A Viewer changes nothing; burying and reporting stay out of shared
|
||||
// studysets, as every member shares the card's row (Halkyon's ADR
|
||||
// 0044, D35, D36).
|
||||
final access = StudysetAccess.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Material(
|
||||
|
|
@ -131,54 +136,58 @@ class const CardTile({
|
|||
),
|
||||
MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(
|
||||
LucideIcons.pencil,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: onOpen,
|
||||
child: Text(l10n.cardTileEdit),
|
||||
),
|
||||
if (report == null)
|
||||
if (!access.viewOnly)
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(
|
||||
LucideIcons.flag,
|
||||
LucideIcons.pencil,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: onReport,
|
||||
child: Text(l10n.cardReportTitle),
|
||||
)
|
||||
else
|
||||
onPressed: onOpen,
|
||||
child: Text(l10n.cardTileEdit),
|
||||
),
|
||||
if (!access.shared)
|
||||
if (report == null)
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(
|
||||
LucideIcons.flag,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: onReport,
|
||||
child: Text(l10n.cardReportTitle),
|
||||
)
|
||||
else
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(
|
||||
LucideIcons.flagOff,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: onResolve,
|
||||
child: Text(l10n.cardResolveReport),
|
||||
),
|
||||
if (!access.shared)
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(
|
||||
LucideIcons.flagOff,
|
||||
leadingIcon: Icon(
|
||||
buried ? LucideIcons.sunrise : LucideIcons.moon,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: onResolve,
|
||||
child: Text(l10n.cardResolveReport),
|
||||
onPressed: onBuryToggle,
|
||||
child: Text(
|
||||
buried ? l10n.cardUnbury : l10n.studyBuryCard,
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(
|
||||
buried ? LucideIcons.sunrise : LucideIcons.moon,
|
||||
size: 18,
|
||||
if (!access.viewOnly)
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(
|
||||
LucideIcons.trash2,
|
||||
size: 18,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
onPressed: onDelete,
|
||||
child: Text(
|
||||
l10n.cardTileDelete,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
onPressed: onBuryToggle,
|
||||
child: Text(
|
||||
buried ? l10n.cardUnbury : l10n.studyBuryCard,
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(
|
||||
LucideIcons.trash2,
|
||||
size: 18,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
onPressed: onDelete,
|
||||
child: Text(
|
||||
l10n.cardTileDelete,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.info, size: 18),
|
||||
onPressed: onInfo,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_composer_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_draft_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_stack_tile.dart';
|
||||
|
|
@ -246,7 +247,10 @@ class _CardComposerScreenState() extends State<CardComposerScreen> {
|
|||
state: state,
|
||||
selected: _selected,
|
||||
expanded: expanded,
|
||||
onAdd: _addDraft,
|
||||
// A Viewer adds none (Halkyon's ADR 0044, D35).
|
||||
onAdd: StudysetAccess.of(context).viewOnly
|
||||
? null
|
||||
: _addDraft,
|
||||
onSelect: _select,
|
||||
onToggle: () =>
|
||||
setState(() => _stackExpanded = !expanded),
|
||||
|
|
@ -307,7 +311,9 @@ class const _Stack({
|
|||
required final CardComposerState state,
|
||||
required final String? selected,
|
||||
required final bool expanded,
|
||||
required final VoidCallback onAdd,
|
||||
|
||||
/// Adds a new card; null for a Viewer, who adds none.
|
||||
required final VoidCallback? onAdd,
|
||||
required final ValueChanged<String> onSelect,
|
||||
required final VoidCallback onToggle,
|
||||
}) extends StatelessWidget {
|
||||
|
|
@ -324,21 +330,23 @@ class const _Stack({
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (expanded)
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(l10n.composerNewFlashcard),
|
||||
)
|
||||
else
|
||||
Center(
|
||||
child: IconButton.filledTonal(
|
||||
tooltip: l10n.composerNewFlashcard,
|
||||
if (onAdd != null) ...[
|
||||
if (expanded)
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(l10n.composerNewFlashcard),
|
||||
)
|
||||
else
|
||||
Center(
|
||||
child: IconButton.filledTonal(
|
||||
tooltip: l10n.composerNewFlashcard,
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: items.length,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:halkyon_learn_flutter/src/core/grammar/grammar_host.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/l10n/language_tags.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/conflict_versions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_composer_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/answers_editor.dart';
|
||||
|
|
@ -163,6 +164,10 @@ class _CardDraftEditorState() extends State<CardDraftEditor> {
|
|||
}
|
||||
|
||||
void _change(CardContent content) {
|
||||
// A Viewer's changes go nowhere (Halkyon's ADR 0044, D35).
|
||||
if (StudysetAccess.viewOnlyNow(context)) {
|
||||
return;
|
||||
}
|
||||
_sent = content;
|
||||
context.read<CardComposerBloc>().add(
|
||||
CardComposerEventChanged(widget.draft.id, content),
|
||||
|
|
@ -304,6 +309,7 @@ class _CardDraftEditorState() extends State<CardDraftEditor> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (StudysetAccess.of(context).viewOnly) const ViewOnlyNote(),
|
||||
Row(
|
||||
children: [
|
||||
TagsPill(
|
||||
|
|
@ -332,18 +338,19 @@ class _CardDraftEditorState() extends State<CardDraftEditor> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_Actions(
|
||||
onAddHint: content.hints.length < CardContent.maxHints
|
||||
? _addHint
|
||||
: null,
|
||||
onAddExplanation: content.explanation == null
|
||||
? _addExplanation
|
||||
: null,
|
||||
onDuplicate: widget.onDuplicate,
|
||||
onClear: () => _send(CardComposerEventCleared(draft.id)),
|
||||
onDelete: widget.onDelete,
|
||||
onSuggest: widget.onSuggest,
|
||||
),
|
||||
if (!StudysetAccess.of(context).viewOnly)
|
||||
_Actions(
|
||||
onAddHint: content.hints.length < CardContent.maxHints
|
||||
? _addHint
|
||||
: null,
|
||||
onAddExplanation: content.explanation == null
|
||||
? _addExplanation
|
||||
: null,
|
||||
onDuplicate: widget.onDuplicate,
|
||||
onClear: () => _send(CardComposerEventCleared(draft.id)),
|
||||
onDelete: widget.onDelete,
|
||||
onSuggest: widget.onSuggest,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/language_tags.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/kanji/widget/character_readings.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
|
@ -80,6 +81,7 @@ class _CharacterFieldsState() extends State<CharacterFields> {
|
|||
Text(label, style: labelStyle),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
readOnly: StudysetAccess.of(context).viewOnly,
|
||||
maxLength: TextLimits.phrase,
|
||||
buildCounter: nearLimitCounter,
|
||||
controller: controller,
|
||||
|
|
@ -106,6 +108,7 @@ class _CharacterFieldsState() extends State<CharacterFields> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
readOnly: StudysetAccess.of(context).viewOnly,
|
||||
maxLength: TextLimits.phrase,
|
||||
buildCounter: nearLimitCounter,
|
||||
controller: meaning,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -99,6 +100,7 @@ class _MatchingFieldsState() extends State<MatchingFields> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
readOnly: StudysetAccess.of(context).viewOnly,
|
||||
maxLength: TextLimits.phrase,
|
||||
buildCounter: nearLimitCounter,
|
||||
controller: target,
|
||||
|
|
@ -154,6 +156,7 @@ class _MatchingFieldsState() extends State<MatchingFields> {
|
|||
Expanded(
|
||||
flex: 2,
|
||||
child: TextField(
|
||||
readOnly: StudysetAccess.of(context).viewOnly,
|
||||
maxLength: TextLimits.phrase,
|
||||
buildCounter: nearLimitCounter,
|
||||
controller: item,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/language_tags.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/jmdict_tags.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
|
@ -69,6 +70,7 @@ class _VocabularyFieldsState() extends State<VocabularyFields> {
|
|||
Text(l10n.vocabularyReading, style: labelStyle),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
readOnly: StudysetAccess.of(context).viewOnly,
|
||||
maxLength: TextLimits.phrase,
|
||||
buildCounter: nearLimitCounter,
|
||||
controller: _reading,
|
||||
|
|
@ -103,6 +105,7 @@ class _VocabularyFieldsState() extends State<VocabularyFields> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
readOnly: StudysetAccess.of(context).viewOnly,
|
||||
maxLength: TextLimits.phrase,
|
||||
buildCounter: nearLimitCounter,
|
||||
controller: meaning,
|
||||
|
|
@ -237,6 +240,7 @@ class _SenseFieldsState() extends State<_SenseFields> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
readOnly: StudysetAccess.of(context).viewOnly,
|
||||
maxLength: TextLimits.phrase,
|
||||
buildCounter: nearLimitCounter,
|
||||
controller: gloss,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
|||
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/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_info.dart';
|
||||
|
|
@ -166,24 +167,29 @@ class const _PathTile({
|
|||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final error = Theme.of(context).colorScheme.error;
|
||||
// A Viewer changes nothing in it (Halkyon's ADR 0044, D35); its
|
||||
// progress is the person's own.
|
||||
final viewOnly = StudysetAccess.of(context).viewOnly;
|
||||
return MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.pencil),
|
||||
onPressed: () =>
|
||||
context.push(RoutePaths.editPath(studysetId, path.id)),
|
||||
child: Text(l10n.pathEdit),
|
||||
),
|
||||
if (!viewOnly)
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.pencil),
|
||||
onPressed: () =>
|
||||
context.push(RoutePaths.editPath(studysetId, path.id)),
|
||||
child: Text(l10n.pathEdit),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.rotateCcw),
|
||||
onPressed: () => _reset(context),
|
||||
child: Text(l10n.pathResetProgress),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
||||
onPressed: () => _delete(context),
|
||||
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
||||
),
|
||||
if (!viewOnly)
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
||||
onPressed: () => _delete(context),
|
||||
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.info),
|
||||
onPressed: () => unawaited(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/editor/save_status.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_host.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/conflict_versions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/card_proposals_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/widget/feedback_actions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/bloc/note_editor_bloc.dart';
|
||||
|
|
@ -138,7 +139,11 @@ class const _Note({
|
|||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final readOnly = state.unresolved.isNotEmpty;
|
||||
final conflicted = state.unresolved.isNotEmpty;
|
||||
// A Viewer reads it as a conflict leaves it, unchanged (Halkyon's ADR
|
||||
// 0044, D35).
|
||||
final viewOnly = StudysetAccess.of(context).viewOnly;
|
||||
final readOnly = conflicted || viewOnly;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||
child: Align(
|
||||
|
|
@ -150,7 +155,8 @@ class const _Note({
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (readOnly) _Conflicts(state: state),
|
||||
if (conflicted) _Conflicts(state: state),
|
||||
if (viewOnly) const ViewOnlyNote(),
|
||||
Expanded(
|
||||
child: SectionCard(
|
||||
padding: EdgeInsets.zero,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
|
|||
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/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/bloc/note_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/widget/edited_ago.dart';
|
||||
|
|
@ -115,24 +116,29 @@ class const _NoteTile({
|
|||
final title = note.content.title;
|
||||
final editedAt = note.editedAt;
|
||||
void open() => context.push(RoutePaths.note(studysetId, note.id));
|
||||
// A Viewer opens it read-only and changes nothing (Halkyon's ADR 0044,
|
||||
// D35).
|
||||
final viewOnly = StudysetAccess.of(context).viewOnly;
|
||||
return MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.pencil),
|
||||
onPressed: open,
|
||||
child: Text(l10n.noteEdit),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(
|
||||
LucideIcons.trash2,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
if (!viewOnly) ...[
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.pencil),
|
||||
onPressed: open,
|
||||
child: Text(l10n.noteEdit),
|
||||
),
|
||||
onPressed: () => _delete(context),
|
||||
child: Text(
|
||||
l10n.noteDelete,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(
|
||||
LucideIcons.trash2,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: () => _delete(context),
|
||||
child: Text(
|
||||
l10n.noteDelete,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.info),
|
||||
onPressed: () =>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:halkyon_learn_flutter/src/core/l10n/file_size.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/sync/uuid_time.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_counts_line.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/bloc/pdf_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/model/deadline_status.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/model/pdf_chapter.dart';
|
||||
|
|
@ -130,7 +131,9 @@ class const PdfListToolbar({
|
|||
if (!state.loaded || state.rows.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final canReorder = state.rows.length >= 2;
|
||||
// A Viewer puts nothing in another order (Halkyon's ADR 0044, D35).
|
||||
final canReorder =
|
||||
state.rows.length >= 2 && !StudysetAccess.of(context).viewOnly;
|
||||
if (reordering || (canReorder && folded < mostFolded)) {
|
||||
final reorder = ReorderToggle(
|
||||
reordering: reordering,
|
||||
|
|
@ -359,32 +362,39 @@ class const _PdfTile({
|
|||
final bloc = context.read<PdfListBloc>();
|
||||
final id = row.source.id;
|
||||
final pageCount = content.pageCount;
|
||||
// A Viewer changes nothing of the file, only their own reading
|
||||
// (Halkyon's ADR 0044, D35).
|
||||
final viewOnly = StudysetAccess.of(context).viewOnly;
|
||||
return [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.pencil),
|
||||
onPressed: () => unawaited(_rename(context)),
|
||||
child: Text(l10n.pdfRename),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.calendarClock),
|
||||
onPressed: () async {
|
||||
switch (await chooseReadingDeadline(
|
||||
context,
|
||||
row.source,
|
||||
chapters: chaptersOfPdf(context, row.source),
|
||||
)) {
|
||||
case DeadlineSet(:final deadline):
|
||||
bloc.add(PdfListEventDeadlineSet(id, deadline));
|
||||
case DeadlineRemoved():
|
||||
bloc.add(PdfListEventDeadlineSet(id, null));
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
content.deadline == null ? l10n.pdfSetDeadline : l10n.pdfEditDeadline,
|
||||
if (!viewOnly) ...[
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.pencil),
|
||||
onPressed: () => unawaited(_rename(context)),
|
||||
child: Text(l10n.pdfRename),
|
||||
),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.calendarClock),
|
||||
onPressed: () async {
|
||||
switch (await chooseReadingDeadline(
|
||||
context,
|
||||
row.source,
|
||||
chapters: chaptersOfPdf(context, row.source),
|
||||
)) {
|
||||
case DeadlineSet(:final deadline):
|
||||
bloc.add(PdfListEventDeadlineSet(id, deadline));
|
||||
case DeadlineRemoved():
|
||||
bloc.add(PdfListEventDeadlineSet(id, null));
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
content.deadline == null
|
||||
? l10n.pdfSetDeadline
|
||||
: l10n.pdfEditDeadline,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (pageCount != null && (row.furthestPage ?? 0) < pageCount)
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.bookCheck),
|
||||
|
|
@ -411,11 +421,12 @@ class const _PdfTile({
|
|||
onPressed: () => unawaited(_moreInfo(context)),
|
||||
child: Text(l10n.pdfMoreInfo),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
||||
onPressed: () => unawaited(_delete(context)),
|
||||
child: Text(l10n.pdfDelete, style: TextStyle(color: error)),
|
||||
),
|
||||
if (!viewOnly)
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
||||
onPressed: () => unawaited(_delete(context)),
|
||||
child: Text(l10n.pdfDelete, style: TextStyle(color: error)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/japanese/kana_input.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/language_tags.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_extras.dart';
|
||||
|
|
@ -125,6 +126,9 @@ class const StudyCardHeader({
|
|||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final error = Theme.of(context).colorScheme.error;
|
||||
// A Viewer changes no card; nobody buries or reports one every member
|
||||
// shares (Halkyon's ADR 0044, D35, D36).
|
||||
final access = StudysetAccess.of(context);
|
||||
PopupMenuItem<_CardAction> item(
|
||||
_CardAction action,
|
||||
IconData icon,
|
||||
|
|
@ -171,21 +175,25 @@ class const StudyCardHeader({
|
|||
},
|
||||
itemBuilder: (context) => [
|
||||
item(_CardAction.tags, LucideIcons.tag, l10n.cardTagsAdd),
|
||||
item(_CardAction.edit, LucideIcons.pencil, l10n.cardTileEdit),
|
||||
item(_CardAction.report, LucideIcons.flag, l10n.cardReportTitle),
|
||||
if (!access.viewOnly)
|
||||
item(_CardAction.edit, LucideIcons.pencil, l10n.cardTileEdit),
|
||||
if (!access.shared)
|
||||
item(_CardAction.report, LucideIcons.flag, l10n.cardReportTitle),
|
||||
if (onAsk != null)
|
||||
item(
|
||||
_CardAction.ask,
|
||||
LucideIcons.messageCircleQuestionMark,
|
||||
l10n.discussionAskAbout,
|
||||
),
|
||||
item(_CardAction.bury, LucideIcons.moon, l10n.studyBuryCard),
|
||||
item(
|
||||
_CardAction.delete,
|
||||
LucideIcons.trash2,
|
||||
l10n.cardTileDelete,
|
||||
color: error,
|
||||
),
|
||||
if (!access.shared)
|
||||
item(_CardAction.bury, LucideIcons.moon, l10n.studyBuryCard),
|
||||
if (!access.viewOnly)
|
||||
item(
|
||||
_CardAction.delete,
|
||||
LucideIcons.trash2,
|
||||
l10n.cardTileDelete,
|
||||
color: error,
|
||||
),
|
||||
item(_CardAction.info, LucideIcons.info, l10n.moreInfo),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart'
|
|||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_importer.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/anki/widget/anki_import_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topic.dart';
|
||||
|
|
@ -505,26 +506,9 @@ class _StudysetViewState() extends State<_StudysetView> {
|
|||
// them apart by space rather than a line (decided 2026-10-12).
|
||||
const SizedBox(height: headerGap),
|
||||
if (studyset.viewOnly)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.eye,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).studysetViewOnly,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ViewOnlyNote(),
|
||||
),
|
||||
if (!wide)
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_tile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
/// What a member of a shared studyset may do in it (Halkyon's ADR 0044,
|
||||
/// D35, D36): a Viewer opens cards and notes read-only and is offered
|
||||
/// nothing that writes; nobody buries or reports a card every member
|
||||
/// shares; in the person's own studyset, all of it as before.
|
||||
void main() {
|
||||
late FakeStudysetGateway studysets;
|
||||
late FakeCardGateway cardGateway;
|
||||
late FakeNoteGateway noteGateway;
|
||||
late String cardId;
|
||||
late String noteId;
|
||||
|
||||
setUp(() async {
|
||||
studysets = FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
);
|
||||
cardGateway = FakeCardGateway();
|
||||
noteGateway = FakeNoteGateway();
|
||||
final cards = CardRepository(cardGateway);
|
||||
addTearDown(cards.dispose);
|
||||
cardId = await cards.create(
|
||||
'japanese',
|
||||
CardContent(
|
||||
front: RichContent.plain('Capital of Japan?'),
|
||||
back: RichContent.plain('Tokyo'),
|
||||
),
|
||||
);
|
||||
final notes = NoteRepository(noteGateway);
|
||||
addTearDown(notes.dispose);
|
||||
noteId = (await notes.create(
|
||||
'japanese',
|
||||
NoteContent(title: 'Particles', body: RichContent.plain('は')),
|
||||
)).noteId;
|
||||
});
|
||||
|
||||
Future<void> showApp(WidgetTester tester, String location) async {
|
||||
tester.view.physicalSize = const Size(1200, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: studysets,
|
||||
cardGateway: cardGateway,
|
||||
noteGateway: noteGateway,
|
||||
),
|
||||
location: location,
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// The options of the card's row in the Flashcards tab, opened.
|
||||
Future<void> openCardOptions(WidgetTester tester) async {
|
||||
await showApp(tester, RoutePaths.studyset('japanese'));
|
||||
await tester.tap(find.text('Flashcards'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(CardTile),
|
||||
matching: find.byTooltip('Options'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void shared({required bool viewOnly}) =>
|
||||
studysets.shared['japanese'] = (space: 'space-1', viewOnly: viewOnly);
|
||||
|
||||
testWidgets("a Viewer's card offers its info alone", (tester) async {
|
||||
shared(viewOnly: true);
|
||||
await openCardOptions(tester);
|
||||
|
||||
expect(find.text('More info'), findsOneWidget);
|
||||
expect(find.text('Edit'), findsNothing);
|
||||
expect(find.text('Delete'), findsNothing);
|
||||
expect(find.text('Bury until tomorrow'), findsNothing);
|
||||
expect(find.text('Report an error'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("an Editor's card is edited and deleted, never buried or "
|
||||
'reported for everyone', (tester) async {
|
||||
shared(viewOnly: false);
|
||||
await openCardOptions(tester);
|
||||
|
||||
expect(find.text('Edit'), findsOneWidget);
|
||||
expect(find.text('Delete'), findsOneWidget);
|
||||
expect(find.text('Bury until tomorrow'), findsNothing);
|
||||
expect(find.text('Report an error'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("the person's own card offers all of it", (tester) async {
|
||||
await openCardOptions(tester);
|
||||
|
||||
expect(find.text('Edit'), findsOneWidget);
|
||||
expect(find.text('Bury until tomorrow'), findsOneWidget);
|
||||
expect(find.text('Report an error'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a Viewer opens a card read-only, adding none', (tester) async {
|
||||
shared(viewOnly: true);
|
||||
await showApp(tester, RoutePaths.card('japanese', cardId));
|
||||
|
||||
expect(
|
||||
find.text('You can view and study this, not change it.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('New flashcard'), findsNothing);
|
||||
expect(find.byTooltip('More actions'), findsNothing);
|
||||
expect(find.byType(RichContentEditor), findsNothing);
|
||||
expect(
|
||||
find.textContaining('Capital of Japan?', findRichText: true),
|
||||
findsWidgets,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a Viewer opens a note read-only', (tester) async {
|
||||
shared(viewOnly: true);
|
||||
await showApp(tester, RoutePaths.note('japanese', noteId));
|
||||
|
||||
expect(
|
||||
find.text('You can view and study this, not change it.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(RichContentEditor), findsNothing);
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField).first).readOnly,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue