sync initial
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
|
||||
part 'sync_repository_state.g.dart';
|
||||
|
||||
enum SyncEvent {
|
||||
started,
|
||||
completed,
|
||||
error;
|
||||
}
|
||||
|
||||
@CopyWith()
|
||||
class SyncRepositoryState with FastEquatable {
|
||||
final SyncAccountInfo account;
|
||||
final List<SyncDeviceTabs> remoteTabs;
|
||||
final List<SyncDevice> devices;
|
||||
final String? deviceName;
|
||||
final SyncEvent? lastSyncEvent;
|
||||
final String? lastSyncError;
|
||||
|
||||
SyncRepositoryState({
|
||||
required this.account,
|
||||
this.remoteTabs = const <SyncDeviceTabs>[],
|
||||
this.devices = const <SyncDevice>[],
|
||||
this.deviceName,
|
||||
this.lastSyncEvent,
|
||||
this.lastSyncError,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters =>
|
||||
[account, remoteTabs, devices, deviceName, lastSyncEvent, lastSyncError];
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sync_repository_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$SyncRepositoryStateCWProxy {
|
||||
SyncRepositoryState account(SyncAccountInfo account);
|
||||
|
||||
SyncRepositoryState remoteTabs(List<SyncDeviceTabs> remoteTabs);
|
||||
|
||||
SyncRepositoryState devices(List<SyncDevice> devices);
|
||||
|
||||
SyncRepositoryState deviceName(String? deviceName);
|
||||
|
||||
SyncRepositoryState lastSyncEvent(SyncEvent? lastSyncEvent);
|
||||
|
||||
SyncRepositoryState lastSyncError(String? lastSyncError);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `SyncRepositoryState(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// SyncRepositoryState(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
SyncRepositoryState call({
|
||||
SyncAccountInfo account,
|
||||
List<SyncDeviceTabs> remoteTabs,
|
||||
List<SyncDevice> devices,
|
||||
String? deviceName,
|
||||
SyncEvent? lastSyncEvent,
|
||||
String? lastSyncError,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfSyncRepositoryState.copyWith(...)` or call `instanceOfSyncRepositoryState.copyWith.fieldName(value)` for a single field.
|
||||
class _$SyncRepositoryStateCWProxyImpl implements _$SyncRepositoryStateCWProxy {
|
||||
const _$SyncRepositoryStateCWProxyImpl(this._value);
|
||||
|
||||
final SyncRepositoryState _value;
|
||||
|
||||
@override
|
||||
SyncRepositoryState account(SyncAccountInfo account) =>
|
||||
call(account: account);
|
||||
|
||||
@override
|
||||
SyncRepositoryState remoteTabs(List<SyncDeviceTabs> remoteTabs) =>
|
||||
call(remoteTabs: remoteTabs);
|
||||
|
||||
@override
|
||||
SyncRepositoryState devices(List<SyncDevice> devices) =>
|
||||
call(devices: devices);
|
||||
|
||||
@override
|
||||
SyncRepositoryState deviceName(String? deviceName) =>
|
||||
call(deviceName: deviceName);
|
||||
|
||||
@override
|
||||
SyncRepositoryState lastSyncEvent(SyncEvent? lastSyncEvent) =>
|
||||
call(lastSyncEvent: lastSyncEvent);
|
||||
|
||||
@override
|
||||
SyncRepositoryState lastSyncError(String? lastSyncError) =>
|
||||
call(lastSyncError: lastSyncError);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `SyncRepositoryState(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// SyncRepositoryState(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
SyncRepositoryState call({
|
||||
Object? account = const $CopyWithPlaceholder(),
|
||||
Object? remoteTabs = const $CopyWithPlaceholder(),
|
||||
Object? devices = const $CopyWithPlaceholder(),
|
||||
Object? deviceName = const $CopyWithPlaceholder(),
|
||||
Object? lastSyncEvent = const $CopyWithPlaceholder(),
|
||||
Object? lastSyncError = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return SyncRepositoryState(
|
||||
account: account == const $CopyWithPlaceholder() || account == null
|
||||
? _value.account
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: account as SyncAccountInfo,
|
||||
remoteTabs:
|
||||
remoteTabs == const $CopyWithPlaceholder() || remoteTabs == null
|
||||
? _value.remoteTabs
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: remoteTabs as List<SyncDeviceTabs>,
|
||||
devices: devices == const $CopyWithPlaceholder() || devices == null
|
||||
? _value.devices
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: devices as List<SyncDevice>,
|
||||
deviceName: deviceName == const $CopyWithPlaceholder()
|
||||
? _value.deviceName
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: deviceName as String?,
|
||||
lastSyncEvent: lastSyncEvent == const $CopyWithPlaceholder()
|
||||
? _value.lastSyncEvent
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: lastSyncEvent as SyncEvent?,
|
||||
lastSyncError: lastSyncError == const $CopyWithPlaceholder()
|
||||
? _value.lastSyncError
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: lastSyncError as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $SyncRepositoryStateCopyWith on SyncRepositoryState {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfSyncRepositoryState.copyWith(...)` or `instanceOfSyncRepositoryState.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$SyncRepositoryStateCWProxy get copyWith =>
|
||||
_$SyncRepositoryStateCWProxyImpl(this);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
|
||||
class SyncedTabItem with FastEquatable {
|
||||
final String deviceId;
|
||||
final String deviceName;
|
||||
final SyncRemoteTab tab;
|
||||
|
||||
SyncedTabItem({
|
||||
required this.deviceId,
|
||||
required this.deviceName,
|
||||
required this.tab,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [deviceId, deviceName, tab];
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||
import 'package:weblibre/features/sync/domain/entities/synced_tab_item.dart';
|
||||
|
||||
part 'sync.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
bool syncIsAuthenticated(Ref ref) {
|
||||
return ref.watch(
|
||||
syncRepositoryProvider.select(
|
||||
(value) => value.value?.account.authenticated == true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
enum TabsTrayScope { local, synced }
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabsTrayScopeController extends _$TabsTrayScopeController {
|
||||
@override
|
||||
TabsTrayScope build() => TabsTrayScope.local;
|
||||
|
||||
void showLocal() {
|
||||
state = TabsTrayScope.local;
|
||||
}
|
||||
|
||||
void showSynced() {
|
||||
state = TabsTrayScope.synced;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
TabsTrayScope effectiveTabsTrayScope(Ref ref) {
|
||||
final scope = ref.watch(tabsTrayScopeControllerProvider);
|
||||
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||
|
||||
if (!isAuthenticated && scope == TabsTrayScope.synced) {
|
||||
return TabsTrayScope.local;
|
||||
}
|
||||
|
||||
return scope;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SelectedSyncedTabsDeviceId extends _$SelectedSyncedTabsDeviceId {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
// ignore: use_setters_to_change_properties
|
||||
void selectDevice(String? value) {
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<int> syncedTabsTotalCount(Ref ref) {
|
||||
return ref.watch(
|
||||
syncRemoteTabsProvider.selectAsync(
|
||||
(devices) =>
|
||||
devices.fold<int>(0, (count, device) => count + device.tabs.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<String?> effectiveSyncedTabsDeviceId(Ref ref) async {
|
||||
final devices = await ref.watch(
|
||||
syncRemoteTabsProvider.selectAsync((tabs) => tabs),
|
||||
);
|
||||
final selectedDeviceId = ref.watch(selectedSyncedTabsDeviceIdProvider);
|
||||
|
||||
if (devices.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selectedDeviceId != null &&
|
||||
devices.any((device) => device.deviceId == selectedDeviceId)) {
|
||||
return selectedDeviceId;
|
||||
}
|
||||
|
||||
return devices.first.deviceId;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SyncedTabItem>> syncedTabsForSelectedDevice(Ref ref) async {
|
||||
final devices = await ref.watch(
|
||||
syncRemoteTabsProvider.selectAsync((tabs) => tabs),
|
||||
);
|
||||
|
||||
final selectedDeviceId = ref.watch(selectedSyncedTabsDeviceIdProvider);
|
||||
|
||||
final effectiveSelectedDeviceId =
|
||||
selectedDeviceId != null &&
|
||||
devices.any((device) => device.deviceId == selectedDeviceId)
|
||||
? selectedDeviceId
|
||||
: devices.firstOrNull?.deviceId;
|
||||
|
||||
if (effectiveSelectedDeviceId == null) {
|
||||
return const <SyncedTabItem>[];
|
||||
}
|
||||
|
||||
final device = devices.firstWhereOrNull(
|
||||
(item) => item.deviceId == effectiveSelectedDeviceId,
|
||||
);
|
||||
|
||||
if (device == null) {
|
||||
return const <SyncedTabItem>[];
|
||||
}
|
||||
|
||||
final tabs = device.tabs
|
||||
.map(
|
||||
(tab) => SyncedTabItem(
|
||||
deviceId: device.deviceId,
|
||||
deviceName: device.deviceName,
|
||||
tab: tab,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
|
||||
tabs.sort((a, b) => b.tab.lastUsed.compareTo(a.tab.lastUsed));
|
||||
return tabs;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SyncRepository extends _$SyncRepository {
|
||||
final _service = GeckoSyncService();
|
||||
|
||||
StreamSubscription<SyncAccountInfo>? _authStateSub;
|
||||
StreamSubscription<void>? _syncStartedSub;
|
||||
StreamSubscription<void>? _syncCompletedSub;
|
||||
StreamSubscription<String?>? _syncErrorSub;
|
||||
|
||||
Future<void> _awaitInitialized() => future;
|
||||
|
||||
void _update(SyncRepositoryState Function(SyncRepositoryState) updater) {
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
state = AsyncData(updater(current));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshAccount() async {
|
||||
await _awaitInitialized();
|
||||
|
||||
final account = await _service.getAccountInfo();
|
||||
_update((s) => s.copyWith(account: account));
|
||||
}
|
||||
|
||||
Future<void> _refreshTabs() async {
|
||||
await _awaitInitialized();
|
||||
|
||||
try {
|
||||
final tabs = await _service.getSyncedTabs();
|
||||
_update((s) => s.copyWith(remoteTabs: tabs));
|
||||
} on Exception catch (e) {
|
||||
logger.e('Failed to refresh synced tabs', error: e);
|
||||
_update(
|
||||
(s) => s.copyWith(
|
||||
lastSyncEvent: SyncEvent.error,
|
||||
lastSyncError: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshDevices() async {
|
||||
await _awaitInitialized();
|
||||
|
||||
try {
|
||||
final devices = await _service.getDevices();
|
||||
_update((s) => s.copyWith(devices: devices));
|
||||
} on Exception catch (e) {
|
||||
logger.e('Failed to refresh devices', error: e);
|
||||
_update(
|
||||
(s) => s.copyWith(
|
||||
lastSyncEvent: SyncEvent.error,
|
||||
lastSyncError: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshDeviceName() async {
|
||||
await _awaitInitialized();
|
||||
|
||||
final deviceName = await _service.getDeviceName();
|
||||
_update((s) => s.copyWith(deviceName: deviceName));
|
||||
}
|
||||
|
||||
Future<SyncAccountInfo> refresh() async {
|
||||
await _refreshAccount();
|
||||
return state.value!.account;
|
||||
}
|
||||
|
||||
Future<void> signIn() async {
|
||||
await _service.beginAuthentication();
|
||||
}
|
||||
|
||||
Future<void> signInWithPairing(String pairingUrl) async {
|
||||
await _service.beginPairingAuthentication(pairingUrl);
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
await _service.logout();
|
||||
}
|
||||
|
||||
Future<void> syncNow() async {
|
||||
await _service.syncNow();
|
||||
await Future.wait([_refreshAccount(), _refreshTabs(), _refreshDevices()]);
|
||||
}
|
||||
|
||||
Future<void> setEngineEnabled(SyncEngineValue engine, bool enabled) async {
|
||||
await _service.setEngineEnabled(engine, enabled);
|
||||
await Future.wait([_refreshAccount(), _refreshTabs(), _refreshDevices()]);
|
||||
}
|
||||
|
||||
Future<bool> sendTabToDevice({
|
||||
required String deviceId,
|
||||
required String title,
|
||||
required String url,
|
||||
}) {
|
||||
return _service.sendTabToDevice(deviceId, title, url);
|
||||
}
|
||||
|
||||
Future<bool> setDeviceName(String newName) async {
|
||||
final result = await _service.setDeviceName(newName);
|
||||
|
||||
if (result) {
|
||||
await Future.wait([_refreshDevices(), _refreshDeviceName()]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> refreshDevices() async {
|
||||
await _service.refreshDevices();
|
||||
await _refreshDevices();
|
||||
}
|
||||
|
||||
Future<int> pollIncomingTabsAndOpen() async {
|
||||
await _service.pollDeviceCommands();
|
||||
final incomingTabs = await _service.drainIncomingTabs();
|
||||
|
||||
for (final tab in incomingTabs) {
|
||||
await _openUrlInAssignedContainer(tab.url);
|
||||
}
|
||||
|
||||
await _refreshTabs();
|
||||
return incomingTabs.length;
|
||||
}
|
||||
|
||||
Future<void> openSyncedTab(SyncRemoteTab tab) async {
|
||||
await _openUrlInAssignedContainer(tab.url);
|
||||
}
|
||||
|
||||
Future<void> _openUrlInAssignedContainer(String url) async {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final containerRepository = ref.read(containerRepositoryProvider.notifier);
|
||||
|
||||
final containerId = await containerRepository.siteAssignedContainerId(uri);
|
||||
|
||||
final assignedContainer = containerId == null
|
||||
? null
|
||||
: await containerRepository.getContainerData(containerId);
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: uri,
|
||||
selectTab: true,
|
||||
private: false,
|
||||
container: Value(assignedContainer),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncRepositoryState> build() async {
|
||||
final syncStateService = ref.read(geckoSyncStateServiceProvider);
|
||||
|
||||
_syncStartedSub = syncStateService.syncStartedEvents.listen((_) {
|
||||
_update(
|
||||
(s) => s.copyWith(
|
||||
lastSyncEvent: SyncEvent.started,
|
||||
// ignore: avoid_redundant_argument_values
|
||||
lastSyncError: null,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
_syncCompletedSub = syncStateService.syncCompletedEvents.listen((_) {
|
||||
_update(
|
||||
(s) => s.copyWith(
|
||||
lastSyncEvent: SyncEvent.completed,
|
||||
// ignore: avoid_redundant_argument_values
|
||||
lastSyncError: null,
|
||||
),
|
||||
);
|
||||
unawaited(Future.wait([_refreshTabs(), _refreshDevices()]));
|
||||
});
|
||||
|
||||
_syncErrorSub = syncStateService.syncErrorEvents.listen((error) {
|
||||
logger.e('Sync failed', error: error ?? 'Unknown synchronization error');
|
||||
_update(
|
||||
(s) => s.copyWith(lastSyncEvent: SyncEvent.error, lastSyncError: error),
|
||||
);
|
||||
unawaited(Future.wait([_refreshTabs(), _refreshDevices()]));
|
||||
});
|
||||
|
||||
_authStateSub = syncStateService.authStateEvents.listen((info) {
|
||||
final previous = state.value;
|
||||
|
||||
if (previous == null) {
|
||||
state = AsyncData(SyncRepositoryState(account: info));
|
||||
return;
|
||||
}
|
||||
|
||||
final unauthenticated = !info.authenticated;
|
||||
state = AsyncData(
|
||||
unauthenticated
|
||||
? SyncRepositoryState(account: info)
|
||||
: previous.copyWith(account: info),
|
||||
);
|
||||
|
||||
final authStateChanged =
|
||||
previous.account.authenticated != info.authenticated ||
|
||||
previous.account.needsReauth != info.needsReauth;
|
||||
|
||||
if (authStateChanged && info.authenticated) {
|
||||
unawaited(
|
||||
Future.wait([
|
||||
_refreshAccount(),
|
||||
_refreshTabs(),
|
||||
_refreshDevices(),
|
||||
_refreshDeviceName(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
unawaited(_authStateSub?.cancel());
|
||||
unawaited(_syncStartedSub?.cancel());
|
||||
unawaited(_syncCompletedSub?.cancel());
|
||||
unawaited(_syncErrorSub?.cancel());
|
||||
});
|
||||
|
||||
final (account, tabs, devices, deviceName) = await (
|
||||
_service.getAccountInfo(),
|
||||
_service.getSyncedTabs(),
|
||||
_service.getDevices(),
|
||||
_service.getDeviceName(),
|
||||
).wait;
|
||||
|
||||
return SyncRepositoryState(
|
||||
account: account,
|
||||
remoteTabs: tabs,
|
||||
devices: devices,
|
||||
deviceName: deviceName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
GeckoSyncStateService geckoSyncStateService(Ref ref) {
|
||||
final service = GeckoSyncStateService.setUp();
|
||||
ref.onDispose(() => service.dispose());
|
||||
return service;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<String?> syncDeviceName(Ref ref) {
|
||||
return ref.watch(
|
||||
syncRepositoryProvider.selectAsync((value) => value.deviceName),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SyncDeviceTabs>> syncRemoteTabs(Ref ref) {
|
||||
return ref.watch(
|
||||
syncRepositoryProvider.selectAsync((value) => value.remoteTabs),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SyncDevice>> syncDevices(Ref ref) {
|
||||
return ref.watch(
|
||||
syncRepositoryProvider.selectAsync((value) => value.devices),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<(SyncEvent?, String?)> syncEvent(Ref ref) {
|
||||
return ref.watch(
|
||||
syncRepositoryProvider.selectAsync(
|
||||
(s) => (s.lastSyncEvent, s.lastSyncError),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sync.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(syncIsAuthenticated)
|
||||
final syncIsAuthenticatedProvider = SyncIsAuthenticatedProvider._();
|
||||
|
||||
final class SyncIsAuthenticatedProvider
|
||||
extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
SyncIsAuthenticatedProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncIsAuthenticatedProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncIsAuthenticatedHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return syncIsAuthenticated(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$syncIsAuthenticatedHash() =>
|
||||
r'40e108a5c1d4d885fd31edde04146bf4131dd51c';
|
||||
|
||||
@ProviderFor(TabsTrayScopeController)
|
||||
final tabsTrayScopeControllerProvider = TabsTrayScopeControllerProvider._();
|
||||
|
||||
final class TabsTrayScopeControllerProvider
|
||||
extends $NotifierProvider<TabsTrayScopeController, TabsTrayScope> {
|
||||
TabsTrayScopeControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'tabsTrayScopeControllerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$tabsTrayScopeControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
TabsTrayScopeController create() => TabsTrayScopeController();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(TabsTrayScope value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<TabsTrayScope>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabsTrayScopeControllerHash() =>
|
||||
r'e9415c1f57e1a78804ec4eee122c9db9ee416066';
|
||||
|
||||
abstract class _$TabsTrayScopeController extends $Notifier<TabsTrayScope> {
|
||||
TabsTrayScope build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<TabsTrayScope, TabsTrayScope>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<TabsTrayScope, TabsTrayScope>,
|
||||
TabsTrayScope,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(effectiveTabsTrayScope)
|
||||
final effectiveTabsTrayScopeProvider = EffectiveTabsTrayScopeProvider._();
|
||||
|
||||
final class EffectiveTabsTrayScopeProvider
|
||||
extends $FunctionalProvider<TabsTrayScope, TabsTrayScope, TabsTrayScope>
|
||||
with $Provider<TabsTrayScope> {
|
||||
EffectiveTabsTrayScopeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'effectiveTabsTrayScopeProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$effectiveTabsTrayScopeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<TabsTrayScope> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
TabsTrayScope create(Ref ref) {
|
||||
return effectiveTabsTrayScope(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(TabsTrayScope value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<TabsTrayScope>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$effectiveTabsTrayScopeHash() =>
|
||||
r'ac295d7413a7014882835d3533bf31cd3c031a1d';
|
||||
|
||||
@ProviderFor(SelectedSyncedTabsDeviceId)
|
||||
final selectedSyncedTabsDeviceIdProvider =
|
||||
SelectedSyncedTabsDeviceIdProvider._();
|
||||
|
||||
final class SelectedSyncedTabsDeviceIdProvider
|
||||
extends $NotifierProvider<SelectedSyncedTabsDeviceId, String?> {
|
||||
SelectedSyncedTabsDeviceIdProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedSyncedTabsDeviceIdProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedSyncedTabsDeviceIdHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedSyncedTabsDeviceId create() => SelectedSyncedTabsDeviceId();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedSyncedTabsDeviceIdHash() =>
|
||||
r'61f86d2f17d6c2f04e1fb76dae900c4695aa6af0';
|
||||
|
||||
abstract class _$SelectedSyncedTabsDeviceId extends $Notifier<String?> {
|
||||
String? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<String?, String?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String?, String?>,
|
||||
String?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(syncedTabsTotalCount)
|
||||
final syncedTabsTotalCountProvider = SyncedTabsTotalCountProvider._();
|
||||
|
||||
final class SyncedTabsTotalCountProvider
|
||||
extends $FunctionalProvider<AsyncValue<int>, int, FutureOr<int>>
|
||||
with $FutureModifier<int>, $FutureProvider<int> {
|
||||
SyncedTabsTotalCountProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncedTabsTotalCountProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncedTabsTotalCountHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<int> create(Ref ref) {
|
||||
return syncedTabsTotalCount(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$syncedTabsTotalCountHash() =>
|
||||
r'507b14aac187c434cf02925d895151c1a25159ea';
|
||||
|
||||
@ProviderFor(effectiveSyncedTabsDeviceId)
|
||||
final effectiveSyncedTabsDeviceIdProvider =
|
||||
EffectiveSyncedTabsDeviceIdProvider._();
|
||||
|
||||
final class EffectiveSyncedTabsDeviceIdProvider
|
||||
extends $FunctionalProvider<AsyncValue<String?>, String?, FutureOr<String?>>
|
||||
with $FutureModifier<String?>, $FutureProvider<String?> {
|
||||
EffectiveSyncedTabsDeviceIdProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'effectiveSyncedTabsDeviceIdProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$effectiveSyncedTabsDeviceIdHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<String?> create(Ref ref) {
|
||||
return effectiveSyncedTabsDeviceId(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$effectiveSyncedTabsDeviceIdHash() =>
|
||||
r'26ed7e56191a8019cab5c89dc8d5622e1a709e20';
|
||||
|
||||
@ProviderFor(syncedTabsForSelectedDevice)
|
||||
final syncedTabsForSelectedDeviceProvider =
|
||||
SyncedTabsForSelectedDeviceProvider._();
|
||||
|
||||
final class SyncedTabsForSelectedDeviceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SyncedTabItem>>,
|
||||
List<SyncedTabItem>,
|
||||
FutureOr<List<SyncedTabItem>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SyncedTabItem>>,
|
||||
$FutureProvider<List<SyncedTabItem>> {
|
||||
SyncedTabsForSelectedDeviceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncedTabsForSelectedDeviceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncedTabsForSelectedDeviceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SyncedTabItem>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SyncedTabItem>> create(Ref ref) {
|
||||
return syncedTabsForSelectedDevice(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$syncedTabsForSelectedDeviceHash() =>
|
||||
r'a85c4e972ab55b872f1de64cd27ba18d7dcc023a';
|
||||
|
||||
@ProviderFor(SyncRepository)
|
||||
final syncRepositoryProvider = SyncRepositoryProvider._();
|
||||
|
||||
final class SyncRepositoryProvider
|
||||
extends $AsyncNotifierProvider<SyncRepository, SyncRepositoryState> {
|
||||
SyncRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SyncRepository create() => SyncRepository();
|
||||
}
|
||||
|
||||
String _$syncRepositoryHash() => r'5312aed1abf60ce3e04afa7d2f60b38367c09312';
|
||||
|
||||
abstract class _$SyncRepository extends $AsyncNotifier<SyncRepositoryState> {
|
||||
FutureOr<SyncRepositoryState> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref as $Ref<AsyncValue<SyncRepositoryState>, SyncRepositoryState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<SyncRepositoryState>, SyncRepositoryState>,
|
||||
AsyncValue<SyncRepositoryState>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(geckoSyncStateService)
|
||||
final geckoSyncStateServiceProvider = GeckoSyncStateServiceProvider._();
|
||||
|
||||
final class GeckoSyncStateServiceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
GeckoSyncStateService,
|
||||
GeckoSyncStateService,
|
||||
GeckoSyncStateService
|
||||
>
|
||||
with $Provider<GeckoSyncStateService> {
|
||||
GeckoSyncStateServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'geckoSyncStateServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$geckoSyncStateServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<GeckoSyncStateService> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
GeckoSyncStateService create(Ref ref) {
|
||||
return geckoSyncStateService(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(GeckoSyncStateService value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<GeckoSyncStateService>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$geckoSyncStateServiceHash() =>
|
||||
r'1e96db4a6229a3d2a31862cc9cf88c463405819b';
|
||||
|
||||
@ProviderFor(syncDeviceName)
|
||||
final syncDeviceNameProvider = SyncDeviceNameProvider._();
|
||||
|
||||
final class SyncDeviceNameProvider
|
||||
extends $FunctionalProvider<AsyncValue<String?>, String?, FutureOr<String?>>
|
||||
with $FutureModifier<String?>, $FutureProvider<String?> {
|
||||
SyncDeviceNameProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncDeviceNameProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncDeviceNameHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<String?> create(Ref ref) {
|
||||
return syncDeviceName(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$syncDeviceNameHash() => r'3d7e531cba481c4233a895b1d80e915d646fc6b6';
|
||||
|
||||
@ProviderFor(syncRemoteTabs)
|
||||
final syncRemoteTabsProvider = SyncRemoteTabsProvider._();
|
||||
|
||||
final class SyncRemoteTabsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SyncDeviceTabs>>,
|
||||
List<SyncDeviceTabs>,
|
||||
FutureOr<List<SyncDeviceTabs>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SyncDeviceTabs>>,
|
||||
$FutureProvider<List<SyncDeviceTabs>> {
|
||||
SyncRemoteTabsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncRemoteTabsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncRemoteTabsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SyncDeviceTabs>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SyncDeviceTabs>> create(Ref ref) {
|
||||
return syncRemoteTabs(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$syncRemoteTabsHash() => r'5040a010e578f2fe395302e7d34a91df1854f8a9';
|
||||
|
||||
@ProviderFor(syncDevices)
|
||||
final syncDevicesProvider = SyncDevicesProvider._();
|
||||
|
||||
final class SyncDevicesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SyncDevice>>,
|
||||
List<SyncDevice>,
|
||||
FutureOr<List<SyncDevice>>
|
||||
>
|
||||
with $FutureModifier<List<SyncDevice>>, $FutureProvider<List<SyncDevice>> {
|
||||
SyncDevicesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncDevicesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncDevicesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SyncDevice>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SyncDevice>> create(Ref ref) {
|
||||
return syncDevices(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$syncDevicesHash() => r'6249b4891f95fc6f8e2f70f1555aff0e6356ccad';
|
||||
|
||||
@ProviderFor(syncEvent)
|
||||
final syncEventProvider = SyncEventProvider._();
|
||||
|
||||
final class SyncEventProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<(SyncEvent?, String?)>,
|
||||
(SyncEvent?, String?),
|
||||
FutureOr<(SyncEvent?, String?)>
|
||||
>
|
||||
with
|
||||
$FutureModifier<(SyncEvent?, String?)>,
|
||||
$FutureProvider<(SyncEvent?, String?)> {
|
||||
SyncEventProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'syncEventProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$syncEventHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<(SyncEvent?, String?)> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<(SyncEvent?, String?)> create(Ref ref) {
|
||||
return syncEvent(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$syncEventHash() => r'c84d251502578779f66e17ee79e353ffa3b23e1a';
|
||||
@@ -0,0 +1,463 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||
import 'package:weblibre/core/providers/format.dart';
|
||||
import 'package:weblibre/features/qr_scanner/presentation/dialogs/qr_scanner_dialog.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
||||
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
class SyncSettingsScreen extends HookConsumerWidget {
|
||||
const SyncSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final syncInfo = ref.watch(
|
||||
syncRepositoryProvider.select((value) => value.value?.account),
|
||||
);
|
||||
|
||||
final generalSettings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||
|
||||
final syncStarted = ref.watch(
|
||||
syncEventProvider.select(
|
||||
(value) => value.isLoading || value.value?.$1 == SyncEvent.started,
|
||||
),
|
||||
);
|
||||
final isSyncing = syncStarted || syncInfo?.syncing == true;
|
||||
|
||||
final syncText = useMemoized(() {
|
||||
if (isSyncing) {
|
||||
return 'Synchronization in progress';
|
||||
}
|
||||
|
||||
final timestamp = syncInfo?.lastSyncedAt;
|
||||
if (timestamp == null || timestamp <= 0) {
|
||||
return 'Never synced';
|
||||
}
|
||||
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
final formattedDate = ref
|
||||
.read(formatProvider.notifier)
|
||||
.fullDateTime(date.toLocal());
|
||||
|
||||
return 'Last synced: $formattedDate';
|
||||
}, [syncInfo, isSyncing]);
|
||||
|
||||
final syncController = useAnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (isSyncing) {
|
||||
unawaited(syncController.repeat());
|
||||
} else {
|
||||
syncController.stop();
|
||||
syncController.reset();
|
||||
}
|
||||
return null;
|
||||
}, [isSyncing]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Firefox Sync')),
|
||||
body: SafeArea(
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
children: [
|
||||
const SettingSection(name: 'Account'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.account_circle_outlined),
|
||||
title: Text(syncInfo?.email ?? 'Not signed in'),
|
||||
subtitle: Text(
|
||||
syncInfo?.needsReauth == true
|
||||
? 'Authentication expired. Sign in again to continue syncing.'
|
||||
: syncInfo?.authenticated == true
|
||||
? (syncInfo?.displayName ?? 'Signed in')
|
||||
: 'Sign in to synchronize tabs, bookmarks, and history',
|
||||
),
|
||||
trailing: syncInfo?.authenticated == true
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: 'Sign Out',
|
||||
onPressed: isSyncing
|
||||
? null
|
||||
: () async {
|
||||
final confirmed =
|
||||
await _showSignOutConfirmation(context);
|
||||
if (confirmed == true) {
|
||||
await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.signOut();
|
||||
}
|
||||
},
|
||||
)
|
||||
: const Icon(Icons.login),
|
||||
onTap: (syncInfo?.authenticated == true || isSyncing)
|
||||
? null
|
||||
: () async {
|
||||
await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.signIn();
|
||||
},
|
||||
),
|
||||
if (syncInfo?.authenticated != true && !isSyncing)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: const Text('Scan QR Code to pair'),
|
||||
subtitle: const Text(
|
||||
'Scan a QR code from firefox.com/pair on desktop',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
final barcode = await showDialog<Barcode>(
|
||||
context: context,
|
||||
builder: (_) => const QrScannerDialog(),
|
||||
);
|
||||
|
||||
final code = barcode?.code;
|
||||
if (code == null || code.isEmpty) return;
|
||||
|
||||
final uri = Uri.tryParse(code);
|
||||
if (uri == null || !uri.hasScheme) {
|
||||
if (context.mounted) {
|
||||
ui_helper.showErrorMessage(
|
||||
context,
|
||||
'Invalid QR code: not a valid URL',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.signInWithPairing(code);
|
||||
},
|
||||
),
|
||||
if (syncInfo?.authenticated == true)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.devices),
|
||||
title: const Text('Device Name'),
|
||||
subtitle: ref
|
||||
.watch(syncDeviceNameProvider)
|
||||
.when(
|
||||
data: (name) => Text(name ?? 'Unknown'),
|
||||
loading: () => const Text('Loading...'),
|
||||
error: (_, _) => const Text('Unknown'),
|
||||
),
|
||||
trailing: const Icon(Icons.edit_outlined),
|
||||
onTap: isSyncing
|
||||
? null
|
||||
: () async {
|
||||
final currentName = ref
|
||||
.read(syncRepositoryProvider)
|
||||
.value
|
||||
?.deviceName;
|
||||
|
||||
if (currentName != null && context.mounted) {
|
||||
await _showDeviceNameDialog(
|
||||
context,
|
||||
currentName: currentName,
|
||||
onSave: (newName) {
|
||||
return ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.setDeviceName(newName);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SettingSection(name: 'Synchronization'),
|
||||
ListTile(
|
||||
leading: RotationTransition(
|
||||
turns: Tween<double>(
|
||||
begin: 0,
|
||||
end: -1,
|
||||
).animate(syncController),
|
||||
child: const Icon(Icons.sync),
|
||||
),
|
||||
title: const Text('Sync Now'),
|
||||
subtitle: Text(syncText),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: isSyncing
|
||||
? null
|
||||
: () async {
|
||||
await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.syncNow();
|
||||
},
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Sync History'),
|
||||
value: _engineEnabled(syncInfo, SyncEngineValue.history),
|
||||
onChanged: (syncInfo == null || isSyncing)
|
||||
? null
|
||||
: (value) async {
|
||||
await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.setEngineEnabled(SyncEngineValue.history, value);
|
||||
},
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Sync Bookmarks'),
|
||||
value: _engineEnabled(syncInfo, SyncEngineValue.bookmarks),
|
||||
onChanged: (syncInfo == null || isSyncing)
|
||||
? null
|
||||
: (value) async {
|
||||
await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.setEngineEnabled(
|
||||
SyncEngineValue.bookmarks,
|
||||
value,
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Sync Open Tabs'),
|
||||
value: _engineEnabled(syncInfo, SyncEngineValue.tabs),
|
||||
onChanged: (syncInfo == null || isSyncing)
|
||||
? null
|
||||
: (value) async {
|
||||
await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.setEngineEnabled(SyncEngineValue.tabs, value);
|
||||
},
|
||||
),
|
||||
const SettingSection(name: 'Server Overrides'),
|
||||
ListTile(
|
||||
title: const Text('FxA Server Override'),
|
||||
subtitle: Text(
|
||||
generalSettings.syncServerOverride.isEmpty
|
||||
? 'Default Mozilla server'
|
||||
: generalSettings.syncServerOverride,
|
||||
),
|
||||
trailing: const Icon(Icons.edit_outlined),
|
||||
onTap: isSyncing
|
||||
? null
|
||||
: () => _showTextSettingDialog(
|
||||
context,
|
||||
title: 'FxA Server Override',
|
||||
initialValue: generalSettings.syncServerOverride,
|
||||
hint: 'https://accounts.firefox.com',
|
||||
onSave: (value) {
|
||||
return ref
|
||||
.read(
|
||||
saveGeneralSettingsControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.save(
|
||||
(current) => current.copyWith
|
||||
.syncServerOverride(value.trim()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Sync Token Server Override'),
|
||||
subtitle: Text(
|
||||
generalSettings.syncTokenServerOverride.isEmpty
|
||||
? 'Automatic from FxA server'
|
||||
: generalSettings.syncTokenServerOverride,
|
||||
),
|
||||
trailing: const Icon(Icons.edit_outlined),
|
||||
onTap: isSyncing
|
||||
? null
|
||||
: () => _showTextSettingDialog(
|
||||
context,
|
||||
title: 'Sync Token Server Override',
|
||||
initialValue: generalSettings.syncTokenServerOverride,
|
||||
hint:
|
||||
'https://token.services.mozilla.com/1.0/sync/1.5',
|
||||
onSave: (value) {
|
||||
return ref
|
||||
.read(
|
||||
saveGeneralSettingsControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.save(
|
||||
(current) => current.copyWith
|
||||
.syncTokenServerOverride(value.trim()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
'Restart the app after changing server overrides.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static bool _engineEnabled(SyncAccountInfo? info, SyncEngineValue engine) {
|
||||
final engines = info?.engines;
|
||||
if (engines == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final status in engines) {
|
||||
if (status.engine == engine) {
|
||||
return status.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static Future<bool?> _showSignOutConfirmation(BuildContext context) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Sign out?'),
|
||||
content: const Text(
|
||||
'Are you sure you want to sign out of Firefox Sync?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Sign Out'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _showDeviceNameDialog(
|
||||
BuildContext context, {
|
||||
required String currentName,
|
||||
required Future<bool> Function(String name) onSave,
|
||||
}) async {
|
||||
final controller = TextEditingController(text: currentName);
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Device Name'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(hintText: 'Enter device name'),
|
||||
autofocus: true,
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(128)],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final newName = controller.text.trim();
|
||||
if (newName.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Device name cannot be empty'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await onSave(newName).catchError((_) => false);
|
||||
|
||||
if (!success) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Failed to update device name'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _showTextSettingDialog(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String initialValue,
|
||||
required String hint,
|
||||
required Future<void> Function(String value) onSave,
|
||||
}) async {
|
||||
final controller = TextEditingController(text: initialValue);
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(hintText: hint),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final value = controller.text.trim();
|
||||
if (value.isNotEmpty) {
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || uri.scheme != 'https') {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Must be a valid HTTPS URL'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
await onSave(controller.text);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user