mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-08-18 20:07:16 +02:00
feat:
added "Scheduled Blocking" improved reel blocking logic changed from topbar to sidepanel improved seddings page added about page
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user