feat: add Gemini and Spartan protocol support

This commit is contained in:
randogoth 2026-10-11 22:07:25 +03:00
parent a4a49f41d7
commit fa7aa5557c
48 changed files with 4992 additions and 161 deletions

View file

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'dart:ui' as ui show TextDirection;
import 'package:flutter/gestures.dart';
@ -20,6 +22,7 @@ class MewsPageView extends StatefulWidget {
required this.visitedUrls,
required this.onLinkTap,
required this.onFormSubmit,
this.fetchImageBytes,
this.scrollController,
this.ownPage = false,
super.key,
@ -32,6 +35,10 @@ class MewsPageView extends StatefulWidget {
final Set<String> visitedUrls;
final void Function(String url) onLinkTap;
final void Function(FormBlock form, Map<String, String> values) onFormSubmit;
/// The bytes of an image living on a protocol page, where no address exists
/// that the platform could load from. Null when the page holds none.
final Future<Uint8List?> Function(String url)? fetchImageBytes;
final ScrollController? scrollController;
/// Whether this is one of the client's own pages. Those are built from the
@ -47,6 +54,10 @@ class MewsPageView extends StatefulWidget {
class _MewsPageViewState extends State<MewsPageView> {
final _recognizers = <MewsLink, TapGestureRecognizer>{};
/// Images already asked for, so a rebuild shows the same one arriving
/// instead of asking again. A failure is forgotten, so trying again works.
final _images = <String, Future<Uint8List?>>{};
@override
void didUpdateWidget(MewsPageView oldWidget) {
super.didUpdateWidget(oldWidget);
@ -312,13 +323,7 @@ class _MewsPageViewState extends State<MewsPageView> {
),
ImageBlock(:final src, :final alt, :final allowed) => Padding(
padding: EdgeInsets.only(bottom: base),
child: allowed
? Image.network(
src,
fit: BoxFit.scaleDown,
errorBuilder: (_, _, _) => _imageFallback(alt, base, unavailable: true),
)
: _imageFallback(alt, base),
child: allowed ? _image(src, alt, base) : _imageFallback(alt, base),
),
FormBlock() => Padding(
padding: EdgeInsets.only(bottom: base),
@ -332,6 +337,40 @@ class _MewsPageViewState extends State<MewsPageView> {
};
}
/// An allowed image, from the platform on the web or from the protocol's
/// own bytes on a capsule. A blocked or unreachable image falls back to its
/// alt text, the way a missing image does.
Widget _image(String src, String alt, double base) {
final scheme = Uri.tryParse(src)?.scheme;
if (scheme != 'gemini' && scheme != 'spartan') {
return Image.network(
src,
fit: BoxFit.scaleDown,
errorBuilder: (_, _, _) => _imageFallback(alt, base, unavailable: true),
);
}
final loader = widget.fetchImageBytes;
if (loader == null) return _imageFallback(alt, base, unavailable: true);
return FutureBuilder<Uint8List?>(
future: _images.putIfAbsent(src, () {
final arriving = loader(src);
arriving.then((bytes) {
if (bytes == null) _images.remove(src);
});
return arriving;
}),
builder: (context, snapshot) => snapshot.connectionState != ConnectionState.done
? const SizedBox.shrink()
: snapshot.data == null
? _imageFallback(alt, base, unavailable: true)
: Image.memory(
snapshot.data!,
fit: BoxFit.scaleDown,
errorBuilder: (_, _, _) => _imageFallback(alt, base, unavailable: true),
),
);
}
Widget _imageFallback(String alt, double base, {bool unavailable = false}) {
final palette = widget.palette;
final strings = StringsScope.of(context);