/// Turning pasted image bytes and picked or dropped files into links in the /// open note. /// /// Shared by the capture and detail screens: each owns one /// [ImageAttachController], hands it to its [NoteEditor] (which binds the /// insert-at-cursor), and routes pastes through [attachImages] and picks and /// drops through [attachFiles]. library; import 'dart:typed_data'; import 'package:design_system/design_system.dart'; import 'package:flutter/widgets.dart'; import 'package:todo/attachments/image_store_api.dart'; import 'package:todo/attachments/image_store_scope.dart'; import 'package:todo/images/attachment_ref.dart'; /// Lets a screen insert lines into whichever editor is currently mounted. class ImageAttachController { void Function(List lines)? _insert; /// Called by the editor on mount with its insert-at-cursor function; /// returns the call that undoes it (a no-op once another editor bound). VoidCallback bind(void Function(List lines) insert) { _insert = insert; return () { if (identical(_insert, insert)) _insert = null; }; } /// Inserts [lines] at the editor's cursor; false when no editor is bound. bool insert(List lines) { final insert = _insert; if (insert == null || lines.isEmpty) return false; insert(lines); return true; } } /// Stores each of [images] for the note [noteId] names and inserts their /// links, in order, through [target]. /// /// [noteId] is a callback because the capture screen's draft only gets an id /// once there is something to save; asking for it is what reserves one. /// Reserving writes nothing, so a rejected paste still never creates an /// empty note. [rejected] counts inputs already known /// not to be images (e.g. a pasted PDF), so one message covers both. Future attachImages( BuildContext context, { required String Function() noteId, required ImageAttachController target, required List images, int rejected = 0, }) async { final store = ImageStoreScope.read(context); final refs = []; var notImages = rejected; String? failure; for (final bytes in images) { try { final ref = await store.attach(noteId(), bytes); if (ref == null) { notImages++; } else { refs.add(ref); } } on ImageStoreException catch (e) { failure = e.message; break; } } target.insert([for (final ref in refs) ref.imageMarkdown]); if (!context.mounted) return; if (failure != null) { showError(context, failure); } else if (notImages > 0) { showError( context, notImages == 1 ? 'Not an image' : '$notImages files were not images', ); } } /// Stores each of [files] (picked or dropped, any type and size) for the note /// [noteId] names and inserts their links, in order, through [target]: /// images inline, anything else as a file link. Stops at the first file that /// cannot be stored, keeping the ones before it. Future attachFiles( BuildContext context, { required String Function() noteId, required ImageAttachController target, required List files, }) async { final store = ImageStoreScope.read(context); final attached = []; String? failure; for (final file in files) { try { attached.add(await store.attachFile(noteId(), file)); } on ImageStoreException catch (e) { failure = e.message; break; } } target.insert([for (final attachment in attached) attachment.markdown]); if (failure != null && context.mounted) showError(context, failure); }