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:
Ujwal
2026-02-24 00:04:23 +05:45
parent 878e625f0e
commit 5232b8b0a9
48 changed files with 5258 additions and 1127 deletions
+13
View File
@@ -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
+3 -2
View File
@@ -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',
+11 -6
View File
@@ -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');
}
}
}
+140 -12
View File
@@ -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();
+141 -14
View File
@@ -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();
}
}