fix proxy auto start issue
This commit is contained in:
@@ -130,7 +130,7 @@ final class GenericWebsiteServiceProvider
|
||||
}
|
||||
|
||||
String _$genericWebsiteServiceHash() =>
|
||||
r'83c62a15492606a7ffe4a7990fc1f9bdbfee87cc';
|
||||
r'189cbd17a06be382859c697221355cd623608123';
|
||||
|
||||
abstract class _$GenericWebsiteService extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -1186,7 +1186,7 @@ final class GroupedTabListItemsProvider
|
||||
}
|
||||
|
||||
String _$groupedTabListItemsHash() =>
|
||||
r'15106bfa42146270245c7178928a834237042c1c';
|
||||
r'dbb510f22c00858fcbadfb94547d46841221fca4';
|
||||
|
||||
/// Grouped flat-list rendering for the list and grid views.
|
||||
///
|
||||
|
||||
-27
@@ -26,9 +26,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/models/container_
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
|
||||
import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart';
|
||||
import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart';
|
||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
|
||||
@@ -43,25 +41,6 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
final _recomputeLock = Lock();
|
||||
var _recomputeDirty = false;
|
||||
|
||||
Future<void> _ensureProxyConnectionAvailable(
|
||||
ProxyConnectionId proxyConnectionId,
|
||||
) async {
|
||||
if (proxyConnectionId is! SingboxProxyConnectionId) return;
|
||||
|
||||
try {
|
||||
await ref
|
||||
.read(singboxProxyRuntimeRepositoryProvider.notifier)
|
||||
.ensureProxyConnectionAvailable(proxyConnectionId);
|
||||
} catch (error, stackTrace) {
|
||||
logger.e(
|
||||
'Failed to make proxy connection $proxyConnectionId available; '
|
||||
'assigned traffic will remain blocked',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _queueIsolatedProxyAliasesRecompute() async {
|
||||
_recomputeDirty = true;
|
||||
if (_recomputeLock.inLock) return;
|
||||
@@ -187,7 +166,6 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
await containerProxy.clearContainerProxy('private');
|
||||
} else {
|
||||
await containerProxy.setContainerProxy('private', next.encode());
|
||||
await _ensureProxyConnectionAvailable(next);
|
||||
}
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
@@ -220,7 +198,6 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
'general',
|
||||
proxyConnectionId.encode(),
|
||||
);
|
||||
await _ensureProxyConnectionAvailable(proxyConnectionId);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -315,10 +292,6 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
}
|
||||
try {
|
||||
if (value != null) {
|
||||
final proxyConnectionId = ProxyConnectionId.decode(value);
|
||||
if (proxyConnectionId != null) {
|
||||
await _ensureProxyConnectionAvailable(proxyConnectionId);
|
||||
}
|
||||
await repo.setContainerProxy(key, value);
|
||||
} else {
|
||||
await repo.clearContainerProxy(key);
|
||||
|
||||
+159
-37
@@ -58,14 +58,19 @@ import 'package:weblibre/features/geckoview/features/find_in_page/presentation/c
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_autofocus.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
|
||||
import 'package:weblibre/features/proxy/presentation/controllers/ensure_proxy_started.dart';
|
||||
import 'package:weblibre/features/small_web/presentation/controllers/small_web_mode_controller.dart';
|
||||
import 'package:weblibre/features/small_web/presentation/widgets/small_web_browser_overlay.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/data/models/proxy_routing_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
|
||||
import 'package:weblibre/utils/move_to_background.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
@@ -230,6 +235,86 @@ class _TabBar extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProxyConnectionId?> _proxyConnectionIdForLoadError(
|
||||
WidgetRef ref, {
|
||||
required String tabId,
|
||||
required String? contextId,
|
||||
}) async {
|
||||
final routing = ref.read(proxyRoutingSettingsWithDefaultsProvider);
|
||||
final tabState = ref.read(tabStateProvider(tabId));
|
||||
|
||||
if (contextId == 'private' || tabState?.tabMode is PrivateTabMode) {
|
||||
return routing.privateTabsProxyConnectionId;
|
||||
}
|
||||
|
||||
if (contextId != null && contextId.isNotEmpty && contextId != 'general') {
|
||||
final isolatedContextProxyConnectionId =
|
||||
await _isolatedContextProxyConnectionId(ref, contextId);
|
||||
if (isolatedContextProxyConnectionId != null) {
|
||||
return isolatedContextProxyConnectionId;
|
||||
}
|
||||
|
||||
final contextContainer = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerByContextualIdentity(contextId);
|
||||
final contextProxyConnectionId =
|
||||
contextContainer?.metadata.proxyConnectionId;
|
||||
if (contextProxyConnectionId != null) {
|
||||
return contextProxyConnectionId;
|
||||
}
|
||||
}
|
||||
|
||||
final tabContainer = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabContainerData(tabId);
|
||||
final tabProxyConnectionId = tabContainer?.metadata.proxyConnectionId;
|
||||
if (tabProxyConnectionId != null) {
|
||||
return tabProxyConnectionId;
|
||||
}
|
||||
|
||||
if (routing.regularTabsMode == ProxyRegularTabRoutingMode.all) {
|
||||
return routing.regularTabsProxyConnectionId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ProxyConnectionId?> _isolatedContextProxyConnectionId(
|
||||
WidgetRef ref,
|
||||
String contextId,
|
||||
) async {
|
||||
final db = ref.read(tabDatabaseProvider);
|
||||
final pairs = await db.tabDao.isolatedContextContainerPairs().get();
|
||||
final containerIds = pairs
|
||||
.where((pair) => pair.isolationContextId == contextId)
|
||||
.map((pair) => pair.containerId)
|
||||
.nonNulls
|
||||
.toSet();
|
||||
|
||||
if (containerIds.isEmpty) return null;
|
||||
|
||||
final containers = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getAllContainersWithCount();
|
||||
final proxyIds =
|
||||
containers
|
||||
.where((container) => containerIds.contains(container.id))
|
||||
.map((container) => container.metadata.proxyConnectionId?.encode())
|
||||
.nonNulls
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
if (proxyIds.isEmpty) return null;
|
||||
return ProxyConnectionId.decode(proxyIds.first);
|
||||
}
|
||||
|
||||
typedef _PendingProxyLoadError = ({
|
||||
String? contextId,
|
||||
String errorType,
|
||||
String? url,
|
||||
});
|
||||
|
||||
class BrowserScreen extends HookConsumerWidget {
|
||||
const BrowserScreen({super.key});
|
||||
|
||||
@@ -238,52 +323,89 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
final eventService = ref.watch(eventServiceProvider);
|
||||
final viewportService = ref.watch(viewportServiceProvider);
|
||||
final activeProxyPromptKeys = useRef(<String>{});
|
||||
final pendingProxyLoadErrors = useRef(<String, _PendingProxyLoadError>{});
|
||||
final selectedTabIdForProxyPrompt = ref.watch(selectedTabProvider);
|
||||
|
||||
Future<void> handleProxyLoadError({
|
||||
required String tabId,
|
||||
required String? contextId,
|
||||
required String? url,
|
||||
required String errorType,
|
||||
}) async {
|
||||
final promptKey = '$tabId:${url ?? errorType}';
|
||||
if (!activeProxyPromptKeys.value.add(promptKey)) return;
|
||||
|
||||
try {
|
||||
final proxyConnectionId = await _proxyConnectionIdForLoadError(
|
||||
ref,
|
||||
tabId: tabId,
|
||||
contextId: contextId,
|
||||
);
|
||||
|
||||
if (!context.mounted || proxyConnectionId == null) return;
|
||||
|
||||
final isProxyStarted = await ensureProxyStartedForConnection(
|
||||
context,
|
||||
ref,
|
||||
proxyConnectionId,
|
||||
);
|
||||
|
||||
if (isProxyStarted &&
|
||||
context.mounted &&
|
||||
ref.read(selectedTabProvider) == tabId) {
|
||||
await ref.read(tabSessionProvider(tabId: tabId).notifier).reload();
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
logger.e(
|
||||
'Failed to handle proxy load error',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
} finally {
|
||||
activeProxyPromptKeys.value.remove(promptKey);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
final tabId = selectedTabIdForProxyPrompt;
|
||||
if (tabId == null) return null;
|
||||
|
||||
final pending = pendingProxyLoadErrors.value.remove(tabId);
|
||||
if (pending == null) return null;
|
||||
|
||||
unawaited(
|
||||
handleProxyLoadError(
|
||||
tabId: tabId,
|
||||
contextId: pending.contextId,
|
||||
url: pending.url,
|
||||
errorType: pending.errorType,
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}, [selectedTabIdForProxyPrompt]);
|
||||
|
||||
useOnStreamChange(
|
||||
eventService.proxyLoadErrorEvents,
|
||||
onData: (event) async {
|
||||
final selectedTabId = ref.read(selectedTabProvider);
|
||||
final tabId = event.tabId ?? selectedTabId;
|
||||
if (tabId == null || tabId != selectedTabId) return;
|
||||
if (tabId == null) return;
|
||||
|
||||
final promptKey = '$tabId:${event.url ?? event.errorType}';
|
||||
if (!activeProxyPromptKeys.value.add(promptKey)) return;
|
||||
|
||||
try {
|
||||
final contextId = event.contextId;
|
||||
final contextContainer = contextId != null && contextId.isNotEmpty
|
||||
? await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerByContextualIdentity(contextId)
|
||||
: null;
|
||||
final container =
|
||||
contextContainer ??
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabContainerData(tabId);
|
||||
|
||||
if (!context.mounted || container == null) return;
|
||||
|
||||
final isProxyStarted = await ensureProxyStartedForContainer(
|
||||
context,
|
||||
ref,
|
||||
container,
|
||||
if (tabId != selectedTabId) {
|
||||
pendingProxyLoadErrors.value[tabId] = (
|
||||
contextId: event.contextId,
|
||||
errorType: event.errorType,
|
||||
url: event.url,
|
||||
);
|
||||
|
||||
if (isProxyStarted &&
|
||||
context.mounted &&
|
||||
ref.read(selectedTabProvider) == tabId) {
|
||||
await ref.read(tabSessionProvider(tabId: tabId).notifier).reload();
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
logger.e(
|
||||
'Failed to handle proxy load error',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
} finally {
|
||||
activeProxyPromptKeys.value.remove(promptKey);
|
||||
return;
|
||||
}
|
||||
|
||||
await handleProxyLoadError(
|
||||
tabId: tabId,
|
||||
contextId: event.contextId,
|
||||
url: event.url,
|
||||
errorType: event.errorType,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ final class InstallCurrentWebAppProvider
|
||||
}
|
||||
|
||||
String _$installCurrentWebAppHash() =>
|
||||
r'0e0013fac7b70441983f7af1e6db3c6470c23900';
|
||||
r'6eeed094b9ea9ade946dab77c691f271c92f89e1';
|
||||
|
||||
/// Installs the current tab as a PWA, embedding profile and container context
|
||||
/// in the shortcut intent so the PWA reopens with the same isolation.
|
||||
@@ -460,7 +460,7 @@ final class InstallBasicShortcutProvider
|
||||
}
|
||||
|
||||
String _$installBasicShortcutHash() =>
|
||||
r'fcb5bec79f375b32a7a0953ad22859a0db222167';
|
||||
r'7155266e635d0016e4d3ed407bc6d8af61434b0f';
|
||||
|
||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
||||
|
||||
|
||||
@@ -139,23 +139,6 @@ class SingboxProxyRuntimeRepository extends _$SingboxProxyRuntimeRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> ensureProxyConnectionAvailable(
|
||||
SingboxProxyConnectionId connectionId,
|
||||
) async {
|
||||
await _lock.synchronized(() async {
|
||||
final currentState = await _stateSnapshotUnlocked();
|
||||
final isRunning = currentState.endpoints.any(
|
||||
(endpoint) => endpoint.profileId == connectionId.encode(),
|
||||
);
|
||||
if (isRunning) return;
|
||||
|
||||
final activeProfileIds = _activeProfileIds(currentState);
|
||||
await _startProfilesUnlocked(
|
||||
{...activeProfileIds, connectionId.profileId}.toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Set<String> _activeProfileIds(SingboxProxyRuntimeState runtimeState) {
|
||||
return runtimeState.endpoints
|
||||
.map((endpoint) => ProxyConnectionId.decode(endpoint.profileId))
|
||||
|
||||
@@ -87,7 +87,7 @@ final class SingboxProxyRuntimeRepositoryProvider
|
||||
}
|
||||
|
||||
String _$singboxProxyRuntimeRepositoryHash() =>
|
||||
r'665908eaff34569f7a19dc5328a067711d1937f2';
|
||||
r'bb84ab57abd3b7261105da85d3e047cef1b31a44';
|
||||
|
||||
abstract class _$SingboxProxyRuntimeRepository
|
||||
extends $AsyncNotifier<SingboxProxyRuntimeState> {
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ProxyInputConsumerProvider
|
||||
}
|
||||
|
||||
String _$proxyInputConsumerHash() =>
|
||||
r'fed529f253a9bdf72d0b1c23a298765b681d07b8';
|
||||
r'8958cb3ff089779c1991390a7d6062cef74b0588';
|
||||
|
||||
abstract class _$ProxyInputConsumer extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -81,7 +81,7 @@ final class SingboxProxyEndpointSyncProvider
|
||||
}
|
||||
|
||||
String _$singboxProxyEndpointSyncHash() =>
|
||||
r'2d6b0641db33638b0b339b510dd63cdddab7f7cf';
|
||||
r'8aeb4097d02c2f6ea37fdb2de26b399d2e6042c2';
|
||||
|
||||
/// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's
|
||||
/// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider]
|
||||
|
||||
+16
-10
@@ -37,7 +37,18 @@ Future<bool> ensureProxyStartedForContainer(
|
||||
WidgetRef ref,
|
||||
ContainerData container,
|
||||
) async {
|
||||
final proxyConnectionId = container.metadata.proxyConnectionId;
|
||||
return ensureProxyStartedForConnection(
|
||||
context,
|
||||
ref,
|
||||
container.metadata.proxyConnectionId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> ensureProxyStartedForConnection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ProxyConnectionId? proxyConnectionId,
|
||||
) async {
|
||||
if (proxyConnectionId == null) return true;
|
||||
|
||||
if (proxyConnectionId is TorProxyConnectionId) {
|
||||
@@ -45,7 +56,7 @@ Future<bool> ensureProxyStartedForContainer(
|
||||
}
|
||||
|
||||
if (proxyConnectionId is SingboxProxyConnectionId) {
|
||||
return await _maybeStartSingboxProxyForContainer(context, ref, container);
|
||||
return await _maybeStartSingboxProxy(context, ref, proxyConnectionId);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -71,16 +82,11 @@ Future<bool> _ensureTorStarted(BuildContext context, WidgetRef ref) async {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> _maybeStartSingboxProxyForContainer(
|
||||
Future<bool> _maybeStartSingboxProxy(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ContainerData container,
|
||||
SingboxProxyConnectionId proxyConnectionId,
|
||||
) async {
|
||||
final proxyConnectionId = container.metadata.proxyConnectionId;
|
||||
if (proxyConnectionId == null) return true;
|
||||
|
||||
if (proxyConnectionId is! SingboxProxyConnectionId) return true;
|
||||
|
||||
// Await any start/stop in flight so we don't prompt the user a second time
|
||||
// while a start they already triggered is still resolving. If the runtime
|
||||
// provider is already in AsyncError, keep treating that as "not running" so
|
||||
@@ -116,7 +122,7 @@ Future<bool> _maybeStartSingboxProxyForContainer(
|
||||
icon: const Icon(Icons.route_outlined),
|
||||
title: const Text('Start Proxy Connection?'),
|
||||
content: Text(
|
||||
'This container uses $proxyTitle, but that connection is not running. Start it now?',
|
||||
'This tab needs $proxyTitle, but that connection is not running. Start it now?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -54,7 +54,7 @@ final class SearchBackendEndpointsProvider
|
||||
}
|
||||
|
||||
String _$searchBackendEndpointsHash() =>
|
||||
r'f7249075bdf7a83271f7f9030dd12a3a9d2bd483';
|
||||
r'e2f367530cac69a2833b75520d1ac66605822f80';
|
||||
|
||||
@ProviderFor(searchClientLogger)
|
||||
final searchClientLoggerProvider = SearchClientLoggerProvider._();
|
||||
|
||||
@@ -110,7 +110,7 @@ final class SharingIntentStreamProvider
|
||||
}
|
||||
|
||||
String _$sharingIntentStreamHash() =>
|
||||
r'02cef0c00bcb9416f8daf420977f58764c8efaca';
|
||||
r'62e9e54bfa168e7400c4c496d0aca0972ccdf4c1';
|
||||
|
||||
/// Stream of account callback handoff codes extracted from deep link intents.
|
||||
|
||||
|
||||
+2
-2
@@ -374,7 +374,7 @@ final class SandboxCaptureErrorsProvider
|
||||
}
|
||||
|
||||
String _$sandboxCaptureErrorsHash() =>
|
||||
r'd6ef9ebba2515f8177a380b822f344128e4ea8c4';
|
||||
r'da9021ba394d2769140408577983bbcf45b0e0fe';
|
||||
|
||||
/// Orchestrates sandbox capture browsing:
|
||||
///
|
||||
@@ -440,7 +440,7 @@ final class SandboxCaptureControllerProvider
|
||||
}
|
||||
|
||||
String _$sandboxCaptureControllerHash() =>
|
||||
r'fb582cb09504dafd1b9addc1bfc0e6f58fabc9b1';
|
||||
r'6ddcb0df7194f3e35ac0eee3c182204a990f9651';
|
||||
|
||||
/// Orchestrates sandbox capture browsing:
|
||||
///
|
||||
|
||||
@@ -42,7 +42,7 @@ final class MetaSearchControllerProvider
|
||||
}
|
||||
|
||||
String _$metaSearchControllerHash() =>
|
||||
r'afb6600f78717df124a6afdb80710eba74666d23';
|
||||
r'74ea7c616213dae53d3ac18b2677b797f5f87d53';
|
||||
|
||||
abstract class _$MetaSearchController extends $Notifier<MetaSearchState> {
|
||||
MetaSearchState build();
|
||||
@@ -108,7 +108,7 @@ final class WebSearchScrollOffsetProvider
|
||||
}
|
||||
|
||||
String _$webSearchScrollOffsetHash() =>
|
||||
r'8d622d93aba7712b2bf0e6da7cbde6c90ee00c07';
|
||||
r'404573c2e5e52a0f260d182b7947f4878df45820';
|
||||
|
||||
/// Persisted vertical scroll offset of the web-search results list. Kept
|
||||
/// alive (like [MetaSearchController]) so returning to the search screen
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:mockito/src/dummies.dart' as _i5;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:mockito/src/dummies.dart' as _i5;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
+1
-27
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_tor/flutter_tor.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -17,7 +16,6 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
|
||||
import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart';
|
||||
import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart';
|
||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
|
||||
@@ -26,7 +24,7 @@ void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test(
|
||||
'container assignments ensure sing-box profiles are running before syncing',
|
||||
'container assignments sync without starting stopped sing-box profiles',
|
||||
() async {
|
||||
const profileId = SingboxProxyConnectionId('profile-1');
|
||||
final assignedContainer = _container(
|
||||
@@ -44,7 +42,6 @@ void main() {
|
||||
);
|
||||
final containerProxyRepository = _FakeContainerProxyRepository();
|
||||
final containerRepository = _FakeContainerRepository([assignedContainer]);
|
||||
final runtimeRepository = _FakeSingboxProxyRuntimeRepository();
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
tabDatabaseProvider.overrideWith((ref) => db),
|
||||
@@ -52,9 +49,6 @@ void main() {
|
||||
() => containerProxyRepository,
|
||||
),
|
||||
containerRepositoryProvider.overrideWith(() => containerRepository),
|
||||
singboxProxyRuntimeRepositoryProvider.overrideWith(
|
||||
() => runtimeRepository,
|
||||
),
|
||||
torProxyServiceProvider.overrideWith(_FakeTorProxyService.new),
|
||||
proxyRoutingSettingsWithDefaultsProvider.overrideWith(
|
||||
(ref) => ProxyRoutingSettings.withDefaults(),
|
||||
@@ -83,7 +77,6 @@ void main() {
|
||||
addTearDown(subscription.close);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(runtimeRepository.ensuredProxyConnectionIds, [profileId]);
|
||||
expect(containerProxyRepository.setContainerProxyCalls, [
|
||||
('context-a', profileId.encode()),
|
||||
]);
|
||||
@@ -144,25 +137,6 @@ class _FakeContainerRepository extends ContainerRepository {
|
||||
void build() {}
|
||||
}
|
||||
|
||||
class _FakeSingboxProxyRuntimeRepository extends SingboxProxyRuntimeRepository {
|
||||
final ensuredProxyConnectionIds = <SingboxProxyConnectionId>[];
|
||||
|
||||
@override
|
||||
Future<void> ensureProxyConnectionAvailable(
|
||||
SingboxProxyConnectionId connectionId,
|
||||
) async {
|
||||
ensuredProxyConnectionIds.add(connectionId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SingboxProxyRuntimeState> build() async {
|
||||
return SingboxProxyRuntimeState(
|
||||
status: SingboxProxyRuntimeStatus.stopped,
|
||||
endpoints: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeTorProxyService extends TorProxyService {
|
||||
@override
|
||||
Stream<TorStatus> build() async* {
|
||||
|
||||
@@ -22,37 +22,6 @@ import 'package:weblibre/features/user/domain/repositories/proxy_routing_setting
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test(
|
||||
'ensureProxyConnectionAvailable starts an assigned stopped profile',
|
||||
() async {
|
||||
final profile = _profile(id: 'profile-1', name: 'First');
|
||||
final client = _FakeSingboxProxyClient(_state(const []));
|
||||
final container = _container(
|
||||
client: client,
|
||||
profilesRepository: _FakeProfilesRepository([profile]),
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(singboxProxyRuntimeRepositoryProvider.future);
|
||||
final repository = container.read(
|
||||
singboxProxyRuntimeRepositoryProvider.notifier,
|
||||
);
|
||||
|
||||
await repository.ensureProxyConnectionAvailable(
|
||||
SingboxProxyConnectionId(profile.id),
|
||||
);
|
||||
await repository.ensureProxyConnectionAvailable(
|
||||
SingboxProxyConnectionId(profile.id),
|
||||
);
|
||||
|
||||
expect(client.startCalls, hasLength(1));
|
||||
expect(
|
||||
client.startCalls.single.map((runtimeProfile) => runtimeProfile.id),
|
||||
[profile.proxyConnectionId],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('startProfile preserves already-running profiles', () async {
|
||||
final profile1 = _profile(id: 'profile-1', name: 'First');
|
||||
final profile2 = _profile(id: 'profile-2', name: 'Second');
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ void main() {
|
||||
|
||||
expect(find.text('Start Proxy Connection?'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('This container uses Mullvad'),
|
||||
find.textContaining('This tab needs Mullvad'),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
|
||||
+44
-1
@@ -17,8 +17,10 @@
|
||||
* 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:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/settings/domain/providers/pending_settings_highlight.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart';
|
||||
|
||||
void main() {
|
||||
@@ -78,4 +80,45 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('clears a pending highlight after the target entry handles it', (
|
||||
tester,
|
||||
) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
container.read(pendingSettingsHighlightProvider.notifier).set('Theme');
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: MaterialApp(
|
||||
home: SettingsDetailScaffold(
|
||||
title: 'Appearance',
|
||||
subtitle: 'Configure app appearance',
|
||||
icon: Icons.palette,
|
||||
sections: const [
|
||||
SettingsSectionDefinition(
|
||||
title: 'Display',
|
||||
entries: [
|
||||
SettingsEntryDefinition(
|
||||
title: 'Theme',
|
||||
child: SizedBox(height: 48, child: Text('Theme')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(container.read(pendingSettingsHighlightProvider), isNull);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pump();
|
||||
|
||||
expect(container.read(pendingSettingsHighlightProvider), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user