diff --git a/AGENTS.md b/AGENTS.md index 89077f5..3c500e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,7 @@ migrate → Settings → Import). A note never holds image bytes, only a line `![](https://kuhy-cloud.duckdns.org/todo-images//.jpg)` -(`lib/images/image_ref.dart` owns that shape), so sync and older builds are +(`lib/images/attachment_ref.dart` owns that shape), so sync and older builds are untouched. Every image is re-encoded before it leaves the device: ≤2560 px, JPEG q85, EXIF stripped after applying its rotation (`image_processing.dart`). `ImageCacheStore` keeps every linked image on-device (offline) plus a diff --git a/lib/attachments/image_store_api.dart b/lib/attachments/image_store_api.dart index 09ed042..a6a83a0 100644 --- a/lib/attachments/image_store_api.dart +++ b/lib/attachments/image_store_api.dart @@ -8,7 +8,7 @@ library; import 'package:flutter/foundation.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; /// Where to draw an image from right now. sealed class ImageSource { @@ -70,24 +70,24 @@ abstract class ImageStore extends ChangeNotifier { /// /// Returns the new ref, or null when [bytes] is not an image. Throws /// [ImageStoreException] when it cannot be stored. - Future attach(String noteId, Uint8List bytes); + Future attach(String noteId, Uint8List bytes); /// Uploads the queue, fetches every image [noteTexts] link to that this /// device lacks, and evicts cached images no note links to. Future reconcile(Iterable noteTexts); /// Where to draw [ref] from. - ImageSource sourceFor(ImageRef ref); + ImageSource sourceFor(AttachmentRef ref); /// Whether [ref] is still waiting to upload (always false when unknown). - bool isPending(ImageRef ref); + bool isPending(AttachmentRef ref); /// Login and queue summary. Future status(); } /// Every image ref across [noteTexts]. -Set imageRefsAcross(Iterable noteTexts) => { +Set imageRefsAcross(Iterable noteTexts) => { for (final text in noteTexts) ...imageRefsIn(text), }; @@ -95,17 +95,17 @@ Set imageRefsAcross(Iterable noteTexts) => { /// outside the app root): attaching is refused, nothing is ever available. class NoImageStore extends ImageStore { @override - Future attach(String noteId, Uint8List bytes) async => + Future attach(String noteId, Uint8List bytes) async => throw const ImageStoreException('Images are not available here'); @override Future reconcile(Iterable noteTexts) async {} @override - ImageSource sourceFor(ImageRef ref) => const MissingImageSource(); + ImageSource sourceFor(AttachmentRef ref) => const MissingImageSource(); @override - bool isPending(ImageRef ref) => false; + bool isPending(AttachmentRef ref) => false; @override Future status() async => diff --git a/lib/attachments/image_store_io.dart b/lib/attachments/image_store_io.dart index 85b2d0e..df52f73 100644 --- a/lib/attachments/image_store_io.dart +++ b/lib/attachments/image_store_io.dart @@ -8,13 +8,12 @@ import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; - import 'package:todo/attachments/image_password_store.dart'; import 'package:todo/attachments/image_store_api.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/images/dufs_image_client.dart'; import 'package:todo/images/image_cache_store.dart'; import 'package:todo/images/image_processing.dart'; -import 'package:todo/images/image_ref.dart'; /// Opens the store under the app-support directory's `images/`. // coverage:ignore-start @@ -47,10 +46,10 @@ class IoImageStore extends ImageStore { final Future Function(Uint8List) _prepare; @override - Future attach(String noteId, Uint8List bytes) async { + Future attach(String noteId, Uint8List bytes) async { final jpeg = await _prepare(bytes); if (jpeg == null) return null; - final ref = ImageRef.create(noteId); + final ref = AttachmentRef.create(noteId); try { await cache.put(ref, jpeg); } on FileSystemException catch (e) { @@ -67,7 +66,7 @@ class IoImageStore extends ImageStore { Future reconcile(Iterable noteTexts) => _sync(imageRefsAcross(noteTexts)); - Future _sync(Set refs) async { + Future _sync(Set refs) async { final login = await _passwords.login(); final client = login == null ? null @@ -83,7 +82,7 @@ class IoImageStore extends ImageStore { } @override - ImageSource sourceFor(ImageRef ref) { + ImageSource sourceFor(AttachmentRef ref) { final file = cache.fileFor(ref); return file.existsSync() ? LocalImageSource(file.path) @@ -91,7 +90,7 @@ class IoImageStore extends ImageStore { } @override - bool isPending(ImageRef ref) => cache.isPending(ref); + bool isPending(AttachmentRef ref) => cache.isPending(ref); @override Future status() async => ImageStoreStatus( diff --git a/lib/attachments/image_store_web.dart b/lib/attachments/image_store_web.dart index 9864b20..8b51291 100644 --- a/lib/attachments/image_store_web.dart +++ b/lib/attachments/image_store_web.dart @@ -9,7 +9,7 @@ import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:todo/attachments/image_store_api.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/sync/desktop_wrapper.dart'; /// Opens the store against the wrapper that served this page. @@ -34,8 +34,8 @@ class WebImageStore extends ImageStore { Uri _images(String rest) => Uri.parse('$origin/images/$rest'); @override - Future attach(String noteId, Uint8List bytes) async { - final ref = ImageRef.create(noteId); + Future attach(String noteId, Uint8List bytes) async { + final ref = AttachmentRef.create(noteId); final http.Response response; try { response = await _http.post(_images(ref.relativePath), body: bytes); @@ -60,7 +60,7 @@ class WebImageStore extends ImageStore { Future reconcile(Iterable noteTexts) => _reconcile(imageRefsAcross(noteTexts)); - Future _reconcile(Set refs) async { + Future _reconcile(Set refs) async { try { final response = await _http.post( _images('reconcile'), @@ -81,11 +81,11 @@ class WebImageStore extends ImageStore { } @override - ImageSource sourceFor(ImageRef ref) => + ImageSource sourceFor(AttachmentRef ref) => NetworkImageSource('$origin/images/${ref.relativePath}?r=$_revision'); @override - bool isPending(ImageRef ref) => false; + bool isPending(AttachmentRef ref) => false; @override Future status() async { diff --git a/lib/data/note_template_parser.dart b/lib/data/note_template_parser.dart index 4850d8d..3c62c69 100644 --- a/lib/data/note_template_parser.dart +++ b/lib/data/note_template_parser.dart @@ -17,7 +17,7 @@ library; import 'package:todo/data/note_template.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; /// Matches a single `http(s)://…` token. final RegExp _linkPattern = RegExp(r'^https?://\S+$'); diff --git a/lib/desktop/wrapper_image_routes.dart b/lib/desktop/wrapper_image_routes.dart index ee394fd..c12ee64 100644 --- a/lib/desktop/wrapper_image_routes.dart +++ b/lib/desktop/wrapper_image_routes.dart @@ -21,13 +21,12 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:http/http.dart' as http; - import 'package:todo/desktop/dufs_login_env.dart'; import 'package:todo/desktop/wrapper_paths.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/images/dufs_image_client.dart'; import 'package:todo/images/image_cache_store.dart'; import 'package:todo/images/image_processing.dart'; -import 'package:todo/images/image_ref.dart'; /// Largest upload accepted from the page, before re-encoding (a raw 50 MP /// phone photo is ~25 MB). @@ -77,7 +76,7 @@ class WrapperImageRoutes { 'configured': _login() != null, 'pending': store.pending().length, }); - } else if (ImageRef.tryParse(rest) case final ref?) { + } else if (AttachmentRef.tryParse(rest) case final ref?) { if (method == 'GET') { await _serve(response, ref); } else if (method == 'POST') { @@ -93,7 +92,7 @@ class WrapperImageRoutes { DufsLogin? _login() => readDufsLoginEnv(File(loginEnvPath)); - Future _serve(HttpResponse response, ImageRef ref) async { + Future _serve(HttpResponse response, AttachmentRef ref) async { final file = store.fileFor(ref); if (!file.existsSync()) { response.statusCode = HttpStatus.notFound; @@ -106,7 +105,7 @@ class WrapperImageRoutes { await response.addStream(file.openRead()); } - Future _put(HttpRequest request, ImageRef ref) async { + Future _put(HttpRequest request, AttachmentRef ref) async { final builder = BytesBuilder(copy: false); var tooLarge = false; // Read to the end even once over the cap: answering while the browser @@ -130,12 +129,12 @@ class WrapperImageRoutes { } Future _reconcile(HttpRequest request) async { - final Set refs; + final Set refs; try { final body = jsonDecode(await utf8.decodeStream(request)); refs = { for (final raw in (body as Map)['refs'] as List) - ?ImageRef.tryParse(raw as String), + ?AttachmentRef.tryParse(raw as String), }; } on Object { request.response.statusCode = HttpStatus.badRequest; diff --git a/lib/images/image_ref.dart b/lib/images/attachment_ref.dart similarity index 85% rename from lib/images/image_ref.dart rename to lib/images/attachment_ref.dart index 4b9bd67..8f5cbcf 100644 --- a/lib/images/image_ref.dart +++ b/lib/images/attachment_ref.dart @@ -40,17 +40,17 @@ final _wholeLine = RegExp('^\\s*${_urlLink.pattern}\\s*\$'); /// /// An extension type over that relative path: equality and hashing are the /// string's, so refs drop straight into sets. -extension type const ImageRef._(String relativePath) { +extension type const AttachmentRef._(String relativePath) { /// A new, unique ref for an image being attached to [noteId]. - factory ImageRef.create(String noteId) { - final ref = ImageRef.tryParse('$noteId/${const Uuid().v4()}.jpg'); + factory AttachmentRef.create(String noteId) { + final ref = AttachmentRef.tryParse('$noteId/${const Uuid().v4()}.jpg'); if (ref == null) throw ArgumentError.value(noteId, 'noteId'); return ref; } /// Parses `/.jpg`, or null when it is not a safe image path. - static ImageRef? tryParse(String relativePath) => - _refPath.hasMatch(relativePath) ? ImageRef._(relativePath) : null; + static AttachmentRef? tryParse(String relativePath) => + _refPath.hasMatch(relativePath) ? AttachmentRef._(relativePath) : null; /// The note the image was attached to (it names the folder only). String get noteId => relativePath.substring(0, relativePath.indexOf('/')); @@ -66,15 +66,15 @@ extension type const ImageRef._(String relativePath) { } /// Every image [text] links to, in order, without duplicates. -Set imageRefsIn(String text) => { +Set imageRefsIn(String text) => { for (final match in _urlLink.allMatches(text)) - ImageRef.tryParse(match.group(1)!)!, + AttachmentRef.tryParse(match.group(1)!)!, }; /// The image when [line] is nothing but one image link, else null. -ImageRef? imageRefOfLine(String line) { +AttachmentRef? imageRefOfLine(String line) { final match = _wholeLine.firstMatch(line); - return match == null ? null : ImageRef.tryParse(match.group(1)!); + return match == null ? null : AttachmentRef.tryParse(match.group(1)!); } /// Rewrites image links to their on-disk path under [kBacklogImagesRoot]. diff --git a/lib/images/dufs_image_client.dart b/lib/images/dufs_image_client.dart index 69338f9..d03cde7 100644 --- a/lib/images/dufs_image_client.dart +++ b/lib/images/dufs_image_client.dart @@ -13,7 +13,7 @@ import 'dart:typed_data'; import 'package:http/http.dart' as http; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; /// A scoped dufs login, e.g. `todo` → `/todo-images`. class DufsLogin { @@ -66,7 +66,7 @@ class DufsImageClient { Uri.parse('${login.baseUrl}/$kImagesDir/$relative'); /// PUTs [bytes] as [ref]; true on a 2xx. - Future upload(ImageRef ref, Uint8List bytes) async { + Future upload(AttachmentRef ref, Uint8List bytes) async { try { final response = await _http.put( _uri(ref.relativePath), @@ -80,12 +80,9 @@ class DufsImageClient { } /// GETs [ref]'s bytes, or null when it is missing or unreachable. - Future download(ImageRef ref) async { + Future download(AttachmentRef ref) async { try { - final response = await _http.get( - _uri(ref.relativePath), - headers: _auth, - ); + final response = await _http.get(_uri(ref.relativePath), headers: _auth); return response.statusCode == 200 ? response.bodyBytes : null; } on Exception { return null; diff --git a/lib/images/image_cache_store.dart b/lib/images/image_cache_store.dart index 6698a08..74cf86c 100644 --- a/lib/images/image_cache_store.dart +++ b/lib/images/image_cache_store.dart @@ -14,9 +14,8 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:path/path.dart' as p; - +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/images/dufs_image_client.dart'; -import 'package:todo/images/image_ref.dart'; /// How long a cached, unreferenced image is kept before eviction. /// @@ -79,15 +78,16 @@ class ImageCacheStore { _Pass? _scheduledPass; /// The cached file for [ref] (it may not exist). - File fileFor(ImageRef ref) => File(p.join(root.path, ref.noteId, ref.file)); + File fileFor(AttachmentRef ref) => + File(p.join(root.path, ref.noteId, ref.file)); - File _marker(ImageRef ref) => File('${fileFor(ref).path}.pending'); + File _marker(AttachmentRef ref) => File('${fileFor(ref).path}.pending'); /// Whether [ref] is waiting to upload. - bool isPending(ImageRef ref) => _marker(ref).existsSync(); + bool isPending(AttachmentRef ref) => _marker(ref).existsSync(); /// Stores [bytes] as [ref] and queues it for upload. - Future put(ImageRef ref, Uint8List bytes) async { + Future put(AttachmentRef ref, Uint8List bytes) async { final file = fileFor(ref); await file.parent.create(recursive: true); // Marker first: a crash between the two writes then leaves a marker with @@ -99,9 +99,9 @@ class ImageCacheStore { } /// Every image waiting to upload. - List pending() => [ + List pending() => [ for (final marker in _files('.pending')) - ?ImageRef.tryParse(_relative(marker).replaceFirst('.pending', '')), + ?AttachmentRef.tryParse(_relative(marker).replaceFirst('.pending', '')), ]; /// Uploads the queue, fetches linked images this device lacks, and evicts @@ -115,7 +115,7 @@ class ImageCacheStore { /// Overlapping calls do not run in parallel: calls made before the next /// pass starts share it, with their refs unioned and the newest client. Future reconcile( - Set referenced, + Set referenced, DufsImageClient? client, ) { // Merge into a pass that has not started yet: union the refs (an @@ -174,11 +174,11 @@ class ImageCacheStore { ); } - int _evict(Set referenced) { + int _evict(Set referenced) { final cutoff = _now().subtract(kEvictionGrace); var evicted = 0; for (final file in _files('.jpg')) { - final ref = ImageRef.tryParse(_relative(file)); + final ref = AttachmentRef.tryParse(_relative(file)); if (ref == null || referenced.contains(ref) || isPending(ref)) continue; if (file.lastModifiedSync().isAfter(cutoff)) continue; file.deleteSync(); @@ -202,6 +202,6 @@ class ImageCacheStore { class _Pass { const _Pass(this.referenced, this.client); - final Set referenced; + final Set referenced; final DufsImageClient? client; } diff --git a/lib/sync/notes_markdown.dart b/lib/sync/notes_markdown.dart index 73ee3fe..7e420df 100644 --- a/lib/sync/notes_markdown.dart +++ b/lib/sync/notes_markdown.dart @@ -1,5 +1,5 @@ import 'package:todo/data/note.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:uuid/uuid.dart'; /// Serialises notes to (and parses them back from) a single Markdown file. diff --git a/lib/ui/image_attach.dart b/lib/ui/image_attach.dart index eb318af..b495981 100644 --- a/lib/ui/image_attach.dart +++ b/lib/ui/image_attach.dart @@ -12,7 +12,7 @@ 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/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; /// Lets a screen insert lines into whichever editor is currently mounted. class ImageAttachController { @@ -52,7 +52,7 @@ Future attachImages( int rejected = 0, }) async { final store = ImageStoreScope.read(context); - final refs = []; + final refs = []; var notImages = rejected; String? failure; for (final bytes in images) { diff --git a/lib/ui/images/image_embed.dart b/lib/ui/images/image_embed.dart index 014f3dd..451c2b1 100644 --- a/lib/ui/images/image_embed.dart +++ b/lib/ui/images/image_embed.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:todo/attachments/image_store_api.dart'; import 'package:todo/attachments/image_store_scope.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/ui/images/image_viewer_screen.dart'; import 'package:todo/ui/images/local_image.dart'; @@ -19,14 +19,10 @@ const kImageWaitingToUpload = 'Waiting to upload'; /// Renders [ref] from the nearest [ImageStoreScope]. class ImageEmbed extends StatelessWidget { /// Creates an embed; [fit] is `cover` for thumbnails, `contain` inline. - const ImageEmbed({ - required this.ref, - this.fit = BoxFit.cover, - super.key, - }); + const ImageEmbed({required this.ref, this.fit = BoxFit.cover, super.key}); /// The image to show. - final ImageRef ref; + final AttachmentRef ref; /// How the picture fills the embed's box. final BoxFit fit; diff --git a/lib/ui/images/image_strip.dart b/lib/ui/images/image_strip.dart index c2010d3..0d8b003 100644 --- a/lib/ui/images/image_strip.dart +++ b/lib/ui/images/image_strip.dart @@ -1,7 +1,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/ui/images/image_embed.dart'; /// Height of the thumbnail row under the editor. diff --git a/lib/ui/markdown_view.dart b/lib/ui/markdown_view.dart index 8ebc9f1..f85a542 100644 --- a/lib/ui/markdown_view.dart +++ b/lib/ui/markdown_view.dart @@ -1,7 +1,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/ui/images/image_embed.dart'; /// Tallest an image is drawn inside a rendered note. @@ -11,10 +11,10 @@ const kInlineImageHeight = 320.0; /// /// The app's notes use a small, known Markdown subset — an `#` title, `##` /// section headings, italic `_guidance_` lines, `-` bullet lists and plain -/// paragraphs, plus one attached image per line (see `image_ref.dart`) — so a -/// tailored line-based renderer covers everything we emit -/// without pulling in a full Markdown engine. It keeps the quick-capture app -/// lean and is trivial to test. +/// paragraphs, plus one attached image per line (see `attachment_ref.dart`) +/// — so a tailored line-based renderer covers everything we emit without +/// pulling in a full Markdown engine. It keeps the quick-capture app lean and +/// is trivial to test. /// /// The whole rendered note is wrapped in a [SelectionArea] so it can be copied /// out in one go (the note is meant to be pasted into an LLM). diff --git a/pubspec.yaml b/pubspec.yaml index 63b3a82..93b5c2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.39+1 +version: 1.0.40+1 environment: sdk: ^3.12.2 diff --git a/test/attachments/image_store_api_test.dart b/test/attachments/image_store_api_test.dart index 9e6eb7d..fd4482a 100644 --- a/test/attachments/image_store_api_test.dart +++ b/test/attachments/image_store_api_test.dart @@ -4,10 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:todo/attachments/image_store_api.dart'; import 'package:todo/attachments/image_store_scope.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; void main() { - final ref = ImageRef.create('n1'); + final ref = AttachmentRef.create('n1'); test('NoImageStore refuses to attach and has nothing', () async { final store = NoImageStore(); @@ -27,7 +27,7 @@ void main() { }); test('imageRefsAcross unions every note', () { - final other = ImageRef.create('n2'); + final other = AttachmentRef.create('n2'); expect( imageRefsAcross([ref.markdown, '${other.markdown}\n${ref.markdown}']), {ref, other}, diff --git a/test/attachments/image_store_web_test.dart b/test/attachments/image_store_web_test.dart index ba27793..1a96575 100644 --- a/test/attachments/image_store_web_test.dart +++ b/test/attachments/image_store_web_test.dart @@ -7,7 +7,7 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:todo/attachments/image_store_api.dart'; import 'package:todo/attachments/image_store_web.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; void main() { final requests = []; @@ -84,7 +84,7 @@ void main() { ); test('reconcile sends every linked ref; a download bumps the url', () async { - final ref = ImageRef.create('n1'); + final ref = AttachmentRef.create('n1'); final store = make((_) async => report(downloaded: 1)); await store.reconcile(['a ${ref.markdown}', 'none']); expect(jsonDecode(requests.single.body), { diff --git a/test/desktop/wrapper_image_routes_test.dart b/test/desktop/wrapper_image_routes_test.dart index 7832a0e..38f61d8 100644 --- a/test/desktop/wrapper_image_routes_test.dart +++ b/test/desktop/wrapper_image_routes_test.dart @@ -8,7 +8,7 @@ import 'package:path/path.dart' as p; import 'package:todo/desktop/wrapper_image_routes.dart'; import 'package:todo/desktop/wrapper_server.dart'; import 'package:todo/images/image_cache_store.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import '../images/fake_dufs.dart'; @@ -71,7 +71,7 @@ void main() { body: Uint8List.fromList([0xFF, 1, 2]), ); expect(post.statusCode, 204); - expect(store.isPending(ImageRef.tryParse(rel)!), isTrue); + expect(store.isPending(AttachmentRef.tryParse(rel)!), isTrue); final get = await http.get(Uri.parse('$base/images/$rel')); expect(get.statusCode, 200); expect(get.headers['content-type'], 'image/jpeg'); diff --git a/test/fake_image_store.dart b/test/fake_image_store.dart index da4093c..8f56a45 100644 --- a/test/fake_image_store.dart +++ b/test/fake_image_store.dart @@ -7,22 +7,22 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:todo/attachments/image_store_api.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; /// Records attaches and reconciles; every image is "local" at a fake path /// unless listed in [missing]. class FakeImageStore extends ImageStore { /// Bytes accepted per ref, in attach order. - final Map attached = {}; + final Map attached = {}; /// Note texts passed to each reconcile. final List> reconciles = []; /// Refs reported as not on this device. - final Set missing = {}; + final Set missing = {}; /// Refs reported as waiting to upload. - final Set pending = {}; + final Set pending = {}; /// Inputs whose first byte is this are "not an image". int notImageMarker = 0; @@ -40,12 +40,12 @@ class FakeImageStore extends ImageStore { Future? gate; @override - Future attach(String noteId, Uint8List bytes) async { + Future attach(String noteId, Uint8List bytes) async { await gate; final failure = this.failure; if (failure != null) throw failure; if (bytes.isNotEmpty && bytes.first == notImageMarker) return null; - final ref = ImageRef.create(noteId); + final ref = AttachmentRef.create(noteId); attached[ref] = bytes; return ref; } @@ -55,7 +55,7 @@ class FakeImageStore extends ImageStore { reconciles.add(noteTexts.toList()); @override - ImageSource sourceFor(ImageRef ref) { + ImageSource sourceFor(AttachmentRef ref) { if (missing.contains(ref)) return const MissingImageSource(); return network ? NetworkImageSource('http://localhost/images/$ref') @@ -63,7 +63,7 @@ class FakeImageStore extends ImageStore { } @override - bool isPending(ImageRef ref) => pending.contains(ref); + bool isPending(AttachmentRef ref) => pending.contains(ref); @override Future status() async => diff --git a/test/images/image_ref_test.dart b/test/images/attachment_ref_test.dart similarity index 83% rename from test/images/image_ref_test.dart rename to test/images/attachment_ref_test.dart index 0abad2c..9edc71a 100644 --- a/test/images/image_ref_test.dart +++ b/test/images/attachment_ref_test.dart @@ -1,9 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; void main() { const rel = 'note-1/0815ff8a-203e-4f54-9cc5-8a58b27982ec.jpg'; - final ref = ImageRef.tryParse(rel)!; + final ref = AttachmentRef.tryParse(rel)!; test('a ref exposes its parts, url and markdown', () { expect(ref.noteId, 'note-1'); @@ -14,15 +14,15 @@ void main() { }); test('create files a fresh uuid jpg under the note', () { - final a = ImageRef.create('n1'); - final b = ImageRef.create('n1'); + final a = AttachmentRef.create('n1'); + final b = AttachmentRef.create('n1'); expect(a.noteId, 'n1'); expect(a.file, endsWith('.jpg')); expect(a, isNot(b)); }); test('create refuses an unsafe note id', () { - expect(() => ImageRef.create('../x'), throwsArgumentError); + expect(() => AttachmentRef.create('../x'), throwsArgumentError); }); test('tryParse rejects traversal, nesting and other extensions', () { @@ -35,16 +35,16 @@ void main() { '/a/b.jpg', 'a b/c.jpg', ]) { - expect(ImageRef.tryParse(bad), isNull, reason: bad); + expect(AttachmentRef.tryParse(bad), isNull, reason: bad); } }); test('refs compare by path, so sets dedupe', () { - expect({ref, ImageRef.tryParse(rel)!}, hasLength(1)); + expect({ref, AttachmentRef.tryParse(rel)!}, hasLength(1)); }); test('imageRefsIn finds every dufs image link, in order, once', () { - final other = ImageRef.create('n2'); + final other = AttachmentRef.create('n2'); final text = 'hi ${ref.markdown} and\n${other.markdown}\n${ref.markdown}\n' '![x](https://example.com/a.jpg) ![]($kDufsBaseUrl/Keepass/a.jpg)'; diff --git a/test/images/dufs_image_client_test.dart b/test/images/dufs_image_client_test.dart index 7a1e7b3..e98bcd4 100644 --- a/test/images/dufs_image_client_test.dart +++ b/test/images/dufs_image_client_test.dart @@ -6,10 +6,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:todo/images/dufs_image_client.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; void main() { - final ref = ImageRef.tryParse('n1/a.jpg')!; + final ref = AttachmentRef.tryParse('n1/a.jpg')!; const login = DufsLogin(user: 'todo', password: 'pw', baseUrl: 'https://d'); final auth = 'Basic ${base64.encode(utf8.encode('todo:pw'))}'; diff --git a/test/images/image_cache_store_test.dart b/test/images/image_cache_store_test.dart index 0b5e542..29f1fd9 100644 --- a/test/images/image_cache_store_test.dart +++ b/test/images/image_cache_store_test.dart @@ -5,7 +5,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:todo/images/image_cache_store.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'fake_dufs.dart'; @@ -15,8 +15,8 @@ void main() { late FakeDufs dufs; var now = DateTime(2026, 9, 25, 12); - final a = ImageRef.tryParse('n1/a.jpg')!; - final b = ImageRef.tryParse('n2/b.jpg')!; + final a = AttachmentRef.tryParse('n1/a.jpg')!; + final b = AttachmentRef.tryParse('n2/b.jpg')!; final bytes = Uint8List.fromList([1, 2, 3]); setUp(() { @@ -27,7 +27,7 @@ void main() { }); tearDown(() => root.deleteSync(recursive: true)); - void age(ImageRef ref, Duration by) => + void age(AttachmentRef ref, Duration by) => store.fileFor(ref).setLastModifiedSync(now.subtract(by)); test('put stores the file and queues it', () async { diff --git a/test/ui_images/detail_and_misc_images_test.dart b/test/ui_images/detail_and_misc_images_test.dart index 2e72d6b..f2373c5 100644 --- a/test/ui_images/detail_and_misc_images_test.dart +++ b/test/ui_images/detail_and_misc_images_test.dart @@ -6,7 +6,7 @@ import 'package:todo/attachments/image_store_scope.dart'; import 'package:todo/data/app_settings.dart'; import 'package:todo/data/note.dart'; import 'package:todo/data/note_template_parser.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/sync/desktop_wrapper.dart'; import 'package:todo/sync/notes_markdown.dart'; import 'package:todo/ui/capture_draft.dart'; @@ -30,7 +30,7 @@ Note note(String text) => Note( ); void main() { - final ref = ImageRef.create('n1'); + final ref = AttachmentRef.create('n1'); testWidgets('detail: 📎 inserts at the cursor and persists', (tester) async { tester.view.physicalSize = const Size(1200, 2400); diff --git a/test/ui_images/image_embed_test.dart b/test/ui_images/image_embed_test.dart index 3137767..c2688ed 100644 --- a/test/ui_images/image_embed_test.dart +++ b/test/ui_images/image_embed_test.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:todo/attachments/image_store_scope.dart'; -import 'package:todo/images/image_ref.dart'; +import 'package:todo/images/attachment_ref.dart'; import 'package:todo/ui/images/image_embed.dart'; import 'package:todo/ui/images/image_strip.dart'; import 'package:todo/ui/images/image_viewer_screen.dart'; @@ -12,7 +12,7 @@ import 'package:todo/ui/images/image_viewer_screen.dart'; import '../fake_image_store.dart'; void main() { - final ref = ImageRef.create('n1'); + final ref = AttachmentRef.create('n1'); late FakeImageStore store; late Directory dir; @@ -82,7 +82,7 @@ void main() { }); testWidgets('the strip shows one thumbnail per linked image', (tester) async { - final other = ImageRef.create('n2'); + final other = AttachmentRef.create('n2'); await pump( tester, ImageStrip(text: 'x\n${ref.markdown}\n${other.markdown}'),