feat(studyset): every menu of a studyset offers all of its options
The library's card offered six options, a topic three, the studyset's own screen all of them. Each now offers the same, from one list: edit, move, study options, import and export content, reset progress, archive or unarchive, delete, more info, and for a copy reporting to its author and its reports and suggestions. Importing runs where the studyset's blocs are, so from the library or a topic it opens the studyset's screen, which asks what to import as it opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b70d8e4ed2
commit
b7e5b695b9
10 changed files with 460 additions and 409 deletions
|
|
@ -86,7 +86,12 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
|
|||
return MaterialPage<void>(
|
||||
key: ValueKey('studyset-$id'),
|
||||
name: RoutePaths.studyset(id),
|
||||
child: _studysetScreen(context, id),
|
||||
child: _studysetScreen(
|
||||
context,
|
||||
id,
|
||||
importOnOpen:
|
||||
state.uri.queryParameters['import'] == 'content',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -314,10 +319,15 @@ Widget _stepEditor(
|
|||
);
|
||||
}
|
||||
|
||||
Widget _studysetScreen(BuildContext context, String id) => _inStudyset(
|
||||
Widget _studysetScreen(
|
||||
BuildContext context,
|
||||
String id, {
|
||||
bool importOnOpen = false,
|
||||
}) => _inStudyset(
|
||||
context,
|
||||
id,
|
||||
(studyset, all) => StudysetScreen(studyset: studyset, all: all),
|
||||
(studyset, all) =>
|
||||
StudysetScreen(studyset: studyset, all: all, importOnOpen: importOnOpen),
|
||||
wayBack: (
|
||||
label: AppLocalizations.of(context).studysetBackToLibrary,
|
||||
location: RoutePaths.library,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyse
|
|||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_actions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_navigation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_sheet.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -512,7 +513,6 @@ class _GridState() extends State<_Grid> {
|
|||
}
|
||||
|
||||
Widget _card(BuildContext context, Studyset studyset) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final bloc = context.read<StudysetListBloc>();
|
||||
return StudysetCard(
|
||||
key: ValueKey(studyset.id),
|
||||
|
|
@ -533,50 +533,28 @@ class _GridState() extends State<_Grid> {
|
|||
bloc.add(const StudysetListEventProgressRefreshRequested());
|
||||
},
|
||||
onToggleStar: () => bloc.add(StudysetListEventStarToggled(studyset)),
|
||||
onEdit: () async {
|
||||
final content = await showStudysetSheet(
|
||||
context,
|
||||
initial: studyset.content,
|
||||
);
|
||||
if (content != null && content != studyset.content) {
|
||||
bloc.add(StudysetListEventContentChanged(studyset.id, content));
|
||||
}
|
||||
},
|
||||
// The grid's context, as for deleting: it holds the list's bloc and
|
||||
// outlives the card's menu.
|
||||
onMove: () async {
|
||||
final all = await PersonScope.of(this.context).studysets.all();
|
||||
if (mounted) {
|
||||
await moveStudyset(this.context, studyset, all);
|
||||
}
|
||||
},
|
||||
onStudyOptions: () => changeStudyOptions(this.context, studyset),
|
||||
onImport: () => importFromAnki(this.context, studysetId: studyset.id),
|
||||
// Archiving is tidying up, not destroying, so it doesn't ask first:
|
||||
// it offers to undo.
|
||||
onArchive: () {
|
||||
bloc.add(StudysetListEventArchiveRequested(studyset.id));
|
||||
showUndoSnackBar(
|
||||
context,
|
||||
message: l10n.libraryStudysetArchived,
|
||||
onUndo: () =>
|
||||
bloc.add(StudysetListEventUnarchiveRequested(studyset.id)),
|
||||
);
|
||||
},
|
||||
onUnarchive: () {
|
||||
bloc.add(StudysetListEventUnarchiveRequested(studyset.id));
|
||||
showUndoSnackBar(
|
||||
context,
|
||||
message: l10n.libraryStudysetUnarchived,
|
||||
onUndo: () =>
|
||||
bloc.add(StudysetListEventArchiveRequested(studyset.id)),
|
||||
);
|
||||
},
|
||||
// The grid's context, not the card's: the card is about to go.
|
||||
onDelete: () => deleteStudysetWithUndo(this.context, studyset),
|
||||
// The grid's context, not the card's: it holds the list's bloc and
|
||||
// outlives the card's menu, and the card may be about to go.
|
||||
onOption: (option) => unawaited(
|
||||
doStudysetOption(
|
||||
this.context,
|
||||
option,
|
||||
studyset,
|
||||
onImport: () => unawaited(_openToImport(studyset)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens [studyset]'s screen, asking what to import into it: its imports
|
||||
/// run there.
|
||||
Future<void> _openToImport(Studyset studyset) async {
|
||||
final all = await PersonScope.of(context).studysets.all();
|
||||
if (mounted) {
|
||||
openStudyset(context, studyset.id, all, importContent: true);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _deletedCard(BuildContext context, Studyset studyset) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final bloc = context.read<StudysetListBloc>();
|
||||
|
|
|
|||
|
|
@ -1,16 +1,290 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/citation/widget/source_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/report_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/export/widget/export_studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/model/feedback.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/widget/feedback_actions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/widget/feedback_threads.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/widget/studyset_study_options_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_info.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_move_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_sheet.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
// What a studyset's options do the same from the library's card and from the
|
||||
// studyset's own screen, through the StudysetListBloc above them.
|
||||
// What a studyset's options are and do, the same wherever a menu of it
|
||||
// opens (decided 2026-10-12): its card in the library, its own screen, a
|
||||
// topic beside it; through the StudysetListBloc above them.
|
||||
|
||||
/// One of a studyset's options.
|
||||
enum StudysetOption() {
|
||||
edit,
|
||||
move,
|
||||
studyOptions,
|
||||
importContent,
|
||||
exportContent,
|
||||
reset,
|
||||
archive,
|
||||
unarchive,
|
||||
delete,
|
||||
info,
|
||||
reportToAuthor,
|
||||
feedback,
|
||||
}
|
||||
|
||||
/// An option as its menus show it: what it is, its icon and its name, and
|
||||
/// whether it destroys (deleting), in the error colour.
|
||||
typedef StudysetOptionEntry = ({
|
||||
StudysetOption option,
|
||||
IconData icon,
|
||||
String label,
|
||||
bool destroys,
|
||||
});
|
||||
|
||||
/// [studyset]'s options, in order: edit, move, its study options, import
|
||||
/// and export content, reset its progress, archive or unarchive, delete,
|
||||
/// what there is to know about it; for a copy of a published studyset,
|
||||
/// reporting to its author and its reports and suggestions.
|
||||
List<StudysetOptionEntry> studysetOptions(
|
||||
AppLocalizations l10n,
|
||||
Studyset studyset,
|
||||
) {
|
||||
StudysetOptionEntry entry(
|
||||
StudysetOption option,
|
||||
IconData icon,
|
||||
String label, {
|
||||
bool destroys = false,
|
||||
}) => (option: option, icon: icon, label: label, destroys: destroys);
|
||||
return [
|
||||
entry(StudysetOption.edit, LucideIcons.pencil, l10n.studysetMenuEdit),
|
||||
entry(StudysetOption.move, LucideIcons.folderInput, l10n.studysetMenuMove),
|
||||
entry(
|
||||
StudysetOption.studyOptions,
|
||||
LucideIcons.slidersHorizontal,
|
||||
l10n.studyOptionsTitle,
|
||||
),
|
||||
entry(
|
||||
StudysetOption.importContent,
|
||||
LucideIcons.fileInput,
|
||||
l10n.studysetImportContent,
|
||||
),
|
||||
entry(
|
||||
StudysetOption.exportContent,
|
||||
LucideIcons.fileOutput,
|
||||
l10n.studysetMenuExport,
|
||||
),
|
||||
entry(StudysetOption.reset, LucideIcons.rotateCcw, l10n.studysetMenuReset),
|
||||
if (studyset.isArchived)
|
||||
entry(
|
||||
StudysetOption.unarchive,
|
||||
LucideIcons.archiveRestore,
|
||||
l10n.studysetMenuUnarchive,
|
||||
)
|
||||
else
|
||||
entry(
|
||||
StudysetOption.archive,
|
||||
LucideIcons.archive,
|
||||
l10n.studysetMenuArchive,
|
||||
),
|
||||
entry(
|
||||
StudysetOption.delete,
|
||||
LucideIcons.trash2,
|
||||
l10n.studysetMenuDelete,
|
||||
destroys: true,
|
||||
),
|
||||
entry(StudysetOption.info, LucideIcons.info, l10n.moreInfo),
|
||||
if (fromRelease(studyset.content.published)) ...[
|
||||
entry(
|
||||
StudysetOption.reportToAuthor,
|
||||
LucideIcons.flag,
|
||||
l10n.reportToAuthor,
|
||||
),
|
||||
entry(
|
||||
StudysetOption.feedback,
|
||||
LucideIcons.messagesSquare,
|
||||
l10n.studysetFeedback,
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/// [studyset]'s options as a ⋮ (or [icon]) button's menu, each chosen
|
||||
/// handed to [onSelected].
|
||||
class const StudysetOptionsButton({
|
||||
required final Studyset studyset,
|
||||
required final ValueChanged<StudysetOption> onSelected,
|
||||
final Widget icon = const Icon(LucideIcons.ellipsisVertical),
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final error = Theme.of(context).colorScheme.error;
|
||||
return PopupMenuButton<StudysetOption>(
|
||||
icon: icon,
|
||||
tooltip: l10n.studysetOptions,
|
||||
onSelected: onSelected,
|
||||
itemBuilder: (context) => [
|
||||
for (final entry in studysetOptions(l10n, studyset))
|
||||
PopupMenuItem(
|
||||
value: entry.option,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(entry.icon, color: entry.destroys ? error : null),
|
||||
title: Text(
|
||||
entry.label,
|
||||
style: TextStyle(color: entry.destroys ? error : null),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Does [option] for [studyset], with [all] the person's studysets (read
|
||||
/// when not given). [onImport] imports content into it, as where the menu
|
||||
/// is can; [onDeleted] runs once it is deleted, as to close its screen.
|
||||
Future<void> doStudysetOption(
|
||||
BuildContext context,
|
||||
StudysetOption option,
|
||||
Studyset studyset, {
|
||||
required VoidCallback onImport,
|
||||
List<Studyset>? all,
|
||||
VoidCallback? onDeleted,
|
||||
}) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
Future<List<Studyset>> everything() async =>
|
||||
all ?? await PersonScope.of(context).studysets.all();
|
||||
switch (option) {
|
||||
case StudysetOption.edit:
|
||||
final studysets = await everything();
|
||||
if (context.mounted) {
|
||||
await editStudyset(context, studyset, studysets);
|
||||
}
|
||||
case StudysetOption.move:
|
||||
final studysets = await everything();
|
||||
if (context.mounted) {
|
||||
await moveStudyset(context, studyset, studysets);
|
||||
}
|
||||
case StudysetOption.studyOptions:
|
||||
await changeStudyOptions(context, studyset);
|
||||
case StudysetOption.importContent:
|
||||
onImport();
|
||||
case StudysetOption.exportContent:
|
||||
await exportStudyset(context, studyset);
|
||||
case StudysetOption.reset:
|
||||
await resetStudysetProgress(context, studyset);
|
||||
case StudysetOption.archive || StudysetOption.unarchive:
|
||||
archiveStudyset(
|
||||
context,
|
||||
studyset,
|
||||
archive: option == StudysetOption.archive,
|
||||
);
|
||||
case StudysetOption.delete:
|
||||
deleteStudysetWithUndo(context, studyset, onDeleted: onDeleted);
|
||||
case StudysetOption.info:
|
||||
await showStudysetInfo(context, studyset);
|
||||
case StudysetOption.reportToAuthor:
|
||||
if (studyset.content.published case final origin?) {
|
||||
await reportStudysetToAuthor(context, origin);
|
||||
}
|
||||
case StudysetOption.feedback:
|
||||
if (studyset.content.published case final origin?) {
|
||||
await showStudysetFeedback(
|
||||
context,
|
||||
publicId: origin.publication,
|
||||
title: l10n.studysetFeedback,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Edits [studyset]'s title, colour and the like, with what it inherits
|
||||
/// from the studyset it sits in among [all].
|
||||
Future<void> editStudyset(
|
||||
BuildContext context,
|
||||
Studyset studyset,
|
||||
List<Studyset> all,
|
||||
) async {
|
||||
final bloc = context.read<StudysetListBloc>();
|
||||
final parentId = studyset.parentId;
|
||||
final content = await showStudysetSheet(
|
||||
context,
|
||||
initial: studyset.content,
|
||||
insideAnother: parentId != null,
|
||||
inheritedColorName: parentId == null ? null : colorNameOf(parentId, all),
|
||||
inheritedLanguages: parentId == null
|
||||
? (front: null, back: null)
|
||||
: languagesOf(parentId, all),
|
||||
);
|
||||
if (content != null && content != studyset.content) {
|
||||
bloc.add(StudysetListEventContentChanged(studyset.id, content));
|
||||
}
|
||||
}
|
||||
|
||||
/// Archives [studyset], or takes it out of the archive, and offers to undo
|
||||
/// that: archiving is tidying up, not destroying, so it doesn't ask first.
|
||||
void archiveStudyset(
|
||||
BuildContext context,
|
||||
Studyset studyset, {
|
||||
required bool archive,
|
||||
}) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final bloc = context.read<StudysetListBloc>()
|
||||
..add(
|
||||
archive
|
||||
? StudysetListEventArchiveRequested(studyset.id)
|
||||
: StudysetListEventUnarchiveRequested(studyset.id),
|
||||
);
|
||||
showUndoSnackBar(
|
||||
context,
|
||||
message: archive
|
||||
? l10n.libraryStudysetArchived
|
||||
: l10n.libraryStudysetUnarchived,
|
||||
onUndo: () => bloc.add(
|
||||
archive
|
||||
? StudysetListEventUnarchiveRequested(studyset.id)
|
||||
: StudysetListEventArchiveRequested(studyset.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Reports what is wrong with the studyset [origin] names, a copy's, to
|
||||
/// the author of the published one.
|
||||
Future<void> reportStudysetToAuthor(
|
||||
BuildContext context,
|
||||
PublishedRef origin,
|
||||
) async {
|
||||
if (!canWriteToAuthors(context)) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).feedbackNeedsIdentity),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (await showReportSheet(context, authorOnly: true)
|
||||
case (:final report, toAuthor: _) when context.mounted) {
|
||||
await sendReportToAuthor(
|
||||
context,
|
||||
origin,
|
||||
FeedbackTargetKind.studyset,
|
||||
report,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks where [studyset] goes among [all] the person's studysets, moves it
|
||||
/// there and says where it went.
|
||||
|
|
@ -117,7 +391,7 @@ Future<void> resetStudysetProgress(
|
|||
}
|
||||
final chosen = await showDialog<({bool cards, bool pdfs, bool paths})>(
|
||||
context: context,
|
||||
builder: (_) => _ResetDialog(parts: parts),
|
||||
builder: (dialogContext) => _ResetDialog(parts: parts),
|
||||
);
|
||||
if (chosen == null || !context.mounted) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/core/widget/content_counts_line.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/widget/title_with_kicker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_actions.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -48,26 +49,8 @@ class const StudysetCard({
|
|||
/// Stars it, or takes its star away.
|
||||
required final VoidCallback onToggleStar,
|
||||
|
||||
/// Edits its title, colour and the like.
|
||||
required final VoidCallback onEdit,
|
||||
|
||||
/// Moves it into another studyset or to the top level.
|
||||
required final VoidCallback onMove,
|
||||
|
||||
/// Changes how it is studied.
|
||||
required final VoidCallback onStudyOptions,
|
||||
|
||||
/// Imports an Anki deck into it.
|
||||
required final VoidCallback onImport,
|
||||
|
||||
/// Archives it.
|
||||
required final VoidCallback onArchive,
|
||||
|
||||
/// Takes it out of the archive.
|
||||
required final VoidCallback onUnarchive,
|
||||
|
||||
/// Deletes it.
|
||||
required final VoidCallback onDelete,
|
||||
/// Does one of its options, as every menu of a studyset has them.
|
||||
required final ValueChanged<StudysetOption> onOption,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -146,15 +129,9 @@ class _StudysetCardState() extends State<StudysetCard> {
|
|||
onShare: widget.onShare,
|
||||
starred: studyset.isStarred,
|
||||
starVisible: studyset.isStarred || _hovering,
|
||||
archived: archived,
|
||||
onToggleStar: widget.onToggleStar,
|
||||
onEdit: widget.onEdit,
|
||||
onMove: widget.onMove,
|
||||
onStudyOptions: widget.onStudyOptions,
|
||||
onImport: widget.onImport,
|
||||
onArchive: widget.onArchive,
|
||||
onUnarchive: widget.onUnarchive,
|
||||
onDelete: widget.onDelete,
|
||||
studyset: studyset,
|
||||
onOption: widget.onOption,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -322,16 +299,6 @@ class const _Paused() extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
enum _Option() {
|
||||
edit,
|
||||
move,
|
||||
studyOptions,
|
||||
import,
|
||||
archive,
|
||||
unarchive,
|
||||
delete,
|
||||
}
|
||||
|
||||
/// Who made it, the star, whether it is public, and the options.
|
||||
class const _BottomBar({
|
||||
required final String by,
|
||||
|
|
@ -339,15 +306,9 @@ class const _BottomBar({
|
|||
required final VoidCallback onShare,
|
||||
required final bool starred,
|
||||
required final bool starVisible,
|
||||
required final bool archived,
|
||||
required final VoidCallback onToggleStar,
|
||||
required final VoidCallback onEdit,
|
||||
required final VoidCallback onMove,
|
||||
required final VoidCallback onStudyOptions,
|
||||
required final VoidCallback onImport,
|
||||
required final VoidCallback onArchive,
|
||||
required final VoidCallback onUnarchive,
|
||||
required final VoidCallback onDelete,
|
||||
required final Studyset studyset,
|
||||
required final ValueChanged<StudysetOption> onOption,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -388,75 +349,15 @@ class const _BottomBar({
|
|||
: l10n.studysetVisibilityPrivate,
|
||||
onPressed: onShare,
|
||||
),
|
||||
PopupMenuButton<_Option>(
|
||||
StudysetOptionsButton(
|
||||
studyset: studyset,
|
||||
icon: const Icon(LucideIcons.ellipsis),
|
||||
tooltip: l10n.studysetOptions,
|
||||
onSelected: (option) => switch (option) {
|
||||
_Option.edit => onEdit(),
|
||||
_Option.move => onMove(),
|
||||
_Option.studyOptions => onStudyOptions(),
|
||||
_Option.import => onImport(),
|
||||
_Option.archive => onArchive(),
|
||||
_Option.unarchive => onUnarchive(),
|
||||
_Option.delete => onDelete(),
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
_item(_Option.edit, LucideIcons.pencil, l10n.studysetMenuEdit),
|
||||
_item(
|
||||
_Option.move,
|
||||
LucideIcons.folderInput,
|
||||
l10n.studysetMenuMove,
|
||||
),
|
||||
_item(
|
||||
_Option.studyOptions,
|
||||
LucideIcons.slidersHorizontal,
|
||||
l10n.studyOptionsTitle,
|
||||
),
|
||||
_item(
|
||||
_Option.import,
|
||||
LucideIcons.fileInput,
|
||||
l10n.ankiImportMenu,
|
||||
),
|
||||
if (archived)
|
||||
_item(
|
||||
_Option.unarchive,
|
||||
LucideIcons.archiveRestore,
|
||||
l10n.studysetMenuUnarchive,
|
||||
)
|
||||
else
|
||||
_item(
|
||||
_Option.archive,
|
||||
LucideIcons.archive,
|
||||
l10n.studysetMenuArchive,
|
||||
),
|
||||
_item(
|
||||
_Option.delete,
|
||||
LucideIcons.trash2,
|
||||
l10n.studysetMenuDelete,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
onSelected: onOption,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// An option; a [color] marks one that destroys, as deleting.
|
||||
PopupMenuItem<_Option> _item(
|
||||
_Option option,
|
||||
IconData icon,
|
||||
String label, {
|
||||
Color? color,
|
||||
}) => PopupMenuItem(
|
||||
value: option,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(icon, color: color),
|
||||
title: Text(label, style: TextStyle(color: color)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Who made the studyset, where a text button would sit: it only says.
|
||||
|
|
|
|||
|
|
@ -17,16 +17,28 @@ import 'package:material_ui/material_ui.dart';
|
|||
|
||||
/// Opens the studyset [id] in the library tab with the screens of the
|
||||
/// studysets it sits in beneath it, the top level first; [all] are the
|
||||
/// person's studysets.
|
||||
void openStudyset(BuildContext context, String id, List<Studyset> all) {
|
||||
/// person's studysets. With [importContent], its screen asks what to import
|
||||
/// into it as it opens.
|
||||
void openStudyset(
|
||||
BuildContext context,
|
||||
String id,
|
||||
List<Studyset> all, {
|
||||
bool importContent = false,
|
||||
}) {
|
||||
final router = GoRouter.of(context);
|
||||
final path = [for (final studyset in ancestorsOf(id, all)) studyset.id, id];
|
||||
router.go(RoutePaths.studyset(path.first));
|
||||
String location(String each) => each == id && importContent
|
||||
? _importing(each)
|
||||
: RoutePaths.studyset(each);
|
||||
router.go(location(path.first));
|
||||
for (final next in path.skip(1)) {
|
||||
unawaited(router.push<void>(RoutePaths.studyset(next)));
|
||||
unawaited(router.push<void>(location(next)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The studyset [id]'s screen, asking what to import into it as it opens.
|
||||
String _importing(String id) => '${RoutePaths.studyset(id)}?import=content';
|
||||
|
||||
/// Opens the studyset [id] from the screen of the studyset [from] it sits
|
||||
/// in, at any depth: the screens of the studysets between them come in
|
||||
/// beneath it. From anywhere else, as [openStudyset] does.
|
||||
|
|
@ -35,16 +47,23 @@ void openStudysetWithin(
|
|||
required String from,
|
||||
required String id,
|
||||
required List<Studyset> all,
|
||||
bool importContent = false,
|
||||
}) {
|
||||
final ancestors = [for (final studyset in ancestorsOf(id, all)) studyset.id];
|
||||
final start = ancestors.indexOf(from);
|
||||
if (start < 0) {
|
||||
openStudyset(context, id, all);
|
||||
openStudyset(context, id, all, importContent: importContent);
|
||||
return;
|
||||
}
|
||||
final router = GoRouter.of(context);
|
||||
for (final next in [...ancestors.skip(start + 1), id]) {
|
||||
unawaited(router.push<void>(RoutePaths.studyset(next)));
|
||||
unawaited(
|
||||
router.push<void>(
|
||||
next == id && importContent
|
||||
? _importing(next)
|
||||
: RoutePaths.studyset(next),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
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/app_loading_indicator.dart';
|
||||
|
|
@ -7,7 +9,6 @@ import 'package:halkyon_learn_flutter/src/feature/study/widget/start_studying.da
|
|||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_actions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_navigation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_sheet.dart';
|
||||
|
|
@ -83,20 +84,6 @@ class _StudysetRailState() extends State<StudysetRail> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _edit(Studyset topic) async {
|
||||
final bloc = context.read<StudysetListBloc>();
|
||||
final content = await showStudysetSheet(
|
||||
context,
|
||||
initial: topic.content,
|
||||
insideAnother: true,
|
||||
inheritedColorName: colorNameOf(widget.parent.id, widget.all),
|
||||
inheritedLanguages: languagesOf(widget.parent.id, widget.all),
|
||||
);
|
||||
if (content != null && content != topic.content) {
|
||||
bloc.add(StudysetListEventContentChanged(topic.id, content));
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens [studyset]'s screen, with those of the levels between beneath.
|
||||
void _open(Studyset studyset) {
|
||||
if (widget.onOpen case final open?) {
|
||||
|
|
@ -276,12 +263,22 @@ class _StudysetRailState() extends State<StudysetRail> {
|
|||
: () => _toggle(topic.id),
|
||||
onOpen: () => _open(topic),
|
||||
onStudy: () => _study(topic),
|
||||
onEdit: () => _edit(topic),
|
||||
onReset: () =>
|
||||
resetStudysetProgress(this.context, topic),
|
||||
onDelete: () => deleteStudysetWithUndo(
|
||||
this.context,
|
||||
topic,
|
||||
// The rail's context: it holds the list's bloc and
|
||||
// outlives the topic's menu.
|
||||
onOption: (option) => unawaited(
|
||||
doStudysetOption(
|
||||
this.context,
|
||||
option,
|
||||
topic,
|
||||
all: widget.all,
|
||||
onImport: () => openStudysetWithin(
|
||||
this.context,
|
||||
from: widget.parent.id,
|
||||
id: topic.id,
|
||||
all: widget.all,
|
||||
importContent: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_unfolded.contains(topic.id))
|
||||
|
|
@ -464,9 +461,7 @@ class const _Topic({
|
|||
required final VoidCallback? onToggle,
|
||||
required final VoidCallback onOpen,
|
||||
required final VoidCallback onStudy,
|
||||
required final VoidCallback onEdit,
|
||||
required final VoidCallback onReset,
|
||||
required final VoidCallback onDelete,
|
||||
required final ValueChanged<StudysetOption> onOption,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -485,22 +480,17 @@ class _TopicState() extends State<_Topic> {
|
|||
final topic = widget.topic;
|
||||
return MenuAnchor(
|
||||
controller: _options,
|
||||
// Every studyset's options, as its card and its screen have them.
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.pencil),
|
||||
onPressed: widget.onEdit,
|
||||
child: Text(l10n.studysetMenuEdit),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(LucideIcons.rotateCcw),
|
||||
onPressed: widget.onReset,
|
||||
child: Text(l10n.studysetMenuReset),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(LucideIcons.trash2, color: error),
|
||||
onPressed: widget.onDelete,
|
||||
child: Text(l10n.studysetMenuDelete, style: TextStyle(color: error)),
|
||||
),
|
||||
for (final entry in studysetOptions(l10n, topic))
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(entry.icon, color: entry.destroys ? error : null),
|
||||
onPressed: () => widget.onOption(entry.option),
|
||||
child: Text(
|
||||
entry.label,
|
||||
style: TextStyle(color: entry.destroys ? error : null),
|
||||
),
|
||||
),
|
||||
],
|
||||
builder: (context, menu, _) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:flutter/rendering.dart' show OverflowBoxFit;
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
|
|
@ -21,21 +20,15 @@ import 'package:halkyon_learn_flutter/src/core/widget/scroll_hiding_fab.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/title_with_kicker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/top_gradient.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.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';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_field.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_text_import_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/report_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/widget/studyset_dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/explore/widget/copy_update_banner.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/export/widget/export_studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/model/feedback.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/widget/feedback_actions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/feedback/widget/feedback_threads.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_import_texts.dart';
|
||||
|
|
@ -52,15 +45,12 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bl
|
|||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/license_footer.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_actions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_breadcrumb.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_info.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_navigation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_parts.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_rail.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_sheet.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -82,6 +72,10 @@ class const StudysetScreen({
|
|||
|
||||
/// All the person's studysets, for where it sits and where it can move.
|
||||
required final List<Studyset> all,
|
||||
|
||||
/// Whether it opens asking what to import into it, as Import content
|
||||
/// from a menu elsewhere asks for: its imports run here.
|
||||
final bool importOnOpen = false,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
|
|
@ -212,7 +206,12 @@ class const StudysetScreen({
|
|||
],
|
||||
child: CardListScope(
|
||||
studysetId: studyset.id,
|
||||
child: _StudysetView(studyset: studyset, all: all),
|
||||
child: importOnOpen
|
||||
? _ImportOnOpen(
|
||||
studysetId: studyset.id,
|
||||
child: _StudysetView(studyset: studyset, all: all),
|
||||
)
|
||||
: _StudysetView(studyset: studyset, all: all),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -1353,6 +1352,31 @@ class const _AddMaterial({
|
|||
}
|
||||
}
|
||||
|
||||
/// [child], asking once, as it first shows, what to import into the
|
||||
/// studyset [studysetId].
|
||||
class const _ImportOnOpen({
|
||||
required final String studysetId,
|
||||
required final Widget child,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<_ImportOnOpen> createState() => _ImportOnOpenState();
|
||||
}
|
||||
|
||||
class _ImportOnOpenState() extends State<_ImportOnOpen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
unawaited(_importContent(context, widget.studysetId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
/// Asks what to import into the studyset [studysetId], and imports it: a
|
||||
/// learning path from a file in the paths' import format, an Anki deck, or
|
||||
/// flashcards as text.
|
||||
|
|
@ -1569,187 +1593,25 @@ final class const _StudyTopic(final Studyset studyset) extends _TopicsChoice;
|
|||
/// Adding material to the studyset.
|
||||
final class const _AddMaterialChoice() extends _TopicsChoice;
|
||||
|
||||
enum _Option() {
|
||||
edit,
|
||||
move,
|
||||
studyOptions,
|
||||
importContent,
|
||||
exportContent,
|
||||
reset,
|
||||
archive,
|
||||
unarchive,
|
||||
delete,
|
||||
info,
|
||||
reportToAuthor,
|
||||
feedback,
|
||||
}
|
||||
|
||||
/// The studyset's options: edit, move, its study options, import and export
|
||||
/// content, reset its progress, archive or unarchive, delete, and what there is to
|
||||
/// know about it; for a copy of a published studyset, reporting to its author
|
||||
/// and its reports and suggestions.
|
||||
/// The studyset's options, as every menu of a studyset has them
|
||||
/// (`studysetOptions`); deleting it leaves its screen, up to the studyset
|
||||
/// it sat in.
|
||||
class const _Options({
|
||||
required final Studyset studyset,
|
||||
required final List<Studyset> all,
|
||||
}) extends StatelessWidget {
|
||||
Future<void> _edit(BuildContext context) async {
|
||||
final bloc = context.read<StudysetListBloc>();
|
||||
final parentId = studyset.parentId;
|
||||
final content = await showStudysetSheet(
|
||||
context,
|
||||
initial: studyset.content,
|
||||
insideAnother: parentId != null,
|
||||
inheritedColorName: parentId == null ? null : colorNameOf(parentId, all),
|
||||
inheritedLanguages: parentId == null
|
||||
? (front: null, back: null)
|
||||
: languagesOf(parentId, all),
|
||||
);
|
||||
if (content != null && content != studyset.content) {
|
||||
bloc.add(StudysetListEventContentChanged(studyset.id, content));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports what is wrong with the studyset [origin] names, a copy's, to
|
||||
/// the author of the published one.
|
||||
Future<void> _reportToAuthor(
|
||||
BuildContext context,
|
||||
PublishedRef origin,
|
||||
) async {
|
||||
if (!canWriteToAuthors(context)) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).feedbackNeedsIdentity),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (await showReportSheet(context, authorOnly: true)
|
||||
case (:final report, toAuthor: _) when context.mounted) {
|
||||
await sendReportToAuthor(
|
||||
context,
|
||||
origin,
|
||||
FeedbackTargetKind.studyset,
|
||||
report,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _archive(BuildContext context, {required bool archive}) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final bloc = context.read<StudysetListBloc>();
|
||||
if (archive) {
|
||||
bloc.add(StudysetListEventArchiveRequested(studyset.id));
|
||||
} else {
|
||||
bloc.add(StudysetListEventUnarchiveRequested(studyset.id));
|
||||
}
|
||||
showUndoSnackBar(
|
||||
context,
|
||||
message: archive
|
||||
? l10n.libraryStudysetArchived
|
||||
: l10n.libraryStudysetUnarchived,
|
||||
onUndo: () => bloc.add(
|
||||
archive
|
||||
? StudysetListEventUnarchiveRequested(studyset.id)
|
||||
: StudysetListEventArchiveRequested(studyset.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Up to the studyset it sat in, whose screen is beneath, or to the
|
||||
/// library.
|
||||
void _leave(BuildContext context) => goUpTo(context, studyset.parentId, all);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
// A color marks an option that destroys, as deleting.
|
||||
PopupMenuItem<_Option> item(
|
||||
_Option option,
|
||||
IconData icon,
|
||||
String text, {
|
||||
Color? color,
|
||||
}) => PopupMenuItem(
|
||||
value: option,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(icon, color: color),
|
||||
title: Text(text, style: TextStyle(color: color)),
|
||||
Widget build(BuildContext context) => StudysetOptionsButton(
|
||||
studyset: studyset,
|
||||
onSelected: (option) => unawaited(
|
||||
doStudysetOption(
|
||||
context,
|
||||
option,
|
||||
studyset,
|
||||
all: all,
|
||||
onImport: () => unawaited(_importContent(context, studyset.id)),
|
||||
onDeleted: () => goUpTo(context, studyset.parentId, all),
|
||||
),
|
||||
);
|
||||
return PopupMenuButton<_Option>(
|
||||
icon: const Icon(LucideIcons.ellipsisVertical),
|
||||
tooltip: l10n.studysetOptions,
|
||||
onSelected: (option) => switch (option) {
|
||||
_Option.edit => _edit(context),
|
||||
_Option.move => moveStudyset(context, studyset, all),
|
||||
_Option.studyOptions => changeStudyOptions(context, studyset),
|
||||
_Option.importContent => _importContent(context, studyset.id),
|
||||
_Option.exportContent => exportStudyset(context, studyset),
|
||||
_Option.reset => resetStudysetProgress(context, studyset),
|
||||
_Option.archive => _archive(context, archive: true),
|
||||
_Option.unarchive => _archive(context, archive: false),
|
||||
_Option.delete => deleteStudysetWithUndo(
|
||||
context,
|
||||
studyset,
|
||||
onDeleted: () => _leave(context),
|
||||
),
|
||||
_Option.info => showStudysetInfo(context, studyset),
|
||||
_Option.reportToAuthor => _reportToAuthor(
|
||||
context,
|
||||
studyset.content.published!,
|
||||
),
|
||||
_Option.feedback => showStudysetFeedback(
|
||||
context,
|
||||
publicId: studyset.content.published!.publication,
|
||||
title: l10n.studysetFeedback,
|
||||
),
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
item(_Option.edit, LucideIcons.pencil, l10n.studysetMenuEdit),
|
||||
item(_Option.move, LucideIcons.folderInput, l10n.studysetMenuMove),
|
||||
item(
|
||||
_Option.studyOptions,
|
||||
LucideIcons.slidersHorizontal,
|
||||
l10n.studyOptionsTitle,
|
||||
),
|
||||
item(
|
||||
_Option.importContent,
|
||||
LucideIcons.fileInput,
|
||||
l10n.studysetImportContent,
|
||||
),
|
||||
item(
|
||||
_Option.exportContent,
|
||||
LucideIcons.fileOutput,
|
||||
l10n.studysetMenuExport,
|
||||
),
|
||||
item(_Option.reset, LucideIcons.rotateCcw, l10n.studysetMenuReset),
|
||||
if (studyset.isArchived)
|
||||
item(
|
||||
_Option.unarchive,
|
||||
LucideIcons.archiveRestore,
|
||||
l10n.studysetMenuUnarchive,
|
||||
)
|
||||
else
|
||||
item(_Option.archive, LucideIcons.archive, l10n.studysetMenuArchive),
|
||||
item(
|
||||
_Option.delete,
|
||||
LucideIcons.trash2,
|
||||
l10n.studysetMenuDelete,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
item(_Option.info, LucideIcons.info, l10n.moreInfo),
|
||||
if (fromRelease(studyset.content.published)) ...[
|
||||
item(_Option.reportToAuthor, LucideIcons.flag, l10n.reportToAuthor),
|
||||
item(
|
||||
_Option.feedback,
|
||||
LucideIcons.messagesSquare,
|
||||
l10n.studysetFeedback,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -393,8 +393,8 @@ void main() {
|
|||
expect(find.text('No studysets match your search'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the options offer, in this order: Edit, Move, Study options, '
|
||||
'Import from Anki, Archive and Delete', (tester) async {
|
||||
testWidgets("the options are every studyset's, as its screen has them, "
|
||||
'in this order', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
|
|
@ -406,9 +406,12 @@ void main() {
|
|||
'Edit',
|
||||
'Move to…',
|
||||
'Study options',
|
||||
'Import from Anki',
|
||||
'Import content',
|
||||
'Export content',
|
||||
'Reset progress',
|
||||
'Archive',
|
||||
'Delete',
|
||||
'More info',
|
||||
])
|
||||
tester.getTopLeft(find.text(label)).dy,
|
||||
];
|
||||
|
|
@ -437,13 +440,19 @@ void main() {
|
|||
expect(gateway.moves.single.$2, 'languages');
|
||||
});
|
||||
|
||||
testWidgets('imports from Anki into the studyset from its options', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('Import content opens the studyset, asking there what to '
|
||||
'import: from Anki, say', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
|
||||
await choose(tester, 'Japanese', 'Import from Anki');
|
||||
await choose(tester, 'Japanese', 'Import content');
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: find.byType(AppBar), matching: find.text('Japanese')),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text('Anki deck'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(files.asked, [
|
||||
|
|
|
|||
|
|
@ -50,13 +50,7 @@ void main() {
|
|||
onOpen: () => calls.add('open'),
|
||||
onStudy: () => calls.add('study'),
|
||||
onToggleStar: () => calls.add('star'),
|
||||
onEdit: () => calls.add('edit'),
|
||||
onMove: () => calls.add('move'),
|
||||
onStudyOptions: () => calls.add('study options'),
|
||||
onImport: () => calls.add('import'),
|
||||
onArchive: () => calls.add('archive'),
|
||||
onUnarchive: () => calls.add('unarchive'),
|
||||
onDelete: () => calls.add('delete'),
|
||||
onOption: (option) => calls.add(option.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -237,6 +231,9 @@ void main() {
|
|||
|
||||
for (final (option, call) in [
|
||||
('Edit', 'edit'),
|
||||
('Import content', 'importContent'),
|
||||
('Export content', 'exportContent'),
|
||||
('Reset progress', 'reset'),
|
||||
('Archive', 'archive'),
|
||||
('Delete', 'delete'),
|
||||
]) {
|
||||
|
|
|
|||
|
|
@ -328,8 +328,8 @@ void main() {
|
|||
expect(find.text('All topics'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a long press on a topic opens its options: edit, reset its '
|
||||
'progress, delete', (tester) async {
|
||||
testWidgets("a long press on a topic opens its options, every "
|
||||
"studyset's", (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('kana', 'Kana', parent: 'japanese');
|
||||
await openFromLibrary(tester, 'Japanese');
|
||||
|
|
@ -337,9 +337,19 @@ void main() {
|
|||
await tester.longPress(find.text('Kana'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Edit'), findsOneWidget);
|
||||
expect(find.text('Reset progress'), findsOneWidget);
|
||||
expect(find.text('Delete'), findsOneWidget);
|
||||
for (final option in [
|
||||
'Edit',
|
||||
'Move to…',
|
||||
'Study options',
|
||||
'Import content',
|
||||
'Export content',
|
||||
'Reset progress',
|
||||
'Archive',
|
||||
'Delete',
|
||||
'More info',
|
||||
]) {
|
||||
expect(find.text(option), findsOneWidget, reason: option);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets("resetting a topic's progress asks what of it starts afresh, "
|
||||
|
|
|
|||
Loading…
Reference in a new issue