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
18 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/app_shell.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.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/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/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/bloc/card_composer_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_browser_screen.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_browser_screen.dart';
|
||||||
|
|
@ -427,7 +428,13 @@ Widget _inStudyset(
|
||||||
color:
|
color:
|
||||||
PaletteColor.byName(colorNameOf(studyset.id, all))?.background ??
|
PaletteColor.byName(colorNameOf(studyset.id, all))?.background ??
|
||||||
Brand.primary,
|
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/dependencies/person_dependencies.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/editor/core_blocks.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/grammar/grammar_host.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/widget/studyset_access.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// halkyon_editor's [RichContentEditor] with Learn's blocks ([blocksOf]) and,
|
/// halkyon_editor's [RichContentEditor] with Learn's blocks ([blocksOf]) and,
|
||||||
/// with [grammar], spelling and grammar checked as the text is written, in
|
/// 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({
|
class const LearnContentEditor({
|
||||||
/// The content the editor opens with; later changes to it are ignored.
|
/// The content the editor opens with; later changes to it are ignored.
|
||||||
required final RichContent initialContent,
|
required final RichContent initialContent,
|
||||||
|
|
@ -32,16 +34,23 @@ class const LearnContentEditor({
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => RichContentEditor(
|
Widget build(BuildContext context) => StudysetAccess.of(context).viewOnly
|
||||||
initialContent: initialContent,
|
// A Viewer reads what the editor would hold, unchanged (Halkyon's ADR
|
||||||
onChanged: onChanged,
|
// 0044, D35); a long text scrolls where the editor would have.
|
||||||
autofocus: autofocus,
|
? SingleChildScrollView(
|
||||||
focusNode: focusNode,
|
padding: padding,
|
||||||
formatBar: formatBar,
|
child: LearnContentView(content: initialContent),
|
||||||
padding: padding,
|
)
|
||||||
blocks: blocksOf(context),
|
: RichContentEditor(
|
||||||
wrapEditor: grammar == null ? null : _checked,
|
initialContent: initialContent,
|
||||||
);
|
onChanged: onChanged,
|
||||||
|
autofocus: autofocus,
|
||||||
|
focusNode: focusNode,
|
||||||
|
formatBar: formatBar,
|
||||||
|
padding: padding,
|
||||||
|
blocks: blocksOf(context),
|
||||||
|
wrapEditor: grammar == null ? null : _checked,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _checked(
|
Widget _checked(
|
||||||
BuildContext context,
|
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/editor/ruby.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.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/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/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.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_browser.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 study() => context.push(RoutePaths.study(widget.studyset.id));
|
||||||
void write() => context.push(RoutePaths.newCard(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
|
// 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.
|
// back, and says so in place of the message that they moved.
|
||||||
return BlocListener<CardBrowserBloc, CardBrowserState>(
|
return BlocListener<CardBrowserBloc, CardBrowserState>(
|
||||||
|
|
@ -370,8 +373,9 @@ class _BrowserState() extends State<_Browser> {
|
||||||
child: AppLoadingIndicator(),
|
child: AppLoadingIndicator(),
|
||||||
),
|
),
|
||||||
CardBrowserState(cards: []) => _Empty(
|
CardBrowserState(cards: []) => _Empty(
|
||||||
onCreate: () =>
|
onCreate: viewOnly
|
||||||
context.push(RoutePaths.newCard(widget.studyset.id)),
|
? null
|
||||||
|
: () => context.push(RoutePaths.newCard(widget.studyset.id)),
|
||||||
),
|
),
|
||||||
_ => Column(
|
_ => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
|
@ -486,11 +490,12 @@ class _BrowserState() extends State<_Browser> {
|
||||||
label: Text(l10n.cardBrowserStudy),
|
label: Text(l10n.cardBrowserStudy),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
if (!viewOnly)
|
||||||
onPressed: write,
|
TextButton.icon(
|
||||||
icon: const Icon(LucideIcons.squarePen, size: 18),
|
onPressed: write,
|
||||||
label: Text(l10n.cardBrowserEditor),
|
icon: const Icon(LucideIcons.squarePen, size: 18),
|
||||||
),
|
label: Text(l10n.cardBrowserEditor),
|
||||||
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
IconButton.filled(
|
IconButton.filled(
|
||||||
style: filledIconButtonStyle(context),
|
style: filledIconButtonStyle(context),
|
||||||
|
|
@ -498,11 +503,12 @@ class _BrowserState() extends State<_Browser> {
|
||||||
icon: const Icon(LucideIcons.play),
|
icon: const Icon(LucideIcons.play),
|
||||||
tooltip: l10n.cardBrowserStudy,
|
tooltip: l10n.cardBrowserStudy,
|
||||||
),
|
),
|
||||||
IconButton(
|
if (!viewOnly)
|
||||||
onPressed: write,
|
IconButton(
|
||||||
icon: const Icon(LucideIcons.squarePen),
|
onPressed: write,
|
||||||
tooltip: l10n.cardBrowserEditor,
|
icon: const Icon(LucideIcons.squarePen),
|
||||||
),
|
tooltip: l10n.cardBrowserEditor,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
PopupMenuButton<_Menu>(
|
PopupMenuButton<_Menu>(
|
||||||
icon: const Icon(LucideIcons.ellipsisVertical),
|
icon: const Icon(LucideIcons.ellipsisVertical),
|
||||||
|
|
@ -512,15 +518,16 @@ class _BrowserState() extends State<_Browser> {
|
||||||
_Menu.tags => unawaited(showManageTagsSheet(context)),
|
_Menu.tags => unawaited(showManageTagsSheet(context)),
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
if (!viewOnly)
|
||||||
value: _Menu.import,
|
PopupMenuItem(
|
||||||
child: ListTile(
|
value: _Menu.import,
|
||||||
dense: true,
|
child: ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
dense: true,
|
||||||
leading: const Icon(LucideIcons.fileInput),
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l10n.cardBrowserImport),
|
leading: const Icon(LucideIcons.fileInput),
|
||||||
|
title: Text(l10n.cardBrowserImport),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _Menu.tags,
|
value: _Menu.tags,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
|
@ -549,12 +556,15 @@ class _BrowserState() extends State<_Browser> {
|
||||||
|
|
||||||
_Actions _actions(CardBrowserState state) {
|
_Actions _actions(CardBrowserState state) {
|
||||||
final some = state.selected.isNotEmpty;
|
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 (
|
return (
|
||||||
move: some ? () => _move(state) : null,
|
move: writes ? () => _move(state) : null,
|
||||||
addTags: some ? () => _addTags(state) : null,
|
addTags: some ? () => _addTags(state) : null,
|
||||||
removeTags: some ? () => _removeTags(state) : null,
|
removeTags: some ? () => _removeTags(state) : null,
|
||||||
reset: some ? () => _resetProgress(state) : null,
|
reset: some ? () => _resetProgress(state) : null,
|
||||||
delete: some ? () => _delete(state) : null,
|
delete: writes ? () => _delete(state) : null,
|
||||||
done: () => _bloc.add(const CardBrowserEventSelectingToggled()),
|
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.
|
/// 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 {
|
extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -1715,12 +1725,14 @@ class const _Empty({required final VoidCallback onCreate})
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
if (onCreate != null) ...[
|
||||||
FilledButton.icon(
|
const SizedBox(height: 16),
|
||||||
onPressed: onCreate,
|
FilledButton.icon(
|
||||||
icon: const Icon(LucideIcons.plus, size: 18),
|
onPressed: onCreate,
|
||||||
label: Text(l10n.cardBrowserCreate),
|
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/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/editor/file_blocks.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/model/card.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/report_sheet.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/widget/report_sheet.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.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 report = card.content.report;
|
||||||
final hasMeta = tags.isNotEmpty;
|
final hasMeta = tags.isNotEmpty;
|
||||||
final hasBadges = report != null || buried;
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Material(
|
child: Material(
|
||||||
|
|
@ -131,54 +136,58 @@ class const CardTile({
|
||||||
),
|
),
|
||||||
MenuAnchor(
|
MenuAnchor(
|
||||||
menuChildren: [
|
menuChildren: [
|
||||||
MenuItemButton(
|
if (!access.viewOnly)
|
||||||
leadingIcon: const Icon(
|
|
||||||
LucideIcons.pencil,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
onPressed: onOpen,
|
|
||||||
child: Text(l10n.cardTileEdit),
|
|
||||||
),
|
|
||||||
if (report == null)
|
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(
|
leadingIcon: const Icon(
|
||||||
LucideIcons.flag,
|
LucideIcons.pencil,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
onPressed: onReport,
|
onPressed: onOpen,
|
||||||
child: Text(l10n.cardReportTitle),
|
child: Text(l10n.cardTileEdit),
|
||||||
)
|
),
|
||||||
else
|
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(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(
|
leadingIcon: Icon(
|
||||||
LucideIcons.flagOff,
|
buried ? LucideIcons.sunrise : LucideIcons.moon,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
onPressed: onResolve,
|
onPressed: onBuryToggle,
|
||||||
child: Text(l10n.cardResolveReport),
|
child: Text(
|
||||||
|
buried ? l10n.cardUnbury : l10n.studyBuryCard,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
if (!access.viewOnly)
|
||||||
leadingIcon: Icon(
|
MenuItemButton(
|
||||||
buried ? LucideIcons.sunrise : LucideIcons.moon,
|
leadingIcon: Icon(
|
||||||
size: 18,
|
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(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(LucideIcons.info, size: 18),
|
leadingIcon: const Icon(LucideIcons.info, size: 18),
|
||||||
onPressed: onInfo,
|
onPressed: onInfo,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.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/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_draft_editor.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_stack_tile.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,
|
state: state,
|
||||||
selected: _selected,
|
selected: _selected,
|
||||||
expanded: expanded,
|
expanded: expanded,
|
||||||
onAdd: _addDraft,
|
// A Viewer adds none (Halkyon's ADR 0044, D35).
|
||||||
|
onAdd: StudysetAccess.of(context).viewOnly
|
||||||
|
? null
|
||||||
|
: _addDraft,
|
||||||
onSelect: _select,
|
onSelect: _select,
|
||||||
onToggle: () =>
|
onToggle: () =>
|
||||||
setState(() => _stackExpanded = !expanded),
|
setState(() => _stackExpanded = !expanded),
|
||||||
|
|
@ -307,7 +311,9 @@ class const _Stack({
|
||||||
required final CardComposerState state,
|
required final CardComposerState state,
|
||||||
required final String? selected,
|
required final String? selected,
|
||||||
required final bool expanded,
|
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 ValueChanged<String> onSelect,
|
||||||
required final VoidCallback onToggle,
|
required final VoidCallback onToggle,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
|
|
@ -324,21 +330,23 @@ class const _Stack({
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (expanded)
|
if (onAdd != null) ...[
|
||||||
FilledButton.tonalIcon(
|
if (expanded)
|
||||||
onPressed: onAdd,
|
FilledButton.tonalIcon(
|
||||||
icon: const Icon(LucideIcons.plus),
|
|
||||||
label: Text(l10n.composerNewFlashcard),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Center(
|
|
||||||
child: IconButton.filledTonal(
|
|
||||||
tooltip: l10n.composerNewFlashcard,
|
|
||||||
onPressed: onAdd,
|
onPressed: onAdd,
|
||||||
icon: const Icon(LucideIcons.plus),
|
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(
|
Expanded(
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
itemCount: items.length,
|
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/l10n/language_tags.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/conflict_versions.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/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/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/model/card_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/answers_editor.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) {
|
void _change(CardContent content) {
|
||||||
|
// A Viewer's changes go nowhere (Halkyon's ADR 0044, D35).
|
||||||
|
if (StudysetAccess.viewOnlyNow(context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_sent = content;
|
_sent = content;
|
||||||
context.read<CardComposerBloc>().add(
|
context.read<CardComposerBloc>().add(
|
||||||
CardComposerEventChanged(widget.draft.id, content),
|
CardComposerEventChanged(widget.draft.id, content),
|
||||||
|
|
@ -304,6 +309,7 @@ class _CardDraftEditorState() extends State<CardDraftEditor> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
if (StudysetAccess.of(context).viewOnly) const ViewOnlyNote(),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
TagsPill(
|
TagsPill(
|
||||||
|
|
@ -332,18 +338,19 @@ class _CardDraftEditorState() extends State<CardDraftEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_Actions(
|
if (!StudysetAccess.of(context).viewOnly)
|
||||||
onAddHint: content.hints.length < CardContent.maxHints
|
_Actions(
|
||||||
? _addHint
|
onAddHint: content.hints.length < CardContent.maxHints
|
||||||
: null,
|
? _addHint
|
||||||
onAddExplanation: content.explanation == null
|
: null,
|
||||||
? _addExplanation
|
onAddExplanation: content.explanation == null
|
||||||
: null,
|
? _addExplanation
|
||||||
onDuplicate: widget.onDuplicate,
|
: null,
|
||||||
onClear: () => _send(CardComposerEventCleared(draft.id)),
|
onDuplicate: widget.onDuplicate,
|
||||||
onDelete: widget.onDelete,
|
onClear: () => _send(CardComposerEventCleared(draft.id)),
|
||||||
onSuggest: widget.onSuggest,
|
onDelete: widget.onDelete,
|
||||||
),
|
onSuggest: widget.onSuggest,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
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/l10n/language_tags.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.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/card_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/kanji/widget/character_readings.dart';
|
import 'package:halkyon_learn_flutter/src/feature/kanji/widget/character_readings.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
|
@ -80,6 +81,7 @@ class _CharacterFieldsState() extends State<CharacterFields> {
|
||||||
Text(label, style: labelStyle),
|
Text(label, style: labelStyle),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
|
readOnly: StudysetAccess.of(context).viewOnly,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|
@ -106,6 +108,7 @@ class _CharacterFieldsState() extends State<CharacterFields> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
readOnly: StudysetAccess.of(context).viewOnly,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
controller: meaning,
|
controller: meaning,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.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/model/card_content.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
@ -99,6 +100,7 @@ class _MatchingFieldsState() extends State<MatchingFields> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
readOnly: StudysetAccess.of(context).viewOnly,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
controller: target,
|
controller: target,
|
||||||
|
|
@ -154,6 +156,7 @@ class _MatchingFieldsState() extends State<MatchingFields> {
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
readOnly: StudysetAccess.of(context).viewOnly,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
controller: item,
|
controller: item,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
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/l10n/language_tags.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.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/card_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/model/jmdict_tags.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/model/jmdict_tags.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
|
@ -69,6 +70,7 @@ class _VocabularyFieldsState() extends State<VocabularyFields> {
|
||||||
Text(l10n.vocabularyReading, style: labelStyle),
|
Text(l10n.vocabularyReading, style: labelStyle),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
|
readOnly: StudysetAccess.of(context).viewOnly,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
controller: _reading,
|
controller: _reading,
|
||||||
|
|
@ -103,6 +105,7 @@ class _VocabularyFieldsState() extends State<VocabularyFields> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
readOnly: StudysetAccess.of(context).viewOnly,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
controller: meaning,
|
controller: meaning,
|
||||||
|
|
@ -237,6 +240,7 @@ class _SenseFieldsState() extends State<_SenseFields> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
readOnly: StudysetAccess.of(context).viewOnly,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
controller: gloss,
|
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/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_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/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/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/model/learning_path.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_info.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) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final error = Theme.of(context).colorScheme.error;
|
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(
|
return MenuAnchor(
|
||||||
menuChildren: [
|
menuChildren: [
|
||||||
MenuItemButton(
|
if (!viewOnly)
|
||||||
leadingIcon: const Icon(LucideIcons.pencil),
|
MenuItemButton(
|
||||||
onPressed: () =>
|
leadingIcon: const Icon(LucideIcons.pencil),
|
||||||
context.push(RoutePaths.editPath(studysetId, path.id)),
|
onPressed: () =>
|
||||||
child: Text(l10n.pathEdit),
|
context.push(RoutePaths.editPath(studysetId, path.id)),
|
||||||
),
|
child: Text(l10n.pathEdit),
|
||||||
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(LucideIcons.rotateCcw),
|
leadingIcon: const Icon(LucideIcons.rotateCcw),
|
||||||
onPressed: () => _reset(context),
|
onPressed: () => _reset(context),
|
||||||
child: Text(l10n.pathResetProgress),
|
child: Text(l10n.pathResetProgress),
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
if (!viewOnly)
|
||||||
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
MenuItemButton(
|
||||||
onPressed: () => _delete(context),
|
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
||||||
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
onPressed: () => _delete(context),
|
||||||
),
|
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
||||||
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(LucideIcons.info),
|
leadingIcon: const Icon(LucideIcons.info),
|
||||||
onPressed: () => unawaited(
|
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/editor/save_status.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_host.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/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/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/feedback/widget/feedback_actions.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/note/bloc/note_editor_bloc.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) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.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(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||||
child: Align(
|
child: Align(
|
||||||
|
|
@ -150,7 +155,8 @@ class const _Note({
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (readOnly) _Conflicts(state: state),
|
if (conflicted) _Conflicts(state: state),
|
||||||
|
if (viewOnly) const ViewOnlyNote(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SectionCard(
|
child: SectionCard(
|
||||||
padding: EdgeInsets.zero,
|
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/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_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/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/bloc/note_list_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note.dart';
|
import 'package:halkyon_learn_flutter/src/feature/note/model/note.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/note/widget/edited_ago.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 title = note.content.title;
|
||||||
final editedAt = note.editedAt;
|
final editedAt = note.editedAt;
|
||||||
void open() => context.push(RoutePaths.note(studysetId, note.id));
|
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(
|
return MenuAnchor(
|
||||||
menuChildren: [
|
menuChildren: [
|
||||||
MenuItemButton(
|
if (!viewOnly) ...[
|
||||||
leadingIcon: const Icon(LucideIcons.pencil),
|
MenuItemButton(
|
||||||
onPressed: open,
|
leadingIcon: const Icon(LucideIcons.pencil),
|
||||||
child: Text(l10n.noteEdit),
|
onPressed: open,
|
||||||
),
|
child: Text(l10n.noteEdit),
|
||||||
MenuItemButton(
|
|
||||||
leadingIcon: Icon(
|
|
||||||
LucideIcons.trash2,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
),
|
||||||
onPressed: () => _delete(context),
|
MenuItemButton(
|
||||||
child: Text(
|
leadingIcon: Icon(
|
||||||
l10n.noteDelete,
|
LucideIcons.trash2,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
onPressed: () => _delete(context),
|
||||||
|
child: Text(
|
||||||
|
l10n.noteDelete,
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(LucideIcons.info),
|
leadingIcon: const Icon(LucideIcons.info),
|
||||||
onPressed: () =>
|
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/router/route_paths.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/sync/uuid_time.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/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/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/deadline_status.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/model/pdf_chapter.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) {
|
if (!state.loaded || state.rows.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
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)) {
|
if (reordering || (canReorder && folded < mostFolded)) {
|
||||||
final reorder = ReorderToggle(
|
final reorder = ReorderToggle(
|
||||||
reordering: reordering,
|
reordering: reordering,
|
||||||
|
|
@ -359,32 +362,39 @@ class const _PdfTile({
|
||||||
final bloc = context.read<PdfListBloc>();
|
final bloc = context.read<PdfListBloc>();
|
||||||
final id = row.source.id;
|
final id = row.source.id;
|
||||||
final pageCount = content.pageCount;
|
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 [
|
return [
|
||||||
MenuItemButton(
|
if (!viewOnly) ...[
|
||||||
leadingIcon: const Icon(LucideIcons.pencil),
|
MenuItemButton(
|
||||||
onPressed: () => unawaited(_rename(context)),
|
leadingIcon: const Icon(LucideIcons.pencil),
|
||||||
child: Text(l10n.pdfRename),
|
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,
|
|
||||||
),
|
),
|
||||||
),
|
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)
|
if (pageCount != null && (row.furthestPage ?? 0) < pageCount)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(LucideIcons.bookCheck),
|
leadingIcon: const Icon(LucideIcons.bookCheck),
|
||||||
|
|
@ -411,11 +421,12 @@ class const _PdfTile({
|
||||||
onPressed: () => unawaited(_moreInfo(context)),
|
onPressed: () => unawaited(_moreInfo(context)),
|
||||||
child: Text(l10n.pdfMoreInfo),
|
child: Text(l10n.pdfMoreInfo),
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
if (!viewOnly)
|
||||||
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
MenuItemButton(
|
||||||
onPressed: () => unawaited(_delete(context)),
|
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
||||||
child: Text(l10n.pdfDelete, style: TextStyle(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/japanese/kana_input.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/l10n/language_tags.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/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.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.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';
|
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_extras.dart';
|
||||||
|
|
@ -125,6 +126,9 @@ class const StudyCardHeader({
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final error = Theme.of(context).colorScheme.error;
|
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(
|
PopupMenuItem<_CardAction> item(
|
||||||
_CardAction action,
|
_CardAction action,
|
||||||
IconData icon,
|
IconData icon,
|
||||||
|
|
@ -171,21 +175,25 @@ class const StudyCardHeader({
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
item(_CardAction.tags, LucideIcons.tag, l10n.cardTagsAdd),
|
item(_CardAction.tags, LucideIcons.tag, l10n.cardTagsAdd),
|
||||||
item(_CardAction.edit, LucideIcons.pencil, l10n.cardTileEdit),
|
if (!access.viewOnly)
|
||||||
item(_CardAction.report, LucideIcons.flag, l10n.cardReportTitle),
|
item(_CardAction.edit, LucideIcons.pencil, l10n.cardTileEdit),
|
||||||
|
if (!access.shared)
|
||||||
|
item(_CardAction.report, LucideIcons.flag, l10n.cardReportTitle),
|
||||||
if (onAsk != null)
|
if (onAsk != null)
|
||||||
item(
|
item(
|
||||||
_CardAction.ask,
|
_CardAction.ask,
|
||||||
LucideIcons.messageCircleQuestionMark,
|
LucideIcons.messageCircleQuestionMark,
|
||||||
l10n.discussionAskAbout,
|
l10n.discussionAskAbout,
|
||||||
),
|
),
|
||||||
item(_CardAction.bury, LucideIcons.moon, l10n.studyBuryCard),
|
if (!access.shared)
|
||||||
item(
|
item(_CardAction.bury, LucideIcons.moon, l10n.studyBuryCard),
|
||||||
_CardAction.delete,
|
if (!access.viewOnly)
|
||||||
LucideIcons.trash2,
|
item(
|
||||||
l10n.cardTileDelete,
|
_CardAction.delete,
|
||||||
color: error,
|
LucideIcons.trash2,
|
||||||
),
|
l10n.cardTileDelete,
|
||||||
|
color: error,
|
||||||
|
),
|
||||||
item(_CardAction.info, LucideIcons.info, l10n.moreInfo),
|
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/dependencies.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_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/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/data/anki_importer.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/anki/widget/anki_import_sheet.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';
|
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).
|
// them apart by space rather than a line (decided 2026-10-12).
|
||||||
const SizedBox(height: headerGap),
|
const SizedBox(height: headerGap),
|
||||||
if (studyset.viewOnly)
|
if (studyset.viewOnly)
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Row(
|
child: ViewOnlyNote(),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!wide)
|
if (!wide)
|
||||||
Padding(
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -189,6 +189,15 @@ member reads everything in the space.
|
||||||
recommendation): the first seal bringing a key ID the space never had, in the same
|
recommendation): the first seal bringing a key ID the space never had, in the same
|
||||||
transaction. Members the rolling device missed (keys changed, not trusted anew) get
|
transaction. Members the rolling device missed (keys changed, not trusted anew) get
|
||||||
it later as newcomers do; waiting for every member would roll again and again.
|
it later as newcomers do; waiting for every member would roll again and again.
|
||||||
|
- **D35.** **A Viewer opens cards, notes and steps read-only** (decided 11 Nov 2026, the
|
||||||
|
agent's recommendation, reading D30): the same screens as an Editor's, their content
|
||||||
|
shown but not editable, without Save, saying the studyset is theirs to view and
|
||||||
|
study; the lists' menus offer them nothing that writes.
|
||||||
|
- **D36.** **Burying and reporting a card are hidden in shared studysets for now**
|
||||||
|
(decided 11 Nov 2026, the agent's recommendation): both are written on the card's
|
||||||
|
row, so one member's would be everyone's, and a Viewer's would be refused. They return
|
||||||
|
once they are each person's own rows by the card's ID, as reviews are (D26), a change
|
||||||
|
of the synced tables shipped with a release.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
|
|
@ -209,8 +218,8 @@ member reads everything in the space.
|
||||||
@handle, the members, their roles, leaving), each member's own reviews.
|
@handle, the members, their roles, leaving), each member's own reviews.
|
||||||
Built 11 Nov 2026 (D27 to D34): the copy into a space of its own, the Share sheet
|
Built 11 Nov 2026 (D27 to D34): the copy into a space of its own, the Share sheet
|
||||||
with its members, roles, leaving and safety codes, a Viewer's screens read-only,
|
with its members, roles, leaving and safety codes, a Viewer's screens read-only,
|
||||||
and the key rolled once a member goes. Left: a Viewer's per-item buttons inside the
|
the key rolled once a member goes, and a Viewer's material opened read-only (D35),
|
||||||
material tabs, and a message for a write the engine refuses them.
|
burying and reporting hidden in shared studysets (D36).
|
||||||
5. **Invitations by email and by link** (D4, D5), the Inbox.
|
5. **Invitations by email and by link** (D4, D5), the Inbox.
|
||||||
6. **Agents** (D6, D7, D10, D12).
|
6. **Agents** (D6, D7, D10, D12).
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue