mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
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>
104 lines
3.2 KiB
Dart
104 lines
3.2 KiB
Dart
/// Desktop (web build): every image operation goes through the local
|
|
/// wrapper's `/images/*` routes; the page never sees the dufs password.
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:todo/attachments/image_store_api.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.
|
|
Future<ImageStore> openImageStore() async =>
|
|
WebImageStore(origin: servingWrapperOrigin());
|
|
|
|
/// [ImageStore] backed by the desktop wrapper at [origin].
|
|
class WebImageStore extends ImageStore {
|
|
/// Creates a store talking to [origin]; [httpClient] is injectable.
|
|
WebImageStore({required this.origin, http.Client? httpClient})
|
|
: _http = httpClient ?? http.Client();
|
|
|
|
/// The wrapper's origin, e.g. `http://localhost:8730`.
|
|
final String origin;
|
|
|
|
final http.Client _http;
|
|
|
|
// Bumped whenever the wrapper may hold new bytes, so an image that failed
|
|
// to load (not downloaded yet) is requested again under a fresh URL.
|
|
int _revision = 0;
|
|
|
|
Uri _images(String rest) => Uri.parse('$origin/images/$rest');
|
|
|
|
@override
|
|
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);
|
|
} on Exception {
|
|
throw const ImageStoreException(
|
|
'The desktop helper is not running; the image was not saved',
|
|
);
|
|
}
|
|
if (response.statusCode == 415) return null;
|
|
if (response.statusCode != 204) {
|
|
throw ImageStoreException(
|
|
'The desktop helper refused the image (${response.statusCode})',
|
|
);
|
|
}
|
|
_revision++;
|
|
notifyListeners();
|
|
unawaited(_reconcile(const {}));
|
|
return ref;
|
|
}
|
|
|
|
@override
|
|
Future<void> reconcile(Iterable<String> noteTexts) =>
|
|
_reconcile(imageRefsAcross(noteTexts));
|
|
|
|
Future<void> _reconcile(Set<AttachmentRef> refs) async {
|
|
try {
|
|
final response = await _http.post(
|
|
_images('reconcile'),
|
|
headers: {'content-type': 'application/json'},
|
|
body: jsonEncode({
|
|
'refs': [for (final ref in refs) ref.relativePath],
|
|
}),
|
|
);
|
|
if (response.statusCode != 200) return;
|
|
final report = jsonDecode(response.body) as Map<String, dynamic>;
|
|
if ((report['downloaded'] as int? ?? 0) > 0) {
|
|
_revision++;
|
|
notifyListeners();
|
|
}
|
|
} on Exception {
|
|
// Best-effort: the next sync, resume or attach reconciles again.
|
|
}
|
|
}
|
|
|
|
@override
|
|
ImageSource sourceFor(AttachmentRef ref) =>
|
|
NetworkImageSource('$origin/images/${ref.relativePath}?r=$_revision');
|
|
|
|
@override
|
|
bool isPending(AttachmentRef ref) => false;
|
|
|
|
@override
|
|
Future<ImageStoreStatus> status() async {
|
|
try {
|
|
final response = await _http.get(_images('status'));
|
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
|
return ImageStoreStatus(
|
|
configured: body['configured'] as bool? ?? false,
|
|
pending: body['pending'] as int? ?? 0,
|
|
);
|
|
} on Exception {
|
|
return const ImageStoreStatus(configured: false, pending: 0);
|
|
}
|
|
}
|
|
}
|