added "Scheduled Blocking"
improved reel blocking logic
changed from topbar to sidepanel
improved seddings page
added about page
This commit is contained in:
Ujwal
2026-02-22 23:59:20 +05:45
parent 9ab4fc503a
commit 354f7413d1
15 changed files with 1336 additions and 508 deletions
+60 -77
View File
@@ -9,9 +9,9 @@
class InjectionController {
/// iOS Safari user-agent — reduces login friction with Instagram.
static const String iOSUserAgent =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) '
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) '
'AppleWebKit/605.1.15 (KHTML, like Gecko) '
'Version/17.5 Mobile/15E148 Safari/604.1';
'Version/17.0 Mobile/15E148 Safari/604.1';
// ── CSS injection ───────────────────────────────────────────────────────────
@@ -72,9 +72,25 @@ class InjectionController {
''';
/// CSS that adds bottom padding so feed content doesn't hide behind our bar.
/// Added more selectors to cover dynamic drawers like Notes and Reactions.
static const String _bottomPaddingCSS = '''
body, #react-root > div {
padding-bottom: 64px !important;
body, #react-root > div, [role="presentation"] > div {
padding-bottom: 72px !important;
}
/* Special handling for dynamic bottom drawers */
div[style*="bottom: 0px"], div[style*="bottom: 0"] {
padding-bottom: 72px !important;
}
''';
/// CSS to push IG content down so it doesn't hide behind our status bar.
static const String _topPaddingCSS = '''
header, #react-root > div > div > div:first-child {
margin-top: 44px !important;
}
/* Shift fixed headers down */
div[style*="position: fixed"][style*="top: 0"] {
top: 44px !important;
}
''';
@@ -257,84 +273,50 @@ class InjectionController {
})();
''';
/// JS to disable vertical swipe gestures that drive reel-to-reel transition.
static const String reelSwipeBlockerJS = '''
/// MutationObserver to watch for Reel players and lock their scrolling.
static const String reelsMutationObserverJS = '''
(function() {
let _touchStartY = 0;
document.addEventListener('touchstart', function(e) {
_touchStartY = e.touches[0].clientY;
}, { passive: true });
document.addEventListener('touchmove', function(e) {
const deltaY = e.touches[0].clientY - _touchStartY;
// If swiping UP (negative delta), block it to prevent next reel load
if (deltaY < -10) {
if (e.cancelable) {
e.preventDefault();
e.stopPropagation();
}
}
}, { passive: false });
})();
''';
// ── Reel scroll-lock ────────────────────────────────────────────────────────
/// JS that prevents the user from scrolling to a different reel.
/// Intercepts history changes — if a /reel/ URL changes, navigate back.
static String reelScrollLockJS(String canonicalUrl) {
final escapedUrl = _escapeJsString(canonicalUrl);
return '''
(function lockReel() {
const LOCKED_URL = $escapedUrl;
function extractReelId(url) {
const m = url.match(/\\/reel\\/([^\\/\\?#]+)/);
return m ? m[1] : null;
}
const lockedId = extractReelId(LOCKED_URL);
if (!lockedId) return;
// Override pushState and replaceState
const _pushState = history.pushState.bind(history);
const _replaceState = history.replaceState.bind(history);
function checkAndRevert(newUrl) {
const newId = extractReelId(newUrl || window.location.href);
if (newId && newId !== lockedId) {
// Different reel — go back to ours
setTimeout(function() {
window.location.replace(LOCKED_URL);
}, 50);
}
}
history.pushState = function(state, title, url) {
_pushState(state, title, url);
checkAndRevert(url);
};
history.replaceState = function(state, title, url) {
_replaceState(state, title, url);
checkAndRevert(url);
};
window.addEventListener('popstate', function() {
checkAndRevert(window.location.href);
});
// Also disable vertical swipe gestures that drive reel-to-reel
function lockReelScroll(reelContainer) {
if (reelContainer.dataset.scrollLocked) return;
reelContainer.dataset.scrollLocked = 'true';
let startY = 0;
document.addEventListener('touchstart', function(e) {
reelContainer.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
}, { passive: true });
document.addEventListener('touchmove', function(e) {
const dy = e.touches[0].clientY - startY;
if (Math.abs(dy) > 20) {
e.preventDefault();
reelContainer.addEventListener('touchmove', (e) => {
const deltaY = e.touches[0].clientY - startY;
// Block upward swipe (next reel), allow downward (go back)
if (deltaY < -10) {
if (e.cancelable) {
e.preventDefault();
e.stopPropagation();
}
}
}, { passive: false });
})();
''';
}
}
// Watch for reel player being injected into DOM
const observer = new MutationObserver(() => {
// Instagram's reel player containers — multiple selectors for resilience
const reelContainers = document.querySelectorAll(
'[class*="reel"], [class*="Reel"], video'
);
reelContainers.forEach((el) => {
// If it's a video or a reel container, wrap it
lockReelScroll(el);
// Also try parent if it's a video
if (el.tagName === 'VIDEO' && el.parentElement) {
lockReelScroll(el.parentElement);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
''';
/// JS to disable swipe-to-next behavior inside the isolated Reel player.
static const String disableReelSwipeJS = '''
@@ -357,7 +339,8 @@ class InjectionController {
}) {
final StringBuffer css = StringBuffer();
css.write(_hideInstagramNavCSS);
css.write(_bottomPaddingCSS); // Ensure content isn't behind our bar
css.write(_bottomPaddingCSS);
css.write(_topPaddingCSS);
if (!sessionActive) css.write(_hideReelsCSS);
if (blurExplore) css.write(_blurExploreCSS);
+110 -10
View File
@@ -28,6 +28,11 @@ class SessionManager extends ChangeNotifier {
static const _keyAppSessionExtUsed = 'app_sess_ext_used';
static const _keyLastAppSessEnd = 'app_sess_last_end_ts';
static const _keyDailyOpenCount = 'app_open_count';
static const _keyScheduleEnabled = 'sched_enabled';
static const _keyScheduleStartHour = 'sched_start_h';
static const _keyScheduleStartMin = 'sched_start_m';
static const _keyScheduleEndHour = 'sched_end_h';
static const _keyScheduleEndMin = 'sched_end_m';
SharedPreferences? _prefs;
@@ -46,6 +51,17 @@ class SessionManager extends ChangeNotifier {
false; // set when time runs out, waiting for user action
int _dailyOpenCount = 0;
// ── Scheduled Blocking runtime ─────────────────────────────
bool _scheduleEnabled = false;
int _schedStartHour = 22; // Default 10 PM
int _schedStartMin = 0;
int _schedEndHour = 7; // Default 7 AM
int _schedEndMin = 0;
bool _isInForeground = true; // Tracking app lifecycle state
int _cachedRemainingSessionSeconds = 0;
int _cachedRemainingAppSessionSeconds = 0;
// ── Settings defaults ──────────────────────────────────────
int _dailyLimitSeconds = 30 * 60; // 30 min
int _perSessionSeconds = 5 * 60; // 5 min
@@ -56,6 +72,7 @@ class SessionManager extends ChangeNotifier {
int get remainingSessionSeconds {
if (!_isSessionActive || _sessionExpiry == null) return 0;
// If not in foreground, the clock "freezes" visually too (or we could shift the expiry)
final diff = _sessionExpiry!.difference(DateTime.now()).inSeconds;
return diff > 0 ? diff : 0;
}
@@ -123,6 +140,29 @@ class SessionManager extends ChangeNotifier {
/// How many times the user has opened the app today.
int get dailyOpenCount => _dailyOpenCount;
// ── Scheduled Blocking Getters ─────────────────────────────
bool get scheduleEnabled => _scheduleEnabled;
int get schedStartHour => _schedStartHour;
int get schedStartMin => _schedStartMin;
int get schedEndHour => _schedEndHour;
int get schedEndMin => _schedEndMin;
bool get isScheduledBlockActive {
if (!_scheduleEnabled) return false;
final now = DateTime.now();
final currentTime = now.hour * 60 + now.minute;
final startTime = _schedStartHour * 60 + _schedStartMin;
final endTime = _schedEndHour * 60 + _schedEndMin;
if (startTime < endTime) {
// Simple range (e.g., 9:00 to 17:00)
return currentTime >= startTime && currentTime < endTime;
} else {
// Over-midnight range (e.g., 22:00 to 07:00)
return currentTime >= startTime || currentTime < endTime;
}
}
// ── Initialization ─────────────────────────────────────────
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
@@ -132,6 +172,31 @@ class SessionManager extends ChangeNotifier {
_incrementOpenCount();
}
void setAppForeground(bool v) {
if (_isInForeground == v) return;
_isInForeground = v;
if (v) {
// Returning to foreground: resume sessions by shifting expiry
final now = DateTime.now();
if (_isSessionActive) {
_sessionExpiry = now.add(
Duration(seconds: _cachedRemainingSessionSeconds),
);
}
if (_appSessionEnd != null) {
_appSessionEnd = now.add(
Duration(seconds: _cachedRemainingAppSessionSeconds),
);
}
} else {
// Entering background: cache remaining time
_cachedRemainingSessionSeconds = remainingSessionSeconds;
_cachedRemainingAppSessionSeconds = appSessionRemainingSeconds;
}
notifyListeners();
}
Future<void> _resetDailyIfNeeded() async {
final today = DateFormat('yyyy-MM-dd').format(DateTime.now());
final stored = _prefs!.getString(_keyDailyDate) ?? '';
@@ -176,6 +241,12 @@ class SessionManager extends ChangeNotifier {
if (lastAppEndMs > 0) {
_lastAppSessionEnd = DateTime.fromMillisecondsSinceEpoch(lastAppEndMs);
}
_scheduleEnabled = _prefs!.getBool(_keyScheduleEnabled) ?? false;
_schedStartHour = _prefs!.getInt(_keyScheduleStartHour) ?? 22;
_schedStartMin = _prefs!.getInt(_keyScheduleStartMin) ?? 0;
_schedEndHour = _prefs!.getInt(_keyScheduleEndHour) ?? 7;
_schedEndMin = _prefs!.getInt(_keyScheduleEndMin) ?? 0;
}
void _incrementOpenCount() {
@@ -189,10 +260,17 @@ class SessionManager extends ChangeNotifier {
}
void _tick() {
if (!_isInForeground) return; // Freeze everything when in background
bool changed = false;
// Reel session countdown
if (_isSessionActive) {
// Recalculate expiry every tick to "pause" it while backgrounded:
// We don't change _sessionExpiry, but we increment _dailyUsedSeconds.
// If we want it to actually pause, we should probably store "remaining seconds"
// and update expiry ONLY when in foreground.
if (remainingSessionSeconds <= 0) {
_cleanupExpiredReelSession();
changed = true;
@@ -205,14 +283,20 @@ class SessionManager extends ChangeNotifier {
}
// App session expiry check
if (_appSessionEnd != null &&
!_appSessionExpiredFlag &&
DateTime.now().isAfter(_appSessionEnd!)) {
_appSessionExpiredFlag = true;
changed = true;
if (_appSessionEnd != null && !_appSessionExpiredFlag) {
if (DateTime.now().isAfter(_appSessionEnd!)) {
_appSessionExpiredFlag = true;
changed = true;
}
}
if (isCooldownActive) changed = true;
if (isCooldownActive) {
changed = true;
} else if (appOpenCooldownRemainingSeconds <= 0 &&
_lastAppSessionEnd != null) {
// Just expired
changed = true;
}
if (changed) notifyListeners();
}
@@ -313,10 +397,26 @@ class SessionManager extends ChangeNotifier {
notifyListeners();
}
Future<void> resetDailyCounter() async {
_dailyUsedSeconds = 0;
await _prefs?.setInt(_keyDailyUsedSeconds, 0);
if (_isSessionActive) endSession();
Future<void> setScheduleEnabled(bool v) async {
_scheduleEnabled = v;
await _prefs?.setBool(_keyScheduleEnabled, v);
notifyListeners();
}
Future<void> setScheduleTime({
required int startH,
required int startM,
required int endH,
required int endM,
}) async {
_schedStartHour = startH;
_schedStartMin = startM;
_schedEndHour = endH;
_schedEndMin = endM;
await _prefs?.setInt(_keyScheduleStartHour, startH);
await _prefs?.setInt(_keyScheduleStartMin, startM);
await _prefs?.setInt(_keyScheduleEndHour, endH);
await _prefs?.setInt(_keyScheduleEndMin, endM);
notifyListeners();
}
+11
View File
@@ -7,6 +7,7 @@ class SettingsService extends ChangeNotifier {
static const _keyBlurReels = 'set_blur_reels';
static const _keyRequireLongPress = 'set_require_long_press';
static const _keyShowBreathGate = 'set_show_breath_gate';
static const _keyRequireWordChallenge = 'set_require_word_challenge';
SharedPreferences? _prefs;
@@ -14,11 +15,14 @@ class SettingsService extends ChangeNotifier {
bool _blurReels = false; // If false: hide reels in feed (after session ends)
bool _requireLongPress = true; // Long-press FAB to start session
bool _showBreathGate = true; // Show breathing gate on every open
bool _requireWordChallenge =
true; // Random word sequence challenge before changes
bool get blurExplore => _blurExplore;
bool get blurReels => _blurReels;
bool get requireLongPress => _requireLongPress;
bool get showBreathGate => _showBreathGate;
bool get requireWordChallenge => _requireWordChallenge;
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
@@ -26,6 +30,7 @@ class SettingsService extends ChangeNotifier {
_blurReels = _prefs!.getBool(_keyBlurReels) ?? false;
_requireLongPress = _prefs!.getBool(_keyRequireLongPress) ?? true;
_showBreathGate = _prefs!.getBool(_keyShowBreathGate) ?? true;
_requireWordChallenge = _prefs!.getBool(_keyRequireWordChallenge) ?? true;
notifyListeners();
}
@@ -52,4 +57,10 @@ class SettingsService extends ChangeNotifier {
await _prefs?.setBool(_keyShowBreathGate, v);
notifyListeners();
}
Future<void> setRequireWordChallenge(bool v) async {
_requireWordChallenge = v;
await _prefs?.setBool(_keyRequireWordChallenge, v);
notifyListeners();
}
}