diff --git a/.fdroid.yml b/.fdroid.yml deleted file mode 100644 index a345f17..0000000 --- a/.fdroid.yml +++ /dev/null @@ -1,45 +0,0 @@ -Categories: - - Connectivity - - Social Network -License: AGPL-3.0-only -AuthorName: Ujwal Chapagain -AuthorEmail: notujwal@proton.me -SourceCode: https://github.com/Ujwal223/FocusGram -IssueTracker: https://github.com/Ujwal223/FocusGram/issues -Changelog: https://github.com/Ujwal223/FocusGram/blob/main/CHANGELOG.md - -AutoName: FocusGram - -RepoType: git -Repo: https://github.com/Ujwal223/FocusGram - -Builds: - - versionName: 1.0.0 - versionCode: 3 - commit: v1.0.0 - output: build/app/outputs/flutter-apk/app-release.apk - srclibs: - - flutter@stable - prebuild: - - flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" .github/workflows/release.yml) - - '[[ $flutterVersion ]]' - - git -C $$flutter$$ checkout -f $flutterVersion - - export PUB_CACHE=$(pwd)/.pub-cache - - .flutter/bin/flutter config --no-analytics - - .flutter/bin/flutter pub get - scanignore: - - .flutter/bin/cache - scandelete: - - .flutter - - .pub-cache - build: - - export PUB_CACHE=$(pwd)/.pub-cache - - .flutter/bin/flutter build apk --release --split-per-abi --target-platform="android-arm64" - -AutoUpdateMode: Version -UpdateCheckMode: Tags -VercodeOperation: - - '%c * 10 + 1' -UpdateCheckData: pubspec.yaml|version:\s.+\+(\d+)|.|version:\s(.+)\+ -CurrentVersion: 1.0.0 -CurrentVersionCode: 3 diff --git a/.gitignore b/.gitignore index 80df92f..8fec584 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ migrate_working_dir/ PRD.md .agents/ TODO.md +v2/FOCUSGRAM_V2_PLAN.md +v2/FocusGram_Feed_Filtering_Reference.docx +.codex # IntelliJ related *.iml @@ -27,7 +30,6 @@ TODO.md #.vscode/ RELEASE_GUIDE.md android/key.properties -android/fdroid-config.properties android/app/*.jks upload-keystore.jks diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..9353b22 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,10 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - v2/** + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/android/NOTICE b/android/NOTICE new file mode 100644 index 0000000..06a9081 --- /dev/null +++ b/android/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2014, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 54f5680..701b94c 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/assets/scripts/ad_blocker_dom.js b/assets/scripts/ad_blocker_dom.js new file mode 100644 index 0000000..2d6cb7b --- /dev/null +++ b/assets/scripts/ad_blocker_dom.js @@ -0,0 +1,61 @@ +/** + * FocusGram DOM Ad Blocker (Fallback) + * + * DEPRECATED: Use fetch_interceptor.js for reliable ad blocking. + * + * This script provides DOM-based ad removal as a FALLBACK for ads that slip through + * GraphQL filtering. It's not reliable because Instagram has already rendered the content. + * + * Injected at DOCUMENT_END + * Removes sponsored/posts/tracking elements from the DOM. + */ +(function () { + 'use strict'; + + const AD_SIGNALS = [ + 'Sponsored', + 'paid partnership', + 'Promoted', + ]; + + const textMatchesSignal = (txt) => { + if (!txt) return false; + const t = txt.trim().toLowerCase(); + return AD_SIGNALS.some((s) => t === s.toLowerCase()); + }; + + const removeSponsoredArticles = () => { + try { + // aria-label routes (best-effort; localization may break) + document.querySelectorAll('a[aria-label]').forEach((a) => { + const aria = a.getAttribute('aria-label') || ''; + if (textMatchesSignal(aria)) { + const article = a.closest('article'); + if (article) article.remove(); + } + }); + + // Text-based removal inside feed articles (best-effort) + document.querySelectorAll('article').forEach((article) => { + const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT); + let node; + while ((node = walker.nextNode())) { + const txt = node.nodeValue; + if (textMatchesSignal(txt)) { + article.remove(); + break; + } + } + }); + + // Suggested content is intentionally left alone. Removing suggested + // units after Instagram has virtualized the feed can snap the viewport + // back to the top on some accounts. + } catch (_) {} + }; + + const observer = new MutationObserver(() => removeSponsoredArticles()); + observer.observe(document.body, { childList: true, subtree: true }); + + removeSponsoredArticles(); +})(); diff --git a/assets/scripts/autoplay_blocker.js b/assets/scripts/autoplay_blocker.js new file mode 100644 index 0000000..d171be5 --- /dev/null +++ b/assets/scripts/autoplay_blocker.js @@ -0,0 +1,129 @@ +/** + * 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'; + + // This script is only registered when the setting is enabled, so default ON. + window.__fgBlockAutoplay = typeof window.__fgBlockAutoplay === 'boolean' + ? window.__fgBlockAutoplay : true; + const ALLOW_KEY = '__fgUserStartedPlayback'; + let userGestureUntil = 0; + + function isReelRoute() { + const path = window.location.pathname || ''; + return path.indexOf('/reel/') >= 0 || path === '/reels' || path.indexOf('/reels/') >= 0; + } + + function isUserGestureActive() { + return Date.now() < userGestureUntil; + } + + function markUserGesture(target) { + userGestureUntil = Date.now() + 1200; + try { + let video = target && target.closest ? target.closest('video') : null; + if (!video && target && target.querySelector) video = target.querySelector('video'); + if (video) video[ALLOW_KEY] = true; + } catch (_) {} + } + + document.addEventListener('pointerdown', function (event) { + markUserGesture(event.target); + }, true); + document.addEventListener('touchstart', function (event) { + markUserGesture(event.target); + }, true); + document.addEventListener('click', function (event) { + markUserGesture(event.target); + }, true); + + // Override HTMLMediaElement.play() to check our flag + const _play = HTMLMediaElement.prototype.play; + HTMLMediaElement.prototype.play = function () { + if ( + window.__fgBlockAutoplay && + !isReelRoute() && + this[ALLOW_KEY] !== true && + !isUserGestureActive() + ) { + // Return a resolved promise to avoid breaking Instagram's code + try { this.pause(); } catch (_) {} + 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 && !isReelRoute() && el[ALLOW_KEY] !== true) { + el.autoplay = false; + el.removeAttribute('autoplay'); + el.removeAttribute('preload'); + try { el.preload = 'none'; } catch (_) {} + 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(); + } + }; + + document.addEventListener('play', function (event) { + if (event.target && event.target.tagName === 'VIDEO' && isUserGestureActive()) { + event.target[ALLOW_KEY] = true; + } + }, true); +})(); diff --git a/assets/scripts/content_hider.js b/assets/scripts/content_hider.js new file mode 100644 index 0000000..0e8f718 --- /dev/null +++ b/assets/scripts/content_hider.js @@ -0,0 +1,304 @@ +/** + * FocusGram Content Hider + * Toggleable visibility for: stories tray, feed posts, reels, suggested content. + * Flutter controls via window.__fgContent.* + * Injected at DOCUMENT_END. + * + * Key fixes applied: + * - Blank-feed fix: hideReels uses DOM removal (not display:none) so layout doesn't collapse + * - MutationObserver callback now re-applies CSS AND re-runs all hide functions each cycle + * - SPA-heartbeat via window event listener re-applies CSS on pushState/replaceState + * - Stories tray detection strengthened for fresh SPA navigations + * - Suggested posts detection uses multiple text-node matching strategies + */ +(function () { + 'use strict'; + + if (window.__fgContent && window.__fgContent.__focusgramReady) { + return; + } + + const STYLE_ID = 'fg-content-hider'; + let hideStories = false; + let hidePosts = false; + let hideSuggested = false; + let hideReels = false; + + // ─── CSS rules ───────────────────────────────────────────────────────────── + + function buildCSS() { + const selectors = []; + + if (hideStories) { + selectors.push( + '[role="list"]:has([aria-label*="tory"])', + '[role="listbox"]:has([aria-label*="tory"])', + '[role="menu"] > ul', + 'section > div > div:first-child [style*="overflow"]', + '[role="list"] [style*="overflow"]', + ); + } + + if (hidePosts) { + selectors.push( + 'body:not([path*="direct"]):not([data-fg-path*="direct"]) article:not([aria-label])', + 'body:not([path*="direct"]):not([data-fg-path*="direct"]) [data-pressable-container] > article', + ); + } + + // hideReels CSS is intentionally NOT added here. + // We use DOM removal instead (see removeReels()) so that room is never left + // blank in the feed, and Instagram's infinite-scroll can prove scroll height. + + return selectors.length + ? selectors.join(',\n') + ' { display: none !important; visibility: hidden !important; }' + : ''; + } + + function applyCSS() { + if (document.body) { + document.body.setAttribute('data-fg-path', window.location.pathname || '/'); + } + let style = document.getElementById(STYLE_ID); + if (!style) { + style = document.createElement('style'); + style.id = STYLE_ID; + document.head.appendChild(style); + } + style.textContent = buildCSS(); + } + + // ─── Story tray JS ───────────────────────────────────────────────────────── + + function hideStoryTray() { + if (!hideStories) return; + + // Strategy 1: