mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 18:07:03 +01:00
Rename ImageRef to AttachmentRef ahead of non-image attachments
A ref is about to name any file stored under todo-images/, not only a re-encoded JPEG. Mechanical rename of the type and its file; no behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2bbfe0b663
commit
05056cf8c2
@ -103,7 +103,7 @@ migrate → Settings → Import).
|
||||
|
||||
A note never holds image bytes, only a line
|
||||
``
|
||||
(`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
|
||||
|
||||
@ -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<ImageRef?> attach(String noteId, Uint8List bytes);
|
||||
Future<AttachmentRef?> 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<void> reconcile(Iterable<String> 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<ImageStoreStatus> status();
|
||||
}
|
||||
|
||||
/// Every image ref across [noteTexts].
|
||||
Set<ImageRef> imageRefsAcross(Iterable<String> noteTexts) => {
|
||||
Set<AttachmentRef> imageRefsAcross(Iterable<String> noteTexts) => {
|
||||
for (final text in noteTexts) ...imageRefsIn(text),
|
||||
};
|
||||
|
||||
@ -95,17 +95,17 @@ Set<ImageRef> imageRefsAcross(Iterable<String> noteTexts) => {
|
||||
/// outside the app root): attaching is refused, nothing is ever available.
|
||||
class NoImageStore extends ImageStore {
|
||||
@override
|
||||
Future<ImageRef?> attach(String noteId, Uint8List bytes) async =>
|
||||
Future<AttachmentRef?> attach(String noteId, Uint8List bytes) async =>
|
||||
throw const ImageStoreException('Images are not available here');
|
||||
|
||||
@override
|
||||
Future<void> reconcile(Iterable<String> 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<ImageStoreStatus> status() async =>
|
||||
|
||||
@ -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<Uint8List?> Function(Uint8List) _prepare;
|
||||
|
||||
@override
|
||||
Future<ImageRef?> attach(String noteId, Uint8List bytes) async {
|
||||
Future<AttachmentRef?> 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<void> reconcile(Iterable<String> noteTexts) =>
|
||||
_sync(imageRefsAcross(noteTexts));
|
||||
|
||||
Future<void> _sync(Set<ImageRef> refs) async {
|
||||
Future<void> _sync(Set<AttachmentRef> 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<ImageStoreStatus> status() async => ImageStoreStatus(
|
||||
|
||||
@ -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<ImageRef?> attach(String noteId, Uint8List bytes) async {
|
||||
final ref = ImageRef.create(noteId);
|
||||
Future<AttachmentRef?> 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<void> reconcile(Iterable<String> noteTexts) =>
|
||||
_reconcile(imageRefsAcross(noteTexts));
|
||||
|
||||
Future<void> _reconcile(Set<ImageRef> refs) async {
|
||||
Future<void> _reconcile(Set<AttachmentRef> 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<ImageStoreStatus> status() async {
|
||||
|
||||
@ -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+$');
|
||||
|
||||
@ -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<void> _serve(HttpResponse response, ImageRef ref) async {
|
||||
Future<void> _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<void> _put(HttpRequest request, ImageRef ref) async {
|
||||
Future<void> _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<void> _reconcile(HttpRequest request) async {
|
||||
final Set<ImageRef> refs;
|
||||
final Set<AttachmentRef> refs;
|
||||
try {
|
||||
final body = jsonDecode(await utf8.decodeStream(request));
|
||||
refs = {
|
||||
for (final raw in (body as Map<String, dynamic>)['refs'] as List)
|
||||
?ImageRef.tryParse(raw as String),
|
||||
?AttachmentRef.tryParse(raw as String),
|
||||
};
|
||||
} on Object {
|
||||
request.response.statusCode = HttpStatus.badRequest;
|
||||
|
||||
@ -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 `<noteId>/<file>.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<ImageRef> imageRefsIn(String text) => {
|
||||
Set<AttachmentRef> 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].
|
||||
@ -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<bool> upload(ImageRef ref, Uint8List bytes) async {
|
||||
Future<bool> 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<Uint8List?> download(ImageRef ref) async {
|
||||
Future<Uint8List?> 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;
|
||||
|
||||
@ -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<void> put(ImageRef ref, Uint8List bytes) async {
|
||||
Future<void> 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<ImageRef> pending() => [
|
||||
List<AttachmentRef> 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<ReconcileReport> reconcile(
|
||||
Set<ImageRef> referenced,
|
||||
Set<AttachmentRef> 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<ImageRef> referenced) {
|
||||
int _evict(Set<AttachmentRef> 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<ImageRef> referenced;
|
||||
final Set<AttachmentRef> referenced;
|
||||
final DufsImageClient? client;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<void> attachImages(
|
||||
int rejected = 0,
|
||||
}) async {
|
||||
final store = ImageStoreScope.read(context);
|
||||
final refs = <ImageRef>[];
|
||||
final refs = <AttachmentRef>[];
|
||||
var notImages = rejected;
|
||||
String? failure;
|
||||
for (final bytes in images) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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 = <http.Request>[];
|
||||
@ -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), {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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<ImageRef, Uint8List> attached = {};
|
||||
final Map<AttachmentRef, Uint8List> attached = {};
|
||||
|
||||
/// Note texts passed to each reconcile.
|
||||
final List<List<String>> reconciles = [];
|
||||
|
||||
/// Refs reported as not on this device.
|
||||
final Set<ImageRef> missing = {};
|
||||
final Set<AttachmentRef> missing = {};
|
||||
|
||||
/// Refs reported as waiting to upload.
|
||||
final Set<ImageRef> pending = {};
|
||||
final Set<AttachmentRef> pending = {};
|
||||
|
||||
/// Inputs whose first byte is this are "not an image".
|
||||
int notImageMarker = 0;
|
||||
@ -40,12 +40,12 @@ class FakeImageStore extends ImageStore {
|
||||
Future<void>? gate;
|
||||
|
||||
@override
|
||||
Future<ImageRef?> attach(String noteId, Uint8List bytes) async {
|
||||
Future<AttachmentRef?> 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<ImageStoreStatus> status() async =>
|
||||
|
||||
@ -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'
|
||||
' ';
|
||||
@ -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'))}';
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user