fix proxy auto start issue
This commit is contained in:
@@ -130,7 +130,7 @@ final class GenericWebsiteServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$genericWebsiteServiceHash() =>
|
String _$genericWebsiteServiceHash() =>
|
||||||
r'83c62a15492606a7ffe4a7990fc1f9bdbfee87cc';
|
r'189cbd17a06be382859c697221355cd623608123';
|
||||||
|
|
||||||
abstract class _$GenericWebsiteService extends $Notifier<void> {
|
abstract class _$GenericWebsiteService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -1186,7 +1186,7 @@ final class GroupedTabListItemsProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$groupedTabListItemsHash() =>
|
String _$groupedTabListItemsHash() =>
|
||||||
r'15106bfa42146270245c7178928a834237042c1c';
|
r'dbb510f22c00858fcbadfb94547d46841221fca4';
|
||||||
|
|
||||||
/// Grouped flat-list rendering for the list and grid views.
|
/// 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/data/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/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/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/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/tor/domain/services/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
|
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/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();
|
final _recomputeLock = Lock();
|
||||||
var _recomputeDirty = false;
|
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 {
|
Future<void> _queueIsolatedProxyAliasesRecompute() async {
|
||||||
_recomputeDirty = true;
|
_recomputeDirty = true;
|
||||||
if (_recomputeLock.inLock) return;
|
if (_recomputeLock.inLock) return;
|
||||||
@@ -187,7 +166,6 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
|||||||
await containerProxy.clearContainerProxy('private');
|
await containerProxy.clearContainerProxy('private');
|
||||||
} else {
|
} else {
|
||||||
await containerProxy.setContainerProxy('private', next.encode());
|
await containerProxy.setContainerProxy('private', next.encode());
|
||||||
await _ensureProxyConnectionAvailable(next);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error, stackTrace) {
|
onError: (error, stackTrace) {
|
||||||
@@ -220,7 +198,6 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
|||||||
'general',
|
'general',
|
||||||
proxyConnectionId.encode(),
|
proxyConnectionId.encode(),
|
||||||
);
|
);
|
||||||
await _ensureProxyConnectionAvailable(proxyConnectionId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -315,10 +292,6 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
final proxyConnectionId = ProxyConnectionId.decode(value);
|
|
||||||
if (proxyConnectionId != null) {
|
|
||||||
await _ensureProxyConnectionAvailable(proxyConnectionId);
|
|
||||||
}
|
|
||||||
await repo.setContainerProxy(key, value);
|
await repo.setContainerProxy(key, value);
|
||||||
} else {
|
} else {
|
||||||
await repo.clearContainerProxy(key);
|
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/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/readerview/presentation/controllers/readerable.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_autofocus.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/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.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/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/controllers/small_web_mode_controller.dart';
|
||||||
import 'package:weblibre/features/small_web/presentation/widgets/small_web_browser_overlay.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/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.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/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/move_to_background.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
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 {
|
class BrowserScreen extends HookConsumerWidget {
|
||||||
const BrowserScreen({super.key});
|
const BrowserScreen({super.key});
|
||||||
|
|
||||||
@@ -238,52 +323,89 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
final eventService = ref.watch(eventServiceProvider);
|
final eventService = ref.watch(eventServiceProvider);
|
||||||
final viewportService = ref.watch(viewportServiceProvider);
|
final viewportService = ref.watch(viewportServiceProvider);
|
||||||
final activeProxyPromptKeys = useRef(<String>{});
|
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(
|
useOnStreamChange(
|
||||||
eventService.proxyLoadErrorEvents,
|
eventService.proxyLoadErrorEvents,
|
||||||
onData: (event) async {
|
onData: (event) async {
|
||||||
final selectedTabId = ref.read(selectedTabProvider);
|
final selectedTabId = ref.read(selectedTabProvider);
|
||||||
final tabId = event.tabId ?? selectedTabId;
|
final tabId = event.tabId ?? selectedTabId;
|
||||||
if (tabId == null || tabId != selectedTabId) return;
|
if (tabId == null) return;
|
||||||
|
|
||||||
final promptKey = '$tabId:${event.url ?? event.errorType}';
|
if (tabId != selectedTabId) {
|
||||||
if (!activeProxyPromptKeys.value.add(promptKey)) return;
|
pendingProxyLoadErrors.value[tabId] = (
|
||||||
|
contextId: event.contextId,
|
||||||
try {
|
errorType: event.errorType,
|
||||||
final contextId = event.contextId;
|
url: event.url,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await handleProxyLoadError(
|
||||||
|
tabId: tabId,
|
||||||
|
contextId: event.contextId,
|
||||||
|
url: event.url,
|
||||||
|
errorType: event.errorType,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ final class InstallCurrentWebAppProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$installCurrentWebAppHash() =>
|
String _$installCurrentWebAppHash() =>
|
||||||
r'0e0013fac7b70441983f7af1e6db3c6470c23900';
|
r'6eeed094b9ea9ade946dab77c691f271c92f89e1';
|
||||||
|
|
||||||
/// Installs the current tab as a PWA, embedding profile and container context
|
/// Installs the current tab as a PWA, embedding profile and container context
|
||||||
/// in the shortcut intent so the PWA reopens with the same isolation.
|
/// in the shortcut intent so the PWA reopens with the same isolation.
|
||||||
@@ -460,7 +460,7 @@ final class InstallBasicShortcutProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$installBasicShortcutHash() =>
|
String _$installBasicShortcutHash() =>
|
||||||
r'fcb5bec79f375b32a7a0953ad22859a0db222167';
|
r'7155266e635d0016e4d3ed407bc6d8af61434b0f';
|
||||||
|
|
||||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
/// 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) {
|
Set<String> _activeProfileIds(SingboxProxyRuntimeState runtimeState) {
|
||||||
return runtimeState.endpoints
|
return runtimeState.endpoints
|
||||||
.map((endpoint) => ProxyConnectionId.decode(endpoint.profileId))
|
.map((endpoint) => ProxyConnectionId.decode(endpoint.profileId))
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ final class SingboxProxyRuntimeRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$singboxProxyRuntimeRepositoryHash() =>
|
String _$singboxProxyRuntimeRepositoryHash() =>
|
||||||
r'665908eaff34569f7a19dc5328a067711d1937f2';
|
r'bb84ab57abd3b7261105da85d3e047cef1b31a44';
|
||||||
|
|
||||||
abstract class _$SingboxProxyRuntimeRepository
|
abstract class _$SingboxProxyRuntimeRepository
|
||||||
extends $AsyncNotifier<SingboxProxyRuntimeState> {
|
extends $AsyncNotifier<SingboxProxyRuntimeState> {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class ProxyInputConsumerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$proxyInputConsumerHash() =>
|
String _$proxyInputConsumerHash() =>
|
||||||
r'fed529f253a9bdf72d0b1c23a298765b681d07b8';
|
r'8958cb3ff089779c1991390a7d6062cef74b0588';
|
||||||
|
|
||||||
abstract class _$ProxyInputConsumer extends $Notifier<void> {
|
abstract class _$ProxyInputConsumer extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ final class SingboxProxyEndpointSyncProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$singboxProxyEndpointSyncHash() =>
|
String _$singboxProxyEndpointSyncHash() =>
|
||||||
r'2d6b0641db33638b0b339b510dd63cdddab7f7cf';
|
r'8aeb4097d02c2f6ea37fdb2de26b399d2e6042c2';
|
||||||
|
|
||||||
/// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's
|
/// Mirrors the sing-box runtime's active SOCKS endpoints into Gecko's
|
||||||
/// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider]
|
/// container-proxy registry. Listens to [singboxProxyRuntimeRepositoryProvider]
|
||||||
|
|||||||
+16
-10
@@ -37,7 +37,18 @@ Future<bool> ensureProxyStartedForContainer(
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
ContainerData container,
|
ContainerData container,
|
||||||
) async {
|
) 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 == null) return true;
|
||||||
|
|
||||||
if (proxyConnectionId is TorProxyConnectionId) {
|
if (proxyConnectionId is TorProxyConnectionId) {
|
||||||
@@ -45,7 +56,7 @@ Future<bool> ensureProxyStartedForContainer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (proxyConnectionId is SingboxProxyConnectionId) {
|
if (proxyConnectionId is SingboxProxyConnectionId) {
|
||||||
return await _maybeStartSingboxProxyForContainer(context, ref, container);
|
return await _maybeStartSingboxProxy(context, ref, proxyConnectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -71,16 +82,11 @@ Future<bool> _ensureTorStarted(BuildContext context, WidgetRef ref) async {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _maybeStartSingboxProxyForContainer(
|
Future<bool> _maybeStartSingboxProxy(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
ContainerData container,
|
SingboxProxyConnectionId proxyConnectionId,
|
||||||
) async {
|
) 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
|
// 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
|
// while a start they already triggered is still resolving. If the runtime
|
||||||
// provider is already in AsyncError, keep treating that as "not running" so
|
// 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),
|
icon: const Icon(Icons.route_outlined),
|
||||||
title: const Text('Start Proxy Connection?'),
|
title: const Text('Start Proxy Connection?'),
|
||||||
content: Text(
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ final class SearchBackendEndpointsProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$searchBackendEndpointsHash() =>
|
String _$searchBackendEndpointsHash() =>
|
||||||
r'f7249075bdf7a83271f7f9030dd12a3a9d2bd483';
|
r'e2f367530cac69a2833b75520d1ac66605822f80';
|
||||||
|
|
||||||
@ProviderFor(searchClientLogger)
|
@ProviderFor(searchClientLogger)
|
||||||
final searchClientLoggerProvider = SearchClientLoggerProvider._();
|
final searchClientLoggerProvider = SearchClientLoggerProvider._();
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ final class SharingIntentStreamProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$sharingIntentStreamHash() =>
|
String _$sharingIntentStreamHash() =>
|
||||||
r'02cef0c00bcb9416f8daf420977f58764c8efaca';
|
r'62e9e54bfa168e7400c4c496d0aca0972ccdf4c1';
|
||||||
|
|
||||||
/// Stream of account callback handoff codes extracted from deep link intents.
|
/// Stream of account callback handoff codes extracted from deep link intents.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -374,7 +374,7 @@ final class SandboxCaptureErrorsProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$sandboxCaptureErrorsHash() =>
|
String _$sandboxCaptureErrorsHash() =>
|
||||||
r'd6ef9ebba2515f8177a380b822f344128e4ea8c4';
|
r'da9021ba394d2769140408577983bbcf45b0e0fe';
|
||||||
|
|
||||||
/// Orchestrates sandbox capture browsing:
|
/// Orchestrates sandbox capture browsing:
|
||||||
///
|
///
|
||||||
@@ -440,7 +440,7 @@ final class SandboxCaptureControllerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$sandboxCaptureControllerHash() =>
|
String _$sandboxCaptureControllerHash() =>
|
||||||
r'fb582cb09504dafd1b9addc1bfc0e6f58fabc9b1';
|
r'6ddcb0df7194f3e35ac0eee3c182204a990f9651';
|
||||||
|
|
||||||
/// Orchestrates sandbox capture browsing:
|
/// Orchestrates sandbox capture browsing:
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class MetaSearchControllerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$metaSearchControllerHash() =>
|
String _$metaSearchControllerHash() =>
|
||||||
r'afb6600f78717df124a6afdb80710eba74666d23';
|
r'74ea7c616213dae53d3ac18b2677b797f5f87d53';
|
||||||
|
|
||||||
abstract class _$MetaSearchController extends $Notifier<MetaSearchState> {
|
abstract class _$MetaSearchController extends $Notifier<MetaSearchState> {
|
||||||
MetaSearchState build();
|
MetaSearchState build();
|
||||||
@@ -108,7 +108,7 @@ final class WebSearchScrollOffsetProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$webSearchScrollOffsetHash() =>
|
String _$webSearchScrollOffsetHash() =>
|
||||||
r'8d622d93aba7712b2bf0e6da7cbde6c90ee00c07';
|
r'404573c2e5e52a0f260d182b7947f4878df45820';
|
||||||
|
|
||||||
/// Persisted vertical scroll offset of the web-search results list. Kept
|
/// Persisted vertical scroll offset of the web-search results list. Kept
|
||||||
/// alive (like [MetaSearchController]) so returning to the search screen
|
/// 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;
|
import 'package:mockito/src/dummies.dart' as _i5;
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: avoid_redundant_argument_values
|
||||||
// ignore_for_file: avoid_setters_without_getters
|
// ignore_for_file: avoid_setters_without_getters
|
||||||
// ignore_for_file: comment_references
|
// ignore_for_file: comment_references
|
||||||
// ignore_for_file: deprecated_member_use
|
// 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;
|
import 'package:mockito/src/dummies.dart' as _i5;
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: avoid_redundant_argument_values
|
||||||
// ignore_for_file: avoid_setters_without_getters
|
// ignore_for_file: avoid_setters_without_getters
|
||||||
// ignore_for_file: comment_references
|
// ignore_for_file: comment_references
|
||||||
// ignore_for_file: deprecated_member_use
|
// ignore_for_file: deprecated_member_use
|
||||||
|
|||||||
+1
-27
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter_tor/flutter_tor.dart';
|
import 'package:flutter_tor/flutter_tor.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/proxy/data/proxy_connection.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/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/tor/domain/services/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
|
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
|
||||||
@@ -26,7 +24,7 @@ void main() {
|
|||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'container assignments ensure sing-box profiles are running before syncing',
|
'container assignments sync without starting stopped sing-box profiles',
|
||||||
() async {
|
() async {
|
||||||
const profileId = SingboxProxyConnectionId('profile-1');
|
const profileId = SingboxProxyConnectionId('profile-1');
|
||||||
final assignedContainer = _container(
|
final assignedContainer = _container(
|
||||||
@@ -44,7 +42,6 @@ void main() {
|
|||||||
);
|
);
|
||||||
final containerProxyRepository = _FakeContainerProxyRepository();
|
final containerProxyRepository = _FakeContainerProxyRepository();
|
||||||
final containerRepository = _FakeContainerRepository([assignedContainer]);
|
final containerRepository = _FakeContainerRepository([assignedContainer]);
|
||||||
final runtimeRepository = _FakeSingboxProxyRuntimeRepository();
|
|
||||||
final container = ProviderContainer(
|
final container = ProviderContainer(
|
||||||
overrides: [
|
overrides: [
|
||||||
tabDatabaseProvider.overrideWith((ref) => db),
|
tabDatabaseProvider.overrideWith((ref) => db),
|
||||||
@@ -52,9 +49,6 @@ void main() {
|
|||||||
() => containerProxyRepository,
|
() => containerProxyRepository,
|
||||||
),
|
),
|
||||||
containerRepositoryProvider.overrideWith(() => containerRepository),
|
containerRepositoryProvider.overrideWith(() => containerRepository),
|
||||||
singboxProxyRuntimeRepositoryProvider.overrideWith(
|
|
||||||
() => runtimeRepository,
|
|
||||||
),
|
|
||||||
torProxyServiceProvider.overrideWith(_FakeTorProxyService.new),
|
torProxyServiceProvider.overrideWith(_FakeTorProxyService.new),
|
||||||
proxyRoutingSettingsWithDefaultsProvider.overrideWith(
|
proxyRoutingSettingsWithDefaultsProvider.overrideWith(
|
||||||
(ref) => ProxyRoutingSettings.withDefaults(),
|
(ref) => ProxyRoutingSettings.withDefaults(),
|
||||||
@@ -83,7 +77,6 @@ void main() {
|
|||||||
addTearDown(subscription.close);
|
addTearDown(subscription.close);
|
||||||
await pumpEventQueue();
|
await pumpEventQueue();
|
||||||
|
|
||||||
expect(runtimeRepository.ensuredProxyConnectionIds, [profileId]);
|
|
||||||
expect(containerProxyRepository.setContainerProxyCalls, [
|
expect(containerProxyRepository.setContainerProxyCalls, [
|
||||||
('context-a', profileId.encode()),
|
('context-a', profileId.encode()),
|
||||||
]);
|
]);
|
||||||
@@ -144,25 +137,6 @@ class _FakeContainerRepository extends ContainerRepository {
|
|||||||
void build() {}
|
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 {
|
class _FakeTorProxyService extends TorProxyService {
|
||||||
@override
|
@override
|
||||||
Stream<TorStatus> build() async* {
|
Stream<TorStatus> build() async* {
|
||||||
|
|||||||
@@ -22,37 +22,6 @@ import 'package:weblibre/features/user/domain/repositories/proxy_routing_setting
|
|||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
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 {
|
test('startProfile preserves already-running profiles', () async {
|
||||||
final profile1 = _profile(id: 'profile-1', name: 'First');
|
final profile1 = _profile(id: 'profile-1', name: 'First');
|
||||||
final profile2 = _profile(id: 'profile-2', name: 'Second');
|
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.text('Start Proxy Connection?'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
find.textContaining('This container uses Mullvad'),
|
find.textContaining('This tab needs Mullvad'),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+44
-1
@@ -17,8 +17,10 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* 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: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';
|
import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart';
|
||||||
|
|
||||||
void main() {
|
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