diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index d42a42f..0000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.19.5", - "flavors": {} -} \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..0c75e23 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.29.0", + "flavors": {} +} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52fcfe4..716ba3f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest env: - FLUTTER_VERSION: '3.19.5' # Update with your desired default version + FLUTTER_VERSION: '3.29.0' # Update with your desired default version steps: - uses: actions/checkout@v4 @@ -23,4 +23,6 @@ jobs: run: flutter pub get - name: Run tests - run: flutter test + run: | + flutter test + cd packages/adblocker_core && flutter test diff --git a/.gitignore b/.gitignore index 33c8e17..c292d99 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,10 @@ migrate_working_dir/ *.iws .idea/ +#vscode related +.vscode/ + #fvm related -.fvm/flutter_sdk # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line @@ -33,3 +35,6 @@ migrate_working_dir/ build/ .flutter-plugins .flutter-plugins-dependencies + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 853a697..104c681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ -## 1.2.0 -* Added support for HTML string loading +## 2.0.0-beta +* Added support for easylist and adguard filters +* Added support for resource rules parsing +* Removed third party package dependency and using official webview_flutter package + +**Breaking Changes** +* Minimum Supported flutter version is 3.27.1 +* Minimum Supported dart version is 3.7.0 ## 1.1.2 * Removed redundant isolate uses diff --git a/LICENSE b/LICENSE index 5046f8f..ffb3cd1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2023, Md Didarul Islam +Copyright (c) 2025, Md Didarul Islam Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index 83e27c9..cc6e974 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,255 @@ [![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis) -- A webview implementation of in Flutter that blocks most of the ads that appear inside of the webpages -- Current implementation is based on official `flutter_inappwebview` packages. So, the features and limitation of that package - is included +# AdBlocker WebView Flutter ->On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview). -On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). +A Flutter WebView implementation that blocks ads and trackers using EasyList and AdGuard filter lists. -| | Android | iOS | -|-------------|----------------|-------| -| **Support** | SDK 19+ or 20+ | 11.0+ | +## Features -## Getting started -Add `adblocker_webview` as a [dependency](https://pub.dev/packages/adblocker_webview/install) in your pubspec.yaml file. +- 🚫 Basic ad and tracker blocking using EasyList and AdGuard filters +- 🌐 Supports both URL and HTML content loading +- 🔄 Navigation control (back, forward, refresh) +- 📱 User agent strings for Android and iOS +- ⚡ Early resource blocking for better performance +- 🎯 Domain-based filtering and element hiding +- 🔍 Detailed logging of blocked resources +- 💉 Custom JavaScript injection support -## Usage -1. Acquire an instance of [AdBlockerWebviewController](https://pub.dev/documentation/adblocker_webview/latest/adblocker_webview/AdBlockerWebviewController-class.html) -```dart - final _adBlockerWebviewController = AdBlockerWebviewController.instance; +## Getting Started + +### Installation + +Add this to your `pubspec.yaml`: + +```yaml +dependencies: + adblocker_webview: ^1.0.0 ``` -It's better to warm up the controller before displaying the webview. It's possible to do that by + +### Basic Usage + ```dart +import 'package:adblocker_webview/adblocker_webview.dart'; + +// Initialize the controller (preferably in main()) +void main() async { + await AdBlockerWebviewController.instance.initialize(); + runApp(MyApp()); +} + +// Use in your widget +class MyWebView extends StatelessWidget { @override - void initState() { - super.initState(); - _adBlockerWebviewController.initialize(); - /// ... Other code here. + Widget build(BuildContext context) { + return AdBlockerWebview( + url: Uri.parse('https://example.com'), + shouldBlockAds: true, + adBlockerWebviewController: AdBlockerWebviewController.instance, + onLoadStart: (url) => print('Started loading: $url'), + onLoadFinished: (url) => print('Finished loading: $url'), + onLoadError: (url, code) => print('Error: $code'), + onProgress: (progress) => print('Progress: $progress%'), + ); } +} +``` + +### Loading HTML Content + +```dart +AdBlockerWebview( + initialHtmlData: 'Hello World!', + shouldBlockAds: true, + adBlockerWebviewController: AdBlockerWebviewController.instance, +) ``` -2. Add the [AdBlockerWebview](https://pub.dev/documentation/adblocker_webview/latest/adblocker_webview/AdBlockerWebview-class.html) in widget tree +### Navigation Control + ```dart - AdBlockerWebview( - url: "Valid url Here", - adBlockerWebviewController: widget.controller, - onProgress: (progress) { - setState(() { - _progress = progress; - }); - }, - shouldBlockAds: true, - /// Other params if required - ); +final controller = AdBlockerWebviewController.instance; + +// Check if can go back +if (await controller.canGoBack()) { + controller.goBack(); +} + +// Reload page +controller.reload(); + +// Execute JavaScript +controller.runJavaScript('console.log("Hello from Flutter!")'); ``` - Supported params of [AdBlockerWebview](https://pub.dev/documentation/adblocker_webview/latest/adblocker_webview/AdBlockerWebview-class.html]) are: - ```dart - const AdBlockerWebview({ - required this.adBlockerWebviewController, - required this.shouldBlockAds, - this.url, - this.initialHtmlData, - this.onLoadStart, - this.onLoadFinished, - this.onProgress, - this.onLoadError, - this.onTitleChanged, - this.options, - this.additionalHostsToBlock = const [], - super.key, - }) : assert( - (url == null && initialHtmlData != null) || - (url != null && initialHtmlData == null), - 'Both url and initialHtmlData can not be non null'); + +## Configuration + +The WebView can be configured with various options: + +```dart +AdBlockerWebview( + url: Uri.parse('https://example.com'), + shouldBlockAds: true, // Enable/disable ad blocking + adBlockerWebviewController: AdBlockerWebviewController.instance, + onLoadStart: (url) { + // Page started loading + }, + onLoadFinished: (url) { + // Page finished loading + }, + onProgress: (progress) { + // Loading progress (0-100) + }, + onLoadError: (url, code) { + // Handle loading errors + }, + onUrlChanged: (url) { + // URL changed + }, +); ``` -#### Caching -- API response for Ad hosts is cached automatically and no config is required! - -### Contribution -Contributions are welcome 😄. Please file an issue [here](https://github.com/islamdidarmd/flutter_adblocker_webview/issues) if you want to include additional feature or found a bug! -#### Guide -1. Create an issue first to make sure your request is not a duplicate one -2. Create a fork of the repository (If it's your first contribution) -3. Make a branch from `develop` -4. Branch name should indicate the contribution type - - `feature/**` for new feature - - `bugfix/**` for a bug fix -5. Raise a PR against the `develop` branch \ No newline at end of file + +## Features in Detail + +### Ad Blocking +- Basic support for EasyList and AdGuard filter lists +- Blocks common ad resources before they load +- Hides ad elements using CSS rules +- Supports exception rules for whitelisting + +### Resource Blocking +- Blocks common trackers and unwanted resources +- Early blocking for better performance +- Basic domain-based filtering +- Exception handling for whitelisted domains + +### Element Hiding +- Hides common ad containers and placeholders +- CSS-based element hiding +- Basic domain-specific rules support +- Batch processing for better performance + +## Migration Guide + +### Migrating from 1.2.0 to 2.0.0-beta + +#### Breaking Changes + +1. **Controller Initialization** + ```dart + // Old (1.2.0) + final controller = AdBlockerWebviewController(); + await controller.initialize(); + + // New (2.0.0-beta) + await AdBlockerWebviewController.instance.initialize( + FilterConfig( + filterTypes: [FilterType.easyList, FilterType.adGuard], + ), + ); + ``` + +2. **URL Parameter Type** + ```dart + // Old (1.2.0) + AdBlockerWebview( + url: "https://example.com", + // ... + ) + + // New (2.0.0-beta) + AdBlockerWebview( + url: Uri.parse("https://example.com"), + // ... + ) + ``` + +3. **Filter Configuration** + ```dart + // Old (1.2.0) + AdBlockerWebview( + //.. other params + additionalHostsToBlock: ['ads.example.com'], + ); + + // New (2.0.0-beta) + // Use FilterConfig for configuration + await AdBlockerWebviewController.instance.initialize( + FilterConfig( + filterTypes: [FilterType.easyList, FilterType.adGuard], + ), + ); + ``` + +4. **Event Handlers** + ```dart + // Old (1.2.0) + onTitleChanged: (title) { ... } + + // New (2.0.0-beta) + // Use onUrlChanged instead + onUrlChanged: (url) { ... } + ``` + +#### Deprecated Features +- `additionalHostsToBlock` parameter is removed +- Individual controller instances are replaced with singleton +- `onTitleChanged` callback is replaced with `onUrlChanged` + +#### New Features +- Singleton controller pattern for better resource management +- Structured filter configuration using `FilterConfig` +- Improved type safety with `Uri` for URLs +- Enhanced filter list parsing and management +- Better performance through early resource blocking + +#### Steps to Migrate +1. Update the package version in `pubspec.yaml`: + ```yaml + dependencies: + adblocker_webview: ^2.0.0-beta + ``` + +2. Replace controller initialization with singleton pattern +3. Update URL parameters to use `Uri` instead of `String` +4. Replace deprecated callbacks with new ones +5. Update filter configuration to use `FilterConfig` +6. Test the application thoroughly after migration + +## Contributing + +We welcome contributions to improve the ad-blocking capabilities! Here's how you can help: + +### Getting Started +1. Fork the repository +2. Create a new branch from `main` for your feature/fix + - Use `feature/` prefix for new features + - Use `fix/` prefix for bug fixes + - Use `docs/` prefix for documentation changes +3. Make your changes +4. Write/update tests if needed +5. Update documentation if needed +6. Run tests and ensure they pass +7. Submit a pull request + +### Before Submitting +- Check that your code follows our style guide (see analysis badge) +- Write clear commit messages +- Include tests for new features +- Update documentation if needed +- Verify all tests pass + +### Pull Request Process +1. Create an issue first to discuss major changes +2. Update the README.md if needed +3. Update the CHANGELOG.md following semantic versioning +4. The PR will be reviewed by maintainers +5. Once approved, it will be merged + +### Code Style +- Follow [Effective Dart](https://dart.dev/guides/language/effective-dart) guidelines +- Use the provided analysis options +- Run `dart format` before committing + +## License + +This project is licensed under the BSD-3-Clause License - see the LICENSE file for details. diff --git a/assets/raw/block_ad_resource_loading.js b/assets/raw/block_ad_resource_loading.js new file mode 100644 index 0000000..6a95ac5 --- /dev/null +++ b/assets/raw/block_ad_resource_loading.js @@ -0,0 +1,98 @@ +(function () { + const rules = window.adBlockerRules || []; + + function domainMatches(rule, target) { + return rule === target || target.includes(rule); + } + + function isBlocked(url, originType) { + // First check exception rules + const isException = rules.some(rule => { + return rule.isException && domainMatches(rule.url, url); + }); + + if (isException) { + console.log(`[EXCEPTION][${originType}] ${url}`, { + domain: url, + currentDomain: window.location.hostname + }); + return false; + } + + // Then check blocking rules + const blockedRule = rules.find(rule => { + return !rule.isException && domainMatches(rule.url, url); + }); + + if (blockedRule) { + console.log(`[BLOCKED][${originType}] ${url}`, { + domain: url, + rule: blockedRule.url, + currentDomain: window.location.hostname + }); + return true; + } + return false; + } + + // Override XMLHttpRequest + const originalXHROpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function (method, url) { + if (isBlocked(url, 'XHR')) { + return new Proxy(new XMLHttpRequest(), { + get: function(target, prop) { + if (prop === 'send') return function() {}; + return target[prop]; + } + }); + } + return originalXHROpen.apply(this, arguments); + }; + + // Override Fetch API + const originalFetch = window.fetch; + window.fetch = function (resource, init) { + const url = resource instanceof Request ? resource.url : resource; + if (isBlocked(url, 'Fetch')) { + return Promise.resolve(new Response('', { + status: 200, + statusText: 'OK' + })); + } + return originalFetch.apply(this, arguments); + }; + + // Block dynamic script loading + const originalCreateElement = document.createElement; + document.createElement = function (tagName) { + const element = originalCreateElement.apply(document, arguments); + + if (tagName.toLowerCase() === 'script') { + const originalSetAttribute = element.setAttribute; + element.setAttribute = function(name, value) { + if (name === 'src' && isBlocked(value, 'Script')) { + return; + } + return originalSetAttribute.call(this, name, value); + }; + } + + return element; + }; + + // Block image loading + const originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src'); + Object.defineProperty(Image.prototype, 'src', { + get: function() { + return originalImageSrc.get.call(this); + }, + set: function(value) { + if (isBlocked(value, 'Image')) { + return; + } + originalImageSrc.set.call(this, value); + } + }); + + console.log('[AdBlocker] Resource blocking initialized with', rules.length, 'rules'); +})(); diff --git a/assets/raw/element_hiding_script.js b/assets/raw/element_hiding_script.js new file mode 100644 index 0000000..c34bf66 --- /dev/null +++ b/assets/raw/element_hiding_script.js @@ -0,0 +1,43 @@ +(function () { + const selectors = window.adBlockerSelectors || []; + const BATCH_SIZE = 1000; + + async function hideElements() { + if (!Array.isArray(selectors) || !selectors.length) { + console.log('[AdBlocker] No selectors to process'); + return; + } + + try { + const batchCount = Math.ceil(selectors.length / BATCH_SIZE); + for (let i = 0; i < batchCount; i++) { + const start = i * BATCH_SIZE; + const end = Math.min(start + BATCH_SIZE, selectors.length); + const batchSelectors = selectors.slice(start, end); + + document.querySelectorAll(batchSelectors.join(',')).forEach((el) => { + console.log('Removing element: ', el.id); + return el.remove(); + }); + await new Promise(resolve => setTimeout(resolve, 300)); + } + console.info('[AdBlocker] Elements hide rules applied: ', selectors.length); + } catch (error) { + console.error('[AdBlocker] Error:', error); + } + } + + // Create a MutationObserver instance + const observer = new MutationObserver(() => hideElements()); + + // Start observing + try { + observer.observe(document.body, { + childList: true, + subtree: true + }); + hideElements(); + } catch (error) { + console.error('[AdBlocker] Observer error:', error); + } +})(); diff --git a/assets/raw/observer_script.js b/assets/raw/observer_script.js new file mode 100644 index 0000000..042e668 --- /dev/null +++ b/assets/raw/observer_script.js @@ -0,0 +1,51 @@ +(function () { + // Listening for the appearance of the body element to execute the script as soon as possible before the `interactive` event. + const config = { attributes: false, childList: true, subtree: true }; + const callback = function (mutationsList, observer) { + for (const mutation of mutationsList) { + if (mutation.type === 'childList') { + if (document.getElementsByTagName('body')[0]) { + console.log('body element has appeared'); + // Execute the script when the body element appears. + script(); + // Mission accomplished, no more to observe. + observer.disconnect(); + } + break; + } + } + }; + const observer = new MutationObserver(callback); + observer.observe(document, config); + + const onReadystatechange = function () { + if (document.readyState == 'interactive') { + script(); + } + } + // The script is mainly executed by MutationObserver, and the following listeners are only used as fallbacks. + const addListeners = function () { + // here don't use document.onreadystatechange, which won't fire sometimes + document.addEventListener('readystatechange', onReadystatechange); + + document.addEventListener('DOMContentLoaded', script, false); + + window.addEventListener('load', script); + } + const removeListeners = function () { + document.removeEventListener('readystatechange', onReadystatechange); + + document.removeEventListener('DOMContentLoaded', script, false); + + window.removeEventListener('load', script); + } + const script = function () { + {{CONTENT}} + removeListeners(); + } + if (document.readyState == 'interactive' || document.readyState == 'complete') { + script(); + } else { + addListeners(); + } +})(); diff --git a/example/.gitignore b/example/.gitignore index 24476c5..6c31954 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 61b6c4d..ee7f0c9 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -22,6 +22,7 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: + use_build_context_synchronously: false # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule diff --git a/example/android/.gitignore b/example/android/.gitignore index 8fa38fa..c9e8004 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -8,9 +8,13 @@ GeneratedPluginRegistrant.java build/ .idea/ +.android/app/.cxx/ # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks + +# NDK build directory +.cxx/ diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index c15fae9..0749ac4 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -21,21 +27,17 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - + compileSdkVersion 35 + ndkVersion = "27.0.12077973" + namespace 'com.islamdidarmd.example.adblockerwebview.flutter.example' compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -43,20 +45,15 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.islamdidarmd.example.adblockerwebview.flutter.example" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion + minSdkVersion 21 + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } @@ -66,6 +63,3 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index b2c67a6..8691c0a 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -31,5 +31,8 @@ + diff --git a/example/android/build.gradle b/example/android/build.gradle index 713d7f6..bc157bd 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..a545497 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sat Dec 14 18:17:24 MYT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bc..e261ebf 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.7.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false +} + +include ":app" diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b45d3c0..7cb50dc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,32 +1,36 @@ PODS: - Flutter (1.0.0) - - flutter_inappwebview (0.0.1): + - path_provider_foundation (0.0.1): - Flutter - - flutter_inappwebview/Core (= 0.0.1) - - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): + - FlutterMacOS + - sqflite_darwin (0.0.4): - Flutter - - OrderedSet (~> 5.0) - - OrderedSet (5.0.0) + - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS DEPENDENCIES: - Flutter (from `Flutter`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - -SPEC REPOS: - trunk: - - OrderedSet + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) EXTERNAL SOURCES: Flutter: :path: Flutter - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 1dd79cb..429eb58 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -68,7 +68,6 @@ 690E6D5C3DFEC1D835732A27 /* Pods-Runner.release.xcconfig */, D5C18CB672469640DCB5A3C0 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -358,8 +357,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 3JH9L9U9Y4; + DEVELOPMENT_TEAM = 8F7KFNGML2; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -368,6 +369,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.islamdidarmd.example.adblockerwebview.flutter.example; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -487,8 +489,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 3JH9L9U9Y4; + DEVELOPMENT_TEAM = 8F7KFNGML2; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -497,6 +501,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.islamdidarmd.example.adblockerwebview.flutter.example; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -510,8 +515,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 3JH9L9U9Y4; + DEVELOPMENT_TEAM = 8F7KFNGML2; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -520,6 +527,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.islamdidarmd.example.adblockerwebview.flutter.example; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d..c53e2b3 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/lib/browser.dart b/example/lib/browser.dart index 48a82d7..f8eb95e 100644 --- a/example/lib/browser.dart +++ b/example/lib/browser.dart @@ -36,7 +36,6 @@ class _BrowserState extends State { }); }, shouldBlockAds: widget.shouldBlockAds, - additionalHostsToBlock: const [], ), ), Row( diff --git a/example/lib/browser_screen.dart b/example/lib/browser_screen.dart new file mode 100644 index 0000000..e16a0da --- /dev/null +++ b/example/lib/browser_screen.dart @@ -0,0 +1,118 @@ +import 'package:adblocker_webview/adblocker_webview.dart'; +import 'package:flutter/material.dart'; + +class BrowserScreen extends StatefulWidget { + const BrowserScreen({ + required this.url, + required this.shouldBlockAds, + super.key, + }); + + final Uri url; + final bool shouldBlockAds; + + @override + State createState() => _BrowserScreenState(); +} + +class _BrowserScreenState extends State { + final _controller = AdBlockerWebviewController.instance; + bool _canGoBack = false; + String _appbarUrl = ""; + + @override + void initState() { + super.initState(); + _appbarUrl = widget.url.host; + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !_canGoBack, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + if (await _controller.canGoBack()) { + _controller.goBack(); + } else { + if (mounted) { + Navigator.of(context).pop(); + } + } + }, + child: Scaffold( + appBar: AppBar( + title: Text(_appbarUrl), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () async { + if (await _controller.canGoBack()) { + _controller.goBack(); + } else { + if (mounted) { + Navigator.of(context).pop(); + } + } + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: _canGoBack + ? () { + _controller.goBack(); + } + : null, + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + _controller.reload(); + }, + ), + ], + ), + body: AdBlockerWebview( + url: widget.url, + shouldBlockAds: widget.shouldBlockAds, + adBlockerWebviewController: _controller, + onLoadStart: (url) { + debugPrint('Started loading: $url'); + }, + onLoadFinished: (url) { + debugPrint('Finished loading: $url'); + _updateNavigationState(url); + }, + onLoadError: (url, code) { + debugPrint('Error loading: $url (code: $code)'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading page: $code'), + backgroundColor: Colors.red, + ), + ); + }, + onProgress: (progress) { + debugPrint('Loading progress: $progress%'); + }, + onUrlChanged: (url) { + _updateNavigationState(url); + }, + ), + ), + ); + } + + Future _updateNavigationState(String? url) async { + if (!mounted) return; + + final canGoBack = await _controller.canGoBack(); + if (canGoBack != _canGoBack) { + setState(() { + _canGoBack = canGoBack; + _appbarUrl = Uri.tryParse(url ?? "")?.host ?? ""; + }); + } + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 901ba1e..ffdd4ac 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,83 +1,98 @@ import 'package:adblocker_webview/adblocker_webview.dart'; -import 'package:example/browser.dart'; -import 'package:example/url_input.dart'; import 'package:flutter/material.dart'; -void main() { - runApp(const MyApp()); +import 'browser_screen.dart'; +import 'url_input_section.dart'; + +void main() async { + await AdBlockerWebviewController.instance.initialize( + FilterConfig( + filterTypes: [FilterType.easyList, FilterType.adGuard], + ), + [], + ); + runApp(const ExampleApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class ExampleApp extends StatelessWidget { + const ExampleApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'AdBlocker WebView Example', theme: ThemeData( - primarySwatch: Colors.blue, + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: const HomePage(), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; +class HomePage extends StatefulWidget { + const HomePage({super.key}); @override - State createState() => _MyHomePageState(); + State createState() => _HomePageState(); } -class _MyHomePageState extends State { - final _adBlockerWebviewController = AdBlockerWebviewController.instance; - bool _showBrowser = false; - bool _shouldBlockAds = false; - String _url = ""; - - @override - void initState() { - super.initState(); - _adBlockerWebviewController.initialize(); - } +class _HomePageState extends State { + bool _shouldBlockAds = true; @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(widget.title)), - floatingActionButton: _showBrowser - ? FloatingActionButton( - child: const Icon(Icons.edit), - onPressed: () { - setState(() { - _showBrowser = false; - }); - }, - ) - : null, - body: _showBrowser - ? Browser( - url: _url, - controller: _adBlockerWebviewController, - shouldBlockAds: _shouldBlockAds, - ) - : UrlInput( - onSubmit: (url) { - setState(() { - _url = url; - _showBrowser = true; - }); - }, - onBlockAdsStatusChange: (shouldBlockAds) { - setState(() { - _shouldBlockAds = shouldBlockAds; - }); - }, - shouldBlockAds: _shouldBlockAds, + appBar: AppBar( + title: const Text('AdBlocker WebView Example'), + actions: [ + Row( + children: [ + const Text('Block Ads'), + Switch( + value: _shouldBlockAds, + onChanged: (value) { + setState(() { + _shouldBlockAds = value; + }); + }, + ), + ], + ), + ], + ), + body: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Enter a URL to test ad blocking', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + UrlInputSection( + onUrlSubmitted: (url) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => BrowserScreen( + url: url, + shouldBlockAds: _shouldBlockAds, + ), + ), + ); + }, + ), + ], ), + ), + ), + ), ); } } diff --git a/example/lib/url_input_section.dart b/example/lib/url_input_section.dart new file mode 100644 index 0000000..c1db39a --- /dev/null +++ b/example/lib/url_input_section.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; + +class UrlInputSection extends StatefulWidget { + const UrlInputSection({ + required this.onUrlSubmitted, + super.key, + }); + + final void Function(Uri url) onUrlSubmitted; + + @override + State createState() => _UrlInputSectionState(); +} + +class _UrlInputSectionState extends State { + final _urlController = TextEditingController(); + final _formKey = GlobalKey(); + + static const _predefinedUrls = [ + 'https://www.theguardian.com', + 'https://www.nytimes.com', + 'https://www.cnn.com', + 'https://www.reddit.com', + 'https://www.youtube.com', + ]; + + final List _recentUrls = []; + + @override + void dispose() { + _urlController.dispose(); + super.dispose(); + } + + void _submitUrl() { + if (_formKey.currentState?.validate() ?? false) { + final url = _normalizeUrl(_urlController.text); + _addToRecent(url.toString()); + widget.onUrlSubmitted(url); + } + } + + Uri _normalizeUrl(String input) { + var url = input.trim().toLowerCase(); + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://$url'; + } + return Uri.parse(url); + } + + void _addToRecent(String url) { + setState(() { + _recentUrls + ..remove(url) + ..insert(0, url); + if (_recentUrls.length > 5) { + _recentUrls.removeLast(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Form( + key: _formKey, + child: TextFormField( + controller: _urlController, + decoration: InputDecoration( + labelText: 'Enter URL', + hintText: 'https://example.com', + prefixIcon: const Icon(Icons.link), + suffixIcon: IconButton( + icon: const Icon(Icons.send), + onPressed: _submitUrl, + ), + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.url, + textInputAction: TextInputAction.go, + onFieldSubmitted: (_) => _submitUrl(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a URL'; + } + try { + final url = _normalizeUrl(value); + if (!url.hasScheme || !url.hasAuthority) { + return 'Please enter a valid URL'; + } + } catch (e) { + return 'Invalid URL format'; + } + return null; + }, + ), + ), + const SizedBox(height: 16), + const Text( + 'Test URLs:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + for (final url in _predefinedUrls) + ActionChip( + label: Text(Uri.parse(url).host), + onPressed: () { + _urlController.text = url; + _submitUrl(); + }, + ), + ], + ), + if (_recentUrls.isNotEmpty) ...[ + const SizedBox(height: 16), + const Text( + 'Recent URLs:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + for (final url in _recentUrls) + InputChip( + label: Text(Uri.parse(url).host), + onPressed: () { + _urlController.text = url; + _submitUrl(); + }, + deleteIcon: const Icon(Icons.close, size: 18), + onDeleted: () { + setState(() { + _recentUrls.remove(url); + }); + }, + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..f066e6f --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,276 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + adblocker_core: + dependency: transitive + description: + name: adblocker_core + sha256: "21ffe452054160fe5df2d2bef2c2f9a6aa4c7184650527a099da8979a104308f" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + adblocker_manager: + dependency: transitive + description: + name: adblocker_manager + sha256: e9a6afe9f4be07f1ba6b07094a46d9f1c9de0572b302601f3ddd1c698be25ebf + url: "https://pub.dev" + source: hosted + version: "1.0.0" + adblocker_webview: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "2.0.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "3d535126f7244871542b2f0b0fcf94629c9a14883250461f9abe1a6644c1c379" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: b7e92f129482460951d96ef9a46b49db34bd2e1621685de26e9eaafd9674e7eb + url: "https://pub.dev" + source: hosted + version: "3.16.3" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cfe3e5b..257d1bf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,8 +4,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=3.0.0 <4.0.0' - flutter: '>=3.19.5' + sdk: ^3.6.0 dependencies: flutter: diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 092d222..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/lib/adblocker_webview.dart b/lib/adblocker_webview.dart index c2cc359..a5f1161 100644 --- a/lib/adblocker_webview.dart +++ b/lib/adblocker_webview.dart @@ -1,3 +1,4 @@ +export 'package:adblocker_manager/adblocker_manager.dart'; export 'src/adblocker_webview.dart'; export 'src/adblocker_webview_controller.dart'; -export 'src/domain/entity/host.dart'; +export 'src/adblocker_webview_controller_impl.dart'; diff --git a/lib/src/adblocker_webview.dart b/lib/src/adblocker_webview.dart index 03307af..7e6ec96 100644 --- a/lib/src/adblocker_webview.dart +++ b/lib/src/adblocker_webview.dart @@ -1,8 +1,13 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:adblocker_manager/adblocker_manager.dart'; import 'package:adblocker_webview/src/adblocker_webview_controller.dart'; -import 'package:adblocker_webview/src/domain/entity/host.dart'; -import 'package:adblocker_webview/src/domain/mapper/host_mapper.dart'; +import 'package:adblocker_webview/src/block_resource_loading.dart'; +import 'package:adblocker_webview/src/elem_hide.dart'; +import 'package:adblocker_webview/src/logger.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:webview_flutter/webview_flutter.dart'; /// A webview implementation of in Flutter that blocks most of the ads that /// appear inside of the webpages. @@ -16,18 +21,23 @@ class AdBlockerWebview extends StatefulWidget { this.onLoadFinished, this.onProgress, this.onLoadError, - this.onTitleChanged, - this.options, - this.additionalHostsToBlock = const [], + this.onUrlChanged, super.key, }) : assert( - (url == null && initialHtmlData != null) || - (url != null && initialHtmlData == null), - 'Both url and initialHtmlData can not be non null'); - - /// Required: The initial [Uri] url that will be displayed in webview. + url != null || initialHtmlData != null, + 'Either url or initialHtmlData must be provided', + ), + assert( + !(url != null && initialHtmlData != null), + 'Cannot provide both url and initialHtmlData', + ); + + /// The initial [Uri] url that will be displayed in webview. + /// Either this or [initialHtmlData] must be provided, but not both. final Uri? url; + /// The initial HTML content to load in the webview. + /// Either this or [url] must be provided, but not both. final String? initialHtmlData; /// Required: The controller for [AdBlockerWebview]. @@ -38,31 +48,19 @@ class AdBlockerWebview extends StatefulWidget { final bool shouldBlockAds; /// Invoked when a page has started loading. - final void Function(InAppWebViewController controller, Uri? uri)? onLoadStart; + final void Function(String? url)? onLoadStart; /// Invoked when a page has finished loading. - final void Function(InAppWebViewController controller, Uri? uri)? - onLoadFinished; + final void Function(String? url)? onLoadFinished; /// Invoked when a page is loading to report the progress. final void Function(int progress)? onProgress; /// Invoked when the page title is changed. - final void Function(InAppWebViewController controller, String? title)? - onTitleChanged; - - final List additionalHostsToBlock; + final void Function(String? url)? onUrlChanged; /// Invoked when a loading error occurred. - final void Function( - InAppWebViewController controller, - Uri? url, - int code, - String message, - )? onLoadError; - - /// Options for InAppWebView. - final InAppWebViewGroupOptions? options; + final void Function(String? url, int code)? onLoadError; @override State createState() => _AdBlockerWebviewState(); @@ -70,55 +68,131 @@ class AdBlockerWebview extends StatefulWidget { class _AdBlockerWebviewState extends State { final _webViewKey = GlobalKey(); - InAppWebViewGroupOptions? _inAppWebViewOptions; + late final WebViewController _webViewController; + + late Future _depsFuture; + final List _urlsToBlock = []; @override void initState() { super.initState(); - _inAppWebViewOptions = widget.options ?? - InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions(), - ); + _depsFuture = _init(); + } - if (widget.shouldBlockAds) { - _setContentBlockers(); + Future _init() async { + _urlsToBlock + ..clear() + ..addAll(widget.adBlockerWebviewController.bannedResourceRules); + + _webViewController = WebViewController(); + await Future.wait([ + _webViewController.setOnConsoleMessage((message) { + debugLog('[FLUTTER_WEBVIEW_LOG]: ${message.message}'); + }), + _webViewController.setUserAgent(_getUserAgent()), + _webViewController.setJavaScriptMode(JavaScriptMode.unrestricted), + ]); + + _setNavigationDelegate(); + widget.adBlockerWebviewController.setInternalController(_webViewController); + + // Load either URL or HTML content + if (widget.url != null) { + unawaited(_webViewController.loadRequest(widget.url!)); + } else if (widget.initialHtmlData != null) { + unawaited(_webViewController.loadHtmlString(widget.initialHtmlData!)); } } - Future _setContentBlockers() async { - final contentBlockerList = - mapHostToContentBlocker(widget.adBlockerWebviewController.bannedHost) - ..addAll(mapHostToContentBlocker(widget.additionalHostsToBlock)); - _inAppWebViewOptions?.crossPlatform.contentBlockers = contentBlockerList; + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _depsFuture, + builder: (_, state) { + if (state.hasError) { + return Text('Error: ${state.error}'); + } else if (state.connectionState == ConnectionState.done) { + return WebViewWidget( + key: _webViewKey, + controller: _webViewController, + ); + } else if (state.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 45, + child: Center(child: CircularProgressIndicator()), + ); + } + return const SizedBox(); + }, + ); } - void _clearContentBlockers() => - _inAppWebViewOptions?.crossPlatform.contentBlockers = []; + void _setNavigationDelegate() { + _webViewController.setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) { + final shouldBlock = widget.adBlockerWebviewController + .shouldBlockResource(request.url); + if (shouldBlock) { + debugLog('Blocking resource: ${request.url}'); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + onPageStarted: (url) async { + if (widget.shouldBlockAds) { + // Inject resource blocking script as early as possible + unawaited( + _webViewController.runJavaScript( + getResourceLoadingBlockerScript(_urlsToBlock), + ), + ); + } + widget.onLoadStart?.call(url); + }, + onPageFinished: (url) { + if (widget.shouldBlockAds) { + // Apply element hiding after page load + final cssRules = + widget.adBlockerWebviewController.getCssRulesForWebsite(url); + unawaited( + _webViewController.runJavaScript(generateHidingScript(cssRules)), + ); + } + + widget.onLoadFinished?.call(url); + }, + onProgress: (progress) => widget.onProgress?.call(progress), + onHttpError: + (error) => widget.onLoadError?.call( + error.request?.uri.toString(), + error.response?.statusCode ?? -1, + ), + onUrlChange: (change) => widget.onUrlChanged?.call(change.url), + ), + ); + } - @override - void didUpdateWidget(AdBlockerWebview oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.shouldBlockAds) { - _setContentBlockers(); + String _getUserAgent() { + final osVersion = Platform.operatingSystemVersion; + + if (Platform.isAndroid) { + // Chrome 120 is the latest stable version as of now + return 'Mozilla/5.0 (Linux; Android $osVersion) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/120.0.0.0 Mobile Safari/537.36'; + } else if (Platform.isIOS) { + // Convert iOS version format from 13.0.0 to 13_0_0 + final iosVersion = osVersion.replaceAll('.', '_'); + // iOS 17 with Safari 17 is the latest stable version + return 'Mozilla/5.0 (iPhone; CPU iPhone OS $iosVersion like Mac OS X) ' + 'AppleWebKit/605.1.15 (KHTML, like Gecko) ' + 'Version/17.0 Mobile/15E148 Safari/604.1'; } else { - _clearContentBlockers(); + // Default to latest Chrome for other platforms + return 'Mozilla/5.0 ($osVersion) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/120.0.0.0 Safari/537.36'; } } - - @override - Widget build(BuildContext context) { - return InAppWebView( - key: _webViewKey, - onWebViewCreated: widget.adBlockerWebviewController.setInternalController, - initialUrlRequest: URLRequest(url: widget.url), - initialOptions: _inAppWebViewOptions, - onLoadStart: widget.onLoadStart, - onLoadStop: widget.onLoadFinished, - onLoadError: widget.onLoadError, - onTitleChanged: widget.onTitleChanged, - initialData: widget.initialHtmlData == null - ? null - : InAppWebViewInitialData(data: widget.initialHtmlData!), - ); - } } diff --git a/lib/src/adblocker_webview_controller.dart b/lib/src/adblocker_webview_controller.dart index c1012b5..d868bb3 100644 --- a/lib/src/adblocker_webview_controller.dart +++ b/lib/src/adblocker_webview_controller.dart @@ -1,7 +1,6 @@ import 'dart:collection'; import 'package:adblocker_webview/adblocker_webview.dart'; -import 'package:adblocker_webview/src/adblocker_webview_controller_impl.dart'; import 'package:adblocker_webview/src/internal_adblocker_webview_controller.dart'; /// The controller for [AdBlockerWebview]. @@ -15,7 +14,7 @@ import 'package:adblocker_webview/src/internal_adblocker_webview_controller.dart /// @override /// void initState() { /// super.initState(); -/// _adBlockerWebviewController.initialize(); +/// _adBlockerWebviewController.initialize(config, []); /// /// ... Other code here. /// } /// ``` @@ -24,7 +23,8 @@ import 'package:adblocker_webview/src/internal_adblocker_webview_controller.dart ///ignore_for_file: avoid-late-keyword ///ignore_for_file: avoid-non-null-assertion -abstract class AdBlockerWebviewController implements InternalWebviewController { +abstract interface class AdBlockerWebviewController + implements InternalWebviewController { static AdBlockerWebviewController? _instance; /// Returns an implementation of this class @@ -33,12 +33,11 @@ abstract class AdBlockerWebviewController implements InternalWebviewController { return _instance!; } - /// Returns the banned host list. - /// This list items are populated after calling the [initialize] method - UnmodifiableListView get bannedHost; - /// Initializes the controller - Future initialize(); + Future initialize( + FilterConfig filterConfig, + List additionalResourceRules, + ); /// Returns decision of if the webview can go back Future canGoBack(); @@ -49,17 +48,20 @@ abstract class AdBlockerWebviewController implements InternalWebviewController { // Clears the cache of webview Future clearCache(); + /// Returns the banned resource rules list. + /// This list items are populated after calling the [initialize] method + UnmodifiableListView get bannedResourceRules; + // Returns the title of currently loaded webpage Future getTitle(); // Loads the given url Future loadUrl(String url); - Future loadData( - String data, { - String mimeType = 'text/html', - String encoding = 'utf8', - }); + Future loadData(String data, {String? baseUrl}); + + /// Returns the css rules for the given url + List getCssRulesForWebsite(String url); /// Navigates webview to previous page Future goBack(); @@ -67,6 +69,12 @@ abstract class AdBlockerWebviewController implements InternalWebviewController { /// Navigates the webview to forward page Future goForward(); + /// Returns decision of if the resource should be blocked + bool shouldBlockResource(String url); + /// Reloads the current page Future reload(); + + /// Runs the given script + Future runScript(String script); } diff --git a/lib/src/adblocker_webview_controller_impl.dart b/lib/src/adblocker_webview_controller_impl.dart index 5494824..4fd4e08 100644 --- a/lib/src/adblocker_webview_controller_impl.dart +++ b/lib/src/adblocker_webview_controller_impl.dart @@ -1,124 +1,131 @@ import 'dart:collection'; -import 'package:adblocker_webview/src/adblocker_webview_controller.dart'; -import 'package:adblocker_webview/src/data/repository/adblocker_repository_impl.dart'; -import 'package:adblocker_webview/src/domain/entity/host.dart'; -import 'package:adblocker_webview/src/domain/repository/adblocker_repository.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:adblocker_webview/adblocker_webview.dart'; +import 'package:webview_flutter/webview_flutter.dart'; ///Implementation for [AdBlockerWebviewController] class AdBlockerWebviewControllerImpl implements AdBlockerWebviewController { - AdBlockerWebviewControllerImpl({AdBlockerRepository? repository}) - : _repository = repository ?? AdBlockerRepositoryImpl(); - final AdBlockerRepository _repository; + AdBlockerWebviewControllerImpl(); - InAppWebViewController? _inAppWebViewController; - - final _bannedHost = []; - - @override - UnmodifiableListView get bannedHost => - UnmodifiableListView(_bannedHost); + WebViewController? _webViewController; + final AdblockFilterManager _adBlockManager = AdblockFilterManager(); + final _bannedResourceRules = []; @override - Future initialize() async { - final hosts = await _repository.fetchBannedHostList(); - _bannedHost + Future initialize( + FilterConfig filterConfig, + List additionalResourceRules, + ) async { + await _adBlockManager.init(filterConfig); + _bannedResourceRules ..clear() - ..addAll(hosts); + ..addAll(_adBlockManager.getAllResourceRules()) + ..addAll(additionalResourceRules); } @override - void setInternalController(InAppWebViewController controller) { - _inAppWebViewController = controller; + UnmodifiableListView get bannedResourceRules => + UnmodifiableListView(_bannedResourceRules); + + @override + void setInternalController(WebViewController controller) { + _webViewController = controller; } @override Future canGoBack() async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { return false; } - return _inAppWebViewController!.canGoBack(); + return _webViewController!.canGoBack(); } @override Future canGoForward() async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { return false; } - return _inAppWebViewController!.canGoForward(); + return _webViewController!.canGoForward(); } @override Future clearCache() async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { return; } - return _inAppWebViewController!.clearCache(); + return _webViewController!.clearCache(); } + @override + List getCssRulesForWebsite(String url) => + _adBlockManager.getCSSRulesForWebsite(url); + @override Future getTitle() async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { return null; } - return _inAppWebViewController!.getTitle(); + return _webViewController!.getTitle(); } @override Future goBack() async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { return; } - return _inAppWebViewController!.goBack(); + return _webViewController!.goBack(); } @override Future goForward() async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { return; } - return _inAppWebViewController!.goForward(); + return _webViewController!.goForward(); } @override Future loadUrl(String url) async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { return; } - return _inAppWebViewController! - .loadUrl(urlRequest: URLRequest(url: Uri.parse(url))); + return _webViewController!.loadRequest(Uri.parse(url)); } @override - Future loadData( - String data, { - String mimeType = 'text/html', - String encoding = 'utf8', - }) async { - if (_inAppWebViewController == null) { + Future loadData(String data, {String? baseUrl}) async { + if (_webViewController == null) { return; } - return _inAppWebViewController!.loadData( - data: data, - mimeType: mimeType, - encoding: encoding, - ); + return _webViewController!.loadHtmlString(data, baseUrl: baseUrl); } + @override + bool shouldBlockResource(String url) => + _adBlockManager.shouldBlockResource(url); + @override Future reload() async { - if (_inAppWebViewController == null) { + if (_webViewController == null) { + return; + } + + return _webViewController!.reload(); + } + + @override + Future runScript(String script) async { + if (_webViewController == null) { return; } - return _inAppWebViewController!.reload(); + return _webViewController!.runJavaScript(script); } } diff --git a/lib/src/block_resource_loading.dart b/lib/src/block_resource_loading.dart new file mode 100644 index 0000000..23a8743 --- /dev/null +++ b/lib/src/block_resource_loading.dart @@ -0,0 +1,169 @@ +import 'package:adblocker_manager/adblocker_manager.dart'; + +String getResourceLoadingBlockerScript(List rules) { + // Convert ResourceRules to JavaScript objects + final jsRules = rules.map((rule) => ''' + { + url: '${rule.url}', + isException: ${rule.isException} + } + ''',).join(',\n'); + + final content = ''' + window.adBlockerRules = [$jsRules]; + + function setupResourceBlocking() { + const rules = window.adBlockerRules || []; + + function domainMatches(rule, target) { + return rule === target || target.includes(rule); + } + + function isBlocked(url, originType) { + // First check exception rules + const isException = rules.some(rule => { + return rule.isException && domainMatches(rule.url, url); + }); + + if (isException) { + console.log(`[EXCEPTION][\${originType}] \${url}`, { + domain: url, + currentDomain: window.location.hostname + }); + return false; + } + + // Then check blocking rules + const blockedRule = rules.find(rule => { + return !rule.isException && domainMatches(rule.url, url); + }); + + if (blockedRule) { + console.log(`[BLOCKED][\${originType}] \${url}`, { + domain: url, + rule: blockedRule.url, + currentDomain: window.location.hostname + }); + return true; + } + return false; + } + + // Override XMLHttpRequest + const originalXHROpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function (method, url) { + if (isBlocked(url, 'XHR')) { + return new Proxy(new XMLHttpRequest(), { + get: function(target, prop) { + if (prop === 'send') return function() {}; + return target[prop]; + } + }); + } + return originalXHROpen.apply(this, arguments); + }; + + // Override Fetch API + const originalFetch = window.fetch; + window.fetch = function (resource, init) { + const url = resource instanceof Request ? resource.url : resource; + if (isBlocked(url, 'Fetch')) { + return Promise.resolve(new Response('', { + status: 200, + statusText: 'OK' + })); + } + return originalFetch.apply(this, arguments); + }; + + // Block dynamic script loading + const originalCreateElement = document.createElement; + document.createElement = function (tagName) { + const element = originalCreateElement.apply(document, arguments); + + if (tagName.toLowerCase() === 'script') { + const originalSetAttribute = element.setAttribute; + element.setAttribute = function(name, value) { + if (name === 'src' && isBlocked(value, 'Script')) { + return; + } + return originalSetAttribute.call(this, name, value); + }; + } + + return element; + }; + + // Block image loading + const originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src'); + Object.defineProperty(Image.prototype, 'src', { + get: function() { + return originalImageSrc.get.call(this); + }, + set: function(value) { + if (isBlocked(value, 'Image')) { + return; + } + originalImageSrc.set.call(this, value); + } + }); + + console.log('[AdBlocker] Resource blocking initialized with', rules.length, 'rules'); + } + '''; + + return ''' + (function () { + // Listening for the appearance of the body element to execute the script as early as possible + const config = { attributes: false, childList: true, subtree: true }; + const callback = function (mutationsList, observer) { + for (const mutation of mutationsList) { + if (mutation.type === 'childList') { + if (document.getElementsByTagName('body')[0]) { + console.log('[AdBlocker] Body element detected, initializing blocking'); + script(); + observer.disconnect(); + } + break; + } + } + }; + const observer = new MutationObserver(callback); + observer.observe(document, config); + + const onReadystatechange = function () { + if (document.readyState == 'interactive') { + script(); + } + } + + const addListeners = function () { + document.addEventListener('readystatechange', onReadystatechange); + document.addEventListener('DOMContentLoaded', script, false); + window.addEventListener('load', script); + } + + const removeListeners = function () { + document.removeEventListener('readystatechange', onReadystatechange); + document.removeEventListener('DOMContentLoaded', script, false); + window.removeEventListener('load', script); + } + + const script = function () { + try { + $content + setupResourceBlocking(); + } catch (error) { + console.error('[AdBlocker] Setup error:', error); + } + removeListeners(); + } + + if (document.readyState == 'interactive' || document.readyState == 'complete') { + script(); + } else { + addListeners(); + } + })(); + '''; +} diff --git a/lib/src/data/repository/adblocker_repository_impl.dart b/lib/src/data/repository/adblocker_repository_impl.dart deleted file mode 100644 index c5f6519..0000000 --- a/lib/src/data/repository/adblocker_repository_impl.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:convert'; - -import 'package:adblocker_webview/src/domain/entity/host.dart'; -import 'package:adblocker_webview/src/domain/repository/adblocker_repository.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; - -class AdBlockerRepositoryImpl implements AdBlockerRepository { - final _url = - 'https://pgl.yoyo.org/as/serverlist.php?hostformat=nohtml&showintro=0'; - - @override - Future> fetchBannedHostList() async { - try { - final response = await _getDataWithCache(_url); - - return LineSplitter.split(response) - .map((e) => Host(authority: e)) - .toList(); - } catch (e) { - if (kDebugMode) { - print(e); - } - - return []; - } - } - - Future _getDataWithCache(String url) async { - final cacheManager = DefaultCacheManager(); - final file = await cacheManager.getSingleFile(url); - return file.readAsString(); - } -} diff --git a/lib/src/domain/entity/host.dart b/lib/src/domain/entity/host.dart deleted file mode 100644 index 0922358..0000000 --- a/lib/src/domain/entity/host.dart +++ /dev/null @@ -1,9 +0,0 @@ -class Host { - const Host({ - required this.authority, - }); - - // This is the domain name that identifies the internet host - // (e.g., www.google.com, example.com) - final String authority; -} diff --git a/lib/src/domain/mapper/host_mapper.dart b/lib/src/domain/mapper/host_mapper.dart deleted file mode 100644 index cf0b08b..0000000 --- a/lib/src/domain/mapper/host_mapper.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:adblocker_webview/src/domain/entity/host.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; - -List mapHostToContentBlocker(List hostList) { - return hostList - .map( - (e) => ContentBlocker( - trigger: ContentBlockerTrigger( - urlFilter: _createUrlFilterFromAuthority(e.authority), - ), - action: ContentBlockerAction( - type: ContentBlockerActionType.BLOCK, - ), - ), - ) - .toList(); -} - -String _createUrlFilterFromAuthority(String authority) => '.*.$authority/.*'; diff --git a/lib/src/domain/repository/adblocker_repository.dart b/lib/src/domain/repository/adblocker_repository.dart deleted file mode 100644 index 8a7e3a7..0000000 --- a/lib/src/domain/repository/adblocker_repository.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:adblocker_webview/src/domain/entity/host.dart'; - -abstract class AdBlockerRepository { - Future> fetchBannedHostList(); -} diff --git a/lib/src/elem_hide.dart b/lib/src/elem_hide.dart new file mode 100644 index 0000000..3e1347c --- /dev/null +++ b/lib/src/elem_hide.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +String generateHidingScript(List selectors) { + final jsSelectorsArray = jsonEncode(selectors); + return ''' + window.adBlockerSelectors = $jsSelectorsArray; +(function () { + const selectors = window.adBlockerSelectors || []; + const BATCH_SIZE = 1000; + + async function hideElements() { + if (!Array.isArray(selectors) || !selectors.length) { + console.log('[AdBlocker] No selectors to process'); + return; + } + + try { + const batchCount = Math.ceil(selectors.length / BATCH_SIZE); + for (let i = 0; i < batchCount; i++) { + const start = i * BATCH_SIZE; + const end = Math.min(start + BATCH_SIZE, selectors.length); + const batchSelectors = selectors.slice(start, end); + + document.querySelectorAll(batchSelectors.join(',')).forEach((el) => { + console.log('Removing element: ', el.id); + return el.remove(); + }); + await new Promise(resolve => setTimeout(resolve, 300)); + } + console.info('[AdBlocker] Elements hide rules applied: ', selectors.length); + } catch (error) { + console.error('[AdBlocker] Error:', error); + } + } + + // Create a MutationObserver instance + const observer = new MutationObserver(() => hideElements()); + + // Start observing + try { + observer.observe(document.body, { + childList: true, + subtree: true + }); + hideElements(); + } catch (error) { + console.error('[AdBlocker] Observer error:', error); + } +})(); + '''; +} diff --git a/lib/src/internal_adblocker_webview_controller.dart b/lib/src/internal_adblocker_webview_controller.dart index 92f2064..4532b0e 100644 --- a/lib/src/internal_adblocker_webview_controller.dart +++ b/lib/src/internal_adblocker_webview_controller.dart @@ -1,7 +1,7 @@ -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:webview_flutter/webview_flutter.dart'; abstract class InternalWebviewController { - /// Sets inAppWebviewController to be used in future + /// Sets WebViewController to be used in future /// Typically not to be used by third parties - void setInternalController(InAppWebViewController controller); + void setInternalController(WebViewController controller); } diff --git a/lib/src/logger.dart b/lib/src/logger.dart new file mode 100644 index 0000000..a5d35b9 --- /dev/null +++ b/lib/src/logger.dart @@ -0,0 +1,9 @@ +import 'package:flutter/foundation.dart'; + +const _tag = 'AdBlockerWebView'; + +void debugLog(Object message) { + if (kDebugMode) { + print('$_tag: $message'); + } +} diff --git a/packages/adblocker_core/.gitignore b/packages/adblocker_core/.gitignore new file mode 100644 index 0000000..eb6c05c --- /dev/null +++ b/packages/adblocker_core/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/packages/adblocker_core/.metadata b/packages/adblocker_core/.metadata new file mode 100644 index 0000000..f5be3bc --- /dev/null +++ b/packages/adblocker_core/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "8495dee1fd4aacbe9de707e7581203232f591b2f" + channel: "stable" + +project_type: package diff --git a/packages/adblocker_core/CHANGELOG.md b/packages/adblocker_core/CHANGELOG.md new file mode 100644 index 0000000..b6b1be1 --- /dev/null +++ b/packages/adblocker_core/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release with basic rules parsing diff --git a/packages/adblocker_core/LICENSE b/packages/adblocker_core/LICENSE new file mode 100644 index 0000000..ffb3cd1 --- /dev/null +++ b/packages/adblocker_core/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, Md Didarul Islam + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/adblocker_core/README.md b/packages/adblocker_core/README.md new file mode 100644 index 0000000..f80a2cf --- /dev/null +++ b/packages/adblocker_core/README.md @@ -0,0 +1 @@ +This package is a core package for the adblocker webview package. It contains the logic for blocking ads and trackers. \ No newline at end of file diff --git a/packages/adblocker_core/analysis_options.yaml b/packages/adblocker_core/analysis_options.yaml new file mode 100644 index 0000000..5ce2a68 --- /dev/null +++ b/packages/adblocker_core/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:very_good_analysis/analysis_options.yaml + +analyzer: + exclude: + - lib/generated_plugin_registrant.dart + - lib/gen/** + - lib/**.g.dart + - lib/**.freezed.dart + - lib/**.config.dart + +linter: + rules: + public_member_api_docs: false + avoid_unused_constructor_parameters: false + one_member_abstracts: false diff --git a/packages/adblocker_core/lib/adblocker_core.dart b/packages/adblocker_core/lib/adblocker_core.dart new file mode 100644 index 0000000..d341a30 --- /dev/null +++ b/packages/adblocker_core/lib/adblocker_core.dart @@ -0,0 +1,4 @@ +export 'src/adblocker_filter.dart'; +export 'src/adblocker_filter_impl.dart'; +export 'src/rules/css_rule.dart'; +export 'src/rules/resource_rule.dart'; diff --git a/packages/adblocker_core/lib/src/adblocker_filter.dart b/packages/adblocker_core/lib/src/adblocker_filter.dart new file mode 100644 index 0000000..c66dfde --- /dev/null +++ b/packages/adblocker_core/lib/src/adblocker_filter.dart @@ -0,0 +1,12 @@ +import 'package:adblocker_core/src/adblocker_filter_impl.dart'; +import 'package:adblocker_core/src/rules/resource_rule.dart'; + +abstract interface class AdblockerFilter { + Future init(String filterData); + List getCSSRulesForWebsite(String url); + List getAllResourceRules(); + bool shouldBlockResource(String url); + Future dispose(); + + static AdblockerFilter createInstance() => AdblockerFilterImpl(); +} diff --git a/packages/adblocker_core/lib/src/adblocker_filter_impl.dart b/packages/adblocker_core/lib/src/adblocker_filter_impl.dart new file mode 100644 index 0000000..59055c3 --- /dev/null +++ b/packages/adblocker_core/lib/src/adblocker_filter_impl.dart @@ -0,0 +1,104 @@ +import 'package:adblocker_core/src/adblocker_filter.dart'; +import 'package:adblocker_core/src/parser/css_rules_parser.dart'; +import 'package:adblocker_core/src/parser/resource_rules_parser.dart'; +import 'package:adblocker_core/src/rules/css_rule.dart'; +import 'package:adblocker_core/src/rules/resource_rule.dart'; + +final _commentPattern = RegExp(r'^\s*!.*'); + +class AdblockerFilterImpl implements AdblockerFilter { + final List _cssRules = []; + final List _resourceRules = []; + final List _resourceExceptionRules = []; + final _cssRulesParser = CSSRulesParser(); + final _resourceRulesParser = ResourceRulesParser(); + + @override + Future init(String filterData) async { + _parseRules(filterData); + } + + @override + List getCSSRulesForWebsite(String url) { + final uri = Uri.tryParse(url); + if (uri == null) return []; + final domain = uri.host; + final applicableRules = []; + final applicableExceptionRules = {}; + + for (final rule in _cssRules) { + if (applicableExceptionRules.containsKey(rule.selector)) continue; + + if (rule.domain.isEmpty || + rule.domain.any((d) => _domainMatches(d, domain))) { + if (rule.isException) { + applicableExceptionRules[rule.selector] = true; + } else { + applicableRules.add(rule.selector); + } + } + } + + applicableExceptionRules.keys.forEach(applicableRules.remove); + + return applicableRules; + } + + @override + List getAllResourceRules() { + return [..._resourceRules, ..._resourceExceptionRules]; + } + + @override + bool shouldBlockResource(String url) { + final isException = + _resourceExceptionRules.any((rule) => _domainMatches(rule.url, url)); + if (isException) return false; + return _resourceRules.any((rule) => _domainMatches(rule.url, url)); + } + + @override + Future dispose() async { + _cssRules.clear(); + _resourceRules.clear(); + _resourceExceptionRules.clear(); + } + + void _parseRules(String content) { + final lines = content.split('\n'); + + for (var line in lines) { + line = line.trim(); + + if (line.isEmpty || line.startsWith(_commentPattern)) continue; + + final isCSSParsed = _parseCSSRule(line); + if (isCSSParsed) continue; + + final isResourceParsed = _parseResourceRule(line); + if (isResourceParsed) continue; + } + } + + bool _parseCSSRule(String line) { + final rule = _cssRulesParser.parseLine(line); + if (rule == null) return false; + _cssRules.add(rule); + return true; + } + + bool _parseResourceRule(String line) { + final rule = _resourceRulesParser.parseLine(line); + if (rule == null) return false; + if (rule.isException) { + _resourceExceptionRules.add(rule); + } else { + _resourceRules.add(rule); + } + return true; + } + + bool _domainMatches(String ruleDomain, String targetDomain) { + return targetDomain == ruleDomain || targetDomain.contains(ruleDomain); + } +} diff --git a/packages/adblocker_core/lib/src/parser/css_rules_parser.dart b/packages/adblocker_core/lib/src/parser/css_rules_parser.dart new file mode 100644 index 0000000..88e30ab --- /dev/null +++ b/packages/adblocker_core/lib/src/parser/css_rules_parser.dart @@ -0,0 +1,28 @@ +import 'package:adblocker_core/src/rules/css_rule.dart'; + +final _cssRulePattern = RegExp(r'^([^#]*)(##|#@#|#\?#)(.+)$'); + +class CSSRulesParser { + CSSRule? parseLine(String line) { + final match = _cssRulePattern.firstMatch(line); + if (match == null) return null; + + final domainGroup = match.group(1); + final domain = []; + if (domainGroup != null && domainGroup.isNotEmpty) { + domain.addAll(domainGroup.split(',')); + } + + final separator = match.group(2) ?? '##'; + final selector = match.group(3) ?? ''; + final isException = separator == '#@#'; + + if (selector.contains('[') && selector.contains(']')) return null; + + return CSSRule( + domain: domain, + selector: selector, + isException: isException, + ); + } +} diff --git a/packages/adblocker_core/lib/src/parser/resource_rules_parser.dart b/packages/adblocker_core/lib/src/parser/resource_rules_parser.dart new file mode 100644 index 0000000..7e2ae0e --- /dev/null +++ b/packages/adblocker_core/lib/src/parser/resource_rules_parser.dart @@ -0,0 +1,15 @@ +import 'package:adblocker_core/src/rules/resource_rule.dart'; + +final _resourceRulePattern = RegExp(r'\|\|([^$*^]+)(?:\^)?\$?(.*)'); + +class ResourceRulesParser { + ResourceRule? parseLine(String line) { + final match = _resourceRulePattern.firstMatch(line); + if (match == null) return null; + + final url = match.group(1) ?? ''; + final isException = line.startsWith('@@'); + + return ResourceRule(url: url, isException: isException); + } +} diff --git a/packages/adblocker_core/lib/src/rules/css_rule.dart b/packages/adblocker_core/lib/src/rules/css_rule.dart new file mode 100644 index 0000000..da04e5f --- /dev/null +++ b/packages/adblocker_core/lib/src/rules/css_rule.dart @@ -0,0 +1,10 @@ +class CSSRule { + CSSRule({ + required this.domain, + required this.selector, + this.isException = false, + }); + final List domain; + final String selector; + final bool isException; +} diff --git a/packages/adblocker_core/lib/src/rules/resource_rule.dart b/packages/adblocker_core/lib/src/rules/resource_rule.dart new file mode 100644 index 0000000..1e63670 --- /dev/null +++ b/packages/adblocker_core/lib/src/rules/resource_rule.dart @@ -0,0 +1,8 @@ +class ResourceRule { + ResourceRule({ + required this.url, + this.isException = false, + }); + final String url; + final bool isException; +} diff --git a/packages/adblocker_core/pubspec.yaml b/packages/adblocker_core/pubspec.yaml new file mode 100644 index 0000000..97e7561 --- /dev/null +++ b/packages/adblocker_core/pubspec.yaml @@ -0,0 +1,18 @@ +name: adblocker_core +description: "Core package for adblocker webview" +version: 0.1.0 +homepage: "https://github.com/islamdidarmd/flutter_adblocker_webview" + +environment: + sdk: ^3.7.0 + flutter: '>=3.29.0' +resolution: workspace + +dependencies: + flutter: + sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + + diff --git a/packages/adblocker_core/test/adblocker_filter_test.dart b/packages/adblocker_core/test/adblocker_filter_test.dart new file mode 100644 index 0000000..35ae5a3 --- /dev/null +++ b/packages/adblocker_core/test/adblocker_filter_test.dart @@ -0,0 +1,77 @@ +import 'package:adblocker_core/src/adblocker_filter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AdBlockerFilter', () { + late AdblockerFilter filter; + + setUp(() async { + filter = AdblockerFilter.createInstance(); + await filter.init(''' + ! Test filter list + ||ads.example.com^ + ||tracker.com^ + @@||whitelist.example.com^ + example.com##.ad-banner + test.com##.sponsored + ~news.com##.advertisement + '''); + }); + + group('Resource blocking', () { + test('blocks matching URLs', () { + expect( + filter.shouldBlockResource('https://ads.example.com/banner.jpg'), + isTrue, + ); + expect( + filter.shouldBlockResource('https://tracker.com/pixel.gif'), + isTrue, + ); + }); + + test('allows non-matching URLs', () { + expect( + filter.shouldBlockResource('https://example.com/image.jpg'), + isFalse, + ); + }); + + test('respects exception rules', () { + expect( + filter.shouldBlockResource('https://whitelist.example.com/ads.js'), + isFalse, + ); + }); + }); + + group('CSS rules', () { + test('returns rules for matching domain', () { + final rules = filter.getCSSRulesForWebsite('https://example.com'); + expect(rules, contains('.ad-banner')); + }); + + test('returns rules for multiple domains', () { + final rules = filter.getCSSRulesForWebsite('https://test.com'); + expect(rules, contains('.sponsored')); + }); + + test('respects domain exclusions', () { + final rules = filter.getCSSRulesForWebsite('https://news.com'); + expect(rules, isNot(contains('.advertisement'))); + }); + + test('returns empty list for non-matching domain', () { + final rules = filter.getCSSRulesForWebsite('https://random.com'); + expect(rules, isEmpty); + }); + }); + + test('getAllResourceRules returns all resource rules', () { + final rules = filter.getAllResourceRules(); + expect(rules, hasLength(3)); // 2 block rules + 1 exception rule + expect(rules.where((rule) => !rule.isException), hasLength(2)); + expect(rules.where((rule) => rule.isException), hasLength(1)); + }); + }); +} diff --git a/packages/adblocker_core/test/css_rules_parser_test.dart b/packages/adblocker_core/test/css_rules_parser_test.dart new file mode 100644 index 0000000..4592288 --- /dev/null +++ b/packages/adblocker_core/test/css_rules_parser_test.dart @@ -0,0 +1,55 @@ +import 'package:adblocker_core/src/parser/css_rules_parser.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CSSRulesParser', () { + late CSSRulesParser parser; + + setUp(() { + parser = CSSRulesParser(); + }); + + test('parses basic element hiding rules', () { + const rule = '##.ad-banner'; + final result = parser.parseLine(rule); + + expect(result, isNotNull); + expect(result?.selector, equals('.ad-banner')); + expect(result?.domain, isEmpty); + }); + + test('parses domain-specific rules', () { + const rule = 'example.com##.ad-banner'; + final result = parser.parseLine(rule); + + expect(result, isNotNull); + expect(result?.selector, equals('.ad-banner')); + expect(result?.domain, contains('example.com')); + expect(result?.domain, isNot(contains('test.com'))); + }); + + test('parses multiple domain rules', () { + const rule = 'example.com,test.com##.ad-banner'; + final result = parser.parseLine(rule); + + expect(result, isNotNull); + expect(result?.selector, equals('.ad-banner')); + expect(result?.domain, contains('example.com')); + expect(result?.domain, contains('test.com')); + }); + + test('ignores comment lines', () { + const rule = '! This is a comment'; + final result = parser.parseLine(rule); + + expect(result, isNull); + }); + + test('ignores empty lines', () { + const rule = ''; + final result = parser.parseLine(rule); + + expect(result, isNull); + }); + }); +} diff --git a/packages/adblocker_core/test/resource_rules_parser_test.dart b/packages/adblocker_core/test/resource_rules_parser_test.dart new file mode 100644 index 0000000..ef25b35 --- /dev/null +++ b/packages/adblocker_core/test/resource_rules_parser_test.dart @@ -0,0 +1,51 @@ +import 'package:adblocker_core/src/parser/resource_rules_parser.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ResourceRulesParser', () { + late ResourceRulesParser parser; + + setUp(() { + parser = ResourceRulesParser(); + }); + + test('parses domain anchor rules correctly', () { + const rule = '||ads.example.com^'; + final result = parser.parseLine(rule); + + expect(result, isNotNull); + expect(result?.url, equals('ads.example.com')); + expect(result?.isException, isFalse); + }); + + test('parses exception rules correctly', () { + const rule = '@@||ads.example.com^'; + final result = parser.parseLine(rule); + + expect(result, isNotNull); + expect(result?.url, equals('ads.example.com')); + expect(result?.isException, isTrue); + }); + + test('ignores comment lines', () { + const rule = '! This is a comment'; + final result = parser.parseLine(rule); + + expect(result, isNull); + }); + + test('ignores empty lines', () { + const rule = ''; + final result = parser.parseLine(rule); + + expect(result, isNull); + }); + + test('ignores invalid rules', () { + const rule = 'not a valid rule'; + final result = parser.parseLine(rule); + + expect(result, isNull); + }); + }); +} diff --git a/packages/adblocker_manager/.gitignore b/packages/adblocker_manager/.gitignore new file mode 100644 index 0000000..7dffd5d --- /dev/null +++ b/packages/adblocker_manager/.gitignore @@ -0,0 +1,38 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json \ No newline at end of file diff --git a/packages/adblocker_manager/CHANGELOG.md b/packages/adblocker_manager/CHANGELOG.md new file mode 100644 index 0000000..6daa3f1 --- /dev/null +++ b/packages/adblocker_manager/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0 + +* Initial release +* Support for EasyList and AdGuard filters +* Aggregation of blocking decisions and CSS rules parsing diff --git a/packages/adblocker_manager/LICENSE b/packages/adblocker_manager/LICENSE new file mode 100644 index 0000000..ffb3cd1 --- /dev/null +++ b/packages/adblocker_manager/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, Md Didarul Islam + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/adblocker_manager/README.md b/packages/adblocker_manager/README.md new file mode 100644 index 0000000..6256495 --- /dev/null +++ b/packages/adblocker_manager/README.md @@ -0,0 +1,33 @@ +# Adblocker Manager + +A Flutter package that manages multiple ad-blocking filters for the adblocker_webview package. + +## Features + +- Support for multiple filter types (EasyList, AdGuard) +- Aggregates blocking decisions from multiple filters +- Combines CSS rules from all active filters +- Easy configuration and initialization + +## Usage + +```dart +// Create configuration +final config = FilterConfig( + filterTypes: [FilterType.easyList, FilterType.adGuard], +); + +// Initialize manager +final manager = AdblockFilterManager(); +await manager.init(config); + +// Check if resource should be blocked +final shouldBlock = manager.shouldBlockResource('https://example.com/ad.js'); + +// Get CSS rules for a website +final cssRules = manager.getCSSRulesForWebsite('example.com'); +``` + +## Additional information + +This package is part of the adblocker_webview_flutter project and works in conjunction with the adblocker_core package. \ No newline at end of file diff --git a/packages/adblocker_manager/analysis_options.yaml b/packages/adblocker_manager/analysis_options.yaml new file mode 100644 index 0000000..1e9de79 --- /dev/null +++ b/packages/adblocker_manager/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:very_good_analysis/analysis_options.yaml + +analyzer: + exclude: + - lib/generated_plugin_registrant.dart + - lib/gen/** + - lib/**.g.dart + - lib/**.freezed.dart + - lib/**.config.dart + +linter: + rules: + public_member_api_docs: false + avoid_unused_constructor_parameters: false + one_member_abstracts: false \ No newline at end of file diff --git a/packages/adblocker_manager/assets/adguard_base.txt b/packages/adblocker_manager/assets/adguard_base.txt new file mode 100644 index 0000000..45fd635 --- /dev/null +++ b/packages/adblocker_manager/assets/adguard_base.txt @@ -0,0 +1,132294 @@ +! Checksum: PizBUYbKv8DgEUy+RHJ8vg +! Diff-Path: ../patches/2/2-s-1727699798-3600.patch +! Title: AdGuard Base filter +! Description: EasyList + AdGuard English filter. This filter is necessary for quality ad blocking. +! Version: 2.3.54.44 +! TimeUpdated: 2024-09-30T12:31:15+00:00 +! Expires: 10 days (update frequency) +! Homepage: https://github.com/AdguardTeam/AdGuardFilters +! License: https://github.com/AdguardTeam/AdguardFilters/blob/master/LICENSE +! +!-------------------------------------------------------------------------------! +!------------------ General JS API ---------------------------------------------! +!-------------------------------------------------------------------------------! +! JS API START +#%#var AG_onLoad=function(func){if(document.readyState==="complete"||document.readyState==="interactive")func();else if(document.addEventListener)document.addEventListener("DOMContentLoaded",func);else if(document.attachEvent)document.attachEvent("DOMContentLoaded",func)}; +#%#var AG_removeElementById = function(id) { var element = document.getElementById(id); if (element && element.parentNode) { element.parentNode.removeChild(element); }}; +#%#var AG_removeElementBySelector = function(selector) { if (!document.querySelectorAll) { return; } var nodes = document.querySelectorAll(selector); if (nodes) { for (var i = 0; i < nodes.length; i++) { if (nodes[i] && nodes[i].parentNode) { nodes[i].parentNode.removeChild(nodes[i]); } } } }; +#%#var AG_each = function(selector, fn) { if (!document.querySelectorAll) return; var elements = document.querySelectorAll(selector); for (var i = 0; i < elements.length; i++) { fn(elements[i]); }; }; +#%#var AG_removeParent = function(el, fn) { while (el && el.parentNode) { if (fn(el)) { el.parentNode.removeChild(el); return; } el = el.parentNode; } }; +! +! AG_removeCookie +! Examples: AG_removeCookie('/REGEX/') or AG_removeCookie('part of the cookie name') +! +#%#var AG_removeCookie=function(a){var e=/./;/^\/.+\/$/.test(a)?e=new RegExp(a.slice(1,-1)):""!==a&&(e=new RegExp(a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")));a=function(){for(var a=document.cookie.split(";"),g=a.length;g--;){cookieStr=a[g];var d=cookieStr.indexOf("=");if(-1!==d&&(d=cookieStr.slice(0,d).trim(),e.test(d)))for(var h=document.location.hostname.split("."),f=0;f element matches specified regular expression. +! Based on AG_defineProperty (https://github.com/AdguardTeam/deep-override) +! +! Examples: +! AG_abortInlineScript(/zfgloadedpopup|zfgloadedpushopt/, 'String.fromCharCode'); +! +! @param regex regular expression that the inline script contents must match +! @param property property or properties chain +! @param debug optional, if true - we will print warning when script is aborted. +! +#%#var AG_abortInlineScript=function(g,b,c){var d=function(){if("currentScript"in document)return document.currentScript;var a=document.getElementsByTagName("script");return a[a.length-1]},e=Math.random().toString(36).substr(2,8),h=d();AG_defineProperty(b,{beforeGet:function(){var a=d();if(a instanceof HTMLScriptElement&&a!==h&&""===a.src&&g.test(a.textContent))throw c&&console.warn("AdGuard aborted execution of an inline script"),new ReferenceError(e);}});var f=window.onerror;window.onerror=function(a){if("string"===typeof a&&-1!==a.indexOf(e))return c&&console.warn("AdGuard has caught window.onerror: "+b),!0;if(f instanceof Function)return f.apply(this,arguments)}}; +! +! AG_setConstant('property.chain', 'true') // defines boolean (true), same for false; +! AG_setConstant('property.chain', '123') // defines Number 123; +! AG_setConstant('property.chain', 'noopFunc') // defines function(){}; +! AG_setConstant('property.chain', 'trueFunc') // defines function(){return true}; +! AG_setConstant('property.chain', 'falseFunc') // defines function(){return false}; +! +#%#var AG_setConstant=function(e,a){if("undefined"===a)a=void 0;else if("false"===a)a=!1;else if("true"===a)a=!0;else if("noopFunc"===a)a=function(){};else if("trueFunc"===a)a=function(){return!0};else if("falseFunc"===a)a=function(){return!1};else if(/^\d+$/.test(a)){if(a=parseFloat(a),isNaN(a)||32767