implemented container site assignements & improved routing
This commit is contained in:
@@ -49,6 +49,6 @@ Future<GoRouter> router(Ref ref) async {
|
||||
return GoRouter(
|
||||
debugLogDiagnostics: true,
|
||||
routes: $appRoutes,
|
||||
initialLocation: initialLocation,
|
||||
initialLocation: initialLocation ?? BrowserRoute().location,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,4 +41,4 @@ final class RouterProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$routerHash() => r'87b6e6c1097a0677f3d25570341b343b4392aa59';
|
||||
String _$routerHash() => r'ab1d1e2ea27dd41fe78d7430bfeba87051d88d36';
|
||||
|
||||
@@ -21,7 +21,7 @@ part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<BrowserRoute>(
|
||||
name: BrowserRoute.name,
|
||||
path: '/',
|
||||
path: '/browser',
|
||||
routes: [
|
||||
TypedGoRoute<SearchRoute>(
|
||||
name: 'SearchRoute',
|
||||
@@ -44,11 +44,11 @@ part of 'routes.dart';
|
||||
routes: [
|
||||
TypedGoRoute<ContainerCreateRoute>(
|
||||
name: 'ContainerCreateRoute',
|
||||
path: 'create',
|
||||
path: 'create/:containerData',
|
||||
),
|
||||
TypedGoRoute<ContainerEditRoute>(
|
||||
name: 'ContainerEditRoute',
|
||||
path: 'edit',
|
||||
path: 'edit/:containerData',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -135,37 +135,45 @@ class ContainerSelectionRoute extends GoRouteData
|
||||
}
|
||||
|
||||
class ContainerEditRoute extends GoRouteData with $ContainerEditRoute {
|
||||
final ContainerDataWithCount $extra;
|
||||
final String containerData;
|
||||
|
||||
ContainerEditRoute(this.$extra);
|
||||
ContainerEditRoute({required this.containerData});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ContainerEditScreen.edit(initialContainer: $extra);
|
||||
return ContainerEditScreen.edit(
|
||||
initialContainer: ContainerData.fromJson(
|
||||
jsonDecode(containerData) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerCreateRoute extends GoRouteData with $ContainerCreateRoute {
|
||||
final ContainerData $extra;
|
||||
final String containerData;
|
||||
|
||||
ContainerCreateRoute(this.$extra);
|
||||
ContainerCreateRoute({required this.containerData});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ContainerEditScreen.create(initialContainer: $extra);
|
||||
return ContainerEditScreen.create(
|
||||
initialContainer: ContainerData.fromJson(
|
||||
jsonDecode(containerData) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuRoute extends GoRouteData with $ContextMenuRoute {
|
||||
final String $extra;
|
||||
final String hitResult;
|
||||
|
||||
const ContextMenuRoute(this.$extra);
|
||||
const ContextMenuRoute({required this.hitResult});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder: (_) =>
|
||||
ContextMenuDialog(hitResult: HitResultJson.fromJson($extra)),
|
||||
ContextMenuDialog(hitResult: HitResultJson.fromJson(hitResult)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/routing/widgets/dialog_page.dart';
|
||||
import 'package:weblibre/features/about/presentation/screens/about.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/categories.dart';
|
||||
@@ -39,6 +40,7 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/c
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_selection.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/onboarding.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/addon_collection.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart';
|
||||
|
||||
@@ -90,15 +90,18 @@ class FeedEditRoute extends GoRouteData with $FeedEditRoute {
|
||||
}
|
||||
|
||||
class FeedAddRoute extends GoRouteData with $FeedAddRoute {
|
||||
final Uri? $extra;
|
||||
final String? uri;
|
||||
|
||||
static const name = 'FeedAddRoute';
|
||||
|
||||
const FeedAddRoute({this.$extra});
|
||||
const FeedAddRoute({required this.uri});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => AddFeedDialog(initialUri: $extra));
|
||||
return DialogPage(
|
||||
builder: (_) =>
|
||||
AddFeedDialog(initialUri: uri.mapNotNull((uri) => Uri.tryParse(uri))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ mixin $AddonCollectionRoute on GoRouteData {
|
||||
}
|
||||
|
||||
RouteBase get $browserRoute => GoRouteData.$route(
|
||||
path: '/',
|
||||
path: '/browser',
|
||||
name: 'BrowserRoute',
|
||||
factory: $BrowserRoute._fromState,
|
||||
routes: [
|
||||
@@ -416,12 +416,12 @@ RouteBase get $browserRoute => GoRouteData.$route(
|
||||
factory: $ContainerListRoute._fromState,
|
||||
routes: [
|
||||
GoRouteData.$route(
|
||||
path: 'create',
|
||||
path: 'create/:containerData',
|
||||
name: 'ContainerCreateRoute',
|
||||
factory: $ContainerCreateRoute._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'edit',
|
||||
path: 'edit/:containerData',
|
||||
name: 'ContainerEditRoute',
|
||||
factory: $ContainerEditRoute._fromState,
|
||||
),
|
||||
@@ -449,7 +449,7 @@ mixin $BrowserRoute on GoRouteData {
|
||||
static BrowserRoute _fromState(GoRouterState state) => BrowserRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/');
|
||||
String get location => GoRouteData.$location('/browser');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
@@ -483,7 +483,7 @@ mixin $SearchRoute on GoRouteData {
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location(
|
||||
'/search/${Uri.encodeComponent(_$TabTypeEnumMap[_self.tabType]!)}/${Uri.encodeComponent(_self.searchText)}',
|
||||
'/browser/search/${Uri.encodeComponent(_$TabTypeEnumMap[_self.tabType]!)}/${Uri.encodeComponent(_self.searchText)}',
|
||||
queryParams: {
|
||||
if (_self.launchedFromIntent != false)
|
||||
'launched-from-intent': _self.launchedFromIntent.toString(),
|
||||
@@ -514,7 +514,7 @@ mixin $TorProxyRoute on GoRouteData {
|
||||
static TorProxyRoute _fromState(GoRouterState state) => TorProxyRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/tor_proxy');
|
||||
String get location => GoRouteData.$location('/browser/tor_proxy');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
@@ -534,7 +534,7 @@ mixin $HistoryRoute on GoRouteData {
|
||||
static HistoryRoute _fromState(GoRouterState state) => HistoryRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/history');
|
||||
String get location => GoRouteData.$location('/browser/history');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
@@ -554,7 +554,7 @@ mixin $TabViewRoute on GoRouteData {
|
||||
static TabViewRoute _fromState(GoRouterState state) => TabViewRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/tab_view');
|
||||
String get location => GoRouteData.$location('/browser/tab_view');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
@@ -572,27 +572,28 @@ mixin $TabViewRoute on GoRouteData {
|
||||
|
||||
mixin $ContextMenuRoute on GoRouteData {
|
||||
static ContextMenuRoute _fromState(GoRouterState state) =>
|
||||
ContextMenuRoute(state.extra as String);
|
||||
ContextMenuRoute(hitResult: state.uri.queryParameters['hit-result']!);
|
||||
|
||||
ContextMenuRoute get _self => this as ContextMenuRoute;
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/context_menu');
|
||||
String get location => GoRouteData.$location(
|
||||
'/browser/context_menu',
|
||||
queryParams: {'hit-result': _self.hitResult},
|
||||
);
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location, extra: _self.$extra);
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
@override
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: _self.$extra);
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
@override
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location, extra: _self.$extra);
|
||||
context.pushReplacement(location);
|
||||
|
||||
@override
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: _self.$extra);
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
mixin $ContainerDraftRoute on GoRouteData {
|
||||
@@ -600,7 +601,7 @@ mixin $ContainerDraftRoute on GoRouteData {
|
||||
ContainerDraftRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/container_draft');
|
||||
String get location => GoRouteData.$location('/browser/container_draft');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
@@ -621,7 +622,7 @@ mixin $ContainerListRoute on GoRouteData {
|
||||
ContainerListRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/containers');
|
||||
String get location => GoRouteData.$location('/browser/containers');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
@@ -639,52 +640,54 @@ mixin $ContainerListRoute on GoRouteData {
|
||||
|
||||
mixin $ContainerCreateRoute on GoRouteData {
|
||||
static ContainerCreateRoute _fromState(GoRouterState state) =>
|
||||
ContainerCreateRoute(state.extra as ContainerData);
|
||||
ContainerCreateRoute(
|
||||
containerData: state.pathParameters['containerData']!,
|
||||
);
|
||||
|
||||
ContainerCreateRoute get _self => this as ContainerCreateRoute;
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/containers/create');
|
||||
String get location => GoRouteData.$location(
|
||||
'/browser/containers/create/${Uri.encodeComponent(_self.containerData)}',
|
||||
);
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location, extra: _self.$extra);
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
@override
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: _self.$extra);
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
@override
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location, extra: _self.$extra);
|
||||
context.pushReplacement(location);
|
||||
|
||||
@override
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: _self.$extra);
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
mixin $ContainerEditRoute on GoRouteData {
|
||||
static ContainerEditRoute _fromState(GoRouterState state) =>
|
||||
ContainerEditRoute(state.extra as ContainerDataWithCount);
|
||||
ContainerEditRoute(containerData: state.pathParameters['containerData']!);
|
||||
|
||||
ContainerEditRoute get _self => this as ContainerEditRoute;
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/containers/edit');
|
||||
String get location => GoRouteData.$location(
|
||||
'/browser/containers/edit/${Uri.encodeComponent(_self.containerData)}',
|
||||
);
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location, extra: _self.$extra);
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
@override
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: _self.$extra);
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
@override
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location, extra: _self.$extra);
|
||||
context.pushReplacement(location);
|
||||
|
||||
@override
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: _self.$extra);
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
mixin $ContainerSelectionRoute on GoRouteData {
|
||||
@@ -692,7 +695,7 @@ mixin $ContainerSelectionRoute on GoRouteData {
|
||||
ContainerSelectionRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/select_container');
|
||||
String get location => GoRouteData.$location('/browser/select_container');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
@@ -716,7 +719,7 @@ mixin $TabTreeRoute on GoRouteData {
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location(
|
||||
'/tab_tree/${Uri.encodeComponent(_self.rootTabId)}',
|
||||
'/browser/tab_tree/${Uri.encodeComponent(_self.rootTabId)}',
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -743,7 +746,7 @@ mixin $OpenSharedContentRoute on GoRouteData {
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location(
|
||||
'/open_content',
|
||||
'/browser/open_content',
|
||||
queryParams: {
|
||||
if (_self.sharedUrl != 'about:blank') 'shared-url': _self.sharedUrl,
|
||||
},
|
||||
@@ -974,27 +977,28 @@ mixin $FeedListRoute on GoRouteData {
|
||||
|
||||
mixin $FeedAddRoute on GoRouteData {
|
||||
static FeedAddRoute _fromState(GoRouterState state) =>
|
||||
FeedAddRoute($extra: state.extra as Uri?);
|
||||
FeedAddRoute(uri: state.uri.queryParameters['uri']);
|
||||
|
||||
FeedAddRoute get _self => this as FeedAddRoute;
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/feeds/add');
|
||||
String get location => GoRouteData.$location(
|
||||
'/feeds/add',
|
||||
queryParams: {if (_self.uri != null) 'uri': _self.uri},
|
||||
);
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location, extra: _self.$extra);
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
@override
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: _self.$extra);
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
@override
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location, extra: _self.$extra);
|
||||
context.pushReplacement(location);
|
||||
|
||||
@override
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: _self.$extra);
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
mixin $FeedArticleListRoute on GoRouteData {
|
||||
|
||||
@@ -388,6 +388,73 @@ class TabRepository extends _$TabRepository {
|
||||
);
|
||||
});
|
||||
|
||||
final containerSiteAssignementSub = eventSerivce.siteAssignementEvent.listen((
|
||||
event,
|
||||
) async {
|
||||
if (event.tabId != null) {
|
||||
// ignore: only_use_keep_alive_inside_keep_alive
|
||||
final tabState = ref.read(tabStateProvider(event.tabId));
|
||||
if (tabState != null) {
|
||||
final uri = Uri.parse(event.url);
|
||||
final originUri = event.originUrl.mapNotNull(Uri.parse);
|
||||
|
||||
final targetContainerId = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.siteAssignedContainerId(Uri.parse(uri.origin));
|
||||
final containerData = await targetContainerId.mapNotNull(
|
||||
(id) => ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(id),
|
||||
);
|
||||
|
||||
if (containerData != null) {
|
||||
final tabIsEmpty =
|
||||
tabState.url == TabState.$default(tabState.id).url &&
|
||||
tabState.historyState.items.isEmpty;
|
||||
|
||||
if (event.blocked || tabIsEmpty) {
|
||||
await addTab(
|
||||
url: uri,
|
||||
private: tabState.isPrivate,
|
||||
container: Value(containerData),
|
||||
parentId: tabState.id,
|
||||
);
|
||||
|
||||
if (tabIsEmpty) {
|
||||
await closeTab(tabState.id);
|
||||
}
|
||||
} else {
|
||||
final tabContainerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabState.id);
|
||||
|
||||
if (targetContainerId != tabContainerId) {
|
||||
if (originUri == null) {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignContainer(tabState.id, containerData);
|
||||
} else if (tabState.url == originUri) {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignContainer(
|
||||
tabState.id,
|
||||
containerData,
|
||||
closeOldTab: false,
|
||||
);
|
||||
} else {
|
||||
logger.w(
|
||||
'Could not match origin url for assignment ${tabState.url} to request ${event.originUrl}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.w('Could not get tab for assignement ${tabState?.url}');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final tabContentSub = tabContentService.tabContentStream.listen((
|
||||
content,
|
||||
) async {
|
||||
@@ -468,6 +535,7 @@ class TabRepository extends _$TabRepository {
|
||||
tabStateDebouncer.dispose();
|
||||
await tabAddedSub.cancel();
|
||||
await tabContentSub.cancel();
|
||||
await containerSiteAssignementSub.cancel();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'c3dd50e6263df20ce803e979151997e0b10e0c01';
|
||||
String _$tabRepositoryHash() => r'84c3abff33a3b2ad6af91b97d6d3c4e9efea4ac2';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+10
-1
@@ -53,7 +53,7 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
|
||||
ref.listen(
|
||||
fireImmediately: true,
|
||||
containersWithCountProvider.select(
|
||||
watchContainersWithCountProvider.select(
|
||||
(value) => EquatableValue(value.value),
|
||||
),
|
||||
(previous, next) async {
|
||||
@@ -134,5 +134,14 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// ignore: only_use_keep_alive_inside_keep_alive
|
||||
ref.listen(watchAllAssignedSitesProvider, (previous, next) async {
|
||||
if (next.hasValue) {
|
||||
await ref
|
||||
.read(torProxyRepositoryProvider.notifier)
|
||||
.setSiteAssignments(next.requireValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ final class ProxySettingsReplicationProvider
|
||||
}
|
||||
|
||||
String _$proxySettingsReplicationHash() =>
|
||||
r'f58752de40db3b59553f86b56ab6c4558dda988d';
|
||||
r'9ded7af1d745e25c59e22edecea582e25230e5e7';
|
||||
|
||||
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -83,7 +83,9 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
useOnStreamChange(
|
||||
eventService.longPressEvent,
|
||||
onData: (event) async {
|
||||
await ContextMenuRoute(event.hitResult.toJson()).push(context);
|
||||
await ContextMenuRoute(
|
||||
hitResult: event.hitResult.toJson(),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
if (next.value.mapNotNull(Uri.tryParse) case final Uri url) {
|
||||
if (GoRouterState.of(context).topRoute?.name != FeedAddRoute.name) {
|
||||
if (ref.read(addFeedDialogBlockingProvider.notifier).canPush(url)) {
|
||||
await FeedAddRoute($extra: url).push(context);
|
||||
await FeedAddRoute(uri: url.toString()).push(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
@@ -353,7 +354,9 @@ class _TabViewHeader extends HookConsumerWidget {
|
||||
.clearContainer();
|
||||
},
|
||||
onLongPress: (container) async {
|
||||
await ContainerEditRoute(container).push(context);
|
||||
await ContainerEditRoute(
|
||||
containerData: jsonEncode(container.toJson()),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -540,10 +543,11 @@ class ViewTabsWidget extends HookConsumerWidget {
|
||||
);
|
||||
} else {
|
||||
if (newIndex < oldIndex) {
|
||||
key = await containerRepository.getOrderKeyAfterTab(
|
||||
filteredTabEntities.value[newIndex - 1].tabId,
|
||||
containerId,
|
||||
);
|
||||
key = (await containerRepository
|
||||
.getOrderKeyAfterTab(
|
||||
filteredTabEntities.value[newIndex - 1].tabId,
|
||||
containerId,
|
||||
))!;
|
||||
} else {
|
||||
key = await containerRepository
|
||||
.getOrderKeyBeforeTab(
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
@@ -24,6 +25,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/database/daos/con
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class ContainerDao extends DatabaseAccessor<TabDatabase>
|
||||
@@ -115,7 +117,7 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
||||
);
|
||||
}
|
||||
|
||||
SingleSelectable<String> generateOrderKeyAfterTabId(
|
||||
SingleOrNullSelectable<String> generateOrderKeyAfterTabId(
|
||||
String? containerId,
|
||||
String tabId,
|
||||
) {
|
||||
@@ -134,4 +136,26 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
||||
tabId: tabId,
|
||||
);
|
||||
}
|
||||
|
||||
SingleSelectable<bool> isSiteAssignedToContainer(Uri uri) {
|
||||
return db.definitionsDrift.isSiteAssignedToContainer(uri: uri.origin);
|
||||
}
|
||||
|
||||
SingleSelectable<bool> areSitesAvailable(
|
||||
Iterable<Uri> origins,
|
||||
String ignoredContainerId,
|
||||
) {
|
||||
return db.definitionsDrift.areSitesAvailable(
|
||||
ignoreContainerId: ignoredContainerId,
|
||||
uriList: jsonEncode(origins.map((value) => value.origin).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
SingleSelectable<String> siteAssignedContainerId(Uri uri) {
|
||||
return db.definitionsDrift.siteAssignedContainerId(uri: uri.origin);
|
||||
}
|
||||
|
||||
Selectable<SiteAssignment> allAssignedSites() {
|
||||
return db.definitionsDrift.allAssignedSites();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,11 +62,14 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
||||
Future<String> _generateOrderKey({
|
||||
required Value<String?> parentId,
|
||||
required Value<String?> containerId,
|
||||
}) {
|
||||
}) async {
|
||||
if (parentId.value.isNotEmpty) {
|
||||
return db.containerDao
|
||||
.generateOrderKeyAfterTabId(containerId.value, parentId.value!)
|
||||
.getSingle();
|
||||
return await db.containerDao
|
||||
.generateOrderKeyAfterTabId(containerId.value, parentId.value!)
|
||||
.getSingleOrNull() ??
|
||||
await db.containerDao
|
||||
.generateLeadingOrderKey(containerId.value)
|
||||
.getSingle();
|
||||
} else {
|
||||
return db.containerDao
|
||||
.generateLeadingOrderKey(containerId.value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:weblibre/data/database/converters/color.dart';
|
||||
import 'package:weblibre/data/database/converters/uri.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.dart';
|
||||
|
||||
@@ -273,3 +274,41 @@ nextTabByOrderKey(:tab_id AS TEXT, :container_id AS TEXT OR NULL, :skip_containe
|
||||
SELECT next_tab_id
|
||||
FROM ranked_tabs
|
||||
WHERE id = :tab_id;
|
||||
|
||||
isSiteAssignedToContainer:
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM container
|
||||
CROSS JOIN json_each(container.metadata, '$.assignedSites')
|
||||
WHERE json_each.value = :uri
|
||||
) AS existing;
|
||||
|
||||
areSitesAvailable:
|
||||
SELECT NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM container
|
||||
CROSS JOIN json_each(container.metadata, '$.assignedSites')
|
||||
WHERE json_each.value IN (
|
||||
SELECT value
|
||||
FROM json_each(:uriList)
|
||||
) AND
|
||||
container.id IS NOT :ignore_container_id
|
||||
) AS existing;
|
||||
|
||||
siteAssignedContainerId:
|
||||
SELECT id
|
||||
FROM container
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM json_each(container.metadata, '$.assignedSites')
|
||||
WHERE value = :uri
|
||||
);
|
||||
|
||||
allAssignedSites WITH SiteAssignment:
|
||||
SELECT
|
||||
container.id,
|
||||
COALESCE(container.metadata ->> '$.contextualIdentity', 'general') AS contextualIdentity,
|
||||
value AS assigned_site
|
||||
FROM container
|
||||
CROSS JOIN json_each(container.metadata, '$.assignedSites')
|
||||
WHERE value IS NOT NULL;
|
||||
|
||||
@@ -13,6 +13,8 @@ import 'package:weblibre/data/database/converters/uri.dart' as i6;
|
||||
import 'package:drift/internal/modular.dart' as i7;
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart'
|
||||
as i8;
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart'
|
||||
as i9;
|
||||
|
||||
typedef $ContainerCreateCompanionBuilder =
|
||||
i3.ContainerCompanion Function({
|
||||
@@ -2277,6 +2279,50 @@ class DefinitionsDrift extends i7.ModularAccessor {
|
||||
).map((i0.QueryRow row) => row.readNullable<String>('next_tab_id'));
|
||||
}
|
||||
|
||||
i0.Selectable<bool> isSiteAssignedToContainer({String? uri}) {
|
||||
return customSelect(
|
||||
'SELECT EXISTS (SELECT 1 AS _c0 FROM container CROSS JOIN json_each(container.metadata, \'\$.assignedSites\')WHERE json_each.value = ?1) AS existing',
|
||||
variables: [i0.Variable<String>(uri)],
|
||||
readsFrom: {container},
|
||||
).map((i0.QueryRow row) => row.read<bool>('existing'));
|
||||
}
|
||||
|
||||
i0.Selectable<bool> areSitesAvailable({
|
||||
required String uriList,
|
||||
required String ignoreContainerId,
|
||||
}) {
|
||||
return customSelect(
|
||||
'SELECT NOT EXISTS (SELECT 1 AS _c0 FROM container CROSS JOIN json_each(container.metadata, \'\$.assignedSites\')WHERE json_each.value IN (SELECT value FROM json_each(?1)) AND container.id IS NOT ?2) AS existing',
|
||||
variables: [
|
||||
i0.Variable<String>(uriList),
|
||||
i0.Variable<String>(ignoreContainerId),
|
||||
],
|
||||
readsFrom: {container},
|
||||
).map((i0.QueryRow row) => row.read<bool>('existing'));
|
||||
}
|
||||
|
||||
i0.Selectable<String> siteAssignedContainerId({String? uri}) {
|
||||
return customSelect(
|
||||
'SELECT id FROM container WHERE EXISTS (SELECT 1 AS _c0 FROM json_each(container.metadata, \'\$.assignedSites\')WHERE value = ?1)',
|
||||
variables: [i0.Variable<String>(uri)],
|
||||
readsFrom: {container},
|
||||
).map((i0.QueryRow row) => row.read<String>('id'));
|
||||
}
|
||||
|
||||
i0.Selectable<i9.SiteAssignment> allAssignedSites() {
|
||||
return customSelect(
|
||||
'SELECT container.id, COALESCE(container.metadata ->> \'\$.contextualIdentity\', \'general\') AS contextualIdentity, value AS assigned_site FROM container CROSS JOIN json_each(container.metadata, \'\$.assignedSites\')WHERE value IS NOT NULL',
|
||||
variables: [],
|
||||
readsFrom: {container},
|
||||
).map(
|
||||
(i0.QueryRow row) => i9.SiteAssignment(
|
||||
id: row.read<String>('id'),
|
||||
contextualIdentity: row.read<String>('contextualIdentity'),
|
||||
assignedSite: row.readNullable<String>('assigned_site'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
i3.TabFts get tabFts => i7.ReadDatabaseContainer(
|
||||
attachedDatabase,
|
||||
).resultSet<i3.TabFts>('tab_fts');
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:weblibre/data/database/converters/color.dart';
|
||||
import 'package:weblibre/data/database/converters/icon_data.dart';
|
||||
|
||||
part 'container_data.g.dart';
|
||||
@@ -72,11 +73,14 @@ class ContainerMetadata with FastEquatable {
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool useProxy;
|
||||
|
||||
final List<Uri>? assignedSites;
|
||||
|
||||
ContainerMetadata({
|
||||
required this.iconData,
|
||||
required this.contextualIdentity,
|
||||
required this.authSettings,
|
||||
required this.useProxy,
|
||||
required this.assignedSites,
|
||||
});
|
||||
|
||||
ContainerMetadata.withDefaults({
|
||||
@@ -84,11 +88,13 @@ class ContainerMetadata with FastEquatable {
|
||||
String? contextualIdentity,
|
||||
ContainerAuthSettings? authSettings,
|
||||
bool? useProxy,
|
||||
List<Uri>? assignedSites,
|
||||
}) : this(
|
||||
iconData: iconData,
|
||||
contextualIdentity: contextualIdentity,
|
||||
authSettings: authSettings ?? ContainerAuthSettings.withDefaults(),
|
||||
useProxy: useProxy ?? false,
|
||||
assignedSites: assignedSites,
|
||||
);
|
||||
|
||||
factory ContainerMetadata.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -102,13 +108,16 @@ class ContainerMetadata with FastEquatable {
|
||||
contextualIdentity,
|
||||
authSettings,
|
||||
useProxy,
|
||||
assignedSites,
|
||||
];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class ContainerData with FastEquatable {
|
||||
final String id;
|
||||
final String? name;
|
||||
@ColorJsonConverter()
|
||||
final Color color;
|
||||
final ContainerMetadata metadata;
|
||||
|
||||
@@ -119,10 +128,16 @@ class ContainerData with FastEquatable {
|
||||
ContainerMetadata? metadata,
|
||||
}) : metadata = metadata ?? ContainerMetadata.withDefaults();
|
||||
|
||||
factory ContainerData.fromJson(Map<String, dynamic> json) =>
|
||||
_$ContainerDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ContainerDataToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [id, name, color, metadata];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ContainerDataWithCount extends ContainerData {
|
||||
final int? tabCount;
|
||||
|
||||
@@ -134,6 +149,12 @@ class ContainerDataWithCount extends ContainerData {
|
||||
required this.tabCount,
|
||||
});
|
||||
|
||||
factory ContainerDataWithCount.fromJson(Map<String, dynamic> json) =>
|
||||
_$ContainerDataWithCountFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$ContainerDataWithCountToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [...super.hashParameters, tabCount];
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
|
||||
ContainerMetadata useProxy(bool useProxy);
|
||||
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
||||
|
||||
/// 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 `ContainerMetadata(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
@@ -110,6 +112,7 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
String? contextualIdentity,
|
||||
ContainerAuthSettings authSettings,
|
||||
bool useProxy,
|
||||
List<Uri>? assignedSites,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,6 +137,10 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
@override
|
||||
ContainerMetadata useProxy(bool useProxy) => call(useProxy: useProxy);
|
||||
|
||||
@override
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
||||
call(assignedSites: assignedSites);
|
||||
|
||||
@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 `ContainerMetadata(...).copyWith.fieldName(value)`.
|
||||
@@ -147,6 +154,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
Object? contextualIdentity = const $CopyWithPlaceholder(),
|
||||
Object? authSettings = const $CopyWithPlaceholder(),
|
||||
Object? useProxy = const $CopyWithPlaceholder(),
|
||||
Object? assignedSites = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return ContainerMetadata(
|
||||
iconData: iconData == const $CopyWithPlaceholder()
|
||||
@@ -166,6 +174,10 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
? _value.useProxy
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: useProxy as bool,
|
||||
assignedSites: assignedSites == const $CopyWithPlaceholder()
|
||||
? _value.assignedSites
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: assignedSites as List<Uri>?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -299,18 +311,23 @@ ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
||||
json['authSettings'] as Map<String, dynamic>,
|
||||
),
|
||||
useProxy: json['useProxy'] as bool? ?? false,
|
||||
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
||||
?.map((e) => Uri.parse(e as String))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ContainerMetadataToJson(ContainerMetadata instance) =>
|
||||
<String, dynamic>{
|
||||
'iconData': _$JsonConverterToJson<Map<String, dynamic>, IconData>(
|
||||
instance.iconData,
|
||||
const IconDataJsonConverter().toJson,
|
||||
),
|
||||
'contextualIdentity': instance.contextualIdentity,
|
||||
'authSettings': instance.authSettings.toJson(),
|
||||
'useProxy': instance.useProxy,
|
||||
};
|
||||
Map<String, dynamic> _$ContainerMetadataToJson(
|
||||
ContainerMetadata instance,
|
||||
) => <String, dynamic>{
|
||||
'iconData': _$JsonConverterToJson<Map<String, dynamic>, IconData>(
|
||||
instance.iconData,
|
||||
const IconDataJsonConverter().toJson,
|
||||
),
|
||||
'contextualIdentity': instance.contextualIdentity,
|
||||
'authSettings': instance.authSettings.toJson(),
|
||||
'useProxy': instance.useProxy,
|
||||
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
||||
};
|
||||
|
||||
Value? _$JsonConverterFromJson<Json, Value>(
|
||||
Object? json,
|
||||
@@ -321,3 +338,44 @@ Json? _$JsonConverterToJson<Json, Value>(
|
||||
Value? value,
|
||||
Json? Function(Value value) toJson,
|
||||
) => value == null ? null : toJson(value);
|
||||
|
||||
ContainerData _$ContainerDataFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => ContainerData(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String?,
|
||||
color: const ColorJsonConverter().fromJson((json['color'] as num).toInt()),
|
||||
metadata: json['metadata'] == null
|
||||
? null
|
||||
: ContainerMetadata.fromJson(json['metadata'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ContainerDataToJson(ContainerData instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'color': const ColorJsonConverter().toJson(instance.color),
|
||||
'metadata': instance.metadata.toJson(),
|
||||
};
|
||||
|
||||
ContainerDataWithCount _$ContainerDataWithCountFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => ContainerDataWithCount(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String?,
|
||||
color: const ColorJsonConverter().fromJson((json['color'] as num).toInt()),
|
||||
metadata: json['metadata'] == null
|
||||
? null
|
||||
: ContainerMetadata.fromJson(json['metadata'] as Map<String, dynamic>),
|
||||
tabCount: (json['tabCount'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ContainerDataWithCountToJson(
|
||||
ContainerDataWithCount instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'color': const ColorJsonConverter().toJson(instance.color),
|
||||
'metadata': instance.metadata.toJson(),
|
||||
'tabCount': instance.tabCount,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class SiteAssignment with FastEquatable {
|
||||
final String id;
|
||||
final String? contextualIdentity;
|
||||
final Uri assignedSite;
|
||||
|
||||
SiteAssignment({
|
||||
required this.id,
|
||||
required this.contextualIdentity,
|
||||
//Drift type casting issue should be non null
|
||||
required String? assignedSite,
|
||||
}) : assignedSite = Uri.parse(assignedSite!);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [id, contextualIdentity, assignedSite];
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:weblibre/features/search/util/tokenized_filter.dart';
|
||||
|
||||
@@ -118,3 +119,8 @@ Stream<String?> watchContainerTabId(Ref ref, String tabId) {
|
||||
.getTabContainerId(tabId)
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
||||
return ref.read(tabDatabaseProvider).containerDao.allAssignedSites().watch();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||
@@ -30,7 +31,16 @@ part 'container.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ContainerRepository extends _$ContainerRepository {
|
||||
Future<void> addContainer(ContainerData container) {
|
||||
Future<void> addContainer(ContainerData container) async {
|
||||
if (container.metadata.assignedSites.isNotEmpty) {
|
||||
if (!await areSitesAvailable(
|
||||
container.metadata.assignedSites!,
|
||||
container.id,
|
||||
)) {
|
||||
throw Exception('Sites already assigned to another container');
|
||||
}
|
||||
}
|
||||
|
||||
return ref.read(tabDatabaseProvider).containerDao.addContainer(container);
|
||||
}
|
||||
|
||||
@@ -42,7 +52,16 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<void> replaceContainer(ContainerData container) {
|
||||
Future<void> replaceContainer(ContainerData container) async {
|
||||
if (container.metadata.assignedSites.isNotEmpty) {
|
||||
if (!await areSitesAvailable(
|
||||
container.metadata.assignedSites!,
|
||||
container.id,
|
||||
)) {
|
||||
throw Exception('Sites already assigned to another container');
|
||||
}
|
||||
}
|
||||
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
@@ -98,12 +117,12 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
.getSingle();
|
||||
}
|
||||
|
||||
Future<String> getOrderKeyAfterTab(String tabId, String? containerId) {
|
||||
Future<String?> getOrderKeyAfterTab(String tabId, String? containerId) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
.generateOrderKeyAfterTabId(containerId, tabId)
|
||||
.getSingle();
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<String> getOrderKeyBeforeTab(String tabId, String? containerId) {
|
||||
@@ -126,6 +145,33 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
return randomColor;
|
||||
}
|
||||
|
||||
Future<bool> isSiteAssignedToContainer(Uri uri) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
.isSiteAssignedToContainer(uri)
|
||||
.getSingle();
|
||||
}
|
||||
|
||||
Future<bool> areSitesAvailable(
|
||||
Iterable<Uri> origins,
|
||||
String ignoreContainerId,
|
||||
) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
.areSitesAvailable(origins, ignoreContainerId)
|
||||
.getSingle();
|
||||
}
|
||||
|
||||
Future<String?> siteAssignedContainerId(Uri uri) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
.siteAssignedContainerId(uri)
|
||||
.getSingle();
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ContainerRepositoryProvider
|
||||
}
|
||||
|
||||
String _$containerRepositoryHash() =>
|
||||
r'6d84d672e24ac3d2cb376e93cbbe61fc223d89b3';
|
||||
r'8c9252a49a096c29279ad3066f6e5936e15524b2';
|
||||
|
||||
abstract class _$ContainerRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
@@ -198,7 +199,7 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
|
||||
if (context.mounted) {
|
||||
final result = await ContainerCreateRoute(
|
||||
initialContainer,
|
||||
containerData: jsonEncode(initialContainer.toJson()),
|
||||
).push<ContainerData?>(context);
|
||||
|
||||
if (result != null) {
|
||||
|
||||
@@ -17,15 +17,18 @@
|
||||
* 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/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/uuid.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/controllers/container_topic.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart';
|
||||
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
|
||||
import 'package:weblibre/presentation/icons/tor_icons.dart';
|
||||
@@ -74,6 +77,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
);
|
||||
final authSettings = useState(initialContainer.metadata.authSettings);
|
||||
final useProxy = useState(initialContainer.metadata.useProxy);
|
||||
final assignedSites = useState(initialContainer.metadata.assignedSites);
|
||||
|
||||
final textController = useTextEditingController(
|
||||
text: initialContainer.name,
|
||||
@@ -101,6 +105,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
contextualIdentity: contextualIdentity.value,
|
||||
authSettings: authSettings.value,
|
||||
useProxy: useProxy.value && contextualIdentity.value != null,
|
||||
assignedSites: assignedSites.value,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -299,6 +304,26 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.web),
|
||||
title: const Text('Assigned Sites'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onTap: () async {
|
||||
final result = await showDialog<Set<Uri>>(
|
||||
context: context,
|
||||
builder: (context) => ContainerSitesScreen(
|
||||
initialSites: assignedSites.value?.toSet() ?? {},
|
||||
),
|
||||
);
|
||||
|
||||
if (result.isEmpty) {
|
||||
assignedSites.value = null;
|
||||
} else {
|
||||
assignedSites.value = result!.toList();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
@@ -40,7 +42,7 @@ class ContainerListScreen extends HookConsumerWidget {
|
||||
appBar: AppBar(title: const Text('Containers')),
|
||||
body: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final containersAsync = ref.watch(containersWithCountProvider);
|
||||
final containersAsync = ref.watch(watchContainersWithCountProvider);
|
||||
final selectedContainer = ref.watch(selectedContainerProvider);
|
||||
|
||||
return Skeletonizer(
|
||||
@@ -128,7 +130,9 @@ class ContainerListScreen extends HookConsumerWidget {
|
||||
container,
|
||||
isSelected: container.id == selectedContainer,
|
||||
onTap: () async {
|
||||
await ContainerEditRoute(container).push(context);
|
||||
await ContainerEditRoute(
|
||||
containerData: jsonEncode(container.toJson()),
|
||||
).push(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -157,7 +161,9 @@ class ContainerListScreen extends HookConsumerWidget {
|
||||
|
||||
if (context.mounted) {
|
||||
await ContainerCreateRoute(
|
||||
ContainerData(id: uuid.v7(), color: initialColor),
|
||||
containerData: jsonEncode(
|
||||
ContainerData(id: uuid.v7(), color: initialColor).toJson(),
|
||||
),
|
||||
).push(context);
|
||||
}
|
||||
},
|
||||
|
||||
+6
-2
@@ -17,6 +17,8 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -38,7 +40,7 @@ class ContainerSelectionScreen extends HookConsumerWidget {
|
||||
appBar: AppBar(title: const Text('Select Container')),
|
||||
body: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final containersAsync = ref.watch(containersWithCountProvider);
|
||||
final containersAsync = ref.watch(watchContainersWithCountProvider);
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: containersAsync.isLoading,
|
||||
@@ -84,7 +86,9 @@ class ContainerSelectionScreen extends HookConsumerWidget {
|
||||
|
||||
if (context.mounted) {
|
||||
await ContainerCreateRoute(
|
||||
ContainerData(id: uuid.v7(), color: initialColor),
|
||||
containerData: jsonEncode(
|
||||
ContainerData(id: uuid.v7(), color: initialColor).toJson(),
|
||||
),
|
||||
).push(context);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
import 'package:weblibre/utils/form_validators.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
class ContainerSitesScreen extends HookConsumerWidget {
|
||||
final Set<Uri> initialSites;
|
||||
|
||||
const ContainerSitesScreen({super.key, required this.initialSites});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final textController = useTextEditingController();
|
||||
|
||||
final sites = useState(initialSites);
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (!didPop) {
|
||||
context.pop(result ?? sites.value);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
context.pop(sites.value);
|
||||
},
|
||||
),
|
||||
title: const Text('Site Assignments'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
label: const Text('Add Site'),
|
||||
hintText: 'example.com',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
suffix: TextButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState?.validate() == true) {
|
||||
formKey.currentState?.save();
|
||||
}
|
||||
},
|
||||
child: const Text('Add'),
|
||||
),
|
||||
),
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
final uriValid = validateUrl(
|
||||
value,
|
||||
onlyHttpProtocol: true,
|
||||
eagerParsing: true,
|
||||
);
|
||||
|
||||
if (uriValid != null) {
|
||||
return uriValid;
|
||||
}
|
||||
|
||||
final origin = Uri.parse(
|
||||
uri_parser
|
||||
.tryParseUrl(value, eagerParsing: true)!
|
||||
.origin,
|
||||
);
|
||||
|
||||
if (sites.value.contains(origin)) {
|
||||
return 'This site has been already assigned';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
onSaved: (newValue) async {
|
||||
final origin = Uri.parse(
|
||||
uri_parser
|
||||
.tryParseUrl(newValue, eagerParsing: true)!
|
||||
.origin,
|
||||
);
|
||||
|
||||
final isAssigned = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.isSiteAssignedToContainer(origin);
|
||||
|
||||
if (!isAssigned) {
|
||||
sites.value = {...sites.value, origin};
|
||||
} else {
|
||||
final assignedContainerId = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.siteAssignedContainerId(origin);
|
||||
final assignedContainer = await assignedContainerId
|
||||
.mapNotNull(
|
||||
(id) => ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(id),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showErrorMessage(
|
||||
context,
|
||||
(assignedContainer?.name.isNotEmpty ?? false)
|
||||
? '$origin has already been assigned to container "${assignedContainer?.name}"'
|
||||
: '$origin has already been assigned to another container',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
textController.clear();
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
if (formKey.currentState?.validate() == true) {
|
||||
formKey.currentState?.save();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: sites.value.length,
|
||||
itemBuilder: (context, index) {
|
||||
final site = sites.value.elementAt(index);
|
||||
|
||||
return ListTile(
|
||||
leading: UrlIcon([site], iconSize: 20),
|
||||
title: Text(site.host),
|
||||
subtitle: Text(site.toString()),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
sites.value = {...sites.value}..remove(site);
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
|
||||
|
||||
part 'tor_proxy.g.dart';
|
||||
|
||||
@@ -35,6 +36,23 @@ class TorProxyRepository extends _$TorProxyRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setSiteAssignments(List<SiteAssignment> assignements) {
|
||||
return _serviceLock.synchronized(() async {
|
||||
await _waitHealthcheck().timeout(const Duration(seconds: 10));
|
||||
|
||||
return _service.setSiteAssignments(
|
||||
Map.fromEntries(
|
||||
assignements.map(
|
||||
(e) => MapEntry(
|
||||
e.assignedSite.origin,
|
||||
e.contextualIdentity ?? 'general',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _waitHealthcheck({
|
||||
Duration timeout = const Duration(seconds: 15),
|
||||
}) async {
|
||||
|
||||
@@ -42,7 +42,7 @@ final class TorProxyRepositoryProvider
|
||||
}
|
||||
|
||||
String _$torProxyRepositoryHash() =>
|
||||
r'3bdb83bdb1c5d1bb5d8953878cd587d042d30342';
|
||||
r'83c2976750f3f7907274b1ae4f926cd1de89be83';
|
||||
|
||||
abstract class _$TorProxyRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -33,7 +33,7 @@ final class TorProxyServiceProvider
|
||||
TorProxyService create() => TorProxyService();
|
||||
}
|
||||
|
||||
String _$torProxyServiceHash() => r'21592493593c7c2a6cf1ac8311526a7fd6114b98';
|
||||
String _$torProxyServiceHash() => r'c63adf96d7b0a865917b8d902071a0b3ae4dfcc6';
|
||||
|
||||
abstract class _$TorProxyService extends $AsyncNotifier<int?> {
|
||||
FutureOr<int?> build();
|
||||
|
||||
@@ -67,7 +67,7 @@ class FeedListScreen extends HookConsumerWidget {
|
||||
label: const Text('Feed'),
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
await const FeedAddRoute().push(context);
|
||||
await const FeedAddRoute(uri: null).push(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+3
-2
@@ -12,6 +12,7 @@ import eu.weblibre.flutter_mozilla_components.feature.BrowserExtensionFeature
|
||||
import eu.weblibre.flutter_mozilla_components.feature.MLEngineFeature
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BounceTrackingProtectionMode
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
@@ -81,7 +82,7 @@ object EngineProvider {
|
||||
return runtime!!
|
||||
}
|
||||
|
||||
fun createEngine(context: Context, defaultSettings: DefaultSettings, extensionEvents: BrowserExtensionEvents): Engine {
|
||||
fun createEngine(context: Context, defaultSettings: DefaultSettings, extensionEvents: BrowserExtensionEvents, stateEvents: GeckoStateEvents): Engine {
|
||||
Logger.debug("Creating Engine")
|
||||
val runtime = getOrCreateRuntime(context)
|
||||
|
||||
@@ -89,7 +90,7 @@ object EngineProvider {
|
||||
WebCompatFeature.install(it)
|
||||
//CookieManagerFeature.install(it)
|
||||
PrefManagerFeature.install(it)
|
||||
ContainerProxyFeature.install(it)
|
||||
ContainerProxyFeature.install(it, stateEvents)
|
||||
BrowserExtensionFeature.install(it, extensionEvents)
|
||||
MLEngineFeature.install(it)
|
||||
}
|
||||
|
||||
+4
@@ -25,6 +25,10 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
|
||||
ContainerProxyFeature.scheduleRequest("removeContainerProxy", contextId)
|
||||
}
|
||||
|
||||
override fun setSiteAssignments(assignments: Map<String, String>) {
|
||||
ContainerProxyFeature.scheduleRequest("setSiteAssignments", JSONObject(assignments))
|
||||
}
|
||||
|
||||
override fun healthcheck(callback: (Result<Boolean>) -> Unit) {
|
||||
ContainerProxyFeature.scheduleRequestWithResponse("healthcheck", Unit, object :
|
||||
ResultConsumer<JSONObject> {
|
||||
|
||||
+2
-2
@@ -114,7 +114,7 @@ class Core(
|
||||
}
|
||||
|
||||
val engine: Engine by lazy {
|
||||
EngineProvider.createEngine(context, engineSettings, extensionEvents)
|
||||
EngineProvider.createEngine(context, engineSettings, extensionEvents, flutterEvents)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +158,7 @@ class Core(
|
||||
collectionUser = components.addonCollection.collectionUser,
|
||||
collectionName = components.addonCollection.collectionName,
|
||||
maxCacheAgeInMinutes = AMO_COLLECTION_MAX_CACHE_AGE
|
||||
) else
|
||||
) else
|
||||
AMOAddonsProvider(
|
||||
context,
|
||||
client,
|
||||
|
||||
+35
-4
@@ -7,6 +7,9 @@
|
||||
package eu.weblibre.flutter_mozilla_components.feature
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ContainerSiteAssignment
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -16,20 +19,27 @@ import mozilla.components.concept.engine.webextension.MessageHandler
|
||||
import mozilla.components.concept.engine.webextension.Port
|
||||
import mozilla.components.concept.engine.webextension.WebExtensionRuntime
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.ktx.android.org.json.tryGetString
|
||||
import mozilla.components.support.webextensions.BuiltInWebExtensionController
|
||||
import org.json.JSONObject
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
|
||||
object ContainerProxyFeature {
|
||||
private val logger = Logger("container_proxy")
|
||||
|
||||
private const val CONTAINER_PROXY_REPORTER_EXTENSION_ID = "container-proxy@weblibre.eu"
|
||||
private const val CONTAINER_PROXY_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/container_proxy/"
|
||||
private const val CONTAINER_PROXY_REPORTER_EXTENSION_URL =
|
||||
"resource://android/assets/extensions/container_proxy/"
|
||||
private const val CONTAINER_PROXY_REPORTER_MESSAGING_ID = "containerProxy"
|
||||
|
||||
private var nextRequestId: Int = 0
|
||||
private val requestHandlers = HashMap<Int, ResultConsumer<JSONObject>>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
// This is an internal var to make it mutable for unit testing purposes only
|
||||
internal var extensionController = BuiltInWebExtensionController(
|
||||
@@ -74,7 +84,9 @@ object ContainerProxyFeature {
|
||||
}
|
||||
}
|
||||
|
||||
private class ContainerProxyBackgroundMessageHandler() : MessageHandler {
|
||||
private class ContainerProxyBackgroundMessageHandler(
|
||||
private var events: GeckoStateEvents
|
||||
) : MessageHandler {
|
||||
override fun onPortMessage(message: Any, port: Port) {
|
||||
runBlocking {
|
||||
withContext(Dispatchers.Default) {
|
||||
@@ -94,6 +106,25 @@ object ContainerProxyFeature {
|
||||
message.getString("error")
|
||||
)
|
||||
}
|
||||
} else if (type == "assignedSiteRequested") {
|
||||
val requestId = messageJSON.getString("id")
|
||||
val status = messageJSON.getString("status")
|
||||
val details = messageJSON.getJSONObject("result")
|
||||
|
||||
if (status == "success") {
|
||||
runOnUiThread {
|
||||
events.onContainerSiteAssignment(
|
||||
System.currentTimeMillis(),
|
||||
ContainerSiteAssignment(
|
||||
requestId = requestId,
|
||||
tabId = components.core.store.state.selectedTabId,
|
||||
originUrl = details.tryGetString("originUrl"),
|
||||
url = details.getString("url"),
|
||||
blocked = details.getBoolean("blocked")
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,9 +139,9 @@ object ContainerProxyFeature {
|
||||
* @param productName a custom product name used to automatically label reports. Defaults to
|
||||
* "android-components".
|
||||
*/
|
||||
fun install(runtime: WebExtensionRuntime) {
|
||||
fun install(runtime: WebExtensionRuntime, events: GeckoStateEvents) {
|
||||
extensionController.registerBackgroundMessageHandler(
|
||||
ContainerProxyBackgroundMessageHandler()
|
||||
ContainerProxyBackgroundMessageHandler(events)
|
||||
)
|
||||
extensionController.install(
|
||||
runtime,
|
||||
|
||||
+90
-5
@@ -2277,6 +2277,46 @@ data class GeckoPref (
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class ContainerSiteAssignment (
|
||||
val requestId: String,
|
||||
val tabId: String? = null,
|
||||
val originUrl: String? = null,
|
||||
val url: String,
|
||||
val blocked: Boolean
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): ContainerSiteAssignment {
|
||||
val requestId = pigeonVar_list[0] as String
|
||||
val tabId = pigeonVar_list[1] as String?
|
||||
val originUrl = pigeonVar_list[2] as String?
|
||||
val url = pigeonVar_list[3] as String
|
||||
val blocked = pigeonVar_list[4] as Boolean
|
||||
return ContainerSiteAssignment(requestId, tabId, originUrl, url, blocked)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
requestId,
|
||||
tabId,
|
||||
originUrl,
|
||||
url,
|
||||
blocked,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ContainerSiteAssignment) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return GeckoPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class GeckoHeader (
|
||||
val key: String,
|
||||
@@ -2743,15 +2783,20 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
}
|
||||
195.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
GeckoHeader.fromList(it)
|
||||
ContainerSiteAssignment.fromList(it)
|
||||
}
|
||||
}
|
||||
196.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
GeckoFetchRequest.fromList(it)
|
||||
GeckoHeader.fromList(it)
|
||||
}
|
||||
}
|
||||
197.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
GeckoFetchRequest.fromList(it)
|
||||
}
|
||||
}
|
||||
198.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
GeckoFetchResponse.fromList(it)
|
||||
}
|
||||
@@ -3025,18 +3070,22 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
stream.write(194)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is GeckoHeader -> {
|
||||
is ContainerSiteAssignment -> {
|
||||
stream.write(195)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is GeckoFetchRequest -> {
|
||||
is GeckoHeader -> {
|
||||
stream.write(196)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is GeckoFetchResponse -> {
|
||||
is GeckoFetchRequest -> {
|
||||
stream.write(197)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is GeckoFetchResponse -> {
|
||||
stream.write(198)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
@@ -4257,6 +4306,7 @@ interface GeckoContainerProxyApi {
|
||||
fun setProxyPort(port: Long)
|
||||
fun addContainerProxy(contextId: String)
|
||||
fun removeContainerProxy(contextId: String)
|
||||
fun setSiteAssignments(assignments: Map<String, String>)
|
||||
fun healthcheck(callback: (Result<Boolean>) -> Unit)
|
||||
|
||||
companion object {
|
||||
@@ -4322,6 +4372,24 @@ interface GeckoContainerProxyApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setSiteAssignments$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val assignmentsArg = args[0] as Map<String, String>
|
||||
val wrapped: List<Any?> = try {
|
||||
api.setSiteAssignments(assignmentsArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
GeckoPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
@@ -4744,6 +4812,23 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
|
||||
}
|
||||
}
|
||||
}
|
||||
fun onContainerSiteAssignment(timestampArg: Long, detailsArg: ContainerSiteAssignment, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(timestampArg, detailsArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
|
||||
class GeckoLogging(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||
|
||||
+37
-140
File diff suppressed because it is too large
Load Diff
+70
-3
@@ -5,6 +5,7 @@ import { ProxyType } from '../domain/ProxyType'
|
||||
import BlockingResponse = browser.webRequest.BlockingResponse
|
||||
import _OnAuthRequiredDetails = browser.webRequest._OnAuthRequiredDetails
|
||||
import _OnRequestDetails = browser.proxy._OnRequestDetails
|
||||
import _OnBeforeRequestDetails = browser.webRequest._OnBeforeRequestDetails
|
||||
|
||||
const localhosts = new Set(['localhost', '127.0.0.1', '[::1]'])
|
||||
|
||||
@@ -127,13 +128,79 @@ export default class BackgroundMain {
|
||||
return doNotProxy
|
||||
}
|
||||
|
||||
run(browser: { proxy: any }): void {
|
||||
const filter = { urls: ['<all_urls>'] }
|
||||
async onBeforeRequest(options: _OnBeforeRequestDetails, port: browser.runtime.Port): Promise<browser.webRequest.BlockingResponse> {
|
||||
const tab = (options.tabId > -1) ? (await browser.tabs.get(options.tabId)) : null
|
||||
|
||||
browser.proxy.onRequest.addListener(this.onRequest.bind(this), filter)
|
||||
if (options.frameId !== 0 || tab === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const url = URL.parse(options.url);
|
||||
if (url !== null && this.store.isSiteOriginAssigned(url)) {
|
||||
let cookieStoreId: string
|
||||
|
||||
if (tab.cookieStoreId?.startsWith(containerIdentifier) === true) {
|
||||
cookieStoreId = tab.cookieStoreId.substring(containerIdentifier.length)
|
||||
} else if (tab.cookieStoreId === privateIdentifier) {
|
||||
// Handle private tabs - use 'private' as identifier
|
||||
cookieStoreId = 'private'
|
||||
} else {
|
||||
cookieStoreId = 'general'
|
||||
}
|
||||
|
||||
if (this.store.isSiteOriginInSameContext(url, cookieStoreId)) {
|
||||
if (tab.highlighted) {
|
||||
port.postMessage({
|
||||
"type": "assignedSiteRequested",
|
||||
"id": options.requestId,
|
||||
"status": "success",
|
||||
"result": {
|
||||
"originUrl": options.originUrl,
|
||||
"url": options.url,
|
||||
"blocked": false
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
} else {
|
||||
//When tab not selected, block the request
|
||||
return {
|
||||
cancel: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
//Only send events when tab is selected
|
||||
if (tab.highlighted) {
|
||||
port.postMessage({
|
||||
"type": "assignedSiteRequested",
|
||||
"id": options.requestId,
|
||||
"status": "success",
|
||||
"result": {
|
||||
"originUrl": options.originUrl,
|
||||
"url": options.url,
|
||||
"blocked": true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cancel: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
run(browser: { proxy: any, webRequest: any }, port: browser.runtime.Port): void {
|
||||
browser.proxy.onRequest.addListener(this.onRequest.bind(this), { urls: ['<all_urls>'] })
|
||||
|
||||
browser.proxy.onError.addListener((e: Error) => {
|
||||
console.error('Proxy error', e)
|
||||
})
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener((options: _OnBeforeRequestDetails) => {
|
||||
return this.onBeforeRequest(options, port);
|
||||
}, { urls: ["<all_urls>"], types: ["main_frame"] }, ["blocking"])
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -8,7 +8,7 @@ const store = new Store()
|
||||
|
||||
interface Message {
|
||||
id: String | undefined;
|
||||
action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy' | 'healthcheck';
|
||||
action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy' | 'healthcheck' | 'setSiteAssignments';
|
||||
args: any;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ port.onMessage.addListener((raw: unknown): void => {
|
||||
store.removeContainerProxyRelation(message.args, "tor")
|
||||
console.log('removed container relation ' + message.args)
|
||||
break
|
||||
case "setSiteAssignments":
|
||||
const entries = new Map(Object.entries(message.args))
|
||||
console.log('set site assignments ' + JSON.stringify(message.args))
|
||||
store.setSiteAssignments(entries);
|
||||
break
|
||||
case "healthcheck":
|
||||
port.postMessage({
|
||||
"type": "healthcheck",
|
||||
@@ -54,4 +59,4 @@ port.onMessage.addListener((raw: unknown): void => {
|
||||
});
|
||||
|
||||
const backgroundListener = new BackgroundMain({ store })
|
||||
backgroundListener.run(browser)
|
||||
backgroundListener.run(browser, port)
|
||||
|
||||
@@ -79,6 +79,24 @@ export class Store {
|
||||
private proxies: ProxyDao[] = []
|
||||
private relations: { [key: string]: string[] } = {}
|
||||
|
||||
private siteAssignments: Map<string, string> = new Map<string, string>()
|
||||
|
||||
setSiteAssignments(sites: Map<string, unknown>): void {
|
||||
this.siteAssignments = new Map(
|
||||
Array.from(sites, ([key, value]) => {
|
||||
return [URL.parse(key)!.origin, value as string]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
isSiteOriginAssigned(uri: URL): boolean {
|
||||
return this.siteAssignments.has(uri.origin)
|
||||
}
|
||||
|
||||
isSiteOriginInSameContext(uri: URL, contextId: string): boolean {
|
||||
return this.siteAssignments.get(uri.origin) === contextId;
|
||||
}
|
||||
|
||||
getAllProxies(): ProxySettings[] {
|
||||
const proxyDaos = this.getAllProxyDaos()
|
||||
return proxyDaos.map(tryFromDao).filter(p => p !== undefined) as ProxySettings[]
|
||||
|
||||
@@ -23,6 +23,10 @@ class GeckoContainerProxyService {
|
||||
return _apiInstance.removeContainerProxy(contextId);
|
||||
}
|
||||
|
||||
Future<void> setSiteAssignments(Map<String, String> assignments) {
|
||||
return _apiInstance.setSiteAssignments(assignments);
|
||||
}
|
||||
|
||||
Future<bool> healthcheck() async {
|
||||
try {
|
||||
return await _apiInstance.healthcheck().timeout(
|
||||
|
||||
@@ -40,6 +40,7 @@ class GeckoEventService extends GeckoStateEvents {
|
||||
final _longPressSubject = PublishSubject<LongPressEvent>();
|
||||
final _scrollEventSubject = PublishSubject<ScrollEvent>();
|
||||
final _prefUpdateSubject = PublishSubject<GeckoPref>();
|
||||
final _siteAssignementSubject = PublishSubject<ContainerSiteAssignment>();
|
||||
|
||||
final _tabAddedSubject = PublishSubject<String>();
|
||||
|
||||
@@ -61,6 +62,8 @@ class GeckoEventService extends GeckoStateEvents {
|
||||
Stream<LongPressEvent> get longPressEvent => _longPressSubject.stream;
|
||||
Stream<ScrollEvent> get scrollEvent => _scrollEventSubject.stream;
|
||||
Stream<GeckoPref> get prefUpdateEvent => _prefUpdateSubject.stream;
|
||||
Stream<ContainerSiteAssignment> get siteAssignementEvent =>
|
||||
_siteAssignementSubject.stream;
|
||||
|
||||
Stream<String> get tabAddedStream => _tabAddedSubject.stream;
|
||||
|
||||
@@ -184,6 +187,18 @@ class GeckoEventService extends GeckoStateEvents {
|
||||
_prefUpdateSubject.addWhenMoreRecent(timestamp, value.name, value);
|
||||
}
|
||||
|
||||
@override
|
||||
void onContainerSiteAssignment(
|
||||
int timestamp,
|
||||
ContainerSiteAssignment details,
|
||||
) {
|
||||
_siteAssignementSubject.addWhenMoreRecent(
|
||||
timestamp,
|
||||
details.requestId,
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
GeckoEventService.setUp({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
@@ -212,5 +227,6 @@ class GeckoEventService extends GeckoStateEvents {
|
||||
unawaited(_scrollEventSubject.close());
|
||||
unawaited(_tabAddedSubject.close());
|
||||
unawaited(_prefUpdateSubject.close());
|
||||
unawaited(_siteAssignementSubject.close());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2875,6 +2875,67 @@ class GeckoPref {
|
||||
;
|
||||
}
|
||||
|
||||
class ContainerSiteAssignment {
|
||||
ContainerSiteAssignment({
|
||||
required this.requestId,
|
||||
this.tabId,
|
||||
this.originUrl,
|
||||
required this.url,
|
||||
required this.blocked,
|
||||
});
|
||||
|
||||
String requestId;
|
||||
|
||||
String? tabId;
|
||||
|
||||
String? originUrl;
|
||||
|
||||
String url;
|
||||
|
||||
bool blocked;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
requestId,
|
||||
tabId,
|
||||
originUrl,
|
||||
url,
|
||||
blocked,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList(); }
|
||||
|
||||
static ContainerSiteAssignment decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return ContainerSiteAssignment(
|
||||
requestId: result[0]! as String,
|
||||
tabId: result[1] as String?,
|
||||
originUrl: result[2] as String?,
|
||||
url: result[3]! as String,
|
||||
blocked: result[4]! as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! ContainerSiteAssignment || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList())
|
||||
;
|
||||
}
|
||||
|
||||
class GeckoHeader {
|
||||
GeckoHeader({
|
||||
required this.key,
|
||||
@@ -3284,15 +3345,18 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
} else if (value is GeckoPref) {
|
||||
buffer.putUint8(194);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is GeckoHeader) {
|
||||
} else if (value is ContainerSiteAssignment) {
|
||||
buffer.putUint8(195);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is GeckoFetchRequest) {
|
||||
} else if (value is GeckoHeader) {
|
||||
buffer.putUint8(196);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is GeckoFetchResponse) {
|
||||
} else if (value is GeckoFetchRequest) {
|
||||
buffer.putUint8(197);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is GeckoFetchResponse) {
|
||||
buffer.putUint8(198);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
@@ -3456,10 +3520,12 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
case 194:
|
||||
return GeckoPref.decode(readValue(buffer)!);
|
||||
case 195:
|
||||
return GeckoHeader.decode(readValue(buffer)!);
|
||||
return ContainerSiteAssignment.decode(readValue(buffer)!);
|
||||
case 196:
|
||||
return GeckoFetchRequest.decode(readValue(buffer)!);
|
||||
return GeckoHeader.decode(readValue(buffer)!);
|
||||
case 197:
|
||||
return GeckoFetchRequest.decode(readValue(buffer)!);
|
||||
case 198:
|
||||
return GeckoFetchResponse.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@@ -4990,6 +5056,29 @@ class GeckoContainerProxyApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setSiteAssignments(Map<String, String> assignments) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setSiteAssignments$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[assignments]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> healthcheck() async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
@@ -5170,6 +5259,8 @@ abstract class GeckoStateEvents {
|
||||
|
||||
void onPreferenceChange(int timestamp, GeckoPref value);
|
||||
|
||||
void onContainerSiteAssignment(int timestamp, ContainerSiteAssignment details);
|
||||
|
||||
static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
@@ -5641,6 +5732,34 @@ abstract class GeckoStateEvents {
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_timestamp = (args[0] as int?);
|
||||
assert(arg_timestamp != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment was null, expected non-null int.');
|
||||
final ContainerSiteAssignment? arg_details = (args[1] as ContainerSiteAssignment?);
|
||||
assert(arg_details != null,
|
||||
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment was null, expected non-null ContainerSiteAssignment.');
|
||||
try {
|
||||
api.onContainerSiteAssignment(arg_timestamp!, arg_details!);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1131,6 +1131,7 @@ abstract class GeckoContainerProxyApi {
|
||||
void setProxyPort(int port);
|
||||
void addContainerProxy(String contextId);
|
||||
void removeContainerProxy(String contextId);
|
||||
void setSiteAssignments(Map<String, String> assignments);
|
||||
|
||||
@async
|
||||
bool healthcheck();
|
||||
@@ -1183,6 +1184,22 @@ abstract class GeckoCookieApi {
|
||||
);
|
||||
}
|
||||
|
||||
class ContainerSiteAssignment {
|
||||
final String requestId;
|
||||
final String? tabId;
|
||||
final String? originUrl;
|
||||
final String url;
|
||||
final bool blocked;
|
||||
|
||||
ContainerSiteAssignment({
|
||||
required this.requestId,
|
||||
required this.tabId,
|
||||
required this.originUrl,
|
||||
required this.url,
|
||||
required this.blocked,
|
||||
});
|
||||
}
|
||||
|
||||
@FlutterApi()
|
||||
abstract class GeckoStateEvents {
|
||||
void onViewReadyStateChange(int timestamp, bool state);
|
||||
@@ -1210,6 +1227,11 @@ abstract class GeckoStateEvents {
|
||||
|
||||
void onScrollChange(int timestamp, String tabId, int scrollY);
|
||||
void onPreferenceChange(int timestamp, GeckoPref value);
|
||||
|
||||
void onContainerSiteAssignment(
|
||||
int timestamp,
|
||||
ContainerSiteAssignment details,
|
||||
);
|
||||
}
|
||||
|
||||
@FlutterApi()
|
||||
|
||||
Reference in New Issue
Block a user