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,9 +1,17 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as p;
import '../database/database.dart';
import '../l10n/strings.dart';
import 'controllers/bookmarks_controller.dart';
import 'controllers/reader_settings_controller.dart'
show ReaderSettingsController;
import 'controllers/subscriptions_controller.dart';
import 'key_store.dart';
import 'pages/mews_urls.dart';
import 'pem.dart';
/// Applies the change an action address encodes, e.g. mews:do/set-theme.
/// Returns whether anything changed, so the navigator can skip a rerender that
@ -12,6 +20,10 @@ import 'pages/mews_urls.dart';
/// An unknown verb or a missing argument does nothing and reports nothing: the
/// only links carrying these addresses are the ones the client builds, so a
/// miss is a bug rather than something to explain to the reader.
/// A PEM file the reader picked, or null when the pick was cancelled. [name]
/// names the identity after its file, which is what the reader chose.
typedef PickedPem = ({String name, String content});
class InternalActions {
InternalActions({
required MewsDatabase db,
@ -19,17 +31,42 @@ class InternalActions {
required ReaderSettingsController settings,
required BookmarksController bookmarks,
required SubscriptionsController subscriptions,
required KeyStore keys,
required Strings strings,
Future<PickedPem?> Function(String title)? pickPemFile,
}) : _db = db,
_applyLanguage = applyLanguage,
_settings = settings,
_bookmarks = bookmarks,
_subscriptions = subscriptions;
_subscriptions = subscriptions,
_keys = keys,
_strings = strings,
_pickPemFile = pickPemFile ?? _pickPemFromFileSystem;
final MewsDatabase _db;
final Future<void> Function() _applyLanguage;
final ReaderSettingsController _settings;
final BookmarksController _bookmarks;
final SubscriptionsController _subscriptions;
final KeyStore _keys;
final Strings _strings;
final Future<PickedPem?> Function(String title) _pickPemFile;
/// Opens the platform file picker for one PEM file, titled with what the
/// step asks for. A single file may hold both the certificate and its key,
/// or either one alone. A cancelled pick comes back as nothing.
static Future<PickedPem?> _pickPemFromFileSystem(String title) async {
final files = await FilePicker.pickFiles(
dialogTitle: title,
type: FileType.custom,
allowedExtensions: const ['pem', 'crt', 'cer', 'key']);
final path = files.singleOrNull?.path;
if (path == null) return null;
return (
name: p.basenameWithoutExtension(path),
content: await File(path).readAsString(),
);
}
Future<bool> apply(String url) async {
final verb = MewsUrls.verbOf(url);
@ -52,10 +89,55 @@ class InternalActions {
'add-tag' => _changeTag(args, add: true),
'remove-tag' => _changeTag(args, add: false),
'delete-tag' => _deleteTag(args['tag']),
'import-certificate' => _importCertificate(),
'rename-certificate' => _renameCertificate(args),
'delete-certificate' => _deleteCertificate(args),
_ => false,
};
}
/// Reads a certificate and its key from picked PEM files. The first pick
/// may hold both; when it holds only one, the other is asked for again, by
/// name, so a key picked on its own is not silently dropped. A cancelled
/// pick imports nothing.
Future<bool> _importCertificate() async {
final first = await _pickPemFile(_strings.t('certificates-pick-title'));
if (first == null) return false;
String? cert = certificateOf(first.content);
String? key = privateKeyOf(first.content);
if (cert == null || key == null) {
final asking = cert == null
? _strings.t('certificates-pick-cert-title')
: _strings.t('certificates-pick-key-title');
final second = await _pickPemFile(asking);
if (second == null) return false;
cert ??= certificateOf(second.content);
key ??= privateKeyOf(second.content);
}
if (cert == null || key == null) return false;
final name = first.name.trim();
final id = await _db.addClientCertificate(
name.isEmpty ? _strings.t('certificates-default-name') : name, cert);
await _keys.save(id, key);
return true;
}
Future<bool> _renameCertificate(Map<String, String> args) async {
final id = int.tryParse(args['id'] ?? '');
final name = args['name']?.trim();
if (id == null || name == null || name.isEmpty) return false;
await _db.renameClientCertificate(id, name);
return true;
}
Future<bool> _deleteCertificate(Map<String, String> args) async {
final id = int.tryParse(args['id'] ?? '');
if (id == null) return false;
await _keys.delete(id);
await _db.deleteClientCertificate(id);
return true;
}
/// Changing the language has to load its messages before the page is built
/// again, or the rerender would still read the old ones.
Future<bool> _setLanguage(String? tag) async {