mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-07-07 11:47:52 +02:00
UPDATES: updated UI from sidebar to topbar(again)
fixed external redirect on instagram m's settings. fixed bug where it opened app session instead of reel session. hided vertical scroll bar. removed custom bottom bar. fixed bug where it wasnt showing searchbar in /explore. FIXED/ADDED/IMPROVED A LOT MORE THINGS. Ready for Release
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Lightweight global router for cross-widget navigation signals.
|
||||
/// Used to allow the Settings page to trigger WebView navigations without
|
||||
/// requiring a BuildContext reference to MainWebViewPage.
|
||||
class FocusGramRouter {
|
||||
FocusGramRouter._();
|
||||
|
||||
/// When this value is non-null, [MainWebViewPage] will load the URL
|
||||
/// in the WebView and clear this value. Settings page sets this to
|
||||
/// trigger in-app navigation (e.g. Instagram Settings).
|
||||
static final pendingUrl = ValueNotifier<String?>(null);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,9 @@ class NavigationGuard {
|
||||
static const _allowedHosts = ['instagram.com', 'www.instagram.com'];
|
||||
|
||||
/// Regex matching the Reels FEED root — NOT individual reels.
|
||||
/// The `(/|\?|$)` suffix ensures query params (e.g. ?fg=blocked) still match.
|
||||
static final _reelsFeedRegex = RegExp(
|
||||
r'instagram\.com/reels/?$',
|
||||
r'instagram\.com/reels(/|\?|$)',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
@@ -36,7 +37,7 @@ class NavigationGuard {
|
||||
|
||||
// Block non-Instagram domains (prevents phishing/external redirects)
|
||||
final host = uri.host.toLowerCase();
|
||||
if (!_allowedHosts.any((h) => host == h || host.endsWith('.$h'))) {
|
||||
if (!_allowedHosts.any((h) => host == h)) {
|
||||
return BlockDecision(
|
||||
blocked: true,
|
||||
reason: 'External domain blocked: $host',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
class NotificationService {
|
||||
@@ -59,11 +60,15 @@ class NotificationService {
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: platformDetails,
|
||||
);
|
||||
try {
|
||||
await _notificationsPlugin.show(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: platformDetails,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Notification error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'notification_service.dart';
|
||||
|
||||
class FocusSchedule {
|
||||
final int startHour;
|
||||
final int startMinute;
|
||||
final int endHour;
|
||||
final int endMinute;
|
||||
|
||||
FocusSchedule({
|
||||
required this.startHour,
|
||||
required this.startMinute,
|
||||
required this.endHour,
|
||||
required this.endMinute,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'startH': startHour,
|
||||
'startM': startMinute,
|
||||
'endH': endHour,
|
||||
'endM': endMinute,
|
||||
};
|
||||
|
||||
factory FocusSchedule.fromJson(Map<String, dynamic> json) => FocusSchedule(
|
||||
startHour: json['startH'] as int,
|
||||
startMinute: json['startM'] as int,
|
||||
endHour: json['endH'] as int,
|
||||
endMinute: json['endM'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// Manages all session logic for FocusGram:
|
||||
///
|
||||
/// **App Session** — how long the user plans to use Instagram today.
|
||||
@@ -34,6 +63,7 @@ class SessionManager extends ChangeNotifier {
|
||||
static const _keyScheduleStartMin = 'sched_start_m';
|
||||
static const _keyScheduleEndHour = 'sched_end_h';
|
||||
static const _keyScheduleEndMin = 'sched_end_m';
|
||||
static const _keySchedulesJson = 'sched_list_json';
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
@@ -56,8 +86,10 @@ class SessionManager extends ChangeNotifier {
|
||||
bool _scheduleEnabled = false;
|
||||
int _schedStartHour = 22; // Default 10 PM
|
||||
int _schedStartMin = 0;
|
||||
int _schedEndHour = 7; // Default 7 AM
|
||||
int _schedEndHour = 7;
|
||||
int _schedEndMin = 0;
|
||||
List<FocusSchedule> _schedules = [];
|
||||
bool _lastScheduleState = false;
|
||||
|
||||
bool _isInForeground = true; // Tracking app lifecycle state
|
||||
int _cachedRemainingSessionSeconds = 0;
|
||||
@@ -148,21 +180,56 @@ class SessionManager extends ChangeNotifier {
|
||||
int get schedStartMin => _schedStartMin;
|
||||
int get schedEndHour => _schedEndHour;
|
||||
int get schedEndMin => _schedEndMin;
|
||||
List<FocusSchedule> get schedules => _schedules;
|
||||
|
||||
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;
|
||||
for (final s in _schedules) {
|
||||
final startTime = s.startHour * 60 + s.startMinute;
|
||||
final endTime = s.endHour * 60 + s.endMinute;
|
||||
|
||||
if (startTime < endTime) {
|
||||
// Simple range (e.g., 9:00 to 17:00)
|
||||
if (currentTime >= startTime && currentTime < endTime) return true;
|
||||
} else {
|
||||
// Over-midnight range (e.g., 22:00 to 07:00)
|
||||
if (currentTime >= startTime || currentTime < endTime) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String? get activeScheduleText {
|
||||
if (!isScheduledBlockActive) return null;
|
||||
final now = DateTime.now();
|
||||
final currentTime = now.hour * 60 + now.minute;
|
||||
|
||||
for (final s in _schedules) {
|
||||
final startTime = s.startHour * 60 + s.startMinute;
|
||||
final endTime = s.endHour * 60 + s.endMinute;
|
||||
|
||||
bool active = false;
|
||||
if (startTime < endTime) {
|
||||
if (currentTime >= startTime && currentTime < endTime) active = true;
|
||||
} else {
|
||||
if (currentTime >= startTime || currentTime < endTime) active = true;
|
||||
}
|
||||
if (active) {
|
||||
return '${formatTime12h(s.startHour, s.startMinute)} to ${formatTime12h(s.endHour, s.endMinute)}';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String formatTime12h(int h, int m) {
|
||||
var hour = h % 12;
|
||||
if (hour == 0) hour = 12;
|
||||
final period = h >= 12 ? 'PM' : 'AM';
|
||||
final min = m.toString().padLeft(2, '0');
|
||||
return '$hour:$min $period';
|
||||
}
|
||||
|
||||
// ── Initialization ─────────────────────────────────────────
|
||||
@@ -170,6 +237,7 @@ class SessionManager extends ChangeNotifier {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await _resetDailyIfNeeded();
|
||||
_loadPersisted();
|
||||
_lastScheduleState = isScheduledBlockActive;
|
||||
_startTicker();
|
||||
_incrementOpenCount();
|
||||
}
|
||||
@@ -249,6 +317,23 @@ class SessionManager extends ChangeNotifier {
|
||||
_schedStartMin = _prefs!.getInt(_keyScheduleStartMin) ?? 0;
|
||||
_schedEndHour = _prefs!.getInt(_keyScheduleEndHour) ?? 7;
|
||||
_schedEndMin = _prefs!.getInt(_keyScheduleEndMin) ?? 0;
|
||||
|
||||
final schedJson = _prefs!.getString(_keySchedulesJson);
|
||||
if (schedJson != null) {
|
||||
final List decode = jsonDecode(schedJson);
|
||||
_schedules = decode.map((m) => FocusSchedule.fromJson(m)).toList();
|
||||
} else {
|
||||
// Migrate old single schedule if it exists
|
||||
_schedules = [
|
||||
FocusSchedule(
|
||||
startHour: _schedStartHour,
|
||||
startMinute: _schedStartMin,
|
||||
endHour: _schedEndHour,
|
||||
endMinute: _schedEndMin,
|
||||
),
|
||||
];
|
||||
_saveSchedulesToPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
void _incrementOpenCount() {
|
||||
@@ -300,6 +385,13 @@ class SessionManager extends ChangeNotifier {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Schedule check
|
||||
final sched = isScheduledBlockActive;
|
||||
if (sched != _lastScheduleState) {
|
||||
_lastScheduleState = sched;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) notifyListeners();
|
||||
}
|
||||
|
||||
@@ -418,17 +510,53 @@ class SessionManager extends ChangeNotifier {
|
||||
required int endH,
|
||||
required int endM,
|
||||
}) async {
|
||||
_schedStartHour = startH;
|
||||
_schedStartMin = startM;
|
||||
_schedEndHour = endH;
|
||||
_schedEndMin = endM;
|
||||
// Update the first schedule for compatibility? Or just replace all?
|
||||
// Let's replace all schedules with this one if this method is called.
|
||||
_schedules = [
|
||||
FocusSchedule(
|
||||
startHour: startH,
|
||||
startMinute: startM,
|
||||
endHour: endH,
|
||||
endMinute: endM,
|
||||
),
|
||||
];
|
||||
await _prefs?.setInt(_keyScheduleStartHour, startH);
|
||||
await _prefs?.setInt(_keyScheduleStartMin, startM);
|
||||
await _prefs?.setInt(_keyScheduleEndHour, endH);
|
||||
await _prefs?.setInt(_keyScheduleEndMin, endM);
|
||||
await _saveSchedulesToPrefs();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _saveSchedulesToPrefs() async {
|
||||
final json = jsonEncode(_schedules.map((s) => s.toJson()).toList());
|
||||
await _prefs?.setString(_keySchedulesJson, json);
|
||||
}
|
||||
|
||||
Future<void> addSchedule(FocusSchedule s) async {
|
||||
_schedules.add(s);
|
||||
await _saveSchedulesToPrefs();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeScheduleAt(int index) async {
|
||||
if (index >= 0 && index < _schedules.length) {
|
||||
_schedules.removeAt(index);
|
||||
await _saveSchedulesToPrefs();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateScheduleAt(int index, FocusSchedule s) async {
|
||||
if (index >= 0 && index < _schedules.length) {
|
||||
_schedules[index] = s;
|
||||
await _saveSchedulesToPrefs();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker?.cancel();
|
||||
|
||||
@@ -8,13 +8,25 @@ class SettingsService extends ChangeNotifier {
|
||||
static const _keyRequireLongPress = 'set_require_long_press';
|
||||
static const _keyShowBreathGate = 'set_show_breath_gate';
|
||||
static const _keyRequireWordChallenge = 'set_require_word_challenge';
|
||||
static const _keyGhostMode = 'set_ghost_mode';
|
||||
static const _keyEnableTextSelection = 'set_enable_text_selection';
|
||||
static const _keyEnabledTabs = 'set_enabled_tabs';
|
||||
static const _keyShowInstaSettings = 'set_show_insta_settings';
|
||||
|
||||
static const _keyIsFirstRun = 'set_is_first_run';
|
||||
|
||||
// Granular Ghost Mode keys
|
||||
static const _keyGhostTyping = 'set_ghost_typing';
|
||||
static const _keyGhostSeen = 'set_ghost_seen';
|
||||
static const _keyGhostStories = 'set_ghost_stories';
|
||||
static const _keyGhostDmPhotos = 'set_ghost_dm_photos';
|
||||
|
||||
// Privacy keys
|
||||
static const _keySanitizeLinks = 'set_sanitize_links';
|
||||
static const _keyNotifyDMs = 'set_notify_dms';
|
||||
static const _keyNotifyActivity = 'set_notify_activity';
|
||||
|
||||
// Legacy key for migration
|
||||
static const _keyGhostModeLegacy = 'set_ghost_mode';
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
bool _blurExplore = true;
|
||||
@@ -22,10 +34,28 @@ class SettingsService extends ChangeNotifier {
|
||||
bool _requireLongPress = true;
|
||||
bool _showBreathGate = true;
|
||||
bool _requireWordChallenge = true;
|
||||
bool _ghostMode = true;
|
||||
bool _enableTextSelection = false;
|
||||
bool _showInstaSettings = true;
|
||||
List<String> _enabledTabs = ['Home', 'Search', 'Create', 'Reels', 'Profile'];
|
||||
bool _isDarkMode = true; // Default to dark as per existing app theme
|
||||
|
||||
// Granular Ghost Mode defaults (all on)
|
||||
bool _ghostTyping = true;
|
||||
bool _ghostSeen = true;
|
||||
bool _ghostStories = true;
|
||||
bool _ghostDmPhotos = true;
|
||||
|
||||
// Privacy defaults
|
||||
bool _sanitizeLinks = true;
|
||||
bool _notifyDMs = true;
|
||||
bool _notifyActivity = true;
|
||||
|
||||
List<String> _enabledTabs = [
|
||||
'Home',
|
||||
'Search',
|
||||
'Reels',
|
||||
'Messages',
|
||||
'Profile',
|
||||
];
|
||||
bool _isFirstRun = true;
|
||||
|
||||
bool get blurExplore => _blurExplore;
|
||||
@@ -33,11 +63,26 @@ class SettingsService extends ChangeNotifier {
|
||||
bool get requireLongPress => _requireLongPress;
|
||||
bool get showBreathGate => _showBreathGate;
|
||||
bool get requireWordChallenge => _requireWordChallenge;
|
||||
bool get ghostMode => _ghostMode;
|
||||
bool get enableTextSelection => _enableTextSelection;
|
||||
bool get showInstaSettings => _showInstaSettings;
|
||||
List<String> get enabledTabs => _enabledTabs;
|
||||
bool get isFirstRun => _isFirstRun;
|
||||
bool get isDarkMode => _isDarkMode;
|
||||
|
||||
// Granular Ghost Mode getters
|
||||
bool get ghostTyping => _ghostTyping;
|
||||
bool get ghostSeen => _ghostSeen;
|
||||
bool get ghostStories => _ghostStories;
|
||||
bool get ghostDmPhotos => _ghostDmPhotos;
|
||||
bool get notifyDMs => _notifyDMs;
|
||||
bool get notifyActivity => _notifyActivity;
|
||||
|
||||
/// True if ANY ghost mode setting is enabled (for injection logic).
|
||||
bool get anyGhostModeEnabled =>
|
||||
_ghostTyping || _ghostSeen || _ghostStories || _ghostDmPhotos;
|
||||
|
||||
// Privacy getters
|
||||
bool get sanitizeLinks => _sanitizeLinks;
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
@@ -46,12 +91,42 @@ class SettingsService extends ChangeNotifier {
|
||||
_requireLongPress = _prefs!.getBool(_keyRequireLongPress) ?? true;
|
||||
_showBreathGate = _prefs!.getBool(_keyShowBreathGate) ?? true;
|
||||
_requireWordChallenge = _prefs!.getBool(_keyRequireWordChallenge) ?? true;
|
||||
_ghostMode = _prefs!.getBool(_keyGhostMode) ?? true;
|
||||
_enableTextSelection = _prefs!.getBool(_keyEnableTextSelection) ?? false;
|
||||
_showInstaSettings = _prefs!.getBool(_keyShowInstaSettings) ?? true;
|
||||
|
||||
// Migrate legacy ghostMode key -> all granular keys
|
||||
final legacyGhostMode = _prefs!.getBool(_keyGhostModeLegacy);
|
||||
if (legacyGhostMode != null) {
|
||||
// Seed all four granular keys with the legacy value
|
||||
_ghostTyping = legacyGhostMode;
|
||||
_ghostSeen = legacyGhostMode;
|
||||
_ghostStories = legacyGhostMode;
|
||||
_ghostDmPhotos = legacyGhostMode;
|
||||
// Save granular keys and remove legacy key
|
||||
await _prefs!.setBool(_keyGhostTyping, legacyGhostMode);
|
||||
await _prefs!.setBool(_keyGhostSeen, legacyGhostMode);
|
||||
await _prefs!.setBool(_keyGhostStories, legacyGhostMode);
|
||||
await _prefs!.setBool(_keyGhostDmPhotos, legacyGhostMode);
|
||||
await _prefs!.remove(_keyGhostModeLegacy);
|
||||
} else {
|
||||
_ghostTyping = _prefs!.getBool(_keyGhostTyping) ?? true;
|
||||
_ghostSeen = _prefs!.getBool(_keyGhostSeen) ?? true;
|
||||
_ghostStories = _prefs!.getBool(_keyGhostStories) ?? true;
|
||||
_ghostDmPhotos = _prefs!.getBool(_keyGhostDmPhotos) ?? true;
|
||||
}
|
||||
|
||||
_sanitizeLinks = _prefs!.getBool(_keySanitizeLinks) ?? true;
|
||||
_notifyDMs = _prefs!.getBool(_keyNotifyDMs) ?? true;
|
||||
_notifyActivity = _prefs!.getBool(_keyNotifyActivity) ?? true;
|
||||
|
||||
_enabledTabs =
|
||||
_prefs!.getStringList(_keyEnabledTabs) ??
|
||||
['Home', 'Search', 'Create', 'Reels', 'Profile'];
|
||||
(_prefs!.getStringList(_keyEnabledTabs) ??
|
||||
['Home', 'Search', 'Reels', 'Messages', 'Profile'])
|
||||
..remove('Create');
|
||||
if (!_enabledTabs.contains('Messages') && _enabledTabs.length < 5) {
|
||||
// Migration: add Messages if missing
|
||||
_enabledTabs.insert(3, 'Messages');
|
||||
}
|
||||
_isFirstRun = _prefs!.getBool(_keyIsFirstRun) ?? true;
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -92,12 +167,6 @@ class SettingsService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setGhostMode(bool v) async {
|
||||
_ghostMode = v;
|
||||
await _prefs?.setBool(_keyGhostMode, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setEnableTextSelection(bool v) async {
|
||||
_enableTextSelection = v;
|
||||
await _prefs?.setBool(_keyEnableTextSelection, v);
|
||||
@@ -110,6 +179,56 @@ class SettingsService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setDarkMode(bool dark) {
|
||||
if (_isDarkMode != dark) {
|
||||
_isDarkMode = dark;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Granular Ghost Mode setters
|
||||
Future<void> setGhostTyping(bool v) async {
|
||||
_ghostTyping = v;
|
||||
await _prefs?.setBool(_keyGhostTyping, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setGhostSeen(bool v) async {
|
||||
_ghostSeen = v;
|
||||
await _prefs?.setBool(_keyGhostSeen, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setGhostStories(bool v) async {
|
||||
_ghostStories = v;
|
||||
await _prefs?.setBool(_keyGhostStories, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setGhostDmPhotos(bool v) async {
|
||||
_ghostDmPhotos = v;
|
||||
await _prefs?.setBool(_keyGhostDmPhotos, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSanitizeLinks(bool v) async {
|
||||
_sanitizeLinks = v;
|
||||
await _prefs?.setBool(_keySanitizeLinks, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setNotifyDMs(bool v) async {
|
||||
_notifyDMs = v;
|
||||
await _prefs?.setBool(_keyNotifyDMs, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setNotifyActivity(bool v) async {
|
||||
_notifyActivity = v;
|
||||
await _prefs?.setBool(_keyNotifyActivity, v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> toggleTab(String tab) async {
|
||||
if (_enabledTabs.contains(tab)) {
|
||||
if (_enabledTabs.length > 1) {
|
||||
@@ -121,4 +240,12 @@ class SettingsService extends ChangeNotifier {
|
||||
await _prefs?.setStringList(_keyEnabledTabs, _enabledTabs);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> reorderTab(int oldIndex, int newIndex) async {
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
final String item = _enabledTabs.removeAt(oldIndex);
|
||||
_enabledTabs.insert(newIndex, item);
|
||||
await _prefs?.setStringList(_keyEnabledTabs, _enabledTabs);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user