Add Supa account and search changes
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/account/domain/repositories/account_auth.dart';
|
||||
import 'package:weblibre/features/share_intent/domain/services/sharing_intent.dart';
|
||||
|
||||
part 'account_callback_handler.g.dart';
|
||||
|
||||
/// Parsed `weblibre://account/callback?code=...` deep link.
|
||||
class AccountCallback {
|
||||
final String handoffCode;
|
||||
|
||||
const AccountCallback({required this.handoffCode});
|
||||
}
|
||||
|
||||
/// Parses [data] as a WebLibre account callback URI. Returns `null` if it
|
||||
/// isn't one (any other intent) or if the `code` query parameter is
|
||||
/// missing/empty. Callers can check `!= null` instead of running two
|
||||
/// passes (used-to-be `isAccountCallbackUri` then `extractHandoffCode`).
|
||||
AccountCallback? tryParseAccountCallback(String data) {
|
||||
final uri = Uri.tryParse(data);
|
||||
if (uri == null) return null;
|
||||
if (uri.scheme != 'weblibre' ||
|
||||
uri.host != 'account' ||
|
||||
uri.path != '/callback') {
|
||||
return null;
|
||||
}
|
||||
final code = uri.queryParameters['code'];
|
||||
if (code == null || code.isEmpty) return null;
|
||||
return AccountCallback(handoffCode: code);
|
||||
}
|
||||
|
||||
/// Listens for account callback deep links and forwards handoff codes
|
||||
/// to the account auth repository.
|
||||
///
|
||||
/// This provider must be watched during app initialization to activate
|
||||
/// the callback listener.
|
||||
@Riverpod(keepAlive: true)
|
||||
void accountCallbackHandler(Ref ref) {
|
||||
final stream = ref.watch(accountCallbackStreamProvider);
|
||||
|
||||
final subscription = stream.listen((code) async {
|
||||
logger.i('Received account handoff callback');
|
||||
await ref
|
||||
.read(accountAuthRepositoryProvider.notifier)
|
||||
.handleHandoffCode(code);
|
||||
});
|
||||
|
||||
ref.onDispose(subscription.cancel);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'account_callback_handler.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Listens for account callback deep links and forwards handoff codes
|
||||
/// to the account auth repository.
|
||||
///
|
||||
/// This provider must be watched during app initialization to activate
|
||||
/// the callback listener.
|
||||
|
||||
@ProviderFor(accountCallbackHandler)
|
||||
final accountCallbackHandlerProvider = AccountCallbackHandlerProvider._();
|
||||
|
||||
/// Listens for account callback deep links and forwards handoff codes
|
||||
/// to the account auth repository.
|
||||
///
|
||||
/// This provider must be watched during app initialization to activate
|
||||
/// the callback listener.
|
||||
|
||||
final class AccountCallbackHandlerProvider
|
||||
extends $FunctionalProvider<void, void, void>
|
||||
with $Provider<void> {
|
||||
/// Listens for account callback deep links and forwards handoff codes
|
||||
/// to the account auth repository.
|
||||
///
|
||||
/// This provider must be watched during app initialization to activate
|
||||
/// the callback listener.
|
||||
AccountCallbackHandlerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'accountCallbackHandlerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$accountCallbackHandlerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<void> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
void create(Ref ref) {
|
||||
return accountCallbackHandler(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$accountCallbackHandlerHash() =>
|
||||
r'8d8e627efed8c030a2a9fe179cc71bdc33dd79c0';
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/account/data/supabase_config.dart';
|
||||
|
||||
part 'handoff_redeem_client.g.dart';
|
||||
|
||||
/// Thrown by [HandoffRedeemClient.redeem] when we want to surface a
|
||||
/// specific, already-safe message to the user (e.g. a server-supplied error
|
||||
/// string). Distinguishes "messages we trust to show verbatim" from
|
||||
/// arbitrary `Exception.toString()` output, which may carry HTTP bodies,
|
||||
/// tokens, or stack frames.
|
||||
class AccountAuthFlowException implements Exception {
|
||||
final String userMessage;
|
||||
AccountAuthFlowException(this.userMessage);
|
||||
|
||||
@override
|
||||
String toString() => userMessage;
|
||||
}
|
||||
|
||||
/// Successful response from the `handoff-redeem` Supabase function. Holds
|
||||
/// the raw `session` and `account` payloads so the caller decides how to
|
||||
/// persist them.
|
||||
class HandoffRedeemResult {
|
||||
final Map<String, dynamic> session;
|
||||
final Map<String, dynamic> account;
|
||||
|
||||
const HandoffRedeemResult({required this.session, required this.account});
|
||||
}
|
||||
|
||||
/// Stateless client for the account web app's `handoff-redeem` endpoint.
|
||||
/// Owns the HTTP transport and the response parsing so the auth repository
|
||||
/// only orchestrates state transitions around it.
|
||||
class HandoffRedeemClient {
|
||||
final http.Client _client;
|
||||
|
||||
HandoffRedeemClient({http.Client? client}) : _client = client ?? http.Client();
|
||||
|
||||
void close() => _client.close();
|
||||
|
||||
/// Exchange a one-time `handoff_code` plus the matching PKCE
|
||||
/// `code_verifier` for a Supabase session. Throws
|
||||
/// [AccountAuthFlowException] on a non-200 status with the server's error
|
||||
/// message when available, or a generic fallback otherwise.
|
||||
Future<HandoffRedeemResult> redeem({
|
||||
required String handoffCode,
|
||||
required String codeVerifier,
|
||||
}) async {
|
||||
const redeemUrl =
|
||||
'${SupabaseConfig.supabaseUrl}/functions/v1/handoff-redeem';
|
||||
final response = await _client.post(
|
||||
Uri.parse(redeemUrl),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': SupabaseConfig.supabaseAnonKey,
|
||||
},
|
||||
body: jsonEncode({
|
||||
'handoff_code': handoffCode,
|
||||
'code_verifier': codeVerifier,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
// Try to extract the server's error field. The full response body
|
||||
// is logged by the caller — we only surface the trusted message
|
||||
// field to the UI to avoid leaking response detail.
|
||||
String? serverMessage;
|
||||
try {
|
||||
final error = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
serverMessage = error['error'] as String?;
|
||||
} catch (_) {
|
||||
// Body wasn't JSON — fall back to a generic message so we don't
|
||||
// echo HTML/HTTP detail to the user.
|
||||
}
|
||||
throw AccountAuthFlowException(
|
||||
serverMessage ?? 'Sign-in failed. Please try again.',
|
||||
);
|
||||
}
|
||||
|
||||
final responseData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return HandoffRedeemResult(
|
||||
session: responseData['session'] as Map<String, dynamic>,
|
||||
account: responseData['account'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
HandoffRedeemClient handoffRedeemClient(Ref ref) {
|
||||
final client = HandoffRedeemClient();
|
||||
ref.onDispose(client.close);
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'handoff_redeem_client.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(handoffRedeemClient)
|
||||
final handoffRedeemClientProvider = HandoffRedeemClientProvider._();
|
||||
|
||||
final class HandoffRedeemClientProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
HandoffRedeemClient,
|
||||
HandoffRedeemClient,
|
||||
HandoffRedeemClient
|
||||
>
|
||||
with $Provider<HandoffRedeemClient> {
|
||||
HandoffRedeemClientProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'handoffRedeemClientProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$handoffRedeemClientHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<HandoffRedeemClient> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
HandoffRedeemClient create(Ref ref) {
|
||||
return handoffRedeemClient(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(HandoffRedeemClient value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<HandoffRedeemClient>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$handoffRedeemClientHash() =>
|
||||
r'6746d14547090c9ff4fcdc1c554fc7aa3d1e6795';
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/account/domain/utils/user_js_parser.dart';
|
||||
import 'package:weblibre/utils/filesystem.dart';
|
||||
|
||||
/// Reads persisted Gecko user prefs directly from the active profile's
|
||||
/// `prefs.js` file.
|
||||
///
|
||||
/// Gecko writes `user_pref(...)` entries only for prefs with a user-set value,
|
||||
/// so reading this file gives us the syncable modified-prefs view without
|
||||
/// needing a native bridge.
|
||||
class PrefsJsReader {
|
||||
final Directory selectedProfileDir;
|
||||
|
||||
PrefsJsReader({required this.selectedProfileDir});
|
||||
|
||||
/// Returns a map of user-set prefs parsed from `prefs.js`, or an empty map
|
||||
/// if no `prefs.js` file exists for the active profile.
|
||||
Future<Map<String, Object>> readUserPrefs() async {
|
||||
final file = _resolvePrefsJsFile();
|
||||
if (file == null || !await file.exists()) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final text = await file.readAsString();
|
||||
return parseUserJs(text).prefs;
|
||||
}
|
||||
|
||||
File? _resolvePrefsJsFile() {
|
||||
final profileIds = getMozillaProfileIds(selectedProfileDir);
|
||||
if (profileIds.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final candidates = profileIds
|
||||
.map(
|
||||
(id) => File(
|
||||
p.join(selectedProfileDir.path, 'files', 'mozilla', id, 'prefs.js'),
|
||||
),
|
||||
)
|
||||
.where((f) => f.existsSync())
|
||||
.toList();
|
||||
|
||||
if (candidates.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (candidates.length == 1) {
|
||||
return candidates.first;
|
||||
}
|
||||
|
||||
logger.w(
|
||||
'Multiple Gecko profiles with prefs.js found (${candidates.length}); '
|
||||
'selecting newest by modification time',
|
||||
);
|
||||
|
||||
candidates.sort(
|
||||
(a, b) => b.statSync().modified.compareTo(a.statSync().modified),
|
||||
);
|
||||
return candidates.first;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/filesystem.dart';
|
||||
import 'package:weblibre/features/account/data/repositories/account_sync_repository.dart';
|
||||
import 'package:weblibre/features/account/domain/services/prefs_js_reader.dart';
|
||||
import 'package:weblibre/features/account/domain/services/sync_document_service.dart';
|
||||
import 'package:weblibre/features/account/domain/utils/user_js_parser.dart';
|
||||
import 'package:weblibre/features/account/domain/utils/user_js_serializer.dart';
|
||||
|
||||
part 'prefs_sync_service.g.dart';
|
||||
|
||||
const _schemaVersion = 1;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class PrefsSyncService extends _$PrefsSyncService
|
||||
implements SyncDocumentService {
|
||||
final GeckoPrefService _prefService = GeckoPrefService();
|
||||
final PrefsJsReader _prefsReader = PrefsJsReader(
|
||||
selectedProfileDir: filesystem.selectedProfileDir,
|
||||
);
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
@override
|
||||
SyncDocumentKind get kind => SyncDocumentKind.geckoUserJs;
|
||||
|
||||
@override
|
||||
int get schemaVersion => _schemaVersion;
|
||||
|
||||
@override
|
||||
Future<List<int>> serializeCurrent() async {
|
||||
final userPrefs = await _prefsReader.readUserPrefs();
|
||||
|
||||
final text = serializeUserJs(
|
||||
userPrefs: userPrefs,
|
||||
schemaVersion: _schemaVersion,
|
||||
);
|
||||
|
||||
return utf8.encode(text);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> applyRestored(List<int> plaintext) async {
|
||||
final text = utf8.decode(plaintext);
|
||||
final parsed = parseUserJs(text);
|
||||
|
||||
if (parsed.schemaVersion != null &&
|
||||
parsed.schemaVersion! > _schemaVersion) {
|
||||
throw Exception(
|
||||
'Unsupported prefs schema version: ${parsed.schemaVersion} '
|
||||
'(this app supports up to $_schemaVersion)',
|
||||
);
|
||||
}
|
||||
|
||||
final remotePrefs = parsed.prefs;
|
||||
|
||||
// Find local user-set prefs whose exported form would have been preserved,
|
||||
// so we can reset those absent from the remote snapshot.
|
||||
final localPrefs = await _prefsReader.readUserPrefs();
|
||||
final syncableLocalKeys = localPrefs.entries
|
||||
.where((e) => isSyncablePrefValue(e.value))
|
||||
.map((e) => e.key)
|
||||
.toSet();
|
||||
|
||||
final prefsToReset = syncableLocalKeys
|
||||
.difference(remotePrefs.keys.toSet())
|
||||
.toList();
|
||||
|
||||
if (prefsToReset.isNotEmpty) {
|
||||
await _prefService.resetPrefs(prefsToReset);
|
||||
}
|
||||
|
||||
if (remotePrefs.isNotEmpty) {
|
||||
await _prefService.applyPrefs(remotePrefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'prefs_sync_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PrefsSyncService)
|
||||
final prefsSyncServiceProvider = PrefsSyncServiceProvider._();
|
||||
|
||||
final class PrefsSyncServiceProvider
|
||||
extends $NotifierProvider<PrefsSyncService, void> {
|
||||
PrefsSyncServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'prefsSyncServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$prefsSyncServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PrefsSyncService create() => PrefsSyncService();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$prefsSyncServiceHash() => r'9e95a066d751c12dc2a46bbe041c49e0389a7874';
|
||||
|
||||
abstract class _$PrefsSyncService extends $Notifier<void> {
|
||||
void build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<void, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<void, void>,
|
||||
void,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/account/data/models/settings_sync_envelope.dart';
|
||||
import 'package:weblibre/features/account/data/repositories/account_sync_repository.dart';
|
||||
import 'package:weblibre/features/account/domain/services/sync_document_service.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/tor_settings.dart';
|
||||
|
||||
part 'settings_sync_service.g.dart';
|
||||
|
||||
const _schemaVersion = 1;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SettingsSyncService extends _$SettingsSyncService
|
||||
implements SyncDocumentService {
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
@override
|
||||
SyncDocumentKind get kind => SyncDocumentKind.weblibreSettings;
|
||||
|
||||
@override
|
||||
int get schemaVersion => _schemaVersion;
|
||||
|
||||
@override
|
||||
Future<List<int>> serializeCurrent() async {
|
||||
final (general, engine, tor) = await (
|
||||
ref.read(generalSettingsRepositoryProvider.notifier).fetchSettings(),
|
||||
ref.read(engineSettingsRepositoryProvider.notifier).fetchSettings(),
|
||||
ref.read(torSettingsRepositoryProvider.notifier).fetchSettings(),
|
||||
).wait;
|
||||
|
||||
final envelope = SettingsSyncEnvelope(
|
||||
schemaVersion: _schemaVersion,
|
||||
exportedAt: DateTime.now().toUtc().toIso8601String(),
|
||||
payload: SettingsSyncPayload(general: general, engine: engine, tor: tor),
|
||||
);
|
||||
|
||||
return utf8.encode(jsonEncode(envelope.toJson()));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> applyRestored(List<int> plaintext) async {
|
||||
final json = jsonDecode(utf8.decode(plaintext)) as Map<String, dynamic>;
|
||||
final envelope = SettingsSyncEnvelope.fromJson(json);
|
||||
|
||||
if (envelope.schemaVersion > _schemaVersion) {
|
||||
throw Exception(
|
||||
'Unsupported settings schema version: ${envelope.schemaVersion} '
|
||||
'(this app supports up to $_schemaVersion)',
|
||||
);
|
||||
}
|
||||
|
||||
final payload = envelope.payload;
|
||||
|
||||
if (payload.general != null) {
|
||||
await ref
|
||||
.read(generalSettingsRepositoryProvider.notifier)
|
||||
.updateSettings((_) => payload.general!);
|
||||
}
|
||||
if (payload.engine != null) {
|
||||
await ref
|
||||
.read(engineSettingsRepositoryProvider.notifier)
|
||||
.updateSettings((_) => payload.engine!);
|
||||
}
|
||||
if (payload.tor != null) {
|
||||
await ref
|
||||
.read(torSettingsRepositoryProvider.notifier)
|
||||
.updateSettings((_) => payload.tor!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'settings_sync_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(SettingsSyncService)
|
||||
final settingsSyncServiceProvider = SettingsSyncServiceProvider._();
|
||||
|
||||
final class SettingsSyncServiceProvider
|
||||
extends $NotifierProvider<SettingsSyncService, void> {
|
||||
SettingsSyncServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'settingsSyncServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$settingsSyncServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SettingsSyncService create() => SettingsSyncService();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$settingsSyncServiceHash() =>
|
||||
r'3f7488d705394417b3d8ca6319c978d65f1ef35d';
|
||||
|
||||
abstract class _$SettingsSyncService extends $Notifier<void> {
|
||||
void build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<void, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<void, void>,
|
||||
void,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:weblibre/features/account/data/repositories/account_sync_repository.dart';
|
||||
|
||||
/// Contract for document kinds that can be synced.
|
||||
///
|
||||
/// Implementations handle serialization and deserialization of their domain
|
||||
/// data. Encryption, repository interaction, and UI are handled externally
|
||||
/// by the reusable [SyncDocumentListSection] widget.
|
||||
abstract class SyncDocumentService {
|
||||
/// The document kind identifier for Supabase storage.
|
||||
SyncDocumentKind get kind;
|
||||
|
||||
/// Current schema version for this document kind.
|
||||
int get schemaVersion;
|
||||
|
||||
/// Serializes the current app state to plaintext bytes for encryption.
|
||||
Future<List<int>> serializeCurrent();
|
||||
|
||||
/// Deserializes decrypted plaintext bytes and applies them to app state.
|
||||
Future<void> applyRestored(List<int> plaintext);
|
||||
}
|
||||
Reference in New Issue
Block a user