Replace the Blossom media-server approach with a fully decentralized one: generate a small (~320px) JPEG thumbnail from the lot's cover photo and embed it in the offer event as a data: URI, so photos ride the relays with no server and no IP leak. The full photo stays in the encrypted inventory. - offer_thumbnail.dart: downscale-until-it-fits data-URI builder + decoder - market_widgets: render data: URIs from memory, http(s) URLs from network - offers_cubit: publish a thumbnail (best-effort) in place of an upload - drop MediaTransport/Blossom from commons_core (http/crypto deps) and the media-server setting; parked on branch parked/offer-photos-blossom Relay event-size limits cap the image to a thumbnail; higher-res sharing would need the parked Blossom path.
56 lines
2.1 KiB
Dart
56 lines
2.1 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:image/image.dart' as img;
|
|
|
|
/// Builds a tiny `data:image/jpeg;base64,…` thumbnail from a full photo, small
|
|
/// enough to ride *inside* a Nostr offer event — no media server, fully
|
|
/// decentralized. The full-resolution photo stays in the encrypted local
|
|
/// inventory; only this shrunken preview is published so peers can see the seed.
|
|
///
|
|
/// Relays cap event size, so we downscale (longest edge) and re-encode until the
|
|
/// base64 fits under [maxBytes]. Returns null when the bytes aren't a decodable
|
|
/// image or no size small enough is reachable — the offer then publishes without
|
|
/// a photo (graceful, never a hard failure).
|
|
String? offerThumbnailDataUri(
|
|
Uint8List bytes, {
|
|
int maxBytes = 40000,
|
|
List<int> edges = const [320, 256, 192, 128],
|
|
int quality = 68,
|
|
}) {
|
|
try {
|
|
final decoded = img.decodeImage(bytes);
|
|
if (decoded == null) return null;
|
|
|
|
for (final edge in edges) {
|
|
final resized = _fitWithin(decoded, edge);
|
|
final jpeg = img.encodeJpg(resized, quality: quality);
|
|
final b64 = base64.encode(jpeg);
|
|
if (b64.length <= maxBytes) return 'data:image/jpeg;base64,$b64';
|
|
}
|
|
return null; // even the smallest edge didn't fit — skip the image
|
|
} catch (_) {
|
|
return null; // undecodable/corrupt bytes — publish without a photo
|
|
}
|
|
}
|
|
|
|
/// Extracts the raw bytes from a `data:...;base64,…` URI, or null when [uri] is
|
|
/// not a base64 data URI. Used by the UI to render an inline thumbnail with
|
|
/// `Image.memory` instead of a network fetch.
|
|
Uint8List? decodeDataUri(String uri) {
|
|
if (!uri.startsWith('data:')) return null;
|
|
try {
|
|
return Uri.parse(uri).data?.contentAsBytes();
|
|
} on FormatException {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Resizes [image] so its longest edge is at most [edge], preserving aspect
|
|
/// ratio. Never upscales a photo that's already smaller.
|
|
img.Image _fitWithin(img.Image image, int edge) {
|
|
if (image.width <= edge && image.height <= edge) return image;
|
|
return image.width >= image.height
|
|
? img.copyResize(image, width: edge)
|
|
: img.copyResize(image, height: edge);
|
|
}
|