mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-08-27 16:02:28 +02:00
V2 Release
This commit is contained in:
@@ -1,54 +0,0 @@
|
|||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
schedule:
|
|
||||||
- cron: '15 14 * * 5'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze (${{ matrix.language }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
packages: read
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- language: java-kotlin
|
|
||||||
build-mode: manual
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: '3.38.7'
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
build-mode: ${{ matrix.build-mode }}
|
|
||||||
|
|
||||||
- name: Build Android (for CodeQL)
|
|
||||||
run: flutter build apk --debug
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v3
|
|
||||||
with:
|
|
||||||
category: "/language:${{ matrix.language }}"
|
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
name: Build APK and Create GitHub Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Release version without leading v. Example: 1.0.0. Leave empty to read the top CHANGELOG heading."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Java 17
|
||||||
|
uses: actions/setup-java@v5.2.0
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Set up Android SDK
|
||||||
|
uses: android-actions/setup-android@v4.0.1
|
||||||
|
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2.23.0
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install required Android SDK packages
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sdkmanager \
|
||||||
|
"platform-tools" \
|
||||||
|
"platforms;android-35" \
|
||||||
|
"build-tools;34.0.0" \
|
||||||
|
"build-tools;35.0.0" \
|
||||||
|
"ndk;28.2.12676356"
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Resolve version and tag
|
||||||
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INPUT_VERSION="${{ github.event.inputs.version }}"
|
||||||
|
|
||||||
|
if [[ -n "${INPUT_VERSION}" ]]; then
|
||||||
|
VERSION="${INPUT_VERSION#v}"
|
||||||
|
else
|
||||||
|
VERSION="$(python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
text = Path("CHANGELOG.md").read_text(encoding="utf-8")
|
||||||
|
m = re.search(r'^##\s+FocusGram\s+([0-9]+\.[0-9]+\.[0-9]+)\s*$', text, re.M)
|
||||||
|
if not m:
|
||||||
|
raise SystemExit("Could not find a top changelog heading like: ## FocusGram 2.0.0")
|
||||||
|
print(m.group(1))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Extract release notes from CHANGELOG.md
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.meta.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
version = os.environ["VERSION"]
|
||||||
|
text = Path("CHANGELOG.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
pattern = rf'(?ms)^##\s+FocusGram\s+{re.escape(version)}\s*$.*?(?=^##\s+|\Z)'
|
||||||
|
m = re.search(pattern, text)
|
||||||
|
if not m:
|
||||||
|
raise SystemExit(f"Could not find changelog section for version {version}")
|
||||||
|
|
||||||
|
Path("release_notes.md").write_text(m.group(0).strip() + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Build release APK
|
||||||
|
run: flutter build apk --release
|
||||||
|
|
||||||
|
- name: Upload APK artifact
|
||||||
|
uses: actions/upload-artifact@v7.0.1
|
||||||
|
with:
|
||||||
|
name: focusgram-apk-${{ steps.meta.outputs.tag }}
|
||||||
|
path: build/app/outputs/flutter-apk/app-release.apk
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Create Git tag
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG: ${{ steps.meta.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
|
||||||
|
echo "Tag already exists on remote: ${TAG}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag -a "${TAG}" -m "Release ${TAG}"
|
||||||
|
git push origin "${TAG}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v3.0.0
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.meta.outputs.tag }}
|
||||||
|
name: FocusGram ${{ steps.meta.outputs.tag }}
|
||||||
|
body_path: release_notes.md
|
||||||
|
files: build/app/outputs/flutter-apk/app-release.apk
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
name: release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: '3.38.7'
|
|
||||||
+1
-2
@@ -16,7 +16,7 @@ PRD.md
|
|||||||
TODO.md
|
TODO.md
|
||||||
v2/FOCUSGRAM_V2_PLAN.md
|
v2/FOCUSGRAM_V2_PLAN.md
|
||||||
v2/FocusGram_Feed_Filtering_Reference.docx
|
v2/FocusGram_Feed_Filtering_Reference.docx
|
||||||
.codex
|
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
@@ -28,7 +28,6 @@ v2/FocusGram_Feed_Filtering_Reference.docx
|
|||||||
# VS Code which you may wish to be included in version control, so this line
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
#.vscode/
|
#.vscode/
|
||||||
RELEASE_GUIDE.md
|
|
||||||
android/key.properties
|
android/key.properties
|
||||||
android/app/*.jks
|
android/app/*.jks
|
||||||
upload-keystore.jks
|
upload-keystore.jks
|
||||||
|
|||||||
+18
-8
@@ -1,13 +1,23 @@
|
|||||||
## FocusGram 1.0.0
|
## FocusGram 2.0.0
|
||||||
|
|
||||||
### What's new
|
### What's new
|
||||||
- Reordered Settings Page.
|
|
||||||
- Added "Click to Unblur" for posts.
|
- NEW: Added Media Downloader for downloading images and videos
|
||||||
- Added Persistent Notification
|
- NEW: Added Ghost Mode
|
||||||
- Improved Grayscale Scheduling.
|
- NEW: Added a toggle for scroll lock in minimal mode
|
||||||
|
- NEW: Added Option to Choose Duration of Mindfulness Gate
|
||||||
|
- NEW: Added ability to customize number of words in typing challenge
|
||||||
|
- UPDATED: Redesigned Focus Control Flyout
|
||||||
|
- UPDATED: Settings and Reordered items
|
||||||
|
- UPDATED: Added more time Choices for reels session
|
||||||
|
- UPDATED: Improved Permission Request invocation in onboarding page.
|
||||||
|
- UPDATED: Improved Notification Alerts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
- Fixed a Bug Where Reels Werent playing despite Reels Sessions being ON.
|
- Fixed: back button on homepage didnt exit the app.
|
||||||
- Fixed a Bug Where Session End Popup could be just dismissed and app ran Normally despite session already ended.
|
- Fixed: Only First image of multiple imaged posts was blurred.
|
||||||
|
- FIxed: Couldn't scroll the home feed after enabling minimal mode
|
||||||
- Perfomance Optimizations
|
- Perfomance Optimizations
|
||||||
- Other Minor Changes.
|
- A lof of other Minor fixes .
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
**Use social media on your terms.**
|
**Use social media on your terms.**
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://flutter.dev)
|
[](https://flutter.dev)
|
||||||
[](https://github.com/ujwal223/focusgram/releases)
|
[](https://github.com/ujwal223/focusgram/releases)
|
||||||
|
|
||||||
<a href='https://focusgram.en.uptodown.com/android' title='Download FocusGram'>
|
<a href='https://focusgram.en.uptodown.com/android' title='Download FocusGram'>
|
||||||
@@ -125,8 +125,6 @@ FocusGram uses a standard Android System WebView to load `instagram.com`. All fe
|
|||||||
- CSS injection (element hiding, grayscale, scroll behaviour)
|
- CSS injection (element hiding, grayscale, scroll behaviour)
|
||||||
- URL interception via NavigationDelegate (Reels blocking, Explore blocking)
|
- URL interception via NavigationDelegate (Reels blocking, Explore blocking)
|
||||||
|
|
||||||
Nothing is modified server-side. The app never reads, intercepts, or stores Instagram content beyond what is explicitly listed
|
|
||||||
|
|
||||||
### Permissions
|
### Permissions
|
||||||
|
|
||||||
| Permission | Reason |
|
| Permission | Reason |
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ android {
|
|||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 4
|
versionCode = 4
|
||||||
versionName = "1.1.0"
|
versionName = "2.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -243,7 +243,7 @@
|
|||||||
let response = await _fetch(input, init);
|
let response = await _fetch(input, init);
|
||||||
|
|
||||||
// Only intercept GraphQL feed queries
|
// Only intercept GraphQL feed queries
|
||||||
if (!url.includes('/graphql/query') && !url.includes('/api/v1/feed')) {
|
if (!url.includes('/graphql') && !url.includes('/api/v1/feed')) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,16 +92,16 @@ class _SkeletonScreenState extends State<SkeletonScreen>
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
SizedBox(
|
||||||
height: 80,
|
height: 80,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
itemBuilder: (context, index) => Padding(
|
itemBuilder: (context, index) => Padding(
|
||||||
padding: const EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 56,
|
width: 56,
|
||||||
@@ -111,13 +111,13 @@ class _SkeletonScreenState extends State<SkeletonScreen>
|
|||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 2),
|
||||||
Container(
|
Container(
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 8,
|
height: 6,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -35,12 +35,11 @@ class NativeBottomNav extends StatelessWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isDark = theme.brightness == Brightness.dark;
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
final bgColor =
|
final bgColor = theme.colorScheme.surface.withValues(
|
||||||
theme.colorScheme.surface.withValues(alpha: isDark ? 0.95 : 0.98);
|
alpha: isDark ? 0.95 : 0.98,
|
||||||
final iconColorInactive =
|
);
|
||||||
isDark ? Colors.white70 : Colors.black54;
|
final iconColorInactive = isDark ? Colors.white70 : Colors.black54;
|
||||||
final iconColorActive =
|
final iconColorActive = theme.colorScheme.primary;
|
||||||
theme.colorScheme.primary;
|
|
||||||
|
|
||||||
final tabs = <_NavItem>[
|
final tabs = <_NavItem>[
|
||||||
_NavItem(
|
_NavItem(
|
||||||
@@ -103,8 +102,7 @@ class NativeBottomNav extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: tabs.map((item) {
|
children: tabs.map((item) {
|
||||||
final color =
|
final color = item.active ? iconColorActive : iconColorInactive;
|
||||||
item.active ? iconColorActive : iconColorInactive;
|
|
||||||
final opacity = item.enabled ? 1.0 : 0.35;
|
final opacity = item.enabled ? 1.0 : 0.35;
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
@@ -129,10 +127,7 @@ class NativeBottomNav extends StatelessWidget {
|
|||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
item.label,
|
item.label,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 10, color: color),
|
||||||
fontSize: 10,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -164,4 +159,3 @@ class _NavItem {
|
|||||||
required this.enabled,
|
required this.enabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ class InstagramPreloader {
|
|||||||
|
|
||||||
static Future<void> start(String userAgent) async {
|
static Future<void> start(String userAgent) async {
|
||||||
if (_headlessWebView != null) return; // don't start twice
|
if (_headlessWebView != null) return; // don't start twice
|
||||||
|
|
||||||
_headlessWebView = HeadlessInAppWebView(
|
_headlessWebView = HeadlessInAppWebView(
|
||||||
keepAlive: keepAlive,
|
keepAlive: keepAlive,
|
||||||
initialUrlRequest: URLRequest(
|
initialUrlRequest: URLRequest(url: WebUri('https://www.instagram.com/')),
|
||||||
url: WebUri('https://www.instagram.com/'),
|
|
||||||
),
|
|
||||||
initialSettings: InAppWebViewSettings(
|
initialSettings: InAppWebViewSettings(
|
||||||
userAgent: userAgent,
|
userAgent: userAgent,
|
||||||
mediaPlaybackRequiresUserGesture: true,
|
mediaPlaybackRequiresUserGesture: true,
|
||||||
@@ -69,4 +67,3 @@ class InstagramPreloader {
|
|||||||
isReady = false;
|
isReady = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ class ReelsHistoryEntry {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
'url': url,
|
'url': url,
|
||||||
'title': title,
|
'title': title,
|
||||||
'thumbnailUrl': thumbnailUrl,
|
'thumbnailUrl': thumbnailUrl,
|
||||||
'visitedAt': visitedAt.toUtc().toIso8601String(),
|
'visitedAt': visitedAt.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
static ReelsHistoryEntry fromJson(Map<String, dynamic> json) {
|
static ReelsHistoryEntry fromJson(Map<String, dynamic> json) {
|
||||||
return ReelsHistoryEntry(
|
return ReelsHistoryEntry(
|
||||||
@@ -31,7 +31,8 @@ class ReelsHistoryEntry {
|
|||||||
url: (json['url'] as String?) ?? '',
|
url: (json['url'] as String?) ?? '',
|
||||||
title: (json['title'] as String?) ?? 'Instagram Reel',
|
title: (json['title'] as String?) ?? 'Instagram Reel',
|
||||||
thumbnailUrl: (json['thumbnailUrl'] as String?) ?? '',
|
thumbnailUrl: (json['thumbnailUrl'] as String?) ?? '',
|
||||||
visitedAt: DateTime.tryParse((json['visitedAt'] as String?) ?? '') ??
|
visitedAt:
|
||||||
|
DateTime.tryParse((json['visitedAt'] as String?) ?? '') ??
|
||||||
DateTime.now().toUtc(),
|
DateTime.now().toUtc(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -114,4 +115,3 @@ class ReelsHistoryService {
|
|||||||
await prefs.setString(_prefsKey, jsonEncode(jsonList));
|
await prefs.setString(_prefsKey, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ class _UpdateBannerState extends State<UpdateBanner> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.secondaryContainer,
|
color: colorScheme.secondaryContainer,
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(color: colorScheme.outlineVariant, width: 0.5),
|
||||||
color: colorScheme.outlineVariant,
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -121,10 +118,11 @@ class _UpdateBannerState extends State<UpdateBanner> {
|
|||||||
text = text.replaceAll(RegExp(r'#{1,6}\s'), '');
|
text = text.replaceAll(RegExp(r'#{1,6}\s'), '');
|
||||||
text = text.replaceAll(RegExp(r'\*\*(.*?)\*\*'), r'\1');
|
text = text.replaceAll(RegExp(r'\*\*(.*?)\*\*'), r'\1');
|
||||||
text = text.replaceAll(RegExp(r'\*(.*?)\*'), r'\1');
|
text = text.replaceAll(RegExp(r'\*(.*?)\*'), r'\1');
|
||||||
text =
|
text = text.replaceAll(
|
||||||
text.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'\1'); // links -> text
|
RegExp(r'\[([^\]]+)\]\([^)]+\)'),
|
||||||
|
r'\1',
|
||||||
|
); // links -> text
|
||||||
text = text.replaceAll(RegExp(r'`([^`]+)`'), r'\1');
|
text = text.replaceAll(RegExp(r'`([^`]+)`'), r'\1');
|
||||||
return text.trim();
|
return text.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ class UpdateCheckerService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final cleanVersion =
|
final cleanVersion = gitVersionTag.startsWith('v')
|
||||||
gitVersionTag.startsWith('v') ? gitVersionTag.substring(1) : gitVersionTag;
|
? gitVersionTag.substring(1)
|
||||||
|
: gitVersionTag;
|
||||||
|
|
||||||
var trimmed = body.trim();
|
var trimmed = body.trim();
|
||||||
if (trimmed.length > 1500) {
|
if (trimmed.length > 1500) {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
class FocusSettings {
|
class FocusSettings {
|
||||||
final bool ghostMode; // hide read receipts
|
final bool ghostMode; // hide read receipts
|
||||||
final bool noAds; // strip ads and sponsored posts
|
final bool noAds; // strip ads and sponsored posts
|
||||||
final bool noStories; // hide story tray
|
final bool noStories; // hide story tray
|
||||||
final bool noReels; // hide reels tab
|
final bool noReels; // hide reels tab
|
||||||
final bool noAutoplay; // stop videos autoplaying
|
final bool noAutoplay; // stop videos autoplaying
|
||||||
final bool noDMs; // block direct messages
|
final bool noDMs; // block direct messages
|
||||||
|
|
||||||
const FocusSettings({
|
const FocusSettings({
|
||||||
this.ghostMode = false,
|
this.ghostMode = false,
|
||||||
this.noAds = false,
|
this.noAds = true,
|
||||||
this.noStories = false,
|
this.noStories = false,
|
||||||
this.noReels = false,
|
this.noReels = false,
|
||||||
this.noAutoplay = false,
|
this.noAutoplay = false,
|
||||||
this.noDMs = false,
|
this.noDMs = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -17,6 +17,7 @@ import 'screens/cooldown_gate_screen.dart';
|
|||||||
import 'services/notification_service.dart';
|
import 'services/notification_service.dart';
|
||||||
import 'features/update_checker/update_checker_service.dart';
|
import 'features/update_checker/update_checker_service.dart';
|
||||||
import 'features/preloader/instagram_preloader.dart';
|
import 'features/preloader/instagram_preloader.dart';
|
||||||
|
import 'widgets/remote_popup_handler.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -88,7 +89,7 @@ class FocusGramApp extends StatelessWidget {
|
|||||||
/// 2. Cooldown Gate (if app-open cooldown active)
|
/// 2. Cooldown Gate (if app-open cooldown active)
|
||||||
/// 3. Breath Gate (if enabled in settings)
|
/// 3. Breath Gate (if enabled in settings)
|
||||||
/// 4. If an app session is already active, resume it
|
/// 4. If an app session is already active, resume it
|
||||||
/// otherwise show App Session Picker
|
/// otherwise show App Session Picker
|
||||||
/// 5. Main WebView
|
/// 5. Main WebView
|
||||||
class InitialRouteHandler extends StatefulWidget {
|
class InitialRouteHandler extends StatefulWidget {
|
||||||
const InitialRouteHandler({super.key});
|
const InitialRouteHandler({super.key});
|
||||||
@@ -108,6 +109,10 @@ class _InitialRouteHandlerState extends State<InitialRouteHandler> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_appLinks = AppLinks();
|
_appLinks = AppLinks();
|
||||||
_initDeepLinks();
|
_initDeepLinks();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
RemotePopupHandler.checkAndShow(context);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initDeepLinks() async {
|
Future<void> _initDeepLinks() async {
|
||||||
|
|||||||
@@ -46,7 +46,39 @@ class ExtrasSettingsPage extends StatelessWidget {
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.withValues(alpha: 0.07),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.amber.withValues(alpha: 0.12)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8, top: 2),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.amber,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'NOTE: The seen indicator is not sent to the sender while you are active in the chat, but as soon as you close and reopen the chat, the seen indicator is sent.',
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.amber),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/* TRIED BUT IT DIDNT WORK 98% oF THE TIME)
|
||||||
|
|
||||||
const _SectionHeader(title: 'FOCUSGRAM V2'),
|
const _SectionHeader(title: 'FOCUSGRAM V2'),
|
||||||
_SwitchTile(
|
_SwitchTile(
|
||||||
title: 'Ad Blocker',
|
title: 'Ad Blocker',
|
||||||
@@ -66,7 +98,7 @@ class ExtrasSettingsPage extends StatelessWidget {
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
*/
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import '../v2_integration/script_engine_v2_overlay.dart';
|
|||||||
import '../v2_integration/script_registry_v2_overlay.dart';
|
import '../v2_integration/script_registry_v2_overlay.dart';
|
||||||
import '../scripts/focus_scripts.dart';
|
import '../scripts/focus_scripts.dart';
|
||||||
import '../focus_settings.dart';
|
import '../focus_settings.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
import '../services/adblock/adblock_content_blocker_loader.dart';
|
import '../services/adblock/adblock_content_blocker_loader.dart';
|
||||||
|
|
||||||
@@ -127,7 +127,10 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
// Check for updates on launch
|
// Check for updates on launch
|
||||||
context.read<UpdateCheckerService>().checkForUpdates();
|
context.read<UpdateCheckerService>().checkForUpdates();
|
||||||
|
|
||||||
unawaited(_loadAdblockerData());
|
// Load adblock data early. If adblock is enabled, we wait for initial data
|
||||||
|
// to be loaded so the WebView can apply contentBlockers on first render.
|
||||||
|
// This prevents ads from loading before filters are applied.
|
||||||
|
unawaited(_loadAdblockerDataEarly());
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<SessionManager>().addListener(_onSessionChanged);
|
context.read<SessionManager>().addListener(_onSessionChanged);
|
||||||
@@ -155,6 +158,9 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
_lastV2AutoplayBlockerEnabled = settings.blockAutoplay;
|
_lastV2AutoplayBlockerEnabled = settings.blockAutoplay;
|
||||||
_lastAdblockToggleValue = settings.v2AdBlockerDomEnabled;
|
_lastAdblockToggleValue = settings.v2AdBlockerDomEnabled;
|
||||||
_onScreenTimeChanged();
|
_onScreenTimeChanged();
|
||||||
|
|
||||||
|
// Load full adblock data with longer timeout after UI is initialized
|
||||||
|
unawaited(_loadAdblockerData());
|
||||||
});
|
});
|
||||||
|
|
||||||
FocusGramRouter.pendingUrl.addListener(_onPendingUrlChanged);
|
FocusGramRouter.pendingUrl.addListener(_onPendingUrlChanged);
|
||||||
@@ -581,6 +587,28 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAdblockerDataEarly() async {
|
||||||
|
final settings = context.read<SettingsService>();
|
||||||
|
if (!settings.v2AdBlockerDomEnabled) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final loader = AdblockContentBlockerLoader();
|
||||||
|
final data = await loader.loadOrUpdateIfNeeded(
|
||||||
|
enabled: true,
|
||||||
|
prefs: prefs,
|
||||||
|
timeoutMs: 5000, // Short timeout for early load
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _adblockData = data);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// If loading fails, continue without blocking app startup
|
||||||
|
// AdblockData will be retried in _loadAdblockerData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _isBlockedByAdblockHostList(WebUri uri, Set<String>? blockedHosts) {
|
bool _isBlockedByAdblockHostList(WebUri uri, Set<String>? blockedHosts) {
|
||||||
if (blockedHosts == null || blockedHosts.isEmpty) return false;
|
if (blockedHosts == null || blockedHosts.isEmpty) return false;
|
||||||
|
|
||||||
@@ -911,6 +939,24 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
pullToRefreshController: _pullToRefreshController,
|
pullToRefreshController: _pullToRefreshController,
|
||||||
shouldInterceptRequest: (controller, request) async {
|
shouldInterceptRequest: (controller, request) async {
|
||||||
final url = request.url.toString();
|
final url = request.url.toString();
|
||||||
|
|
||||||
|
const adDomains = [
|
||||||
|
'an.facebook.com',
|
||||||
|
'connect.facebook.net',
|
||||||
|
'pixel.facebook.com',
|
||||||
|
'graph.facebook.com/logging',
|
||||||
|
'www.instagram.com/ajax/bz',
|
||||||
|
'www.instagram.com/api/v1/web/comet/logcalls',
|
||||||
|
'doubleclick.net',
|
||||||
|
'googletagmanager.com',
|
||||||
|
'scorecardresearch.com',
|
||||||
|
];
|
||||||
|
if (adDomains.any(url.contains)) {
|
||||||
|
return WebResourceResponse(
|
||||||
|
data: Uint8List(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final referrer =
|
final referrer =
|
||||||
request.headers?['Referer'] ??
|
request.headers?['Referer'] ??
|
||||||
request.headers?['referer'];
|
request.headers?['referer'];
|
||||||
@@ -919,7 +965,7 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
_syncDirectThreadState(referrer);
|
_syncDirectThreadState(referrer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isInDirectThread &&
|
/*if (_isInDirectThread &&
|
||||||
_isFktmInstagramCdn(url)) {
|
_isFktmInstagramCdn(url)) {
|
||||||
if (_dmThreadCdnBlockArmed) {
|
if (_dmThreadCdnBlockArmed) {
|
||||||
return WebResourceResponse(
|
return WebResourceResponse(
|
||||||
@@ -928,7 +974,7 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
}
|
}
|
||||||
_dmThreadCdnBlockArmed = true;
|
_dmThreadCdnBlockArmed = true;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
// Strict/high-priority domain blocking from uBlock-style lists.
|
// Strict/high-priority domain blocking from uBlock-style lists.
|
||||||
final adblockHosts = _adblockData?.blockedHosts;
|
final adblockHosts = _adblockData?.blockedHosts;
|
||||||
if (_isBlockedByAdblockHostList(
|
if (_isBlockedByAdblockHostList(
|
||||||
@@ -983,11 +1029,11 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip ads from feed
|
/* Strip ads from feed (JS handles it)
|
||||||
if (settings.noAds &&
|
if (settings.noAds &&
|
||||||
url.contains(
|
url.contains(
|
||||||
'instagram.com/graphql/query',
|
'instagram.com/graphql/query',
|
||||||
)) {
|
)) {/
|
||||||
try {
|
try {
|
||||||
final res = await http.post(
|
final res = await http.post(
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
@@ -1006,10 +1052,76 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
edges.removeWhere((e) {
|
edges.removeWhere((e) {
|
||||||
final node = e['node'];
|
final node = e['node'];
|
||||||
if (node == null) return false;
|
if (node == null) return false;
|
||||||
return node['ad'] != null ||
|
// Strip ads from feed
|
||||||
node['explore_story'] != null ||
|
if (settings.noAds &&
|
||||||
node['media']?['inventory_source'] ==
|
url.contains(
|
||||||
'mixed_unconnected';
|
'instagram.com/graphql',
|
||||||
|
)) {
|
||||||
|
try {
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse(url),
|
||||||
|
headers: Map<String, String>.from(
|
||||||
|
request.headers ?? {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final json = jsonDecode(res.body);
|
||||||
|
|
||||||
|
void filterEdges(dynamic obj) {
|
||||||
|
if (obj == null) return;
|
||||||
|
if (obj is Map) {
|
||||||
|
if (obj['edges'] is List) {
|
||||||
|
(obj['edges'] as List).removeWhere((
|
||||||
|
e,
|
||||||
|
) {
|
||||||
|
final node = e is Map
|
||||||
|
? e['node']
|
||||||
|
: null;
|
||||||
|
if (node == null ||
|
||||||
|
node is! Map)
|
||||||
|
return false;
|
||||||
|
return node['is_ad'] ==
|
||||||
|
true ||
|
||||||
|
node['ad_id'] != null ||
|
||||||
|
node['ad_action_links'] !=
|
||||||
|
null ||
|
||||||
|
node['is_paid_partnership'] ==
|
||||||
|
true ||
|
||||||
|
node['sponsor_tags'] !=
|
||||||
|
null ||
|
||||||
|
node['commerciality_status'] ==
|
||||||
|
'ad' ||
|
||||||
|
node['commerciality_status'] ==
|
||||||
|
'shoppable_feed_ad' ||
|
||||||
|
(node['__typename']
|
||||||
|
?.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(
|
||||||
|
'ad',
|
||||||
|
) ??
|
||||||
|
false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
obj.values.forEach(filterEdges);
|
||||||
|
} else if (obj is List) {
|
||||||
|
obj.forEach(filterEdges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterEdges(json);
|
||||||
|
|
||||||
|
return WebResourceResponse(
|
||||||
|
data: Uint8List.fromList(
|
||||||
|
utf8.encode(jsonEncode(json)),
|
||||||
|
),
|
||||||
|
headers: res.headers,
|
||||||
|
statusCode: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,7 +1137,7 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
// if anything fails, pass through original request unmodified
|
// if anything fails, pass through original request unmodified
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
title: 'Focus Mode',
|
title: 'Focus Mode',
|
||||||
subtitle: settings.minimalModeEnabled
|
subtitle: settings.minimalModeEnabled
|
||||||
? 'Minimal mode on'
|
? 'Minimal mode on'
|
||||||
: 'Blocking, friction, media',
|
: 'Blocking, Content Hider, Feed Blur and more',
|
||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const FocusSettingsPage()),
|
MaterialPageRoute(builder: (_) => const FocusSettingsPage()),
|
||||||
@@ -71,7 +71,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
icon: Icons.download_rounded,
|
icon: Icons.download_rounded,
|
||||||
iconColor: Colors.orangeAccent,
|
iconColor: Colors.orangeAccent,
|
||||||
title: 'Extras',
|
title: 'Extras',
|
||||||
subtitle: 'Download media, Ghost Mode, Ad Blocker',
|
subtitle: 'Download media, Ghost Mode',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -100,7 +100,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
icon: Icons.lock_outline,
|
icon: Icons.lock_outline,
|
||||||
iconColor: Colors.tealAccent,
|
iconColor: Colors.tealAccent,
|
||||||
title: 'Privacy & Notifications',
|
title: 'Privacy & Notifications',
|
||||||
subtitle: 'Session end alerts',
|
subtitle: 'Manage Your Notifications',
|
||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -379,12 +379,16 @@ class FocusSettingsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
const _SectionHeader(title: 'MEDIA'),
|
const _SectionHeader(title: 'MEDIA'),
|
||||||
|
/*
|
||||||
|
( I TRIED SO HARD, AND GOT SO FAR, BUT IN THE END...
|
||||||
|
IT DOESNT EVEN MATTER ..... (didnt work))
|
||||||
|
|
||||||
_SwitchTile(
|
_SwitchTile(
|
||||||
title: 'Block Autoplay Videos',
|
title: 'Block Autoplay Videos',
|
||||||
subtitle: 'Videos won\'t play until you tap them',
|
subtitle: 'Videos won\'t play until you tap them',
|
||||||
value: settings.blockAutoplay,
|
value: settings.blockAutoplay,
|
||||||
onChanged: (v) => settings.setBlockAutoplay(v),
|
onChanged: (v) => settings.setBlockAutoplay(v),
|
||||||
),
|
),*/
|
||||||
_SwitchTile(
|
_SwitchTile(
|
||||||
title: 'Blur Feed & Explore',
|
title: 'Blur Feed & Explore',
|
||||||
subtitle: 'Blurs post thumbnails until tapped',
|
subtitle: 'Blurs post thumbnails until tapped',
|
||||||
@@ -403,48 +407,16 @@ class FocusSettingsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const _SectionHeader(title: 'FOCUSGRAM V2 OVERLAY'),
|
const _SectionHeader(title: 'CONTENT HIDER'),
|
||||||
|
|
||||||
_SwitchTile(
|
_SwitchTile(
|
||||||
title: 'Content Hider',
|
title: 'Hide Feed Posts',
|
||||||
subtitle: 'Hide stories tray, feed posts, reels, suggested content',
|
subtitle:
|
||||||
value: settings.v2ContentHiderEnabled,
|
'Hides home feed posts (stories tray, posts, suggested content)',
|
||||||
onChanged: (v) => settings.setV2ContentHiderEnabled(v),
|
value: settings.contentPosts,
|
||||||
|
onChanged: (v) => settings.setContentPostsEnabled(v),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (settings.v2ContentHiderEnabled)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 32),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_SwitchTile(
|
|
||||||
title: 'Hide Stories Tray',
|
|
||||||
subtitle: 'Story bubbles row',
|
|
||||||
value: settings.contentStories,
|
|
||||||
onChanged: (v) => settings.setContentStoriesEnabled(v),
|
|
||||||
),
|
|
||||||
_SwitchTile(
|
|
||||||
title: 'Hide Feed Posts',
|
|
||||||
subtitle: 'Home feed posts',
|
|
||||||
value: settings.contentPosts,
|
|
||||||
onChanged: (v) => settings.setContentPostsEnabled(v),
|
|
||||||
),
|
|
||||||
_SwitchTile(
|
|
||||||
title: 'Hide Reels (Feed)',
|
|
||||||
subtitle: 'Reels shown in the feed',
|
|
||||||
value: settings.contentReels,
|
|
||||||
onChanged: (v) => settings.setContentReelsEnabled(v),
|
|
||||||
),
|
|
||||||
_SwitchTile(
|
|
||||||
title: 'Hide Suggested Content',
|
|
||||||
subtitle: 'Suggested posts and recommendation units',
|
|
||||||
value: settings.contentSuggested,
|
|
||||||
onChanged: (v) => settings.setContentSuggestedEnabled(v),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -451,18 +451,40 @@ const String kHideSuggestedPostsJS = r'''
|
|||||||
(function() {
|
(function() {
|
||||||
function hideSuggestedPosts() {
|
function hideSuggestedPosts() {
|
||||||
try {
|
try {
|
||||||
document.querySelectorAll('span, h3, h4').forEach(function(el) {
|
// Target text patterns that indicate suggested content
|
||||||
|
const suggestedPatterns = [
|
||||||
|
'Suggested for you',
|
||||||
|
'Suggested posts',
|
||||||
|
"You're all caught up",
|
||||||
|
'Suggested',
|
||||||
|
'Recommendations',
|
||||||
|
'Discover more',
|
||||||
|
'Suggested Accounts',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Find and hide all elements with suggested content text
|
||||||
|
document.querySelectorAll('span, h3, h4, h2, a').forEach(function(el) {
|
||||||
try {
|
try {
|
||||||
const text = el.textContent.trim();
|
const text = el.textContent.trim();
|
||||||
if (
|
const matched = suggestedPatterns.some(pattern =>
|
||||||
text === 'Suggested for you' ||
|
text === pattern || text.includes(pattern)
|
||||||
text === 'Suggested posts' ||
|
);
|
||||||
text === "You're all caught up"
|
|
||||||
) {
|
if (matched) {
|
||||||
let parent = el.parentElement;
|
let parent = el.parentElement;
|
||||||
for (let i = 0; i < 8 && parent; i++) {
|
// Traverse up to find the container section/article
|
||||||
|
for (let i = 0; i < 12 && parent; i++) {
|
||||||
const tag = parent.tagName.toLowerCase();
|
const tag = parent.tagName.toLowerCase();
|
||||||
if (tag === 'article' || tag === 'section' || tag === 'li') {
|
const classList = parent.className || '';
|
||||||
|
|
||||||
|
// Hide articles, sections, lists, and common suggestion containers
|
||||||
|
if (
|
||||||
|
tag === 'article' ||
|
||||||
|
tag === 'section' ||
|
||||||
|
tag === 'li' ||
|
||||||
|
classList.includes('xjx87jv0') || // Instagram suggestion container
|
||||||
|
classList.includes('x1a8lsjc') // Reel suggestion container
|
||||||
|
) {
|
||||||
parent.style.setProperty('display', 'none', 'important');
|
parent.style.setProperty('display', 'none', 'important');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -471,6 +493,21 @@ const String kHideSuggestedPostsJS = r'''
|
|||||||
}
|
}
|
||||||
} catch(_) {}
|
} catch(_) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also hide by attribute patterns
|
||||||
|
document.querySelectorAll('[aria-label*="Suggested"], [data-testid*="suggested"]').forEach(function(el) {
|
||||||
|
try {
|
||||||
|
let parent = el;
|
||||||
|
for (let i = 0; i < 12 && parent; i++) {
|
||||||
|
const tag = parent.tagName.toLowerCase();
|
||||||
|
if (tag === 'article' || tag === 'section' || tag === 'li') {
|
||||||
|
parent.style.setProperty('display', 'none', 'important');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
} catch(_) {}
|
||||||
|
});
|
||||||
} catch(_) {}
|
} catch(_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,3 @@ const String kDmKeyboardFixJS = r'''
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
|||||||
@@ -78,18 +78,22 @@ List<UserScript> buildUserScripts(FocusSettings settings) {
|
|||||||
|
|
||||||
final scripts = <UserScript>[];
|
final scripts = <UserScript>[];
|
||||||
if (startScripts.isNotEmpty) {
|
if (startScripts.isNotEmpty) {
|
||||||
scripts.add(UserScript(
|
scripts.add(
|
||||||
source: startScripts.join('\n'),
|
UserScript(
|
||||||
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
|
source: startScripts.join('\n'),
|
||||||
forMainFrameOnly: false,
|
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
|
||||||
));
|
forMainFrameOnly: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (endScripts.isNotEmpty) {
|
if (endScripts.isNotEmpty) {
|
||||||
scripts.add(UserScript(
|
scripts.add(
|
||||||
source: endScripts.join('\n'),
|
UserScript(
|
||||||
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END,
|
source: endScripts.join('\n'),
|
||||||
forMainFrameOnly: true,
|
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END,
|
||||||
));
|
forMainFrameOnly: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return scripts;
|
return scripts;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,3 @@ const String kHapticBridgeScript = '''
|
|||||||
}, true);
|
}, true);
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,3 @@ const String kScrollSmoothingJS = r'''
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
|||||||
@@ -29,4 +29,3 @@ const String kSpaNavigationMonitorScript = '''
|
|||||||
window.addEventListener('popstate', () => notifyUrlChange());
|
window.addEventListener('popstate', () => notifyUrlChange());
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ const String kVideoDownloadJS = r'''
|
|||||||
btn.innerHTML = icon();
|
btn.innerHTML = icon();
|
||||||
btn.style.cssText = [
|
btn.style.cssText = [
|
||||||
'position:absolute',
|
'position:absolute',
|
||||||
'z-index:2147483647',
|
'z-index:999',
|
||||||
'width:34px',
|
'width:34px',
|
||||||
'height:34px',
|
'height:34px',
|
||||||
'border-radius:10px',
|
'border-radius:10px',
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class RemotePopupData {
|
||||||
|
final bool show;
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final int maxShows;
|
||||||
|
final String buttonText;
|
||||||
|
|
||||||
|
RemotePopupData({
|
||||||
|
required this.show,
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.body,
|
||||||
|
required this.maxShows,
|
||||||
|
required this.buttonText,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RemotePopupData.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RemotePopupData(
|
||||||
|
show: json['show'] ?? false,
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
title: json['header']?.toString() ?? 'Notice',
|
||||||
|
body: json['body']?.toString() ?? '',
|
||||||
|
maxShows: json['max_shows'] ?? 1,
|
||||||
|
buttonText: json['button_text']?.toString() ?? 'OK',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemotePopupService {
|
||||||
|
// Keep placeholder value until you replace it.
|
||||||
|
static const String popupUrl =
|
||||||
|
'https://raw.githubusercontent.com/Ujwal223/FocusGram/refs/heads/main/android/popup.json';
|
||||||
|
|
||||||
|
static Future<RemotePopupData?> fetchPopup() async {
|
||||||
|
try {
|
||||||
|
// Cache-busting to avoid stale popup configs from GitHub raw URLs.
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'$popupUrl?t=${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
uri,
|
||||||
|
headers: const {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) return null;
|
||||||
|
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
if (decoded is! Map<String, dynamic>) return null;
|
||||||
|
|
||||||
|
return RemotePopupData.fromJson(decoded);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> shouldShow(RemotePopupData data) async {
|
||||||
|
if (!data.show) return false;
|
||||||
|
if (data.id.isEmpty) return false;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final key = 'popup_count_${data.id}';
|
||||||
|
final shownCount = prefs.getInt(key) ?? 0;
|
||||||
|
|
||||||
|
return shownCount < data.maxShows;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> markShown(RemotePopupData data) async {
|
||||||
|
if (data.id.isEmpty) return;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final key = 'popup_count_${data.id}';
|
||||||
|
final current = prefs.getInt(key) ?? 0;
|
||||||
|
await prefs.setInt(key, current + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
@@ -407,9 +408,11 @@ class SettingsService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setBreathGateSeconds(int seconds) async {
|
Future<void> setBreathGateSeconds(int seconds) async {
|
||||||
_breathGateSeconds = seconds.clamp(3, 60).toInt();
|
final clamped = seconds.clamp(3, 60);
|
||||||
|
_breathGateSeconds = clamped.toInt();
|
||||||
await _prefs?.setInt(_keyBreathGateSeconds, _breathGateSeconds);
|
await _prefs?.setInt(_keyBreathGateSeconds, _breathGateSeconds);
|
||||||
notifyListeners();
|
// Defer notifyListeners to next microtask to avoid rebuild conflicts
|
||||||
|
Future.microtask(notifyListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setWordChallengeCount(int count) async {
|
Future<void> setWordChallengeCount(int count) async {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../services/remote_popup_service.dart';
|
||||||
|
|
||||||
|
class RemotePopupHandler {
|
||||||
|
static Future<void> checkAndShow(BuildContext context) async {
|
||||||
|
final popup = await RemotePopupService.fetchPopup();
|
||||||
|
if (popup == null) return;
|
||||||
|
|
||||||
|
final shouldShow = await RemotePopupService.shouldShow(popup);
|
||||||
|
if (!shouldShow) return;
|
||||||
|
|
||||||
|
await RemotePopupService.markShown(popup);
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (_) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(popup.title),
|
||||||
|
content: Text(popup.body),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(popup.buttonText),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@ name: focusgram
|
|||||||
description: "FocusGram is a free, open-source digital wellness tool for Android. Blocks distracting content, sets time limits, and helps you use social media intentionally."
|
description: "FocusGram is a free, open-source digital wellness tool for Android. Blocks distracting content, sets time limits, and helps you use social media intentionally."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.1.0
|
version: 2.0.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.7
|
sdk: ^3.10.7
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ void main() {
|
|||||||
test('accepts http(s) instagram-like hosts and calls launcher', () async {
|
test('accepts http(s) instagram-like hosts and calls launcher', () async {
|
||||||
final launched = <Uri>[];
|
final launched = <Uri>[];
|
||||||
final ok = await handleFocusGramMediaDownload(
|
final ok = await handleFocusGramMediaDownload(
|
||||||
raw: '{"type":"video","url":"https://cdninstagram.com/v/1.mp4","filename":"x"}',
|
raw:
|
||||||
|
'{"type":"video","url":"https://cdninstagram.com/v/1.mp4","filename":"x"}',
|
||||||
launch: (uri) async => launched.add(uri),
|
launch: (uri) async => launched.add(uri),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ void main() {
|
|||||||
test('rejects non-instagram hosts even if http(s)', () async {
|
test('rejects non-instagram hosts even if http(s)', () async {
|
||||||
final launched = <Uri>[];
|
final launched = <Uri>[];
|
||||||
final ok = await handleFocusGramMediaDownload(
|
final ok = await handleFocusGramMediaDownload(
|
||||||
raw: '{"type":"video","url":"https://example.com/video.mp4","filename":"x"}',
|
raw:
|
||||||
|
'{"type":"video","url":"https://example.com/video.mp4","filename":"x"}',
|
||||||
launch: (uri) async => launched.add(uri),
|
launch: (uri) async => launched.add(uri),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -38,29 +38,32 @@ void main() {
|
|||||||
expect(raw, isNull);
|
expect(raw, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('startTracking increments today seconds and stopTracking persists', () async {
|
test(
|
||||||
final s = ScreenTimeService();
|
'startTracking increments today seconds and stopTracking persists',
|
||||||
await s.init();
|
() async {
|
||||||
|
final s = ScreenTimeService();
|
||||||
|
await s.init();
|
||||||
|
|
||||||
final beforeTodayKey = DateTime.now();
|
final beforeTodayKey = DateTime.now();
|
||||||
final todayKey =
|
final todayKey =
|
||||||
'${beforeTodayKey.year.toString().padLeft(4, '0')}-'
|
'${beforeTodayKey.year.toString().padLeft(4, '0')}-'
|
||||||
'${beforeTodayKey.month.toString().padLeft(2, '0')}-'
|
'${beforeTodayKey.month.toString().padLeft(2, '0')}-'
|
||||||
'${beforeTodayKey.day.toString().padLeft(2, '0')}';
|
'${beforeTodayKey.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
s.startTracking();
|
s.startTracking();
|
||||||
|
|
||||||
// Wait ~2 seconds (test is unit-ish; still acceptable).
|
// Wait ~2 seconds (test is unit-ish; still acceptable).
|
||||||
await Future<void>.delayed(const Duration(seconds: 2));
|
await Future<void>.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
s.stopTracking();
|
s.stopTracking();
|
||||||
|
|
||||||
expect(s.secondsByDate[todayKey], isNotNull);
|
expect(s.secondsByDate[todayKey], isNotNull);
|
||||||
expect(s.secondsByDate[todayKey]!, greaterThanOrEqualTo(2));
|
expect(s.secondsByDate[todayKey]!, greaterThanOrEqualTo(2));
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final stored = prefs.getString(ScreenTimeService.prefKey);
|
final stored = prefs.getString(ScreenTimeService.prefKey);
|
||||||
expect(stored, isNotNull);
|
expect(stored, isNotNull);
|
||||||
expect(stored, contains(todayKey));
|
expect(stored, contains(todayKey));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* FocusGram DOM Ad Blocker
|
* FocusGram DOM Ad Blocker
|
||||||
* Removes sponsored posts, "Suggested for you" injections, and ad elements.
|
* SHould have Removed sponsored posts, "Suggested for you" injections, and ad elements.
|
||||||
* Uses structure-based selectors — NOT class names (those change weekly).
|
* Uses structure-based selectors — NOT class names (those change weekly).
|
||||||
* Injected at DOCUMENT_END.
|
* Injected at DOCUMENT_END.
|
||||||
*/
|
*/
|
||||||
@@ -93,8 +93,6 @@
|
|||||||
if (hasAdditions) scanAndRemove();
|
if (hasAdditions) scanAndRemove();
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document.body, {
|
const feed = document.querySelector('main') ?? document.body;
|
||||||
childList: true,
|
observer.observe(feed, { childList: true, subtree: true });
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -209,7 +209,7 @@
|
|||||||
let response = await _fetch(input, init);
|
let response = await _fetch(input, init);
|
||||||
|
|
||||||
// Only intercept GraphQL feed queries
|
// Only intercept GraphQL feed queries
|
||||||
if (!url.includes('/graphql/query') && !url.includes('/api/v1/feed')) {
|
if (!url.includes('/graphql') && !url.includes('/api/v1/feed')) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* FocusGram Autoplay Blocker
|
|
||||||
* Injected at DOCUMENT_START — before Instagram's JS loads.
|
|
||||||
* Prevents video autoplay by:
|
|
||||||
* 1. Blocking play() calls on video elements
|
|
||||||
* 2. Disabling autoplay attribute
|
|
||||||
* 3. Removing preload attributes
|
|
||||||
*/
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.__fgBlockAutoplay = false;
|
|
||||||
|
|
||||||
// Override HTMLMediaElement.play() to check our flag
|
|
||||||
const _play = HTMLMediaElement.prototype.play;
|
|
||||||
HTMLMediaElement.prototype.play = function () {
|
|
||||||
if (window.__fgBlockAutoplay) {
|
|
||||||
// Return a resolved promise to avoid breaking Instagram's code
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return _play.call(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Override autoplay property setter
|
|
||||||
const _videoDescriptor = Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'autoplay') || {};
|
|
||||||
const _originalAutoplaySetter = _videoDescriptor.set;
|
|
||||||
|
|
||||||
Object.defineProperty(HTMLVideoElement.prototype, 'autoplay', {
|
|
||||||
set: function (value) {
|
|
||||||
if (window.__fgBlockAutoplay && value) {
|
|
||||||
// Silently ignore autoplay attempts when blocking is enabled
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_originalAutoplaySetter) {
|
|
||||||
_originalAutoplaySetter.call(this, value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get: function () {
|
|
||||||
if (_videoDescriptor.get) {
|
|
||||||
return _videoDescriptor.get.call(this);
|
|
||||||
}
|
|
||||||
return this.getAttribute('autoplay') !== null;
|
|
||||||
},
|
|
||||||
enumerable: _videoDescriptor.enumerable,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// On page load and SPA navigation, scan for video elements and remove autoplay
|
|
||||||
const removeAutoplayFromVideos = () => {
|
|
||||||
document.querySelectorAll('video, [role="video"]').forEach(el => {
|
|
||||||
if (window.__fgBlockAutoplay) {
|
|
||||||
el.autoplay = false;
|
|
||||||
el.removeAttribute('autoplay');
|
|
||||||
if (el.paused === false) {
|
|
||||||
el.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run on load and when document changes
|
|
||||||
removeAutoplayFromVideos();
|
|
||||||
|
|
||||||
if (!window.__fgAutoplayObserver) {
|
|
||||||
let _timer = null;
|
|
||||||
window.__fgAutoplayObserver = new MutationObserver(() => {
|
|
||||||
clearTimeout(_timer);
|
|
||||||
_timer = setTimeout(removeAutoplayFromVideos, 500);
|
|
||||||
});
|
|
||||||
window.__fgAutoplayObserver.observe(document.documentElement, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow Flutter to toggle
|
|
||||||
window.__fgSetBlockAutoplay = function (enabled) {
|
|
||||||
window.__fgBlockAutoplay = !!enabled;
|
|
||||||
if (enabled) {
|
|
||||||
removeAutoplayFromVideos();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
+18
-11
@@ -23,16 +23,22 @@
|
|||||||
|
|
||||||
// Helper: Check if a node is an ad
|
// Helper: Check if a node is an ad
|
||||||
const isAdNode = (node) => {
|
const isAdNode = (node) => {
|
||||||
if (!node || typeof node !== 'object') return false;
|
if (!node || typeof node !== 'object') return false;
|
||||||
|
return !!(
|
||||||
return !!(
|
node.is_ad ||
|
||||||
node.is_ad ||
|
node.ad_id ||
|
||||||
node.ad_action_link ||
|
node.ad_action_link ||
|
||||||
node.ad_id ||
|
node.ad_action_links?.length > 0 ||
|
||||||
(node.product_type && node.product_type === 'ad') ||
|
node.is_paid_partnership ||
|
||||||
(node.ad_header_style && node.ad_header_style !== 'none') ||
|
node.sponsor_tags?.length > 0 ||
|
||||||
(node.__typename && node.__typename === 'GraphAdStory')
|
(node.commerciality_status === 'ad') ||
|
||||||
);
|
(node.commerciality_status === 'shoppable_feed_ad') ||
|
||||||
|
(node.product_type === 'ad') ||
|
||||||
|
(node.ad_header_style && node.ad_header_style !== 'none') ||
|
||||||
|
node.__typename === 'GraphAdStory' ||
|
||||||
|
node.__typename === 'XDTAdFeedUnit' ||
|
||||||
|
(node.__typename?.toLowerCase().includes('ad'))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: Check if a node is sponsored
|
// Helper: Check if a node is sponsored
|
||||||
@@ -158,6 +164,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Override fetch
|
// Override fetch
|
||||||
const _fetch = window.fetch.bind(window);
|
const _fetch = window.fetch.bind(window);
|
||||||
@@ -173,7 +180,7 @@
|
|||||||
let response = await _fetch(input, init);
|
let response = await _fetch(input, init);
|
||||||
|
|
||||||
// Only intercept GraphQL feed queries
|
// Only intercept GraphQL feed queries
|
||||||
if (!url.includes('/graphql/query') && !url.includes('/api/v1/feed')) {
|
if (!url.includes('/graphql') && !url.includes('/api/v1/feed')) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ class InstagramWebViewState extends State<InstagramWebView> {
|
|||||||
|
|
||||||
// ── ContentBlockers — merged base + EasyList rules ──────────────
|
// ── ContentBlockers — merged base + EasyList rules ──────────────
|
||||||
contentBlockers: WebViewConfig.baseContentBlockers,
|
contentBlockers: WebViewConfig.baseContentBlockers,
|
||||||
// TODO Phase 1.5: merge EasyListParser.load() here at startup
|
|
||||||
|
|
||||||
// ── User Scripts — AT_DOCUMENT_START critical for ghost mode ─────
|
// ── User Scripts — AT_DOCUMENT_START critical for ghost mode ─────
|
||||||
initialUserScripts: UnmodifiableListView(
|
initialUserScripts: UnmodifiableListView(
|
||||||
@@ -96,6 +95,33 @@ class InstagramWebViewState extends State<InstagramWebView> {
|
|||||||
onWebViewCreated: (controller) async {
|
onWebViewCreated: (controller) async {
|
||||||
_controller = controller;
|
_controller = controller;
|
||||||
|
|
||||||
|
//Interceptor for adblock
|
||||||
|
shouldInterceptRequest:
|
||||||
|
(controller, request) async {
|
||||||
|
final url = request.url.toString();
|
||||||
|
|
||||||
|
const adDomains = [
|
||||||
|
'an.facebook.com',
|
||||||
|
'connect.facebook.net',
|
||||||
|
'pixel.facebook.com',
|
||||||
|
'graph.facebook.com/logging',
|
||||||
|
'www.instagram.com/ajax/bz',
|
||||||
|
'www.instagram.com/api/v1/web/comet/logcalls',
|
||||||
|
'doubleclick.net',
|
||||||
|
'googletagmanager.com',
|
||||||
|
'scorecardresearch.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (adDomains.any(url.contains)) {
|
||||||
|
return WebResourceResponse(
|
||||||
|
contentType: 'application/json',
|
||||||
|
httpStatus: WebResourceResponseHTTPStatus(statusCode: 200),
|
||||||
|
data: Uint8List.fromList(utf8.encode('{}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize GhostModeService
|
// Initialize GhostModeService
|
||||||
_ghostMode = GhostModeService();
|
_ghostMode = GhostModeService();
|
||||||
await _ghostMode!.load();
|
await _ghostMode!.load();
|
||||||
|
|||||||
Reference in New Issue
Block a user