ublock list management initial
This commit is contained in:
@@ -35,6 +35,7 @@ assets/bangs/
|
|||||||
assets/preferences/builtin-bridges.json
|
assets/preferences/builtin-bridges.json
|
||||||
assets/preferences/url-shortener-list.json
|
assets/preferences/url-shortener-list.json
|
||||||
assets/preferences/url_cleaner_data.minify.json
|
assets/preferences/url_cleaner_data.minify.json
|
||||||
|
/assets/ublock/
|
||||||
|
|
||||||
# Android related
|
# Android related
|
||||||
**/android/**/gradle-wrapper.jar
|
**/android/**/gradle-wrapper.jar
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import 'package:weblibre/features/settings/presentation/screens/search_settings.
|
|||||||
import 'package:weblibre/features/settings/presentation/screens/settings.dart';
|
import 'package:weblibre/features/settings/presentation/screens/settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/toolbar_layout_settings.dart';
|
import 'package:weblibre/features/settings/presentation/screens/toolbar_layout_settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/screens/ublock_filter_lists.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/web_content_settings.dart';
|
import 'package:weblibre/features/settings/presentation/screens/web_content_settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
||||||
|
|||||||
@@ -1615,6 +1615,11 @@ RouteBase get $settingsRoute => GoRouteData.$route(
|
|||||||
name: 'AddonCollectionRoute',
|
name: 'AddonCollectionRoute',
|
||||||
factory: $AddonCollectionRoute._fromState,
|
factory: $AddonCollectionRoute._fromState,
|
||||||
),
|
),
|
||||||
|
GoRouteData.$route(
|
||||||
|
path: 'ublock_filter_lists',
|
||||||
|
name: 'UBlockFilterListsRoute',
|
||||||
|
factory: $UBlockFilterListsRoute._fromState,
|
||||||
|
),
|
||||||
GoRouteData.$route(
|
GoRouteData.$route(
|
||||||
path: 'tracking_protection_exceptions',
|
path: 'tracking_protection_exceptions',
|
||||||
name: 'TrackingProtectionExceptionsRoute',
|
name: 'TrackingProtectionExceptionsRoute',
|
||||||
@@ -2013,6 +2018,27 @@ mixin $AddonCollectionRoute on GoRouteData {
|
|||||||
void replace(BuildContext context) => context.replace(location);
|
void replace(BuildContext context) => context.replace(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mixin $UBlockFilterListsRoute on GoRouteData {
|
||||||
|
static UBlockFilterListsRoute _fromState(GoRouterState state) =>
|
||||||
|
UBlockFilterListsRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get location => GoRouteData.$location('/settings/ublock_filter_lists');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pushReplacement(BuildContext context) =>
|
||||||
|
context.pushReplacement(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void replace(BuildContext context) => context.replace(location);
|
||||||
|
}
|
||||||
|
|
||||||
mixin $TrackingProtectionExceptionsRoute on GoRouteData {
|
mixin $TrackingProtectionExceptionsRoute on GoRouteData {
|
||||||
static TrackingProtectionExceptionsRoute _fromState(GoRouterState state) =>
|
static TrackingProtectionExceptionsRoute _fromState(GoRouterState state) =>
|
||||||
TrackingProtectionExceptionsRoute();
|
TrackingProtectionExceptionsRoute();
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ part of 'routes.dart';
|
|||||||
name: 'AddonCollectionRoute',
|
name: 'AddonCollectionRoute',
|
||||||
path: 'addon_collection',
|
path: 'addon_collection',
|
||||||
),
|
),
|
||||||
|
TypedGoRoute<UBlockFilterListsRoute>(
|
||||||
|
name: 'UBlockFilterListsRoute',
|
||||||
|
path: 'ublock_filter_lists',
|
||||||
|
),
|
||||||
TypedGoRoute<TrackingProtectionExceptionsRoute>(
|
TypedGoRoute<TrackingProtectionExceptionsRoute>(
|
||||||
name: 'TrackingProtectionExceptionsRoute',
|
name: 'TrackingProtectionExceptionsRoute',
|
||||||
path: 'tracking_protection_exceptions',
|
path: 'tracking_protection_exceptions',
|
||||||
@@ -218,6 +222,13 @@ class AddonCollectionRoute extends GoRouteData with $AddonCollectionRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UBlockFilterListsRoute extends GoRouteData with $UBlockFilterListsRoute {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return const UBlockFilterListsScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class WebEngineHardeningRoute extends GoRouteData
|
class WebEngineHardeningRoute extends GoRouteData
|
||||||
with $WebEngineHardeningRoute {
|
with $WebEngineHardeningRoute {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -338,6 +338,16 @@ class _ManagementSection extends ConsumerWidget {
|
|||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => openAddonSettingsFlow(context, ref, addon),
|
onTap: () => openAddonSettingsFlow(context, ref, addon),
|
||||||
),
|
),
|
||||||
|
if (addon.id == 'uBlock0@raymondhill.net')
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.filter_list),
|
||||||
|
title: const Text('Filter Lists & Hardenings'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Manage filter lists and apply WebLibre hardenings',
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => UBlockFilterListsRoute().push<void>(context),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.privacy_tip_outlined),
|
leading: const Icon(Icons.privacy_tip_outlined),
|
||||||
title: const Text('Permissions'),
|
title: const Text('Permissions'),
|
||||||
|
|||||||
+31
@@ -17,6 +17,8 @@
|
|||||||
* 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 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart' show ThemeMode;
|
import 'package:flutter/material.dart' show ThemeMode;
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
@@ -26,6 +28,7 @@ import 'package:weblibre/core/logger.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/engine_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ part 'engine_settings_replication.g.dart';
|
|||||||
|
|
||||||
const _safeBrowsingMalwarePref = 'browser.safebrowsing.malware.enabled';
|
const _safeBrowsingMalwarePref = 'browser.safebrowsing.malware.enabled';
|
||||||
const _safeBrowsingPhishingPref = 'browser.safebrowsing.phishing.enabled';
|
const _safeBrowsingPhishingPref = 'browser.safebrowsing.phishing.enabled';
|
||||||
|
const ublockFilterListsPref = 'browser.weblibre.uBO.filterLists';
|
||||||
|
|
||||||
/// Checks if any Custom ETP setting changed between two EngineSettings instances.
|
/// Checks if any Custom ETP setting changed between two EngineSettings instances.
|
||||||
bool _customEtpSettingsChanged(
|
bool _customEtpSettingsChanged(
|
||||||
@@ -55,6 +59,22 @@ bool _customEtpSettingsChanged(
|
|||||||
previous.allowListConvenience != current.allowListConvenience;
|
previous.allowListConvenience != current.allowListConvenience;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> syncUBlockFilterLists(
|
||||||
|
PreferenceFixator fixator,
|
||||||
|
EngineSettings settings,
|
||||||
|
) async {
|
||||||
|
if (!settings.ublockFilterListSettings.enabled) {
|
||||||
|
await fixator.unregister(ublockFilterListsPref);
|
||||||
|
await GeckoPrefService().resetPrefs([ublockFilterListsPref]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fixator.register(
|
||||||
|
ublockFilterListsPref,
|
||||||
|
jsonEncode(settings.ublockFilterListSettings.resolveFinalList()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class EngineSettingsReplicationService
|
class EngineSettingsReplicationService
|
||||||
extends _$EngineSettingsReplicationService {
|
extends _$EngineSettingsReplicationService {
|
||||||
@@ -296,6 +316,13 @@ class EngineSettingsReplicationService
|
|||||||
if (previous.value?.lnaEnabled != settings.lnaEnabled) {
|
if (previous.value?.lnaEnabled != settings.lnaEnabled) {
|
||||||
await _service.lnaEnabled(settings.lnaEnabled);
|
await _service.lnaEnabled(settings.lnaEnabled);
|
||||||
}
|
}
|
||||||
|
if (previous.value?.ublockFilterListSettings !=
|
||||||
|
settings.ublockFilterListSettings) {
|
||||||
|
await syncUBlockFilterLists(
|
||||||
|
ref.read(preferenceFixatorProvider.notifier),
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await _service.setDefaultSettings(settings);
|
await _service.setDefaultSettings(settings);
|
||||||
await ref
|
await ref
|
||||||
@@ -319,6 +346,10 @@ class EngineSettingsReplicationService
|
|||||||
await ref
|
await ref
|
||||||
.read(preferenceFixatorProvider.notifier)
|
.read(preferenceFixatorProvider.notifier)
|
||||||
.register('intl.accept_languages', settings.locales.join(','));
|
.register('intl.accept_languages', settings.locales.join(','));
|
||||||
|
await syncUBlockFilterLists(
|
||||||
|
ref.read(preferenceFixatorProvider.notifier),
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize unsigned extensions fixator from Gecko pref
|
// Initialize unsigned extensions fixator from Gecko pref
|
||||||
await ref.read(allowUnsignedExtensionsProvider.future);
|
await ref.read(allowUnsignedExtensionsProvider.future);
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$engineSettingsReplicationServiceHash() =>
|
String _$engineSettingsReplicationServiceHash() =>
|
||||||
r'061f0b32a9c49e3bd4b76cd99993620558e0d3b2';
|
r'71b611a99af83187e392d16288cec280ef44269f';
|
||||||
|
|
||||||
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
|
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+1
-2
@@ -36,7 +36,6 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
import 'package:weblibre/core/design/app_colors.dart';
|
import 'package:weblibre/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/core/providers/persisted_bool.dart';
|
import 'package:weblibre/core/providers/persisted_bool.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/addons/presentation/screens/addon_internal_settings.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
@@ -1715,7 +1714,7 @@ class _ExtensionsCard extends HookConsumerWidget {
|
|||||||
final rootContext = Navigator.of(context, rootNavigator: true).context;
|
final rootContext = Navigator.of(context, rootNavigator: true).context;
|
||||||
Future<void> openExtensionSettings(String extensionId) async {
|
Future<void> openExtensionSettings(String extensionId) async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
await openAddonSettingsFlowById(rootContext, ref, extensionId);
|
await AddonDetailsRoute(addonId: extensionId).push<void>(rootContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildMenuCard(
|
return _buildMenuCard(
|
||||||
|
|||||||
+1
@@ -169,6 +169,7 @@ class CompactAppBarTitleView extends StatelessWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: UriBreadcrumb(
|
child: UriBreadcrumb(
|
||||||
uri: tabState.url,
|
uri: tabState.url,
|
||||||
|
showHttpScheme: false,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
|
|||||||
+2
-1
@@ -546,6 +546,7 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: UriBreadcrumb(
|
child: UriBreadcrumb(
|
||||||
uri: tabState.url,
|
uri: tabState.url,
|
||||||
|
showHttpScheme: false,
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: subtitleColor,
|
color: subtitleColor,
|
||||||
),
|
),
|
||||||
@@ -636,7 +637,7 @@ class SyncedListTabPreview extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
UriBreadcrumb(uri: url),
|
UriBreadcrumb(uri: url, showHttpScheme: false),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.open_in_new),
|
trailing: const Icon(Icons.open_in_new),
|
||||||
|
|||||||
+4
-1
@@ -77,7 +77,10 @@ class BookmarkSearch extends HookConsumerWidget {
|
|||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
subtitle: UriBreadcrumb(uri: bookmark.url),
|
subtitle: UriBreadcrumb(
|
||||||
|
uri: bookmark.url,
|
||||||
|
showHttpScheme: false,
|
||||||
|
),
|
||||||
onTap: () => onUriSelected(bookmark.url),
|
onTap: () => onUriSelected(bookmark.url),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-1
@@ -112,7 +112,10 @@ class HistorySuggestions extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
subtitle:
|
subtitle:
|
||||||
uri.mapNotNull(
|
uri.mapNotNull(
|
||||||
(uri) => UriBreadcrumb(uri: uri),
|
(uri) => UriBreadcrumb(
|
||||||
|
uri: uri,
|
||||||
|
showHttpScheme: false,
|
||||||
|
),
|
||||||
) ??
|
) ??
|
||||||
suggestion.description.mapNotNull(
|
suggestion.description.mapNotNull(
|
||||||
(description) => Text(
|
(description) => Text(
|
||||||
|
|||||||
+1
-1
@@ -244,7 +244,7 @@ class TabSearch extends HookConsumerWidget {
|
|||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: UriBreadcrumb(uri: result.url),
|
: UriBreadcrumb(uri: result.url, showHttpScheme: false),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabDataRepositoryHash() => r'c64a44c7d9de566617798a0a3bdcad51bf436658';
|
String _$tabDataRepositoryHash() => r'e6482b5408aa567f626d2f0a9455b2cc908ba84f';
|
||||||
|
|
||||||
abstract class _$TabDataRepository extends $Notifier<void> {
|
abstract class _$TabDataRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -20,11 +20,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/engine_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/ublock_filter_list_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/data/providers/ublock_assets.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
||||||
@@ -36,6 +42,7 @@ class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final installUBlock = useState(true);
|
||||||
|
|
||||||
return BrowserPage(
|
return BrowserPage(
|
||||||
child: BrowserPageContent(
|
child: BrowserPageContent(
|
||||||
@@ -66,6 +73,7 @@ There are many other lists available to block even more.
|
|||||||
FormField(
|
FormField(
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
onSaved: (newValue) {
|
onSaved: (newValue) {
|
||||||
|
installUBlock.value = newValue ?? false;
|
||||||
if (newValue == true) {
|
if (newValue == true) {
|
||||||
unawaited(
|
unawaited(
|
||||||
ref
|
ref
|
||||||
@@ -80,9 +88,53 @@ There are many other lists available to block even more.
|
|||||||
title: const Text('Install uBlock Origin Extension'),
|
title: const Text('Install uBlock Origin Extension'),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
field.didChange(value);
|
field.didChange(value);
|
||||||
|
installUBlock.value = value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
FormField(
|
||||||
|
initialValue: true,
|
||||||
|
onSaved: (newValue) {
|
||||||
|
if (newValue != true || !installUBlock.value) return;
|
||||||
|
|
||||||
|
Future<void> applyOptimizedDefaults() async {
|
||||||
|
try {
|
||||||
|
final registry = await ref.read(
|
||||||
|
ublockAssetsRegistryProvider.future,
|
||||||
|
);
|
||||||
|
final optimized =
|
||||||
|
UBlockFilterListSettings.optimizedDefaults(registry);
|
||||||
|
await ref
|
||||||
|
.read(engineSettingsRepositoryProvider.notifier)
|
||||||
|
.updateSettings(
|
||||||
|
(current) => current.copyWith
|
||||||
|
.ublockFilterListSettings(optimized),
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
logger.w(
|
||||||
|
'Failed applying optimized uBlock defaults during onboarding',
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(applyOptimizedDefaults());
|
||||||
|
},
|
||||||
|
builder: (field) => SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: field.value ?? false,
|
||||||
|
title: const Text('Apply optimized defaults'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Enable WebLibre hardening filter lists.',
|
||||||
|
),
|
||||||
|
onChanged: installUBlock.value
|
||||||
|
? (value) {
|
||||||
|
field.didChange(value);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -292,24 +292,17 @@ class _ErrorLogsTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomListTile(
|
return ListTile(
|
||||||
title: 'Error Logs',
|
leading: Icon(
|
||||||
subtitle: 'View and copy logs for issue reporting',
|
Icons.bug_report,
|
||||||
prefix: Padding(
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
|
||||||
child: Icon(
|
|
||||||
Icons.bug_report,
|
|
||||||
size: 24,
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
suffix: FilledButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
await ErrorLogsRoute().push(context);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.open_in_new),
|
|
||||||
label: const Text('View'),
|
|
||||||
),
|
),
|
||||||
|
title: const Text('Error Logs'),
|
||||||
|
subtitle: const Text('View and copy logs for issue reporting'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
await ErrorLogsRoute().push(context);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,25 +65,18 @@ class _ManageExtensionsTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomListTile(
|
return ListTile(
|
||||||
title: 'Manage Extensions',
|
leading: Icon(
|
||||||
|
MdiIcons.puzzleEdit,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
title: const Text('Manage Extensions'),
|
||||||
subtitle:
|
subtitle:
|
||||||
'Browse installed, disabled, available, and unsupported extensions',
|
const Text('Browse installed, disabled, available, and unsupported extensions'),
|
||||||
prefix: Padding(
|
trailing: const Icon(Icons.chevron_right),
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
onTap: () async {
|
||||||
child: Icon(
|
await const AddonManagerRoute().push<void>(context);
|
||||||
MdiIcons.puzzleEdit,
|
},
|
||||||
size: 24,
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
suffix: FilledButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
await const AddonManagerRoute().push<void>(context);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.open_in_new),
|
|
||||||
label: const Text('Open'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,24 +86,17 @@ class _AddonCollectionTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomListTile(
|
return ListTile(
|
||||||
title: 'Custom Collection',
|
leading: Icon(
|
||||||
subtitle: 'Use a custom Mozilla addon collection',
|
MdiIcons.folderMultiple,
|
||||||
prefix: Padding(
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
|
||||||
child: Icon(
|
|
||||||
MdiIcons.folderMultiple,
|
|
||||||
size: 24,
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
suffix: FilledButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
await AddonCollectionRoute().push(context);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.settings),
|
|
||||||
label: const Text('Configure'),
|
|
||||||
),
|
),
|
||||||
|
title: const Text('Custom Collection'),
|
||||||
|
subtitle: const Text('Use a custom Mozilla addon collection'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
await AddonCollectionRoute().push(context);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -97,6 +97,7 @@ class _TrackingProtectionSection extends StatelessWidget {
|
|||||||
_BounceTrackingProtectionTile(),
|
_BounceTrackingProtectionTile(),
|
||||||
_QueryParameterStrippingSection(),
|
_QueryParameterStrippingSection(),
|
||||||
_TrackingProtectionExceptionsTile(),
|
_TrackingProtectionExceptionsTile(),
|
||||||
|
_UBlockFilterListsTile(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -727,6 +728,27 @@ class _WebEngineHardeningTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _UBlockFilterListsTile extends StatelessWidget {
|
||||||
|
const _UBlockFilterListsTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('uBlock Filter Lists & Hardenings'),
|
||||||
|
subtitle: const Text('Manage filter lists and apply WebLibre hardenings'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.filter_list),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
await UBlockFilterListsRoute().push<void>(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _FissionEnabledTile extends HookConsumerWidget {
|
class _FissionEnabledTile extends HookConsumerWidget {
|
||||||
const _FissionEnabledTile();
|
const _FissionEnabledTile();
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -279,6 +279,7 @@ class _HistoryListItem extends ConsumerWidget {
|
|||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
UriBreadcrumb(
|
UriBreadcrumb(
|
||||||
uri: visit.url,
|
uri: visit.url,
|
||||||
|
showHttpScheme: false,
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/ublock_filter_list_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/entities/fingerprint_overrides.dart';
|
import 'package:weblibre/features/user/domain/entities/fingerprint_overrides.dart';
|
||||||
|
|
||||||
part 'engine_settings.g.dart';
|
part 'engine_settings.g.dart';
|
||||||
@@ -133,6 +134,12 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
|
|||||||
@JsonKey(fromJson: _addonCollectionFromJson, toJson: _addonCollectionToJson)
|
@JsonKey(fromJson: _addonCollectionFromJson, toJson: _addonCollectionToJson)
|
||||||
final AddonCollection? addonCollection;
|
final AddonCollection? addonCollection;
|
||||||
|
|
||||||
|
@JsonKey(
|
||||||
|
fromJson: _ublockFilterListSettingsFromJson,
|
||||||
|
toJson: _ublockFilterListSettingsToJson,
|
||||||
|
)
|
||||||
|
final UBlockFilterListSettings ublockFilterListSettings;
|
||||||
|
|
||||||
final DohSettingsMode dohSettingsMode;
|
final DohSettingsMode dohSettingsMode;
|
||||||
final String dohProviderUrl;
|
final String dohProviderUrl;
|
||||||
final String dohDefaultProviderUrl;
|
final String dohDefaultProviderUrl;
|
||||||
@@ -179,6 +186,7 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
|
|||||||
required this.queryParameterStripping,
|
required this.queryParameterStripping,
|
||||||
required this.bounceTrackingProtectionMode,
|
required this.bounceTrackingProtectionMode,
|
||||||
required this.addonCollection,
|
required this.addonCollection,
|
||||||
|
required this.ublockFilterListSettings,
|
||||||
required this.dohSettingsMode,
|
required this.dohSettingsMode,
|
||||||
required this.dohProviderUrl,
|
required this.dohProviderUrl,
|
||||||
required this.dohDefaultProviderUrl,
|
required this.dohDefaultProviderUrl,
|
||||||
@@ -232,6 +240,7 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
|
|||||||
super.userAgent,
|
super.userAgent,
|
||||||
bool? enterpriseRootsEnabled,
|
bool? enterpriseRootsEnabled,
|
||||||
this.addonCollection,
|
this.addonCollection,
|
||||||
|
UBlockFilterListSettings? ublockFilterListSettings,
|
||||||
DohSettingsMode? dohSettingsMode,
|
DohSettingsMode? dohSettingsMode,
|
||||||
String? dohProviderUrl,
|
String? dohProviderUrl,
|
||||||
String? dohDefaultProviderUrl,
|
String? dohDefaultProviderUrl,
|
||||||
@@ -267,7 +276,9 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
|
|||||||
super.lnaBlocking,
|
super.lnaBlocking,
|
||||||
bool? lnaBlockTrackers,
|
bool? lnaBlockTrackers,
|
||||||
bool? lnaEnabled,
|
bool? lnaEnabled,
|
||||||
}) : queryParameterStripping =
|
}) : ublockFilterListSettings =
|
||||||
|
ublockFilterListSettings ?? UBlockFilterListSettings(),
|
||||||
|
queryParameterStripping =
|
||||||
queryParameterStripping ?? QueryParameterStripping.enabled,
|
queryParameterStripping ?? QueryParameterStripping.enabled,
|
||||||
bounceTrackingProtectionMode =
|
bounceTrackingProtectionMode =
|
||||||
bounceTrackingProtectionMode ?? BounceTrackingProtectionMode.enabled,
|
bounceTrackingProtectionMode ?? BounceTrackingProtectionMode.enabled,
|
||||||
@@ -341,6 +352,20 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
|
|||||||
static String? _addonCollectionToJson(AddonCollection? collection) =>
|
static String? _addonCollectionToJson(AddonCollection? collection) =>
|
||||||
collection.mapNotNull((collection) => jsonEncode(collection.encode()));
|
collection.mapNotNull((collection) => jsonEncode(collection.encode()));
|
||||||
|
|
||||||
|
static UBlockFilterListSettings _ublockFilterListSettingsFromJson(
|
||||||
|
String? json,
|
||||||
|
) =>
|
||||||
|
json.mapNotNull(
|
||||||
|
(encoded) => UBlockFilterListSettings.fromJson(
|
||||||
|
jsonDecode(encoded) as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
UBlockFilterListSettings();
|
||||||
|
|
||||||
|
static String _ublockFilterListSettingsToJson(
|
||||||
|
UBlockFilterListSettings settings,
|
||||||
|
) => jsonEncode(settings.toJson());
|
||||||
|
|
||||||
factory EngineSettings.fromJson(Map<String, dynamic> json) =>
|
factory EngineSettings.fromJson(Map<String, dynamic> json) =>
|
||||||
_$EngineSettingsFromJson(json);
|
_$EngineSettingsFromJson(json);
|
||||||
|
|
||||||
@@ -363,6 +388,7 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
|
|||||||
queryParameterStripping,
|
queryParameterStripping,
|
||||||
bounceTrackingProtectionMode,
|
bounceTrackingProtectionMode,
|
||||||
addonCollection,
|
addonCollection,
|
||||||
|
ublockFilterListSettings,
|
||||||
dohSettingsMode,
|
dohSettingsMode,
|
||||||
dohProviderUrl,
|
dohProviderUrl,
|
||||||
dohDefaultProviderUrl,
|
dohDefaultProviderUrl,
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ abstract class _$EngineSettingsCWProxy {
|
|||||||
|
|
||||||
EngineSettings addonCollection(AddonCollection? addonCollection);
|
EngineSettings addonCollection(AddonCollection? addonCollection);
|
||||||
|
|
||||||
|
EngineSettings ublockFilterListSettings(
|
||||||
|
UBlockFilterListSettings ublockFilterListSettings,
|
||||||
|
);
|
||||||
|
|
||||||
EngineSettings dohSettingsMode(DohSettingsMode dohSettingsMode);
|
EngineSettings dohSettingsMode(DohSettingsMode dohSettingsMode);
|
||||||
|
|
||||||
EngineSettings dohProviderUrl(String dohProviderUrl);
|
EngineSettings dohProviderUrl(String dohProviderUrl);
|
||||||
@@ -152,6 +156,7 @@ abstract class _$EngineSettingsCWProxy {
|
|||||||
QueryParameterStripping queryParameterStripping,
|
QueryParameterStripping queryParameterStripping,
|
||||||
BounceTrackingProtectionMode bounceTrackingProtectionMode,
|
BounceTrackingProtectionMode bounceTrackingProtectionMode,
|
||||||
AddonCollection? addonCollection,
|
AddonCollection? addonCollection,
|
||||||
|
UBlockFilterListSettings ublockFilterListSettings,
|
||||||
DohSettingsMode dohSettingsMode,
|
DohSettingsMode dohSettingsMode,
|
||||||
String dohProviderUrl,
|
String dohProviderUrl,
|
||||||
String dohDefaultProviderUrl,
|
String dohDefaultProviderUrl,
|
||||||
@@ -271,6 +276,11 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy {
|
|||||||
EngineSettings addonCollection(AddonCollection? addonCollection) =>
|
EngineSettings addonCollection(AddonCollection? addonCollection) =>
|
||||||
call(addonCollection: addonCollection);
|
call(addonCollection: addonCollection);
|
||||||
|
|
||||||
|
@override
|
||||||
|
EngineSettings ublockFilterListSettings(
|
||||||
|
UBlockFilterListSettings ublockFilterListSettings,
|
||||||
|
) => call(ublockFilterListSettings: ublockFilterListSettings);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EngineSettings dohSettingsMode(DohSettingsMode dohSettingsMode) =>
|
EngineSettings dohSettingsMode(DohSettingsMode dohSettingsMode) =>
|
||||||
call(dohSettingsMode: dohSettingsMode);
|
call(dohSettingsMode: dohSettingsMode);
|
||||||
@@ -442,6 +452,7 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy {
|
|||||||
Object? queryParameterStripping = const $CopyWithPlaceholder(),
|
Object? queryParameterStripping = const $CopyWithPlaceholder(),
|
||||||
Object? bounceTrackingProtectionMode = const $CopyWithPlaceholder(),
|
Object? bounceTrackingProtectionMode = const $CopyWithPlaceholder(),
|
||||||
Object? addonCollection = const $CopyWithPlaceholder(),
|
Object? addonCollection = const $CopyWithPlaceholder(),
|
||||||
|
Object? ublockFilterListSettings = const $CopyWithPlaceholder(),
|
||||||
Object? dohSettingsMode = const $CopyWithPlaceholder(),
|
Object? dohSettingsMode = const $CopyWithPlaceholder(),
|
||||||
Object? dohProviderUrl = const $CopyWithPlaceholder(),
|
Object? dohProviderUrl = const $CopyWithPlaceholder(),
|
||||||
Object? dohDefaultProviderUrl = const $CopyWithPlaceholder(),
|
Object? dohDefaultProviderUrl = const $CopyWithPlaceholder(),
|
||||||
@@ -554,6 +565,12 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy {
|
|||||||
? _value.addonCollection
|
? _value.addonCollection
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: addonCollection as AddonCollection?,
|
: addonCollection as AddonCollection?,
|
||||||
|
ublockFilterListSettings:
|
||||||
|
ublockFilterListSettings == const $CopyWithPlaceholder() ||
|
||||||
|
ublockFilterListSettings == null
|
||||||
|
? _value.ublockFilterListSettings
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: ublockFilterListSettings as UBlockFilterListSettings,
|
||||||
dohSettingsMode:
|
dohSettingsMode:
|
||||||
dohSettingsMode == const $CopyWithPlaceholder() ||
|
dohSettingsMode == const $CopyWithPlaceholder() ||
|
||||||
dohSettingsMode == null
|
dohSettingsMode == null
|
||||||
@@ -777,6 +794,9 @@ EngineSettings _$EngineSettingsFromJson(
|
|||||||
addonCollection: EngineSettings._addonCollectionFromJson(
|
addonCollection: EngineSettings._addonCollectionFromJson(
|
||||||
json['addonCollection'] as String?,
|
json['addonCollection'] as String?,
|
||||||
),
|
),
|
||||||
|
ublockFilterListSettings: EngineSettings._ublockFilterListSettingsFromJson(
|
||||||
|
json['ublockFilterListSettings'] as String?,
|
||||||
|
),
|
||||||
dohSettingsMode: $enumDecodeNullable(
|
dohSettingsMode: $enumDecodeNullable(
|
||||||
_$DohSettingsModeEnumMap,
|
_$DohSettingsModeEnumMap,
|
||||||
json['dohSettingsMode'],
|
json['dohSettingsMode'],
|
||||||
@@ -893,6 +913,9 @@ Map<String, dynamic> _$EngineSettingsToJson(
|
|||||||
'addonCollection': EngineSettings._addonCollectionToJson(
|
'addonCollection': EngineSettings._addonCollectionToJson(
|
||||||
instance.addonCollection,
|
instance.addonCollection,
|
||||||
),
|
),
|
||||||
|
'ublockFilterListSettings': EngineSettings._ublockFilterListSettingsToJson(
|
||||||
|
instance.ublockFilterListSettings,
|
||||||
|
),
|
||||||
'dohSettingsMode': _$DohSettingsModeEnumMap[instance.dohSettingsMode]!,
|
'dohSettingsMode': _$DohSettingsModeEnumMap[instance.dohSettingsMode]!,
|
||||||
'dohProviderUrl': instance.dohProviderUrl,
|
'dohProviderUrl': instance.dohProviderUrl,
|
||||||
'dohDefaultProviderUrl': instance.dohDefaultProviderUrl,
|
'dohDefaultProviderUrl': instance.dohDefaultProviderUrl,
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'ublock_asset.g.dart';
|
||||||
|
|
||||||
|
enum UBlockAssetGroup {
|
||||||
|
@JsonValue('default')
|
||||||
|
$default,
|
||||||
|
@JsonValue('ads')
|
||||||
|
ads,
|
||||||
|
@JsonValue('privacy')
|
||||||
|
privacy,
|
||||||
|
@JsonValue('malware')
|
||||||
|
malware,
|
||||||
|
@JsonValue('annoyances')
|
||||||
|
annoyances,
|
||||||
|
@JsonValue('multipurpose')
|
||||||
|
multipurpose,
|
||||||
|
@JsonValue('regions')
|
||||||
|
regions;
|
||||||
|
|
||||||
|
String get label => switch (this) {
|
||||||
|
$default => 'Default',
|
||||||
|
ads => 'Ads',
|
||||||
|
privacy => 'Privacy',
|
||||||
|
malware => 'Malware',
|
||||||
|
annoyances => 'Annoyances',
|
||||||
|
multipurpose => 'Multipurpose',
|
||||||
|
regions => 'Regions',
|
||||||
|
};
|
||||||
|
|
||||||
|
static const displayOrder = UBlockAssetGroup.values;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UBlockAssetSubGroup {
|
||||||
|
@JsonValue('cookies')
|
||||||
|
cookies,
|
||||||
|
@JsonValue('social')
|
||||||
|
social;
|
||||||
|
|
||||||
|
String get label => switch (this) {
|
||||||
|
cookies => 'Cookie Notices',
|
||||||
|
social => 'Social Widgets',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _contentUrlFromJson(dynamic value) {
|
||||||
|
if (value is List) return value.cast<String>();
|
||||||
|
if (value is String) return [value];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic _contentUrlToJson(List<String> value) {
|
||||||
|
if (value.length == 1) return value.first;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class UBlockAssetEntry {
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final UBlockAssetGroup? group;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final UBlockAssetSubGroup? group2;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? parent;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
@JsonKey(
|
||||||
|
includeIfNull: false,
|
||||||
|
fromJson: _contentUrlFromJson,
|
||||||
|
toJson: _contentUrlToJson,
|
||||||
|
)
|
||||||
|
final List<String> contentURL;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final List<String>? cdnURLs;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final List<String>? patchURLs;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? supportURL;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? instructionURL;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? tags;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? lang;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? ua;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false, defaultValue: false)
|
||||||
|
final bool off;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false, defaultValue: false)
|
||||||
|
final bool preferred;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final int? updateAfter;
|
||||||
|
|
||||||
|
const UBlockAssetEntry({
|
||||||
|
required this.content,
|
||||||
|
this.group,
|
||||||
|
this.group2,
|
||||||
|
this.parent,
|
||||||
|
this.title,
|
||||||
|
this.contentURL = const [],
|
||||||
|
this.cdnURLs,
|
||||||
|
this.patchURLs,
|
||||||
|
this.supportURL,
|
||||||
|
this.instructionURL,
|
||||||
|
this.tags,
|
||||||
|
this.lang,
|
||||||
|
this.ua,
|
||||||
|
this.off = false,
|
||||||
|
this.preferred = false,
|
||||||
|
this.updateAfter,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isFilterList => content == 'filters';
|
||||||
|
|
||||||
|
bool get isDefaultEnabled => !off;
|
||||||
|
|
||||||
|
UBlockAssetGroup get effectiveGroup =>
|
||||||
|
group2?.toGroup() ?? group ?? UBlockAssetGroup.ads;
|
||||||
|
|
||||||
|
factory UBlockAssetEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UBlockAssetEntryFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$UBlockAssetEntryToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on UBlockAssetSubGroup {
|
||||||
|
UBlockAssetGroup toGroup() => switch (this) {
|
||||||
|
UBlockAssetSubGroup.cookies => UBlockAssetGroup.annoyances,
|
||||||
|
UBlockAssetSubGroup.social => UBlockAssetGroup.annoyances,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class UBlockAssetsRegistry {
|
||||||
|
final Map<String, UBlockAssetEntry> _entries;
|
||||||
|
|
||||||
|
UBlockAssetsRegistry(this._entries);
|
||||||
|
|
||||||
|
Map<String, UBlockAssetEntry> get filterEntries =>
|
||||||
|
Map.fromEntries(_entries.entries.where((e) => e.value.isFilterList));
|
||||||
|
|
||||||
|
List<String> get defaultEnabledTokens => _entries.entries
|
||||||
|
.where((e) => e.value.isFilterList && e.value.isDefaultEnabled)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
UBlockAssetEntry? operator [](String key) => _entries[key];
|
||||||
|
|
||||||
|
Map<UBlockAssetGroup, Map<String?, List<String>>> buildGroupedParentTree() {
|
||||||
|
final result = <UBlockAssetGroup, Map<String?, List<String>>>{};
|
||||||
|
|
||||||
|
for (final group in UBlockAssetGroup.displayOrder) {
|
||||||
|
final groupEntries = <String?, List<String>>{};
|
||||||
|
final processedKeys = <String>{};
|
||||||
|
|
||||||
|
final groupSubGroups = UBlockAssetSubGroup.values
|
||||||
|
.where((sg) => sg.toGroup() == group)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (final subGroup in groupSubGroups) {
|
||||||
|
for (final entry in _entries.entries.where(
|
||||||
|
(e) => e.value.isFilterList && e.value.group2 == subGroup,
|
||||||
|
)) {
|
||||||
|
final parentKey = entry.value.parent;
|
||||||
|
groupEntries.putIfAbsent(parentKey, () => []).add(entry.key);
|
||||||
|
processedKeys.add(entry.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in _entries.entries.where(
|
||||||
|
(e) =>
|
||||||
|
e.value.isFilterList &&
|
||||||
|
e.value.group == group &&
|
||||||
|
e.value.group2 == null &&
|
||||||
|
!processedKeys.contains(e.key),
|
||||||
|
)) {
|
||||||
|
final parentKey = entry.value.parent;
|
||||||
|
groupEntries.putIfAbsent(parentKey, () => []).add(entry.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupEntries.isNotEmpty) {
|
||||||
|
result[group] = groupEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> tokensMatchingLocales(Iterable<String> languageCodes) {
|
||||||
|
final primaryCodes = languageCodes.map((code) {
|
||||||
|
final parts = code.split('-');
|
||||||
|
return parts.first.toLowerCase();
|
||||||
|
}).toSet();
|
||||||
|
|
||||||
|
return _entries.entries
|
||||||
|
.where((e) {
|
||||||
|
if (!e.value.isFilterList || !e.value.off) return false;
|
||||||
|
final lang = e.value.lang;
|
||||||
|
if (lang == null) return false;
|
||||||
|
final entryLangs = lang
|
||||||
|
.split(RegExp(r'\s+'))
|
||||||
|
.map((l) => l.toLowerCase())
|
||||||
|
.toSet();
|
||||||
|
return primaryCodes.intersection(entryLangs).isNotEmpty;
|
||||||
|
})
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
int enabledCountInGroup(UBlockAssetGroup group, Set<String> enabledTokens) {
|
||||||
|
var count = 0;
|
||||||
|
for (final entry in _entries.entries) {
|
||||||
|
if (!entry.value.isFilterList) continue;
|
||||||
|
if (entry.value.effectiveGroup != group) continue;
|
||||||
|
if (enabledTokens.contains(entry.key)) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalCountInGroup(UBlockAssetGroup group) {
|
||||||
|
var count = 0;
|
||||||
|
for (final entry in _entries.entries) {
|
||||||
|
if (!entry.value.isFilterList) continue;
|
||||||
|
if (entry.value.effectiveGroup != group) continue;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UBlockAssetsRegistry fromJson(Map<String, dynamic> json) {
|
||||||
|
final entries = <String, UBlockAssetEntry>{};
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
if (entry.value is Map<String, dynamic>) {
|
||||||
|
entries[entry.key] = UBlockAssetEntry.fromJson(
|
||||||
|
entry.value as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UBlockAssetsRegistry(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'ublock_asset.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
UBlockAssetEntry _$UBlockAssetEntryFromJson(Map<String, dynamic> json) =>
|
||||||
|
UBlockAssetEntry(
|
||||||
|
content: json['content'] as String,
|
||||||
|
group: $enumDecodeNullable(_$UBlockAssetGroupEnumMap, json['group']),
|
||||||
|
group2: $enumDecodeNullable(_$UBlockAssetSubGroupEnumMap, json['group2']),
|
||||||
|
parent: json['parent'] as String?,
|
||||||
|
title: json['title'] as String?,
|
||||||
|
contentURL: json['contentURL'] == null
|
||||||
|
? const []
|
||||||
|
: _contentUrlFromJson(json['contentURL']),
|
||||||
|
cdnURLs: (json['cdnURLs'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
patchURLs: (json['patchURLs'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
supportURL: json['supportURL'] as String?,
|
||||||
|
instructionURL: json['instructionURL'] as String?,
|
||||||
|
tags: json['tags'] as String?,
|
||||||
|
lang: json['lang'] as String?,
|
||||||
|
ua: json['ua'] as String?,
|
||||||
|
off: json['off'] as bool? ?? false,
|
||||||
|
preferred: json['preferred'] as bool? ?? false,
|
||||||
|
updateAfter: (json['updateAfter'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UBlockAssetEntryToJson(UBlockAssetEntry instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'content': instance.content,
|
||||||
|
'group': ?_$UBlockAssetGroupEnumMap[instance.group],
|
||||||
|
'group2': ?_$UBlockAssetSubGroupEnumMap[instance.group2],
|
||||||
|
'parent': ?instance.parent,
|
||||||
|
'title': ?instance.title,
|
||||||
|
'contentURL': ?_contentUrlToJson(instance.contentURL),
|
||||||
|
'cdnURLs': ?instance.cdnURLs,
|
||||||
|
'patchURLs': ?instance.patchURLs,
|
||||||
|
'supportURL': ?instance.supportURL,
|
||||||
|
'instructionURL': ?instance.instructionURL,
|
||||||
|
'tags': ?instance.tags,
|
||||||
|
'lang': ?instance.lang,
|
||||||
|
'ua': ?instance.ua,
|
||||||
|
'off': instance.off,
|
||||||
|
'preferred': instance.preferred,
|
||||||
|
'updateAfter': ?instance.updateAfter,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$UBlockAssetGroupEnumMap = {
|
||||||
|
UBlockAssetGroup.$default: 'default',
|
||||||
|
UBlockAssetGroup.ads: 'ads',
|
||||||
|
UBlockAssetGroup.privacy: 'privacy',
|
||||||
|
UBlockAssetGroup.malware: 'malware',
|
||||||
|
UBlockAssetGroup.annoyances: 'annoyances',
|
||||||
|
UBlockAssetGroup.multipurpose: 'multipurpose',
|
||||||
|
UBlockAssetGroup.regions: 'regions',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$UBlockAssetSubGroupEnumMap = {
|
||||||
|
UBlockAssetSubGroup.cookies: 'cookies',
|
||||||
|
UBlockAssetSubGroup.social: 'social',
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/ublock_asset.dart';
|
||||||
|
|
||||||
|
part 'ublock_filter_list_settings.g.dart';
|
||||||
|
|
||||||
|
const kUBlockMaxExternalUrls = 32;
|
||||||
|
|
||||||
|
const kUBlockHardeningStockTokens = <String>[
|
||||||
|
'adguard-mobile',
|
||||||
|
'adguard-mobile-app-banners',
|
||||||
|
'adguard-spyware-url',
|
||||||
|
'fanboy-cookiemonster',
|
||||||
|
'fanboy-social',
|
||||||
|
'ublock-annoyances',
|
||||||
|
];
|
||||||
|
|
||||||
|
final kUBlockHardeningExternalLists = <UBlockExternalList>[
|
||||||
|
UBlockExternalList(
|
||||||
|
url:
|
||||||
|
'https://raw.githubusercontent.com/DandelionSprout/adfilt/master/LegitimateURLShortener.txt',
|
||||||
|
description: 'Legitimate URL Shortener Tool (DandelionSprout)',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@CopyWith()
|
||||||
|
@JsonSerializable(includeIfNull: true)
|
||||||
|
class UBlockExternalList with FastEquatable {
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
@JsonKey(includeIfNull: false)
|
||||||
|
final String? description;
|
||||||
|
|
||||||
|
UBlockExternalList({required this.url, this.description});
|
||||||
|
|
||||||
|
factory UBlockExternalList.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UBlockExternalListFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$UBlockExternalListToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [url, description];
|
||||||
|
}
|
||||||
|
|
||||||
|
@CopyWith()
|
||||||
|
@JsonSerializable(includeIfNull: true)
|
||||||
|
class UBlockFilterListSettings with FastEquatable {
|
||||||
|
static const String _userFiltersToken = 'user-filters';
|
||||||
|
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
final List<String> enabledStockListTokens;
|
||||||
|
|
||||||
|
final List<String> autoEnabledStockListTokens;
|
||||||
|
|
||||||
|
final bool autoSelectRegionalLists;
|
||||||
|
|
||||||
|
final List<UBlockExternalList> externalFilterLists;
|
||||||
|
|
||||||
|
UBlockFilterListSettings({
|
||||||
|
this.enabled = false,
|
||||||
|
this.enabledStockListTokens = const [],
|
||||||
|
this.autoEnabledStockListTokens = const [],
|
||||||
|
this.autoSelectRegionalLists = false,
|
||||||
|
this.externalFilterLists = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UBlockFilterListSettings.managedDefaults(UBlockAssetsRegistry registry) {
|
||||||
|
return UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: registry.defaultEnabledTokens,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory UBlockFilterListSettings.optimizedDefaults(
|
||||||
|
UBlockAssetsRegistry registry,
|
||||||
|
) {
|
||||||
|
final defaultTokens = registry.defaultEnabledTokens;
|
||||||
|
|
||||||
|
final stockTokens = [...defaultTokens];
|
||||||
|
for (final token in kUBlockHardeningStockTokens) {
|
||||||
|
if (!stockTokens.contains(token)) {
|
||||||
|
stockTokens.add(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final externals = <UBlockExternalList>[];
|
||||||
|
final seenUrls = <String>{};
|
||||||
|
for (final entry in kUBlockHardeningExternalLists) {
|
||||||
|
if (seenUrls.add(entry.url) &&
|
||||||
|
externals.length < kUBlockMaxExternalUrls) {
|
||||||
|
externals.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: stockTokens,
|
||||||
|
externalFilterLists: externals,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory UBlockFilterListSettings.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UBlockFilterListSettingsFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$UBlockFilterListSettingsToJson(this);
|
||||||
|
|
||||||
|
List<String> resolveFinalList() {
|
||||||
|
if (!enabled) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = <String>[];
|
||||||
|
final seen = <String>{_userFiltersToken};
|
||||||
|
|
||||||
|
result.add(_userFiltersToken);
|
||||||
|
|
||||||
|
for (final token in enabledStockListTokens) {
|
||||||
|
if (seen.add(token)) {
|
||||||
|
result.add(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final token in autoEnabledStockListTokens) {
|
||||||
|
if (seen.add(token)) {
|
||||||
|
result.add(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in externalFilterLists) {
|
||||||
|
final url = entry.url.trim();
|
||||||
|
if (url.isEmpty) continue;
|
||||||
|
final parsed = Uri.tryParse(url);
|
||||||
|
if (parsed == null) continue;
|
||||||
|
if (parsed.scheme != 'http' && parsed.scheme != 'https') continue;
|
||||||
|
if (!parsed.hasAuthority) continue;
|
||||||
|
if (seen.add(url)) {
|
||||||
|
result.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTokenEnabled(String token) =>
|
||||||
|
enabledStockListTokens.contains(token) ||
|
||||||
|
autoEnabledStockListTokens.contains(token);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
enabled,
|
||||||
|
enabledStockListTokens,
|
||||||
|
autoEnabledStockListTokens,
|
||||||
|
autoSelectRegionalLists,
|
||||||
|
externalFilterLists,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'ublock_filter_list_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class _$UBlockExternalListCWProxy {
|
||||||
|
UBlockExternalList url(String url);
|
||||||
|
|
||||||
|
UBlockExternalList description(String? description);
|
||||||
|
|
||||||
|
/// 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 `UBlockExternalList(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// UBlockExternalList(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
UBlockExternalList call({String url, String? description});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callable proxy for `copyWith` functionality.
|
||||||
|
/// Use as `instanceOfUBlockExternalList.copyWith(...)` or call `instanceOfUBlockExternalList.copyWith.fieldName(value)` for a single field.
|
||||||
|
class _$UBlockExternalListCWProxyImpl implements _$UBlockExternalListCWProxy {
|
||||||
|
const _$UBlockExternalListCWProxyImpl(this._value);
|
||||||
|
|
||||||
|
final UBlockExternalList _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
UBlockExternalList url(String url) => call(url: url);
|
||||||
|
|
||||||
|
@override
|
||||||
|
UBlockExternalList description(String? description) =>
|
||||||
|
call(description: description);
|
||||||
|
|
||||||
|
@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 `UBlockExternalList(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// UBlockExternalList(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
UBlockExternalList call({
|
||||||
|
Object? url = const $CopyWithPlaceholder(),
|
||||||
|
Object? description = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
|
return UBlockExternalList(
|
||||||
|
url: url == const $CopyWithPlaceholder() || url == null
|
||||||
|
? _value.url
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: url as String,
|
||||||
|
description: description == const $CopyWithPlaceholder()
|
||||||
|
? _value.description
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: description as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $UBlockExternalListCopyWith on UBlockExternalList {
|
||||||
|
/// Returns a callable class used to build a new instance with modified fields.
|
||||||
|
/// Example: `instanceOfUBlockExternalList.copyWith(...)` or `instanceOfUBlockExternalList.copyWith.fieldName(...)`.
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_$UBlockExternalListCWProxy get copyWith =>
|
||||||
|
_$UBlockExternalListCWProxyImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$UBlockFilterListSettingsCWProxy {
|
||||||
|
UBlockFilterListSettings enabled(bool enabled);
|
||||||
|
|
||||||
|
UBlockFilterListSettings enabledStockListTokens(
|
||||||
|
List<String> enabledStockListTokens,
|
||||||
|
);
|
||||||
|
|
||||||
|
UBlockFilterListSettings autoEnabledStockListTokens(
|
||||||
|
List<String> autoEnabledStockListTokens,
|
||||||
|
);
|
||||||
|
|
||||||
|
UBlockFilterListSettings autoSelectRegionalLists(
|
||||||
|
bool autoSelectRegionalLists,
|
||||||
|
);
|
||||||
|
|
||||||
|
UBlockFilterListSettings externalFilterLists(
|
||||||
|
List<UBlockExternalList> externalFilterLists,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 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 `UBlockFilterListSettings(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// UBlockFilterListSettings(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
UBlockFilterListSettings call({
|
||||||
|
bool enabled,
|
||||||
|
List<String> enabledStockListTokens,
|
||||||
|
List<String> autoEnabledStockListTokens,
|
||||||
|
bool autoSelectRegionalLists,
|
||||||
|
List<UBlockExternalList> externalFilterLists,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callable proxy for `copyWith` functionality.
|
||||||
|
/// Use as `instanceOfUBlockFilterListSettings.copyWith(...)` or call `instanceOfUBlockFilterListSettings.copyWith.fieldName(value)` for a single field.
|
||||||
|
class _$UBlockFilterListSettingsCWProxyImpl
|
||||||
|
implements _$UBlockFilterListSettingsCWProxy {
|
||||||
|
const _$UBlockFilterListSettingsCWProxyImpl(this._value);
|
||||||
|
|
||||||
|
final UBlockFilterListSettings _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
UBlockFilterListSettings enabled(bool enabled) => call(enabled: enabled);
|
||||||
|
|
||||||
|
@override
|
||||||
|
UBlockFilterListSettings enabledStockListTokens(
|
||||||
|
List<String> enabledStockListTokens,
|
||||||
|
) => call(enabledStockListTokens: enabledStockListTokens);
|
||||||
|
|
||||||
|
@override
|
||||||
|
UBlockFilterListSettings autoEnabledStockListTokens(
|
||||||
|
List<String> autoEnabledStockListTokens,
|
||||||
|
) => call(autoEnabledStockListTokens: autoEnabledStockListTokens);
|
||||||
|
|
||||||
|
@override
|
||||||
|
UBlockFilterListSettings autoSelectRegionalLists(
|
||||||
|
bool autoSelectRegionalLists,
|
||||||
|
) => call(autoSelectRegionalLists: autoSelectRegionalLists);
|
||||||
|
|
||||||
|
@override
|
||||||
|
UBlockFilterListSettings externalFilterLists(
|
||||||
|
List<UBlockExternalList> externalFilterLists,
|
||||||
|
) => call(externalFilterLists: externalFilterLists);
|
||||||
|
|
||||||
|
@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 `UBlockFilterListSettings(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// UBlockFilterListSettings(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
UBlockFilterListSettings call({
|
||||||
|
Object? enabled = const $CopyWithPlaceholder(),
|
||||||
|
Object? enabledStockListTokens = const $CopyWithPlaceholder(),
|
||||||
|
Object? autoEnabledStockListTokens = const $CopyWithPlaceholder(),
|
||||||
|
Object? autoSelectRegionalLists = const $CopyWithPlaceholder(),
|
||||||
|
Object? externalFilterLists = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
|
return UBlockFilterListSettings(
|
||||||
|
enabled: enabled == const $CopyWithPlaceholder() || enabled == null
|
||||||
|
? _value.enabled
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: enabled as bool,
|
||||||
|
enabledStockListTokens:
|
||||||
|
enabledStockListTokens == const $CopyWithPlaceholder() ||
|
||||||
|
enabledStockListTokens == null
|
||||||
|
? _value.enabledStockListTokens
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: enabledStockListTokens as List<String>,
|
||||||
|
autoEnabledStockListTokens:
|
||||||
|
autoEnabledStockListTokens == const $CopyWithPlaceholder() ||
|
||||||
|
autoEnabledStockListTokens == null
|
||||||
|
? _value.autoEnabledStockListTokens
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: autoEnabledStockListTokens as List<String>,
|
||||||
|
autoSelectRegionalLists:
|
||||||
|
autoSelectRegionalLists == const $CopyWithPlaceholder() ||
|
||||||
|
autoSelectRegionalLists == null
|
||||||
|
? _value.autoSelectRegionalLists
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: autoSelectRegionalLists as bool,
|
||||||
|
externalFilterLists:
|
||||||
|
externalFilterLists == const $CopyWithPlaceholder() ||
|
||||||
|
externalFilterLists == null
|
||||||
|
? _value.externalFilterLists
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: externalFilterLists as List<UBlockExternalList>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $UBlockFilterListSettingsCopyWith on UBlockFilterListSettings {
|
||||||
|
/// Returns a callable class used to build a new instance with modified fields.
|
||||||
|
/// Example: `instanceOfUBlockFilterListSettings.copyWith(...)` or `instanceOfUBlockFilterListSettings.copyWith.fieldName(...)`.
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_$UBlockFilterListSettingsCWProxy get copyWith =>
|
||||||
|
_$UBlockFilterListSettingsCWProxyImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
UBlockExternalList _$UBlockExternalListFromJson(Map<String, dynamic> json) =>
|
||||||
|
UBlockExternalList(
|
||||||
|
url: json['url'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UBlockExternalListToJson(UBlockExternalList instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'url': instance.url,
|
||||||
|
'description': ?instance.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
UBlockFilterListSettings _$UBlockFilterListSettingsFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => UBlockFilterListSettings(
|
||||||
|
enabled: json['enabled'] as bool? ?? false,
|
||||||
|
enabledStockListTokens:
|
||||||
|
(json['enabledStockListTokens'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
autoEnabledStockListTokens:
|
||||||
|
(json['autoEnabledStockListTokens'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
autoSelectRegionalLists: json['autoSelectRegionalLists'] as bool? ?? false,
|
||||||
|
externalFilterLists:
|
||||||
|
(json['externalFilterLists'] as List<dynamic>?)
|
||||||
|
?.map((e) => UBlockExternalList.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UBlockFilterListSettingsToJson(
|
||||||
|
UBlockFilterListSettings instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'enabled': instance.enabled,
|
||||||
|
'enabledStockListTokens': instance.enabledStockListTokens,
|
||||||
|
'autoEnabledStockListTokens': instance.autoEnabledStockListTokens,
|
||||||
|
'autoSelectRegionalLists': instance.autoSelectRegionalLists,
|
||||||
|
'externalFilterLists': instance.externalFilterLists
|
||||||
|
.map((e) => e.toJson())
|
||||||
|
.toList(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/ublock_asset.dart';
|
||||||
|
|
||||||
|
part 'ublock_assets.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Future<UBlockAssetsRegistry> ublockAssetsRegistry(Ref ref) async {
|
||||||
|
final jsonStr = await rootBundle.loadString('assets/ublock/assets.json');
|
||||||
|
|
||||||
|
return UBlockAssetsRegistry.fromJson(
|
||||||
|
jsonDecode(jsonStr) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'ublock_assets.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(ublockAssetsRegistry)
|
||||||
|
final ublockAssetsRegistryProvider = UblockAssetsRegistryProvider._();
|
||||||
|
|
||||||
|
final class UblockAssetsRegistryProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<UBlockAssetsRegistry>,
|
||||||
|
UBlockAssetsRegistry,
|
||||||
|
FutureOr<UBlockAssetsRegistry>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<UBlockAssetsRegistry>,
|
||||||
|
$FutureProvider<UBlockAssetsRegistry> {
|
||||||
|
UblockAssetsRegistryProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'ublockAssetsRegistryProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$ublockAssetsRegistryHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<UBlockAssetsRegistry> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<UBlockAssetsRegistry> create(Ref ref) {
|
||||||
|
return ublockAssetsRegistry(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$ublockAssetsRegistryHash() =>
|
||||||
|
r'512d2bf06d16661f5b49b4b4f02840aad5722cd3';
|
||||||
@@ -100,6 +100,10 @@ class EngineSettingsRepository extends _$EngineSettingsRepository {
|
|||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
),
|
),
|
||||||
|
'ublockFilterListSettings': settings['ublockFilterListSettings']?.readAs(
|
||||||
|
DriftSqlType.string,
|
||||||
|
db.typeMapping,
|
||||||
|
),
|
||||||
'dohSettingsMode': settings['dohSettingsMode']?.readAs(
|
'dohSettingsMode': settings['dohSettingsMode']?.readAs(
|
||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ final class EngineSettingsRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$engineSettingsRepositoryHash() =>
|
String _$engineSettingsRepositoryHash() =>
|
||||||
r'f3fe745e37ec89c2d403906250aea66321c3e505';
|
r'03cfe93b6d4ac8cdb0b33627f4baa75240bff745';
|
||||||
|
|
||||||
abstract class _$EngineSettingsRepository
|
abstract class _$EngineSettingsRepository
|
||||||
extends $StreamNotifier<EngineSettings> {
|
extends $StreamNotifier<EngineSettings> {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
* 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 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:background_fetch/background_fetch.dart';
|
import 'package:background_fetch/background_fetch.dart';
|
||||||
@@ -42,7 +43,9 @@ import 'package:weblibre/core/providers/app_state.dart';
|
|||||||
import 'package:weblibre/core/providers/defaults.dart';
|
import 'package:weblibre/core/providers/defaults.dart';
|
||||||
import 'package:weblibre/core/providers/router.dart';
|
import 'package:weblibre/core/providers/router.dart';
|
||||||
import 'package:weblibre/domain/services/app_initialization.dart';
|
import 'package:weblibre/domain/services/app_initialization.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/features/web_feed/presentation/controllers/fetch_articles.dart';
|
import 'package:weblibre/features/web_feed/presentation/controllers/fetch_articles.dart';
|
||||||
@@ -179,6 +182,14 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
final generalSettings = await ref
|
final generalSettings = await ref
|
||||||
.read(generalSettingsRepositoryProvider.notifier)
|
.read(generalSettingsRepositoryProvider.notifier)
|
||||||
.fetchSettings();
|
.fetchSettings();
|
||||||
|
final startupUBlockFilterListsPref =
|
||||||
|
engineSettings.ublockFilterListSettings.enabled
|
||||||
|
? jsonEncode(
|
||||||
|
engineSettings.ublockFilterListSettings.resolveFinalList(),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
final clearStartupUBlockFilterListsPref =
|
||||||
|
!engineSettings.ublockFilterListSettings.enabled;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await GeckoBrowserService().initialize(
|
await GeckoBrowserService().initialize(
|
||||||
@@ -189,6 +200,8 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
generalSettings.syncServerOverride,
|
generalSettings.syncServerOverride,
|
||||||
generalSettings.syncTokenServerOverride,
|
generalSettings.syncTokenServerOverride,
|
||||||
engineSettings,
|
engineSettings,
|
||||||
|
startupUBlockFilterListsPref,
|
||||||
|
clearStartupUBlockFilterListsPref,
|
||||||
);
|
);
|
||||||
} on PlatformException catch (e, s) {
|
} on PlatformException catch (e, s) {
|
||||||
logger.e(
|
logger.e(
|
||||||
@@ -206,6 +219,21 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror the startup uBO pref into the fixator so later pref changes are
|
||||||
|
// still observed and enforced after native startup initialization.
|
||||||
|
try {
|
||||||
|
await syncUBlockFilterLists(
|
||||||
|
ref.read(preferenceFixatorProvider.notifier),
|
||||||
|
engineSettings,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
logger.w(
|
||||||
|
'Failed to sync uBlock filter list pref at startup',
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await ref.read(appInitializationServiceProvider.notifier).initialize();
|
await ref.read(appInitializationServiceProvider.notifier).initialize();
|
||||||
|
|
||||||
Future<void> preloadUrlCleanerCatalog() async {
|
Future<void> preloadUrlCleanerCatalog() async {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class UriBreadcrumb extends StatelessWidget {
|
|||||||
final Uri uri;
|
final Uri uri;
|
||||||
final Widget? icon;
|
final Widget? icon;
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
|
final bool showHttpScheme;
|
||||||
final void Function()? onTooltipTriggered;
|
final void Function()? onTooltipTriggered;
|
||||||
|
|
||||||
const UriBreadcrumb({
|
const UriBreadcrumb({
|
||||||
@@ -33,6 +34,7 @@ class UriBreadcrumb extends StatelessWidget {
|
|||||||
required this.uri,
|
required this.uri,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.style,
|
this.style,
|
||||||
|
this.showHttpScheme = true,
|
||||||
this.onTooltipTriggered,
|
this.onTooltipTriggered,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,6 +58,22 @@ class UriBreadcrumb extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
?icon,
|
?icon,
|
||||||
if (icon != null) const SizedBox(width: 6),
|
if (icon != null) const SizedBox(width: 6),
|
||||||
|
if (!uri.isHttpOrHttps || showHttpScheme) ...[
|
||||||
|
Text(
|
||||||
|
uri.scheme,
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
' › ',
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
Text(
|
Text(
|
||||||
uri.authority,
|
uri.authority,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class UrlListTile extends StatelessWidget {
|
|||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
final Color? borderColor;
|
final Color? borderColor;
|
||||||
|
final bool showHttpScheme;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const UrlListTile({
|
const UrlListTile({
|
||||||
@@ -36,6 +37,7 @@ class UrlListTile extends StatelessWidget {
|
|||||||
this.leading,
|
this.leading,
|
||||||
this.trailing,
|
this.trailing,
|
||||||
this.borderColor,
|
this.borderColor,
|
||||||
|
this.showHttpScheme = true,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ class UrlListTile extends StatelessWidget {
|
|||||||
const SizedBox(height: 3.0),
|
const SizedBox(height: 3.0),
|
||||||
UriBreadcrumb(
|
UriBreadcrumb(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
|
showHttpScheme: showHttpScheme,
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ flutter:
|
|||||||
- assets/legal/
|
- assets/legal/
|
||||||
- assets/quotes/
|
- assets/quotes/
|
||||||
- assets/small_web/
|
- assets/small_web/
|
||||||
|
- assets/ublock/
|
||||||
fonts:
|
fonts:
|
||||||
- family: TorIcons
|
- family: TorIcons
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/ublock_asset.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/ublock_filter_list_settings.dart';
|
||||||
|
|
||||||
|
UBlockAssetsRegistry _makeRegistry({
|
||||||
|
List<MapEntry<String, UBlockAssetEntry>>? extra,
|
||||||
|
}) {
|
||||||
|
final entries = <String, UBlockAssetEntry>{
|
||||||
|
'ublock-filters': const UBlockAssetEntry(
|
||||||
|
content: 'filters',
|
||||||
|
group: UBlockAssetGroup.$default,
|
||||||
|
parent: 'uBlock filters',
|
||||||
|
title: 'uBlock filters – Ads',
|
||||||
|
tags: 'ads',
|
||||||
|
contentURL: ['https://example.com/filters.txt'],
|
||||||
|
),
|
||||||
|
'ublock-privacy': const UBlockAssetEntry(
|
||||||
|
content: 'filters',
|
||||||
|
group: UBlockAssetGroup.$default,
|
||||||
|
parent: 'uBlock filters',
|
||||||
|
title: 'uBlock filters – Privacy',
|
||||||
|
tags: 'privacy',
|
||||||
|
contentURL: ['https://example.com/privacy.txt'],
|
||||||
|
),
|
||||||
|
'easylist': const UBlockAssetEntry(
|
||||||
|
content: 'filters',
|
||||||
|
group: UBlockAssetGroup.ads,
|
||||||
|
title: 'EasyList',
|
||||||
|
tags: 'ads',
|
||||||
|
preferred: true,
|
||||||
|
contentURL: ['https://example.com/easylist.txt'],
|
||||||
|
),
|
||||||
|
'DEU-0': const UBlockAssetEntry(
|
||||||
|
content: 'filters',
|
||||||
|
group: UBlockAssetGroup.regions,
|
||||||
|
off: true,
|
||||||
|
title: '🇩🇪de: EasyList Germany',
|
||||||
|
tags: 'ads german deutsch',
|
||||||
|
lang: 'de',
|
||||||
|
contentURL: ['https://example.com/deu.txt'],
|
||||||
|
),
|
||||||
|
'NLD-0': const UBlockAssetEntry(
|
||||||
|
content: 'filters',
|
||||||
|
group: UBlockAssetGroup.regions,
|
||||||
|
off: true,
|
||||||
|
title: '🇳🇱nl: EasyList Dutch',
|
||||||
|
tags: 'ads dutch nederlands',
|
||||||
|
lang: 'nl',
|
||||||
|
contentURL: ['https://example.com/nld.txt'],
|
||||||
|
),
|
||||||
|
'assets.json': const UBlockAssetEntry(
|
||||||
|
content: 'internal',
|
||||||
|
updateAfter: 13,
|
||||||
|
contentURL: ['https://example.com/assets.json'],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extra != null) {
|
||||||
|
for (final e in extra) {
|
||||||
|
entries[e.key] = e.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UBlockAssetsRegistry(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('UBlockAssetsRegistry', () {
|
||||||
|
test('defaultEnabledTokens excludes off:true entries', () {
|
||||||
|
final registry = _makeRegistry();
|
||||||
|
expect(
|
||||||
|
registry.defaultEnabledTokens,
|
||||||
|
containsAll(['ublock-filters', 'ublock-privacy', 'easylist']),
|
||||||
|
);
|
||||||
|
expect(registry.defaultEnabledTokens, isNot(contains('DEU-0')));
|
||||||
|
expect(registry.defaultEnabledTokens, isNot(contains('NLD-0')));
|
||||||
|
expect(registry.defaultEnabledTokens, isNot(contains('assets.json')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tokensMatchingLocales matches primary language code', () {
|
||||||
|
final registry = _makeRegistry();
|
||||||
|
expect(registry.tokensMatchingLocales(['de-DE']), containsAll(['DEU-0']));
|
||||||
|
expect(
|
||||||
|
registry.tokensMatchingLocales(['de-DE']),
|
||||||
|
isNot(contains('NLD-0')),
|
||||||
|
);
|
||||||
|
expect(registry.tokensMatchingLocales(['nl-NL']), containsAll(['NLD-0']));
|
||||||
|
expect(
|
||||||
|
registry.tokensMatchingLocales(['nl-NL']),
|
||||||
|
isNot(contains('DEU-0')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tokensMatchingLocales matches multiple locales', () {
|
||||||
|
final registry = _makeRegistry();
|
||||||
|
final matched = registry.tokensMatchingLocales(['de-DE', 'nl-NL']);
|
||||||
|
expect(matched, containsAll(['DEU-0', 'NLD-0']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tokensMatchingLocales only matches off:true entries', () {
|
||||||
|
final registry = _makeRegistry();
|
||||||
|
final matched = registry.tokensMatchingLocales(['en-US']);
|
||||||
|
expect(matched, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildGroupedParentTree creates correct hierarchy', () {
|
||||||
|
final registry = _makeRegistry();
|
||||||
|
final tree = registry.buildGroupedParentTree();
|
||||||
|
|
||||||
|
expect(tree, contains(UBlockAssetGroup.$default));
|
||||||
|
expect(tree[UBlockAssetGroup.$default]!, contains('uBlock filters'));
|
||||||
|
expect(
|
||||||
|
tree[UBlockAssetGroup.$default]!['uBlock filters'],
|
||||||
|
containsAll(['ublock-filters', 'ublock-privacy']),
|
||||||
|
);
|
||||||
|
expect(tree, contains(UBlockAssetGroup.ads));
|
||||||
|
expect(tree[UBlockAssetGroup.ads]!, contains(null));
|
||||||
|
expect(tree[UBlockAssetGroup.ads]![null], contains('easylist'));
|
||||||
|
expect(tree, contains(UBlockAssetGroup.regions));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson parses a full assets.json structure', () {
|
||||||
|
final json =
|
||||||
|
jsonDecode('''
|
||||||
|
{
|
||||||
|
"assets.json": {
|
||||||
|
"content": "internal",
|
||||||
|
"updateAfter": 13,
|
||||||
|
"contentURL": ["https://raw.githubusercontent.com/gorhill/uBlock/master/assets/assets.json"]
|
||||||
|
},
|
||||||
|
"ublock-filters": {
|
||||||
|
"content": "filters",
|
||||||
|
"group": "default",
|
||||||
|
"parent": "uBlock filters",
|
||||||
|
"title": "uBlock filters – Ads",
|
||||||
|
"contentURL": "https://example.com/filters.txt",
|
||||||
|
"off": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final registry = UBlockAssetsRegistry.fromJson(json);
|
||||||
|
expect(registry['ublock-filters'], isNotNull);
|
||||||
|
expect(registry['ublock-filters']!.group, UBlockAssetGroup.$default);
|
||||||
|
expect(registry['ublock-filters']!.parent, 'uBlock filters');
|
||||||
|
expect(registry['ublock-filters']!.off, true);
|
||||||
|
expect(registry['ublock-filters']!.contentURL, [
|
||||||
|
'https://example.com/filters.txt',
|
||||||
|
]);
|
||||||
|
expect(registry['assets.json'], isNotNull);
|
||||||
|
expect(registry.defaultEnabledTokens, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('UBlockFilterListSettings', () {
|
||||||
|
final registry = _makeRegistry();
|
||||||
|
|
||||||
|
test('managedDefaults enables default-on lists without auto tokens', () {
|
||||||
|
final settings = UBlockFilterListSettings.managedDefaults(registry);
|
||||||
|
|
||||||
|
expect(settings.enabled, true);
|
||||||
|
expect(
|
||||||
|
settings.enabledStockListTokens,
|
||||||
|
containsAll(['ublock-filters', 'ublock-privacy', 'easylist']),
|
||||||
|
);
|
||||||
|
expect(settings.autoSelectRegionalLists, false);
|
||||||
|
expect(settings.autoEnabledStockListTokens, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveFinalList merges manual + auto + external with dedup', () {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['ublock-filters', 'easylist'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0', 'ublock-filters'],
|
||||||
|
externalFilterLists: [
|
||||||
|
UBlockExternalList(url: 'https://example.com/external.txt'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final list = settings.resolveFinalList();
|
||||||
|
|
||||||
|
expect(list.first, 'user-filters');
|
||||||
|
expect(
|
||||||
|
list,
|
||||||
|
containsAll([
|
||||||
|
'ublock-filters',
|
||||||
|
'easylist',
|
||||||
|
'DEU-0',
|
||||||
|
'https://example.com/external.txt',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(list.where((t) => t == 'ublock-filters').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveFinalList returns empty when disabled', () {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: false,
|
||||||
|
enabledStockListTokens: ['ublock-filters'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(settings.resolveFinalList(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isTokenEnabled checks both manual and auto lists', () {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['easylist'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(settings.isTokenEnabled('easylist'), true);
|
||||||
|
expect(settings.isTokenEnabled('DEU-0'), true);
|
||||||
|
expect(settings.isTokenEnabled('ublock-filters'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'toggling off an auto-only token removes it from autoEnabledStockListTokens',
|
||||||
|
() {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['easylist'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0', 'NLD-0'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = settings.copyWith(
|
||||||
|
enabledStockListTokens: [...settings.enabledStockListTokens]
|
||||||
|
..remove('DEU-0'),
|
||||||
|
autoEnabledStockListTokens: [...settings.autoEnabledStockListTokens]
|
||||||
|
..remove('DEU-0'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.autoEnabledStockListTokens, isNot(contains('DEU-0')));
|
||||||
|
expect(updated.autoEnabledStockListTokens, contains('NLD-0'));
|
||||||
|
expect(updated.isTokenEnabled('DEU-0'), false);
|
||||||
|
expect(updated.isTokenEnabled('NLD-0'), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'toggling off a manual-only token removes it from enabledStockListTokens',
|
||||||
|
() {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['easylist', 'ublock-filters'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = settings.copyWith(
|
||||||
|
enabledStockListTokens: [...settings.enabledStockListTokens]
|
||||||
|
..remove('easylist'),
|
||||||
|
autoEnabledStockListTokens: [...settings.autoEnabledStockListTokens]
|
||||||
|
..remove('easylist'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.enabledStockListTokens, isNot(contains('easylist')));
|
||||||
|
expect(updated.enabledStockListTokens, contains('ublock-filters'));
|
||||||
|
expect(updated.isTokenEnabled('easylist'), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'toggling off a token in both sets removes it from both',
|
||||||
|
() {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['DEU-0', 'easylist'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0', 'NLD-0'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(settings.isTokenEnabled('DEU-0'), true);
|
||||||
|
|
||||||
|
final updated = settings.copyWith(
|
||||||
|
enabledStockListTokens: [...settings.enabledStockListTokens]
|
||||||
|
..remove('DEU-0'),
|
||||||
|
autoEnabledStockListTokens: [...settings.autoEnabledStockListTokens]
|
||||||
|
..remove('DEU-0'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.enabledStockListTokens, isNot(contains('DEU-0')));
|
||||||
|
expect(updated.autoEnabledStockListTokens, isNot(contains('DEU-0')));
|
||||||
|
expect(updated.isTokenEnabled('DEU-0'), false);
|
||||||
|
expect(updated.isTokenEnabled('NLD-0'), true);
|
||||||
|
expect(updated.isTokenEnabled('easylist'), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('toggling on a disabled token adds to enabledStockListTokens', () {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['easylist'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0'],
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = settings.copyWith.enabledStockListTokens([
|
||||||
|
...settings.enabledStockListTokens,
|
||||||
|
'ublock-filters',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(updated.enabledStockListTokens, contains('ublock-filters'));
|
||||||
|
expect(updated.isTokenEnabled('ublock-filters'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disabling auto-select clears autoEnabledStockListTokens', () {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['easylist'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0'],
|
||||||
|
autoSelectRegionalLists: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = settings.copyWith(
|
||||||
|
autoSelectRegionalLists: false,
|
||||||
|
autoEnabledStockListTokens: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updated.autoSelectRegionalLists, false);
|
||||||
|
expect(updated.autoEnabledStockListTokens, isEmpty);
|
||||||
|
expect(updated.enabledStockListTokens, contains('easylist'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
're-enabling auto-select recomputes autoEnabledStockListTokens from registry',
|
||||||
|
() {
|
||||||
|
var settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['easylist'],
|
||||||
|
autoSelectRegionalLists: false,
|
||||||
|
autoEnabledStockListTokens: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final langCodes = ['de-DE', 'nl-NL'];
|
||||||
|
final autoTokens = registry.tokensMatchingLocales(langCodes);
|
||||||
|
settings = settings.copyWith(
|
||||||
|
autoSelectRegionalLists: true,
|
||||||
|
autoEnabledStockListTokens: autoTokens,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(settings.autoSelectRegionalLists, true);
|
||||||
|
expect(
|
||||||
|
settings.autoEnabledStockListTokens,
|
||||||
|
containsAll(['DEU-0', 'NLD-0']),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('fromJson/toJson round-trips with new fields', () {
|
||||||
|
final settings = UBlockFilterListSettings(
|
||||||
|
enabled: true,
|
||||||
|
enabledStockListTokens: ['easylist'],
|
||||||
|
autoEnabledStockListTokens: ['DEU-0'],
|
||||||
|
autoSelectRegionalLists: true,
|
||||||
|
externalFilterLists: [
|
||||||
|
UBlockExternalList(
|
||||||
|
url: 'https://example.com/list.txt',
|
||||||
|
description: 'Test list',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final json = settings.toJson();
|
||||||
|
final restored = UBlockFilterListSettings.fromJson(json);
|
||||||
|
|
||||||
|
expect(restored.enabled, settings.enabled);
|
||||||
|
expect(restored.enabledStockListTokens, settings.enabledStockListTokens);
|
||||||
|
expect(
|
||||||
|
restored.autoEnabledStockListTokens,
|
||||||
|
settings.autoEnabledStockListTokens,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
restored.autoSelectRegionalLists,
|
||||||
|
settings.autoSelectRegionalLists,
|
||||||
|
);
|
||||||
|
expect(restored.externalFilterLists, settings.externalFilterLists);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fromJson defaults new fields for legacy data', () {
|
||||||
|
final legacyJson = {
|
||||||
|
'enabled': true,
|
||||||
|
'enabledStockListTokens': ['easylist'],
|
||||||
|
'externalFilterLists': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
final restored = UBlockFilterListSettings.fromJson(legacyJson);
|
||||||
|
|
||||||
|
expect(restored.autoEnabledStockListTokens, isEmpty);
|
||||||
|
expect(restored.autoSelectRegionalLists, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at) option later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Widget buildSubject({required Uri uri, bool showHttpScheme = true}) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: UriBreadcrumb(uri: uri, showHttpScheme: showHttpScheme),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> boldTexts(WidgetTester tester) {
|
||||||
|
return tester
|
||||||
|
.widgetList<Text>(find.byType(Text))
|
||||||
|
.where((t) => t.style?.fontWeight == FontWeight.bold)
|
||||||
|
.map((t) => t.data ?? '')
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> allTexts(WidgetTester tester) {
|
||||||
|
return tester
|
||||||
|
.widgetList<Text>(find.byType(Text))
|
||||||
|
.map((t) => t.data ?? '')
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
group('UriBreadcrumb', () {
|
||||||
|
group('showHttpScheme defaults to true', () {
|
||||||
|
testWidgets('shows scheme before authority for https URLs', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildSubject(uri: Uri.parse('https://example.com/path')),
|
||||||
|
);
|
||||||
|
|
||||||
|
final all = allTexts(tester);
|
||||||
|
expect(all, contains('https'));
|
||||||
|
expect(all, contains(' › '));
|
||||||
|
final bold = boldTexts(tester);
|
||||||
|
expect(bold, contains('example.com'));
|
||||||
|
expect(bold, contains('https'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows scheme before authority for http URLs', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildSubject(uri: Uri.parse('http://example.com')),
|
||||||
|
);
|
||||||
|
|
||||||
|
final all = allTexts(tester);
|
||||||
|
expect(all, contains('http'));
|
||||||
|
expect(all, contains(' › '));
|
||||||
|
final bold = boldTexts(tester);
|
||||||
|
expect(bold, contains('example.com'));
|
||||||
|
expect(bold, contains('http'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows scheme before authority for non-HTTP URLs', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildSubject(uri: Uri.parse('ftp://files.example.com/docs')),
|
||||||
|
);
|
||||||
|
|
||||||
|
final all = allTexts(tester);
|
||||||
|
expect(all, contains('ftp'));
|
||||||
|
expect(all, contains(' › '));
|
||||||
|
final bold = boldTexts(tester);
|
||||||
|
expect(bold, contains('files.example.com'));
|
||||||
|
expect(bold, contains('ftp'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('showHttpScheme is false', () {
|
||||||
|
testWidgets('hides scheme for https URLs', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildSubject(
|
||||||
|
uri: Uri.parse('https://example.com/path'),
|
||||||
|
showHttpScheme: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final all = allTexts(tester);
|
||||||
|
expect(all, isNot(contains('https')));
|
||||||
|
final bold = boldTexts(tester);
|
||||||
|
expect(bold, ['example.com']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('hides scheme for http URLs', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildSubject(
|
||||||
|
uri: Uri.parse('http://example.com'),
|
||||||
|
showHttpScheme: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final all = allTexts(tester);
|
||||||
|
expect(all, isNot(contains('http')));
|
||||||
|
final bold = boldTexts(tester);
|
||||||
|
expect(bold, ['example.com']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('still shows scheme for non-HTTP URLs', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildSubject(
|
||||||
|
uri: Uri.parse('ftp://files.example.com/docs'),
|
||||||
|
showHttpScheme: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final all = allTexts(tester);
|
||||||
|
expect(all, contains('ftp'));
|
||||||
|
expect(all, contains(' › '));
|
||||||
|
final bold = boldTexts(tester);
|
||||||
|
expect(bold, contains('files.example.com'));
|
||||||
|
expect(bold, contains('ftp'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+42
@@ -36,7 +36,9 @@ import mozilla.components.browser.state.action.RestoreCompleteAction
|
|||||||
import mozilla.components.browser.state.action.TabListAction
|
import mozilla.components.browser.state.action.TabListAction
|
||||||
import mozilla.components.browser.state.action.CustomTabListAction
|
import mozilla.components.browser.state.action.CustomTabListAction
|
||||||
import mozilla.components.browser.state.selector.findCustomTab
|
import mozilla.components.browser.state.selector.findCustomTab
|
||||||
|
import mozilla.components.ExperimentalAndroidComponentsApi
|
||||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
|
import mozilla.components.concept.engine.preferences.Branch
|
||||||
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
||||||
import mozilla.components.support.base.facts.Facts
|
import mozilla.components.support.base.facts.Facts
|
||||||
import mozilla.components.support.base.facts.processor.LogFactProcessor
|
import mozilla.components.support.base.facts.processor.LogFactProcessor
|
||||||
@@ -50,6 +52,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
private const val HISTORY_METADATA_MAX_AGE_IN_MS = 14L * 24 * 60 * 60 * 1000 // 14 days
|
private const val HISTORY_METADATA_MAX_AGE_IN_MS = 14L * 24 * 60 * 60 * 1000 // 14 days
|
||||||
private const val DEFAULT_QUERY_PARAMETER_STRIPPING_STRIP_LIST =
|
private const val DEFAULT_QUERY_PARAMETER_STRIPPING_STRIP_LIST =
|
||||||
"__hsfp __hssc __hstc __s _bhlid _branch_match_id _branch_referrer _gl _hsenc _kx _openstat at_recipient_id at_recipient_list bbeml bsft_clkid bsft_uid dclid et_rid fb_action_ids fb_comment_id fbclid gbraid gclid guce_referrer guce_referrer_sig hsCtaTracking igshid irclickid mc_eid mkt_tok ml_subscriber ml_subscriber_hash msclkid mtm_cid oft_c oft_ck oft_d oft_id oft_ids oft_k oft_lk oft_sk oly_anon_id oly_enc_id pk_cid rb_clickid s_cid sc_customer sc_eh sc_uid sms_click sms_source sms_uph srsltid ss_email_id syclid ttclid twclid unicorn_click_id vero_conv vero_id vgo_ee wbraid wickedid yclid ymclid ysclid"
|
"__hsfp __hssc __hstc __s _bhlid _branch_match_id _branch_referrer _gl _hsenc _kx _openstat at_recipient_id at_recipient_list bbeml bsft_clkid bsft_uid dclid et_rid fb_action_ids fb_comment_id fbclid gbraid gclid guce_referrer guce_referrer_sig hsCtaTracking igshid irclickid mc_eid mkt_tok ml_subscriber ml_subscriber_hash msclkid mtm_cid oft_c oft_ck oft_d oft_id oft_ids oft_k oft_lk oft_sk oly_anon_id oly_enc_id pk_cid rb_clickid s_cid sc_customer sc_eh sc_uid sms_click sms_source sms_uph srsltid ss_email_id syclid ttclid twclid unicorn_click_id vero_conv vero_id vgo_ee wbraid wickedid yclid ymclid ysclid"
|
||||||
|
private const val UBLOCK_FILTER_LISTS_PREF = "browser.weblibre.uBO.filterLists"
|
||||||
|
|
||||||
object GlobalComponents {
|
object GlobalComponents {
|
||||||
private var _components: Components? = null
|
private var _components: Components? = null
|
||||||
@@ -87,6 +90,10 @@ object GlobalComponents {
|
|||||||
// Startup settings for builder-only GeckoRuntimeSettings (fission, process isolation, etc.)
|
// Startup settings for builder-only GeckoRuntimeSettings (fission, process isolation, etc.)
|
||||||
var startupSettings: GeckoEngineSettings? = null
|
var startupSettings: GeckoEngineSettings? = null
|
||||||
|
|
||||||
|
// Startup pref written before web extensions initialize.
|
||||||
|
var startupUBlockFilterListsPref: String? = null
|
||||||
|
var clearStartupUBlockFilterListsPref: Boolean = false
|
||||||
|
|
||||||
fun shouldOpenLinksInApp(isExternalSession: Boolean = false): Boolean {
|
fun shouldOpenLinksInApp(isExternalSession: Boolean = false): Boolean {
|
||||||
return when (engineSettingsApi!!.getAppLinksMode()) {
|
return when (engineSettingsApi!!.getAppLinksMode()) {
|
||||||
eu.weblibre.flutter_mozilla_components.pigeons.AppLinksMode.ALWAYS -> true
|
eu.weblibre.flutter_mozilla_components.pigeons.AppLinksMode.ALWAYS -> true
|
||||||
@@ -142,6 +149,40 @@ object GlobalComponents {
|
|||||||
newComponents.useCases.downloadsUseCases.restoreDownloads()
|
newComponents.useCases.downloadsUseCases.restoreDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submits the uBO managed-storage pref before web extensions register.
|
||||||
|
// Called from setUp() on the main thread; we do not block awaiting the
|
||||||
|
// ack callback because the engine dispatches it back to the main thread
|
||||||
|
// (which is held by setUp), and waiting would deadlock. The underlying
|
||||||
|
// GeckoView pref store accepts the new value before the callback fires,
|
||||||
|
// so by the time WebExtensionSupport.initialize installs uBO and the
|
||||||
|
// extension reads storage.managed, the pref is already present.
|
||||||
|
@OptIn(ExperimentalAndroidComponentsApi::class)
|
||||||
|
private fun applyStartupUBlockFilterListsPref(newComponents: Components) {
|
||||||
|
if (!clearStartupUBlockFilterListsPref && startupUBlockFilterListsPref == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val onError: (Throwable) -> Unit = {
|
||||||
|
Logger.warn("Failed applying startup uBlock filter list pref", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearStartupUBlockFilterListsPref) {
|
||||||
|
newComponents.core.engine.clearBrowserUserPref(
|
||||||
|
pref = UBLOCK_FILTER_LISTS_PREF,
|
||||||
|
onSuccess = {},
|
||||||
|
onError = onError,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
newComponents.core.engine.setBrowserPref(
|
||||||
|
UBLOCK_FILTER_LISTS_PREF,
|
||||||
|
requireNotNull(startupUBlockFilterListsPref),
|
||||||
|
Branch.USER,
|
||||||
|
onSuccess = {},
|
||||||
|
onError = onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun setUp(
|
fun setUp(
|
||||||
applicationContext: ProfileContext,
|
applicationContext: ProfileContext,
|
||||||
@@ -206,6 +247,7 @@ object GlobalComponents {
|
|||||||
|
|
||||||
if (mode == ComponentsMode.FULL) {
|
if (mode == ComponentsMode.FULL) {
|
||||||
newComponents.core.engine.warmUp()
|
newComponents.core.engine.warmUp()
|
||||||
|
applyStartupUBlockFilterListsPref(newComponents)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restorePreviousCustomTabs() {
|
fun restorePreviousCustomTabs() {
|
||||||
|
|||||||
+5
@@ -180,6 +180,8 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
fxaServerOverride: String?,
|
fxaServerOverride: String?,
|
||||||
syncTokenServerOverride: String?,
|
syncTokenServerOverride: String?,
|
||||||
startupSettings: GeckoEngineSettings?,
|
startupSettings: GeckoEngineSettings?,
|
||||||
|
startupUBlockFilterListsPref: String?,
|
||||||
|
clearStartupUBlockFilterListsPref: Boolean,
|
||||||
) {
|
) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (!isGeckoInitialized) {
|
if (!isGeckoInitialized) {
|
||||||
@@ -196,6 +198,9 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
|
|
||||||
// Store startup settings before runtime creation
|
// Store startup settings before runtime creation
|
||||||
GlobalComponents.startupSettings = startupSettings
|
GlobalComponents.startupSettings = startupSettings
|
||||||
|
GlobalComponents.startupUBlockFilterListsPref = startupUBlockFilterListsPref
|
||||||
|
GlobalComponents.clearStartupUBlockFilterListsPref =
|
||||||
|
clearStartupUBlockFilterListsPref
|
||||||
|
|
||||||
setupGeckoEngine(
|
setupGeckoEngine(
|
||||||
profileFolder,
|
profileFolder,
|
||||||
|
|||||||
+4
-2
@@ -6282,7 +6282,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
|||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface GeckoBrowserApi {
|
interface GeckoBrowserApi {
|
||||||
fun getGeckoVersion(): String
|
fun getGeckoVersion(): String
|
||||||
fun initialize(profileFolder: String, logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection?, fxaServerOverride: String?, syncTokenServerOverride: String?, startupSettings: GeckoEngineSettings?)
|
fun initialize(profileFolder: String, logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection?, fxaServerOverride: String?, syncTokenServerOverride: String?, startupSettings: GeckoEngineSettings?, startupUBlockFilterListsPref: String?, clearStartupUBlockFilterListsPref: Boolean)
|
||||||
fun showNativeFragment(): Boolean
|
fun showNativeFragment(): Boolean
|
||||||
fun onTrimMemory(level: Long)
|
fun onTrimMemory(level: Long)
|
||||||
fun openInCustomTab(url: String, private: Boolean, contextId: String?)
|
fun openInCustomTab(url: String, private: Boolean, contextId: String?)
|
||||||
@@ -6327,8 +6327,10 @@ interface GeckoBrowserApi {
|
|||||||
val fxaServerOverrideArg = args[4] as String?
|
val fxaServerOverrideArg = args[4] as String?
|
||||||
val syncTokenServerOverrideArg = args[5] as String?
|
val syncTokenServerOverrideArg = args[5] as String?
|
||||||
val startupSettingsArg = args[6] as GeckoEngineSettings?
|
val startupSettingsArg = args[6] as GeckoEngineSettings?
|
||||||
|
val startupUBlockFilterListsPrefArg = args[7] as String?
|
||||||
|
val clearStartupUBlockFilterListsPrefArg = args[8] as Boolean
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
api.initialize(profileFolderArg, logLevelArg, contentBlockingArg, addonCollectionArg, fxaServerOverrideArg, syncTokenServerOverrideArg, startupSettingsArg)
|
api.initialize(profileFolderArg, logLevelArg, contentBlockingArg, addonCollectionArg, fxaServerOverrideArg, syncTokenServerOverrideArg, startupSettingsArg, startupUBlockFilterListsPrefArg, clearStartupUBlockFilterListsPrefArg)
|
||||||
listOf(null)
|
listOf(null)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
GeckoPigeonUtils.wrapError(exception)
|
GeckoPigeonUtils.wrapError(exception)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class GeckoBrowserService {
|
|||||||
String? fxaServerOverride,
|
String? fxaServerOverride,
|
||||||
String? syncTokenServerOverride, [
|
String? syncTokenServerOverride, [
|
||||||
GeckoEngineSettings? startupSettings,
|
GeckoEngineSettings? startupSettings,
|
||||||
|
String? startupUBlockFilterListsPref,
|
||||||
|
bool clearStartupUBlockFilterListsPref = false,
|
||||||
]) {
|
]) {
|
||||||
return _api.initialize(
|
return _api.initialize(
|
||||||
profileFolder,
|
profileFolder,
|
||||||
@@ -34,6 +36,8 @@ class GeckoBrowserService {
|
|||||||
fxaServerOverride,
|
fxaServerOverride,
|
||||||
syncTokenServerOverride,
|
syncTokenServerOverride,
|
||||||
startupSettings,
|
startupSettings,
|
||||||
|
startupUBlockFilterListsPref,
|
||||||
|
clearStartupUBlockFilterListsPref,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6394,6 +6394,8 @@ class GeckoBrowserApi {
|
|||||||
String? fxaServerOverride,
|
String? fxaServerOverride,
|
||||||
String? syncTokenServerOverride,
|
String? syncTokenServerOverride,
|
||||||
GeckoEngineSettings? startupSettings,
|
GeckoEngineSettings? startupSettings,
|
||||||
|
String? startupUBlockFilterListsPref,
|
||||||
|
bool clearStartupUBlockFilterListsPref,
|
||||||
) async {
|
) async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$pigeonVar_messageChannelSuffix';
|
||||||
@@ -6411,6 +6413,8 @@ class GeckoBrowserApi {
|
|||||||
fxaServerOverride,
|
fxaServerOverride,
|
||||||
syncTokenServerOverride,
|
syncTokenServerOverride,
|
||||||
startupSettings,
|
startupSettings,
|
||||||
|
startupUBlockFilterListsPref,
|
||||||
|
clearStartupUBlockFilterListsPref,
|
||||||
]);
|
]);
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
|
|||||||
@@ -1328,6 +1328,13 @@ class AddonCollection {
|
|||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class GeckoBrowserApi {
|
abstract class GeckoBrowserApi {
|
||||||
String getGeckoVersion();
|
String getGeckoVersion();
|
||||||
|
// [startupUBlockFilterListsPref] is the JSON-encoded value to write to the
|
||||||
|
// managed-storage pref read by uBlock Origin at extension startup. The pref
|
||||||
|
// must be written before the extension registers, so it is plumbed through
|
||||||
|
// initialize rather than set later.
|
||||||
|
// If [clearStartupUBlockFilterListsPref] is true, the pref is cleared and
|
||||||
|
// [startupUBlockFilterListsPref] is ignored. If both are unset/false, the
|
||||||
|
// pref is left untouched.
|
||||||
void initialize(
|
void initialize(
|
||||||
String profileFolder,
|
String profileFolder,
|
||||||
LogLevel logLevel,
|
LogLevel logLevel,
|
||||||
@@ -1336,6 +1343,8 @@ abstract class GeckoBrowserApi {
|
|||||||
String? fxaServerOverride,
|
String? fxaServerOverride,
|
||||||
String? syncTokenServerOverride,
|
String? syncTokenServerOverride,
|
||||||
GeckoEngineSettings? startupSettings,
|
GeckoEngineSettings? startupSettings,
|
||||||
|
String? startupUBlockFilterListsPref,
|
||||||
|
bool clearStartupUBlockFilterListsPref,
|
||||||
);
|
);
|
||||||
bool showNativeFragment();
|
bool showNativeFragment();
|
||||||
void onTrimMemory(int level);
|
void onTrimMemory(int level);
|
||||||
|
|||||||
@@ -93,9 +93,20 @@ update_url_shorteners() {
|
|||||||
"$dir/url-shortener-list.json"
|
"$dir/url-shortener-list.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_ublock() {
|
||||||
|
local dir="$REPO_ROOT/apps/weblibre/assets/ublock"
|
||||||
|
log "Updating uBlock Origin assets..."
|
||||||
|
|
||||||
|
fetch "https://raw.githubusercontent.com/gorhill/uBlock/master/assets/assets.json" \
|
||||||
|
"$dir/assets.json"
|
||||||
|
|
||||||
|
date -u --iso-8601=seconds > "$dir/last_sync.txt"
|
||||||
|
log "uBlock assets sync completed at $(cat "$dir/last_sync.txt")"
|
||||||
|
}
|
||||||
|
|
||||||
# ── main ─────────────────────────────────────────────────────────────────────
|
# ── main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
ALL_GROUPS=(bangs bridges url-cleaner url-shorteners)
|
ALL_GROUPS=(bangs bridges url-cleaner url-shorteners ublock)
|
||||||
SELECTED_GROUPS=()
|
SELECTED_GROUPS=()
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -117,6 +128,7 @@ for group in "${SELECTED_GROUPS[@]}"; do
|
|||||||
bridges) update_bridges || ((FAILURES++)) ;;
|
bridges) update_bridges || ((FAILURES++)) ;;
|
||||||
url-cleaner) update_url_cleaner || ((FAILURES++)) ;;
|
url-cleaner) update_url_cleaner || ((FAILURES++)) ;;
|
||||||
url-shorteners) update_url_shorteners || ((FAILURES++)) ;;
|
url-shorteners) update_url_shorteners || ((FAILURES++)) ;;
|
||||||
|
ublock) update_ublock || ((FAILURES++)) ;;
|
||||||
*) err "Unknown group: $group"; ((FAILURES++)) ;;
|
*) err "Unknown group: $group"; ((FAILURES++)) ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user