feat: add Gemini and Spartan protocol support
This commit is contained in:
parent
a4a49f41d7
commit
fa7aa5557c
48 changed files with 4992 additions and 161 deletions
|
|
@ -3,7 +3,15 @@ import 'package:flutter/rendering.dart' show ScrollDirection;
|
|||
import 'package:signals/signals_flutter.dart' show SignalBuilder;
|
||||
|
||||
import '../../application/controllers/browser_controller.dart'
|
||||
show BrokenPage, LoadFailed;
|
||||
show
|
||||
BrokenPage,
|
||||
CertificateChanged,
|
||||
ClientCertificateRequired,
|
||||
LoadFailed,
|
||||
ProtocolFailure,
|
||||
RedirectLoop,
|
||||
UnsupportedPage;
|
||||
import '../../database/database.dart' show ClientCertificate;
|
||||
import '../../application/pages/mews_urls.dart';
|
||||
import '../../mews/document.dart';
|
||||
import '../../mews/parser.dart' show mewsPlainText;
|
||||
|
|
@ -137,6 +145,24 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
return switch (error) {
|
||||
BrokenPage(:final source) => _errorPage(context, source),
|
||||
LoadFailed() => _errorPage(context, null),
|
||||
// A protocol status carries the site's own words for what went
|
||||
// wrong.
|
||||
ProtocolFailure(:final message) =>
|
||||
_plainErrorPage(context, 'error-protocol-failed', detail: message),
|
||||
// A certificate request is a choice, not a failure: the reader
|
||||
// presents one of their identities, or none. A refused or invalid
|
||||
// certificate is a failure the reader cannot fix by choosing again.
|
||||
ClientCertificateRequired(:final status, :final meta) => status == 60
|
||||
? _identityPickerPage(context, meta)
|
||||
: _plainErrorPage(
|
||||
context,
|
||||
status == 61
|
||||
? 'error-certificate-not-authorized'
|
||||
: 'error-certificate-invalid',
|
||||
detail: meta),
|
||||
RedirectLoop() => _plainErrorPage(context, 'error-redirect-loop'),
|
||||
UnsupportedPage() => _plainErrorPage(context, 'error-unsupported-type'),
|
||||
CertificateChanged() => _certificateErrorPage(context),
|
||||
null => _emptyPage(context),
|
||||
};
|
||||
});
|
||||
|
|
@ -168,6 +194,7 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
onLinkTap: (url) => deps.browser.open(url),
|
||||
onFormSubmit: (form, values) =>
|
||||
deps.browser.submitForm(form.action, form.method, values),
|
||||
fetchImageBytes: deps.browser.fetchImageBytes,
|
||||
scrollController: _scroll,
|
||||
);
|
||||
});
|
||||
|
|
@ -204,6 +231,111 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
return const SizedBox.expand();
|
||||
}
|
||||
|
||||
/// An error page that says [messageId], optionally the site's own words
|
||||
/// under it, and offers to try again.
|
||||
Widget _plainErrorPage(BuildContext context, String messageId, {String? detail}) {
|
||||
final strings = AppScope.of(context).strings;
|
||||
final deps = AppScope.of(context).deps;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(strings.t(messageId), textAlign: TextAlign.center),
|
||||
if (detail != null && detail.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(detail, textAlign: TextAlign.center),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () => deps.browser.reload(),
|
||||
child: Text(strings.t('error-try-again')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The choice a certificate request asks for: one of the reader's
|
||||
/// identities, presented from now on to the site that asked.
|
||||
Widget _identityPickerPage(BuildContext context, String meta) {
|
||||
final strings = AppScope.of(context).strings;
|
||||
final deps = AppScope.of(context).deps;
|
||||
return FutureBuilder<List<ClientCertificate>>(
|
||||
future: deps.db.allClientCertificates(),
|
||||
builder: (context, snapshot) {
|
||||
final held = snapshot.data ?? const <ClientCertificate>[];
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(strings.t('certificates-choose'), textAlign: TextAlign.center),
|
||||
if (meta.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(meta, textAlign: TextAlign.center),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
if (held.isEmpty)
|
||||
FilledButton(
|
||||
onPressed: () => deps.browser.open(MewsUrls.certificates),
|
||||
child: Text(strings.t('certificates-add')),
|
||||
)
|
||||
else
|
||||
for (final certificate in held)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () => deps.browser.chooseCertificate(certificate.id),
|
||||
child: Text(certificate.name),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => deps.browser.reload(),
|
||||
child: Text(strings.t('error-try-again')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// A changed certificate is the reader's choice to make or refuse, so the
|
||||
/// error page carries both: trust what was presented, or try again.
|
||||
Widget _certificateErrorPage(BuildContext context) {
|
||||
final strings = AppScope.of(context).strings;
|
||||
final deps = AppScope.of(context).deps;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(strings.t('error-certificate-changed'), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () => deps.browser.trustPresentedCertificate(),
|
||||
child: Text(strings.t('error-certificate-trust')),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => deps.browser.reload(),
|
||||
child: Text(strings.t('error-try-again')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorPage(BuildContext context, String? brokenSource) {
|
||||
final strings = AppScope.of(context).strings;
|
||||
final deps = AppScope.of(context).deps;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue