feat(app): More info for studysets, paths, notes and PDFs
Wherever the old menus offered it: the studyset's options, a path's (in the list and in the runner), a note's, and a PDF's, all on the shared sheet the cards use: when it was made (from its UUID v7 id) and last changed where that is kept, its ids to copy (parent, studyset, PDF), and for a PDF its file. 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
252110832a
commit
a35318a5a6
19 changed files with 332 additions and 40 deletions
|
|
@ -681,6 +681,8 @@
|
||||||
"moreInfoItemId": "Element-ID",
|
"moreInfoItemId": "Element-ID",
|
||||||
"moreInfoStudysetId": "Lernset-ID",
|
"moreInfoStudysetId": "Lernset-ID",
|
||||||
"moreInfoPdfId": "PDF-Quellen-ID",
|
"moreInfoPdfId": "PDF-Quellen-ID",
|
||||||
|
"moreInfoParentId": "Übergeordnete ID",
|
||||||
|
"moreInfoFile": "Datei",
|
||||||
"moreInfoReviews": "Wiederholungen",
|
"moreInfoReviews": "Wiederholungen",
|
||||||
"moreInfoReviewCount": "Anzahl Wiederholungen",
|
"moreInfoReviewCount": "Anzahl Wiederholungen",
|
||||||
"moreInfoLastReviewed": "Zuletzt wiederholt",
|
"moreInfoLastReviewed": "Zuletzt wiederholt",
|
||||||
|
|
|
||||||
|
|
@ -764,6 +764,8 @@
|
||||||
"moreInfoItemId": "Item ID",
|
"moreInfoItemId": "Item ID",
|
||||||
"moreInfoStudysetId": "Studyset ID",
|
"moreInfoStudysetId": "Studyset ID",
|
||||||
"moreInfoPdfId": "PDF source ID",
|
"moreInfoPdfId": "PDF source ID",
|
||||||
|
"moreInfoParentId": "Parent ID",
|
||||||
|
"moreInfoFile": "File",
|
||||||
"moreInfoReviews": "Reviews",
|
"moreInfoReviews": "Reviews",
|
||||||
"moreInfoReviewCount": "Review count",
|
"moreInfoReviewCount": "Review count",
|
||||||
"moreInfoLastReviewed": "Last reviewed",
|
"moreInfoLastReviewed": "Last reviewed",
|
||||||
|
|
|
||||||
|
|
@ -4066,6 +4066,18 @@ abstract class AppLocalizations {
|
||||||
/// **'PDF source ID'**
|
/// **'PDF source ID'**
|
||||||
String get moreInfoPdfId;
|
String get moreInfoPdfId;
|
||||||
|
|
||||||
|
/// No description provided for @moreInfoParentId.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Parent ID'**
|
||||||
|
String get moreInfoParentId;
|
||||||
|
|
||||||
|
/// No description provided for @moreInfoFile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'File'**
|
||||||
|
String get moreInfoFile;
|
||||||
|
|
||||||
/// No description provided for @moreInfoReviews.
|
/// No description provided for @moreInfoReviews.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -2467,6 +2467,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get moreInfoPdfId => 'PDF-Quellen-ID';
|
String get moreInfoPdfId => 'PDF-Quellen-ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moreInfoParentId => 'Übergeordnete ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moreInfoFile => 'Datei';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get moreInfoReviews => 'Wiederholungen';
|
String get moreInfoReviews => 'Wiederholungen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2448,6 +2448,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get moreInfoPdfId => 'PDF source ID';
|
String get moreInfoPdfId => 'PDF source ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moreInfoParentId => 'Parent ID';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moreInfoFile => 'File';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get moreInfoReviews => 'Reviews';
|
String get moreInfoReviews => 'Reviews';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/services.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/adaptive_sheet.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
|
@ -12,6 +13,11 @@ typedef InfoRow = ({String label, String value, bool copyable});
|
||||||
/// Facts that belong together, under a title if any.
|
/// Facts that belong together, under a title if any.
|
||||||
typedef InfoSection = ({String? title, List<InfoRow> rows});
|
typedef InfoSection = ({String? title, List<InfoRow> rows});
|
||||||
|
|
||||||
|
/// [at] as a fact shows a moment: the date and the time of day, local.
|
||||||
|
String infoTime(BuildContext context, DateTime at) => DateFormat.yMMMd(
|
||||||
|
Localizations.localeOf(context).toLanguageTag(),
|
||||||
|
).add_jm().format(at.toLocal());
|
||||||
|
|
||||||
/// Shows [sections] of facts about something, each in a card of its own:
|
/// Shows [sections] of facts about something, each in a card of its own:
|
||||||
/// "More info".
|
/// "More info".
|
||||||
Future<void> showMoreInfoSheet(
|
Future<void> showMoreInfoSheet(
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@ Future<void> showCardInfo(
|
||||||
required String studysetId,
|
required String studysetId,
|
||||||
}) async {
|
}) async {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final format = DateFormat.yMMMd(
|
|
||||||
Localizations.localeOf(context).toLanguageTag(),
|
|
||||||
).add_jm();
|
|
||||||
final ratings = [
|
final ratings = [
|
||||||
for (final review in await PersonScope.of(
|
for (final review in await PersonScope.of(
|
||||||
context,
|
context,
|
||||||
|
|
@ -27,7 +24,7 @@ Future<void> showCardInfo(
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String when(DateTime at) => format.format(at.toLocal());
|
String when(DateTime at) => infoTime(context, at);
|
||||||
final created = uuidV7Time(card.id);
|
final created = uuidV7Time(card.id);
|
||||||
// Revision ids sort by when they were written: the last is the latest.
|
// Revision ids sort by when they were written: the last is the latest.
|
||||||
final modified = uuidV7Time((card.heads.toList()..sort()).last);
|
final modified = uuidV7Time((card.heads.toList()..sort()).last);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/sync/uuid_time.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/widget/more_info_sheet.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// Shows what there is to know about the learning path [pathId] of the
|
||||||
|
/// studyset [studysetId]: when it was made, and its ids. A path's content
|
||||||
|
/// keeps no time of its last change.
|
||||||
|
Future<void> showPathInfo(
|
||||||
|
BuildContext context, {
|
||||||
|
required String pathId,
|
||||||
|
required String studysetId,
|
||||||
|
}) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final created = uuidV7Time(pathId);
|
||||||
|
return showMoreInfoSheet(context, [
|
||||||
|
(
|
||||||
|
title: null,
|
||||||
|
rows: [
|
||||||
|
if (created != null)
|
||||||
|
(
|
||||||
|
label: l10n.moreInfoCreated,
|
||||||
|
value: infoTime(context, created),
|
||||||
|
copyable: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
title: null,
|
||||||
|
rows: [
|
||||||
|
(label: l10n.moreInfoItemId, value: pathId, copyable: true),
|
||||||
|
(label: l10n.moreInfoStudysetId, value: studysetId, copyable: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
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';
|
||||||
|
|
@ -10,6 +12,7 @@ import 'package:halkyon_learn_flutter/src/core/widget/title_with_kicker.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.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:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
|
@ -131,6 +134,13 @@ class const _PathTile({
|
||||||
onPressed: () => _delete(context),
|
onPressed: () => _delete(context),
|
||||||
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
||||||
),
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(LucideIcons.info),
|
||||||
|
onPressed: () => unawaited(
|
||||||
|
showPathInfo(context, pathId: path.id, studysetId: studysetId),
|
||||||
|
),
|
||||||
|
child: Text(l10n.moreInfo),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
builder: (context, menu, _) => GestureDetector(
|
builder: (context, menu, _) => GestureDetector(
|
||||||
onLongPressStart: (details) =>
|
onLongPressStart: (details) =>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_runner_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_runner_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path_step.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path_step.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/exercise_views.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/exercise_views.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_info.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ class const PathRunnerScreen({
|
||||||
RoutePaths.pathStep(studysetId, pathId, step.id),
|
RoutePaths.pathStep(studysetId, pathId, step.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (state.loaded) _PathMenu(pathId: pathId),
|
if (state.loaded) _PathMenu(studysetId: studysetId, pathId: pathId),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: switch (state) {
|
body: switch (state) {
|
||||||
|
|
@ -73,8 +74,12 @@ class const PathRunnerScreen({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The path's options, behind the … button: start it afresh, or delete it.
|
/// The path's options, behind the … button: start it afresh, delete it, or
|
||||||
class const _PathMenu({required final String pathId}) extends StatelessWidget {
|
/// see what there is to know about it.
|
||||||
|
class const _PathMenu({
|
||||||
|
required final String studysetId,
|
||||||
|
required final String pathId,
|
||||||
|
}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
@ -91,6 +96,13 @@ class const _PathMenu({required final String pathId}) extends StatelessWidget {
|
||||||
onPressed: () => unawaited(_delete(context)),
|
onPressed: () => unawaited(_delete(context)),
|
||||||
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
child: Text(l10n.pathDelete, style: TextStyle(color: error)),
|
||||||
),
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(LucideIcons.info),
|
||||||
|
onPressed: () => unawaited(
|
||||||
|
showPathInfo(context, pathId: pathId, studysetId: studysetId),
|
||||||
|
),
|
||||||
|
child: Text(l10n.moreInfo),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
builder: (context, menu, _) => IconButton(
|
builder: (context, menu, _) => IconButton(
|
||||||
icon: const Icon(LucideIcons.ellipsis),
|
icon: const Icon(LucideIcons.ellipsis),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/sync/uuid_time.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/widget/more_info_sheet.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/note/model/note.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// Shows what there is to know about [note], in the studyset [studysetId]:
|
||||||
|
/// when it was made and last changed, and its ids.
|
||||||
|
Future<void> showNoteInfo(
|
||||||
|
BuildContext context,
|
||||||
|
Note note, {
|
||||||
|
required String studysetId,
|
||||||
|
}) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final created = uuidV7Time(note.id);
|
||||||
|
return showMoreInfoSheet(context, [
|
||||||
|
(
|
||||||
|
title: null,
|
||||||
|
rows: [
|
||||||
|
if (created != null)
|
||||||
|
(
|
||||||
|
label: l10n.moreInfoCreated,
|
||||||
|
value: infoTime(context, created),
|
||||||
|
copyable: false,
|
||||||
|
),
|
||||||
|
if (note.editedAt case final modified?)
|
||||||
|
(
|
||||||
|
label: l10n.moreInfoModified,
|
||||||
|
value: infoTime(context, modified),
|
||||||
|
copyable: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
title: null,
|
||||||
|
rows: [
|
||||||
|
(label: l10n.moreInfoItemId, value: note.id, copyable: true),
|
||||||
|
(label: l10n.moreInfoStudysetId, value: studysetId, copyable: true),
|
||||||
|
if (note.content.source case (:final pdfId, page: _))
|
||||||
|
(label: l10n.moreInfoPdfId, value: pdfId, copyable: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -10,6 +12,7 @@ import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.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';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/note/widget/note_info.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
|
@ -67,7 +70,8 @@ class const NoteList({required final String studysetId, super.key})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One note: its title ("Untitled note" without one) and when it was last
|
/// One note: its title ("Untitled note" without one) and when it was last
|
||||||
/// edited; its options behind the … button, or a long press.
|
/// edited; its options (edit, delete, more info) behind the … button, or a
|
||||||
|
/// long press.
|
||||||
class const _NoteTile({
|
class const _NoteTile({
|
||||||
required final String studysetId,
|
required final String studysetId,
|
||||||
required final Note note,
|
required final Note note,
|
||||||
|
|
@ -96,6 +100,12 @@ class const _NoteTile({
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(LucideIcons.info),
|
||||||
|
onPressed: () =>
|
||||||
|
unawaited(showNoteInfo(context, note, studysetId: studysetId)),
|
||||||
|
child: Text(l10n.moreInfo),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
builder: (context, menu, _) => ListTile(
|
builder: (context, menu, _) => ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.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/dependencies/person_scope.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/theme/theme_extras.dart';
|
import 'package:halkyon_learn_flutter/src/core/theme/theme_extras.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/confirm_dialog.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/confirm_dialog.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/more_info_sheet.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.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/data/pdf_engine.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart';
|
||||||
|
|
@ -303,37 +304,60 @@ class const _PdfTile({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _moreInfo(BuildContext context) => showAdaptiveSheet<void>(
|
/// What there is to know about the PDF: when it was added, its ids, and
|
||||||
context,
|
/// its file.
|
||||||
builder: (context) {
|
Future<void> _moreInfo(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final content = row.source.content;
|
final source = row.source;
|
||||||
return SheetScaffold(
|
final created = uuidV7Time(source.id);
|
||||||
title: content.title,
|
return showMoreInfoSheet(context, [
|
||||||
body: Column(
|
(
|
||||||
children: [
|
title: null,
|
||||||
ListTile(
|
rows: [
|
||||||
title: Text(l10n.pdfInfoFileName),
|
if (created != null)
|
||||||
subtitle: Text(content.fileName),
|
(
|
||||||
),
|
label: l10n.moreInfoCreated,
|
||||||
ListTile(
|
value: infoTime(context, created),
|
||||||
title: Text(l10n.pdfInfoSize),
|
copyable: false,
|
||||||
subtitle: Text(switch (row.byteSize) {
|
|
||||||
final size? => formatFileSize(l10n, size),
|
|
||||||
null => l10n.pdfInfoUnknown,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text(l10n.pdfInfoPages),
|
|
||||||
subtitle: Text(
|
|
||||||
content.pageCount?.toString() ?? l10n.pdfInfoUnknown,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
(
|
||||||
|
title: null,
|
||||||
|
rows: [
|
||||||
|
(label: l10n.moreInfoItemId, value: source.id, copyable: true),
|
||||||
|
(
|
||||||
|
label: l10n.moreInfoStudysetId,
|
||||||
|
value: source.studysetId,
|
||||||
|
copyable: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
title: l10n.moreInfoFile,
|
||||||
|
rows: [
|
||||||
|
(
|
||||||
|
label: l10n.pdfInfoFileName,
|
||||||
|
value: source.content.fileName,
|
||||||
|
copyable: false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
label: l10n.pdfInfoSize,
|
||||||
|
value: switch (row.byteSize) {
|
||||||
|
final size? => formatFileSize(l10n, size),
|
||||||
|
null => l10n.pdfInfoUnknown,
|
||||||
},
|
},
|
||||||
);
|
copyable: false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
label: l10n.pdfInfoPages,
|
||||||
|
value: source.content.pageCount?.toString() ?? l10n.pdfInfoUnknown,
|
||||||
|
copyable: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/// Asks, then hands the deletion to the person's PDF deletions and offers
|
/// Asks, then hands the deletion to the person's PDF deletions and offers
|
||||||
/// to undo it for a moment; the deletion outlives this tile.
|
/// to undo it for a moment; the deletion outlives this tile.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/sync/uuid_time.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/widget/more_info_sheet.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// Shows what there is to know about [studyset]: when it was made and last
|
||||||
|
/// changed, its id and its parent's.
|
||||||
|
Future<void> showStudysetInfo(BuildContext context, Studyset studyset) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final created = uuidV7Time(studyset.id);
|
||||||
|
return showMoreInfoSheet(context, [
|
||||||
|
(
|
||||||
|
title: null,
|
||||||
|
rows: [
|
||||||
|
if (created != null)
|
||||||
|
(
|
||||||
|
label: l10n.moreInfoCreated,
|
||||||
|
value: infoTime(context, created),
|
||||||
|
copyable: false,
|
||||||
|
),
|
||||||
|
if (studyset.updatedAt case final modified?)
|
||||||
|
(
|
||||||
|
label: l10n.moreInfoModified,
|
||||||
|
value: infoTime(context, modified),
|
||||||
|
copyable: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
title: null,
|
||||||
|
rows: [
|
||||||
|
(label: l10n.moreInfoItemId, value: studyset.id, copyable: true),
|
||||||
|
if (studyset.parentId case final parentId?)
|
||||||
|
(label: l10n.moreInfoParentId, value: parentId, copyable: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyse
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/license_footer.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_actions.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_breadcrumb.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_rail.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:halkyon_learn_flutter/src/feature/studyset/widget/studyset_sheet.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
@ -1349,10 +1350,12 @@ enum _Option() {
|
||||||
archive,
|
archive,
|
||||||
unarchive,
|
unarchive,
|
||||||
delete,
|
delete,
|
||||||
|
info,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The studyset's options: edit, move, its study options, import content,
|
/// The studyset's options: edit, move, its study options, import content,
|
||||||
/// reset its progress, archive or unarchive, delete.
|
/// reset its progress, archive or unarchive, delete, and what there is to
|
||||||
|
/// know about it.
|
||||||
class const _Options({
|
class const _Options({
|
||||||
required final Studyset studyset,
|
required final Studyset studyset,
|
||||||
required final List<Studyset> all,
|
required final List<Studyset> all,
|
||||||
|
|
@ -1439,6 +1442,7 @@ class const _Options({
|
||||||
studyset,
|
studyset,
|
||||||
onDeleted: () => _leave(context),
|
onDeleted: () => _leave(context),
|
||||||
),
|
),
|
||||||
|
_Option.info => showStudysetInfo(context, studyset),
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
item(_Option.edit, LucideIcons.pencil, l10n.studysetMenuEdit),
|
item(_Option.edit, LucideIcons.pencil, l10n.studysetMenuEdit),
|
||||||
|
|
@ -1468,6 +1472,7 @@ class const _Options({
|
||||||
l10n.studysetMenuDelete,
|
l10n.studysetMenuDelete,
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
|
item(_Option.info, LucideIcons.info, l10n.moreInfo),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -409,6 +409,32 @@ void main() {
|
||||||
expect(pathGateway.deletions, isEmpty);
|
expect(pathGateway.deletions, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("a path's More info, from its tile and from its runner, shows "
|
||||||
|
'its ids', (tester) async {
|
||||||
|
final paths = repository();
|
||||||
|
final id = await paths.createPath(
|
||||||
|
'japanese',
|
||||||
|
LearningPathContent.titled('Particles'),
|
||||||
|
);
|
||||||
|
await paths.createStep(id, StepKind.info, step(StepKind.info, 'Read'));
|
||||||
|
await showApp(tester, RoutePaths.studyset('japanese'));
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Path options'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await chooseInMenu(tester, 'More info');
|
||||||
|
expect(find.text(id), findsOneWidget);
|
||||||
|
expect(find.text('japanese'), findsOneWidget);
|
||||||
|
await tester.tap(find.byTooltip('Close'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Particles'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byTooltip('Path options'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await chooseInMenu(tester, 'More info');
|
||||||
|
expect(find.text(id), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets("a step's sources open from below its text, cited as the "
|
testWidgets("a step's sources open from below its text, cited as the "
|
||||||
'device cites, in the order of a reference list', (tester) async {
|
'device cites, in the order of a reference list', (tester) async {
|
||||||
final paths = repository();
|
final paths = repository();
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,25 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("a note's More info shows its ids, to copy", (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final created = await stored('Particles', 'は');
|
||||||
|
await showApp(tester, RoutePaths.studyset('japanese'));
|
||||||
|
await openNotesTab(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Note options'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('More info'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Item ID'), findsOneWidget);
|
||||||
|
expect(find.text(created.noteId), findsOneWidget);
|
||||||
|
expect(find.text('japanese'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('deleting asks, takes the note from the list, and Undo brings '
|
testWidgets('deleting asks, takes the note from the list, and Undo brings '
|
||||||
'it back', (tester) async {
|
'it back', (tester) async {
|
||||||
await stored('Particles', 'は');
|
await stored('Particles', 'は');
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,26 @@ void main() {
|
||||||
expect(pdfGateway.deletions, isEmpty);
|
expect(pdfGateway.deletions, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("a PDF's More info shows its ids and its file, as other "
|
||||||
|
"things' More info does", (tester) async {
|
||||||
|
await repository().import(
|
||||||
|
'japanese',
|
||||||
|
bytes: pdf('kana'),
|
||||||
|
fileName: 'kana.pdf',
|
||||||
|
);
|
||||||
|
await showApp(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Actions'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await choose(tester, 'More info');
|
||||||
|
|
||||||
|
expect(find.text('Item ID'), findsOneWidget);
|
||||||
|
expect(find.text('japanese'), findsOneWidget);
|
||||||
|
expect(find.text('File'), findsOneWidget);
|
||||||
|
expect(find.text('kana.pdf'), findsOneWidget);
|
||||||
|
expect(find.text('Pages'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('a deadline may be to finish a chapter, read from the PDF', (
|
testWidgets('a deadline may be to finish a chapter, read from the PDF', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,22 @@ void main() {
|
||||||
expect(find.text('Learning path'), findsOneWidget);
|
expect(find.text('Learning path'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("a studyset's More info shows its id and its parent's, to "
|
||||||
|
'copy', (tester) async {
|
||||||
|
stored('japanese', 'Japanese');
|
||||||
|
stored('kana', 'Kana', parent: 'japanese');
|
||||||
|
await showApp(tester, location: RoutePaths.studyset('kana'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await choose(tester, 'More info');
|
||||||
|
|
||||||
|
expect(find.text('Item ID'), findsOneWidget);
|
||||||
|
expect(find.text('kana'), findsOneWidget);
|
||||||
|
expect(find.text('Parent ID'), findsOneWidget);
|
||||||
|
expect(find.text('japanese'), findsOneWidget);
|
||||||
|
expect(find.byTooltip('Copy'), findsNWidgets(2));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('the bibliography is a button above the content, not in the '
|
testWidgets('the bibliography is a button above the content, not in the '
|
||||||
'app bar; on a phone at the foot of the topics', (tester) async {
|
'app bar; on a phone at the foot of the topics', (tester) async {
|
||||||
stored('japanese', 'Japanese');
|
stored('japanese', 'Japanese');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue