feat(app): notes in the studyset screen, written in the whole window

- A Notes tab beside Flashcards lists a studyset's notes in order: the
  title ("Untitled note" without one) and when each was last edited
  ("Edited 5 minutes ago", by the calendar from yesterday on, the date
  after a week); Edit and Delete behind its options. Deleting asks,
  then offers Undo for a moment (`NoteDeletions`). With none, the tab
  says so.
- "Create a note" in Add material opens a new note over the tabs; so
  does a note of the list. The note: a title, its tags once it exists,
  and its text at reading width; the app bar says how saving stands.
- `NoteEditorBloc` saves after 5 seconds without typing, when the app
  goes to the background, and when the note is closed (the bloc saves
  as it closes, so leaving never waits, and the write outlives the
  screen). Each save builds on the revisions the editor started from;
  another device's change takes over a note with nothing unsaved, and
  merges with one that has. A note changed differently on two devices
  is read-only until the person keeps a version of each field in
  conflict. A new note is created by its first save, never when left
  blank; a note deleted elsewhere says it no longer exists.
- The editor gains a format bar, docked below it while it has the
  focus (`RichContentEditor(formatBar: true)`): alignment, bold,
  italic, underline, superscript and subscript. It takes no focus, so
  the selection stays. Superscript and subscript are inline marks of
  their own, drawn by the font's raised and lowered glyphs in editors
  and read-only text alike.
- The library's and the topics' counts include notes (a studyset with
  what is inside it, as for cards).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-02 22:30:53 +02:00
commit 502ce36a30
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -10,6 +10,8 @@ import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_composer_bloc.d
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_composer_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/widget/explore_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/home_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/note/bloc/note_editor_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/note/widget/note_editor_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/widget/settings_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/study/bloc/study_session_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/study/widget/study_screen.dart';
@ -124,6 +126,20 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
cardId: state.pathParameters['cardId'],
),
),
// A note, new or stored; the new one's route first, as for cards.
GoRoute(
path: '/studysets/:studysetId/notes/new',
builder: (context, state) =>
_noteEditor(context, state.pathParameters['studysetId']!),
),
GoRoute(
path: '/studysets/:studysetId/notes/:noteId',
builder: (context, state) => _noteEditor(
context,
state.pathParameters['studysetId']!,
noteId: state.pathParameters['noteId'],
),
),
GoRoute(
path: RoutePaths.settings,
builder: (context, state) => const SettingsScreen(),
@ -138,6 +154,24 @@ Widget _studysetScreen(BuildContext context, String id) => FoundById<Studyset>(
builder: (studyset, all) => StudysetScreen(studyset: studyset, all: all),
);
/// The note [noteId] of the studyset [studysetId], or a new one without; a
/// studyset gone (deleted, or an old link) says so. Leaving the note closes
/// its bloc, which saves what is unsaved.
Widget _noteEditor(BuildContext context, String studysetId, {String? noteId}) {
final person = PersonScope.of(context);
return FoundById<Studyset>(
id: studysetId,
items: person.studysets.watchAll,
idOf: (studyset) => studyset.id,
builder: (studyset, _) => BlocProvider(
create: (context) =>
NoteEditorBloc(person.notes, person.tags, studysetId, noteId: noteId)
..add(const NoteEditorEventStarted()),
child: NoteEditorScreen(studysetTitle: studyset.content.title),
),
);
}
/// The composer of the studyset [studysetId]'s cards, opened on [cardId] if
/// given; a studyset gone (deleted, or an old link) says so.
Widget _composer(BuildContext context, String studysetId, {String? cardId}) {