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
+8 -5
View File
@@ -11,7 +11,7 @@ class AboutPage extends StatefulWidget {
}
class _AboutPageState extends State<AboutPage> {
final String _currentVersion = '0.8.5';
final String _currentVersion = '0.9.8';
bool _isChecking = false;
Future<void> _checkUpdate() async {
@@ -110,10 +110,13 @@ class _AboutPageState extends State<AboutPage> {
color: Colors.blue.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.psychology,
color: Colors.blue,
size: 50,
child: ClipOval(
child: Image.asset(
'assets/images/focusgram.png',
width: 60,
height: 60,
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 24),
+164 -82
View File
@@ -1,19 +1,93 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/session_manager.dart';
import '../services/settings_service.dart';
import '../utils/discipline_challenge.dart';
class GuardrailsPage extends StatelessWidget {
class GuardrailsPage extends StatefulWidget {
const GuardrailsPage({super.key});
@override
State<GuardrailsPage> createState() => _GuardrailsPageState();
}
class _GuardrailsPageState extends State<GuardrailsPage> {
Future<void> _handleScheduleAction(
BuildContext context,
SessionManager sm,
Future<void> Function() action,
) async {
if (sm.isScheduledBlockActive) {
final ok = await DisciplineChallenge.show(context, count: 35);
if (!context.mounted || !ok) return;
}
await action();
}
Future<void> _pickNewSchedule(BuildContext context, SessionManager sm) async {
final start = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 22, minute: 0),
helpText: 'Select Start Time',
);
if (!context.mounted || start == null) return;
final end = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
helpText: 'Select End Time',
);
if (!context.mounted || end == null) return;
await sm.addSchedule(
FocusSchedule(
startHour: start.hour,
startMinute: start.minute,
endHour: end.hour,
endMinute: end.minute,
),
);
}
Future<void> _editExistingSchedule(
BuildContext context,
SessionManager sm,
int index,
FocusSchedule s,
) async {
final start = await showTimePicker(
context: context,
initialTime: TimeOfDay(hour: s.startHour, minute: s.startMinute),
helpText: 'Edit Start Time',
);
if (!context.mounted || start == null) return;
final end = await showTimePicker(
context: context,
initialTime: TimeOfDay(hour: s.endHour, minute: s.endMinute),
helpText: 'Edit End Time',
);
if (!context.mounted || end == null) return;
await sm.updateScheduleAt(
index,
FocusSchedule(
startHour: start.hour,
startMinute: start.minute,
endHour: end.hour,
endMinute: end.minute,
),
);
}
@override
Widget build(BuildContext context) {
final sm = context.watch<SessionManager>();
final settings = context.watch<SettingsService>();
final isDark = settings.isDarkMode;
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
title: const Text(
'Guardrails',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
@@ -25,11 +99,14 @@ class GuardrailsPage extends StatelessWidget {
),
body: ListView(
children: [
const Padding(
padding: EdgeInsets.all(16.0),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Set your limits to stay focused. Changes to these settings require a challenge.',
style: TextStyle(color: Colors.white54, fontSize: 13),
style: TextStyle(
color: isDark ? Colors.white54 : Colors.black54,
fontSize: 13,
),
),
),
_buildFrictionSliderTile(
@@ -60,84 +137,83 @@ class GuardrailsPage extends StatelessWidget {
'Reducing cooldown makes it easier to start new sessions. Are you sure?',
onConfirmed: (v) => sm.setCooldownMinutes(v.toInt()),
),
const Divider(color: Colors.white10, height: 32),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'Scheduled Blocking',
Divider(color: isDark ? Colors.white10 : Colors.black12, height: 32),
SwitchListTile(
title: const Text('Scheduled Blocking'),
subtitle: Text(
'Block Instagram during specific hours',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white54 : Colors.black54,
fontSize: 13,
),
),
),
SwitchListTile(
title: const Text(
'Enable Blocking Schedule',
style: TextStyle(color: Colors.white),
),
subtitle: const Text(
'Block Instagram during specific hours',
style: TextStyle(color: Colors.white54, fontSize: 13),
),
value: sm.scheduleEnabled,
onChanged: (v) => sm.setScheduleEnabled(v),
),
if (sm.scheduleEnabled) ...[
ListTile(
title: const Text(
'Start Time',
style: TextStyle(color: Colors.white),
),
trailing: Text(
'${sm.schedStartHour.toString().padLeft(2, '0')}:${sm.schedStartMin.toString().padLeft(2, '0')}',
style: const TextStyle(color: Colors.blue),
),
onTap: () async {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay(
hour: sm.schedStartHour,
minute: sm.schedStartMin,
...sm.schedules.asMap().entries.map((entry) {
final idx = entry.key;
final s = entry.value;
return ListTile(
title: Text(
'Schedule ${idx + 1}',
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
'${sm.formatTime12h(s.startHour, s.startMinute)} - ${sm.formatTime12h(s.endHour, s.endMinute)}',
style: TextStyle(
color: isDark ? Colors.white54 : Colors.black54,
fontSize: 13,
),
);
if (time != null) {
sm.setScheduleTime(
startH: time.hour,
startM: time.minute,
endH: sm.schedEndHour,
endM: sm.schedEndMin,
);
}
},
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.edit,
color: Colors.blue,
size: 20,
),
onPressed: () => _handleScheduleAction(
context,
sm,
() => _editExistingSchedule(context, sm, idx, s),
),
),
IconButton(
icon: const Icon(
Icons.delete_outline,
color: Colors.redAccent,
size: 20,
),
onPressed: () => _handleScheduleAction(
context,
sm,
() => sm.removeScheduleAt(idx),
),
),
],
),
);
}),
ListTile(
leading: const Icon(
Icons.add_circle_outline,
color: Colors.blueAccent,
),
title: const Text(
'End Time',
style: TextStyle(color: Colors.white),
'Add Focus Hours',
style: TextStyle(
color: Colors.blueAccent,
fontWeight: FontWeight.w600,
),
),
trailing: Text(
'${sm.schedEndHour.toString().padLeft(2, '0')}:${sm.schedEndMin.toString().padLeft(2, '0')}',
style: const TextStyle(color: Colors.blue),
onTap: () => _handleScheduleAction(
context,
sm,
() => _pickNewSchedule(context, sm),
),
onTap: () async {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay(
hour: sm.schedEndHour,
minute: sm.schedEndMin,
),
);
if (time != null) {
sm.setScheduleTime(
startH: sm.schedStartHour,
startM: sm.schedStartMin,
endH: time.hour,
endM: time.minute,
);
}
},
),
],
],
@@ -217,18 +293,17 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> {
@override
Widget build(BuildContext context) {
final settings = context.watch<SettingsService>();
final isDark = settings.isDarkMode;
final divisions = ((widget.max - widget.min) / widget.divisor).round();
return Column(
children: [
ListTile(
title: Text(
widget.title,
style: const TextStyle(color: Colors.white),
),
title: Text(widget.title),
subtitle: Text(
'${_draftValue.toInt()} min',
style: const TextStyle(color: Colors.white70),
style: TextStyle(color: isDark ? Colors.white70 : Colors.black54),
),
trailing: _pendingConfirm
? Row(
@@ -241,15 +316,22 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> {
_pendingConfirm = false;
});
},
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white38),
),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
final success = await DisciplineChallenge.show(context);
if (!success) return;
final sm = context.read<SessionManager>();
int wordCount = 15;
// If we are at 0 quota, increase difficulty to 35 words
if (widget.title.contains('Daily Reel Limit') &&
sm.dailyRemainingSeconds <= 0) {
wordCount = 35;
}
final success = await DisciplineChallenge.show(
context,
count: wordCount,
);
if (!context.mounted || !success) return;
await widget.onConfirmed(_draftValue);
setState(() => _pendingConfirm = false);
},
File diff suppressed because it is too large Load Diff
+57 -41
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import '../services/settings_service.dart';
import '../services/notification_service.dart';
@@ -39,6 +40,14 @@ class _OnboardingPageState extends State<OnboardingPage> {
icon: Icons.timer,
color: Colors.orange,
),
OnboardingData(
title: 'Open Links in FocusGram',
description:
'To open Instagram links directly here: Tap "Configure", then "Open by default" -> "Add link" and select all.',
icon: Icons.link,
color: Colors.cyan,
isAppSettingsPage: true,
),
OnboardingData(
title: 'Upload Content',
description:
@@ -101,48 +110,53 @@ class _OnboardingPageState extends State<OnboardingPage> {
child: SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () async {
if (_pages[_currentPage].isPermissionPage) {
if (_pages[_currentPage].permission != null) {
await _pages[_currentPage].permission!.request();
}
if (_pages[_currentPage].title == 'Stay Notified') {
await NotificationService().init();
}
if (_currentPage == _pages.length - 1) {
_finish();
} else {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
} else if (_currentPage < _pages.length - 1) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
_finish();
}
child: Builder(
builder: (context) {
final data = _pages[_currentPage];
return ElevatedButton(
onPressed: () async {
if (data.isAppSettingsPage) {
await AppSettings.openAppSettings(
type: AppSettingsType.settings,
);
} else if (data.isPermissionPage) {
if (data.permission != null) {
await data.permission!.request();
}
if (data.title == 'Stay Notified') {
await NotificationService().init();
}
}
if (_currentPage == _pages.length - 1) {
_finish();
} else {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
_currentPage == _pages.length - 1
? 'Get Started'
: (data.isAppSettingsPage
? 'Configure'
: 'Next'),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
_currentPage == _pages.length - 1
? 'Get Started'
: 'Next',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
),
@@ -166,6 +180,7 @@ class OnboardingData {
final IconData icon;
final Color color;
final bool isPermissionPage;
final bool isAppSettingsPage;
final Permission? permission;
OnboardingData({
@@ -174,6 +189,7 @@ class OnboardingData {
required this.icon,
required this.color,
this.isPermissionPage = false,
this.isAppSettingsPage = false,
this.permission,
});
}
+8 -1
View File
@@ -32,6 +32,10 @@ class _ReelPlayerOverlayState extends State<ReelPlayerOverlay> {
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
// Set isolated player flag to ensure scroll-lock applies even if a session is active globally
_controller.runJavaScript(
'window.__focusgramIsolatedPlayer = true;',
);
// Apply scroll-lock via MutationObserver: prevents swiping to next reel
_controller.runJavaScript(
InjectionController.reelsMutationObserverJS,
@@ -42,7 +46,10 @@ class _ReelPlayerOverlayState extends State<ReelPlayerOverlay> {
sessionActive: true,
blurExplore: false,
blurReels: false,
ghostMode: false,
ghostTyping: false,
ghostSeen: false,
ghostStories: false,
ghostDmPhotos: false,
enableTextSelection: true,
),
);
+314 -131
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/session_manager.dart';
import '../services/settings_service.dart';
import 'package:url_launcher/url_launcher.dart';
import '../services/focusgram_router.dart';
import 'guardrails_page.dart';
import 'about_page.dart';
@@ -13,11 +13,11 @@ class SettingsPage extends StatelessWidget {
Widget build(BuildContext context) {
// Watching services ensures the UI rebuilds when settings or session state change.
final sm = context.watch<SessionManager>();
final settings = context.watch<SettingsService>();
final isDark = settings.isDarkMode;
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
title: const Text(
'FocusGram',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
@@ -55,6 +55,13 @@ class SettingsPage extends StatelessWidget {
icon: Icons.extension_outlined,
destination: const _ExtrasSettingsPage(),
),
_buildSettingsTile(
context: context,
title: 'Notifications',
subtitle: 'Manage message and activity alerts',
icon: Icons.notifications_active_outlined,
destination: const _NotificationSettingsPage(),
),
_buildSettingsTile(
context: context,
title: 'About',
@@ -63,44 +70,39 @@ class SettingsPage extends StatelessWidget {
destination: const AboutPage(),
),
const Divider(
color: Colors.white10,
height: 40,
indent: 16,
endIndent: 16,
),
const Divider(height: 40, indent: 16, endIndent: 16),
ListTile(
leading: const Icon(
Icons.settings_outlined,
color: Colors.purpleAccent,
),
title: const Text(
'Instagram Settings',
style: TextStyle(color: Colors.white),
),
title: const Text('Instagram Settings'),
subtitle: const Text(
'Open native Instagram account settings',
style: TextStyle(color: Colors.white54, fontSize: 12),
style: TextStyle(fontSize: 12),
),
trailing: const Icon(
Icons.open_in_new,
color: Colors.white24,
size: 14,
),
onTap: () async {
final uri = Uri.parse(
'https://www.instagram.com/accounts/settings/?entrypoint=profile',
);
await launchUrl(uri, mode: LaunchMode.externalApplication);
onTap: () {
// Bug 6 fix: navigate inside the WebView instead of external browser
Navigator.pop(context);
FocusGramRouter.pendingUrl.value =
'https://www.instagram.com/accounts/settings/?entrypoint=profile';
},
),
const SizedBox(height: 40),
const Center(
Center(
child: Text(
'FocusGram · Built for discipline',
style: TextStyle(color: Colors.white12, fontSize: 12),
style: TextStyle(
color: isDark ? Colors.white12 : Colors.black12,
fontSize: 12,
),
),
),
const SizedBox(height: 24),
@@ -118,16 +120,9 @@ class SettingsPage extends StatelessWidget {
}) {
return ListTile(
leading: Icon(icon, color: Colors.blue),
title: Text(title, style: const TextStyle(color: Colors.white)),
subtitle: Text(
subtitle,
style: const TextStyle(color: Colors.white54, fontSize: 13),
),
trailing: const Icon(
Icons.arrow_forward_ios,
color: Colors.white24,
size: 14,
),
title: Text(title),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 13)),
trailing: const Icon(Icons.arrow_forward_ios, size: 14),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => destination),
@@ -140,9 +135,9 @@ class SettingsPage extends StatelessWidget {
margin: const EdgeInsets.fromLTRB(16, 20, 16, 4),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF111111),
color: Colors.blue.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white10),
border: Border.all(color: Colors.blue.withValues(alpha: 0.1)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -177,16 +172,16 @@ class SettingsPage extends StatelessWidget {
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(color: Colors.white38, fontSize: 11),
),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 11)),
],
);
}
Widget _dividerCell() =>
Container(width: 1, height: 36, color: Colors.white10);
Widget _dividerCell() => Container(
width: 1,
height: 36,
color: Colors.blue.withValues(alpha: 0.1),
);
}
class _DistractionSettingsPage extends StatelessWidget {
@@ -196,9 +191,7 @@ class _DistractionSettingsPage extends StatelessWidget {
Widget build(BuildContext context) {
final settings = context.watch<SettingsService>();
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
title: const Text(
'Distraction Management',
style: TextStyle(fontSize: 17),
@@ -211,52 +204,30 @@ class _DistractionSettingsPage extends StatelessWidget {
body: ListView(
children: [
SwitchListTile(
title: const Text(
'Blur Explore feed',
style: TextStyle(color: Colors.white),
),
title: const Text('Blur Posts and Explore'),
subtitle: const Text(
'Blurs posts and reels in Explore by default',
style: TextStyle(color: Colors.white54, fontSize: 13),
'Blurs images and videos on the home feed and Explore page',
style: TextStyle(fontSize: 13),
),
value: settings.blurExplore,
onChanged: (v) => settings.setBlurExplore(v),
activeThumbColor: Colors.blue,
),
SwitchListTile(
title: const Text(
'Mindfulness Gate',
style: TextStyle(color: Colors.white),
),
title: const Text('Mindfulness Gate'),
subtitle: const Text(
'Show breathing exercise before opening',
style: TextStyle(color: Colors.white54, fontSize: 13),
style: TextStyle(fontSize: 13),
),
value: settings.showBreathGate,
onChanged: (v) => settings.setShowBreathGate(v),
activeThumbColor: Colors.blue,
),
SwitchListTile(
title: const Text(
'Long-press for Session',
style: TextStyle(color: Colors.white),
),
subtitle: const Text(
'Requires 2s hold to start a Reel session',
style: TextStyle(color: Colors.white54, fontSize: 13),
),
value: settings.requireLongPress,
onChanged: (v) => settings.setRequireLongPress(v),
activeThumbColor: Colors.blue,
),
SwitchListTile(
title: const Text(
'Strict Changes (Word Challenge)',
style: TextStyle(color: Colors.white),
),
title: const Text('Strict Changes (Word Challenge)'),
subtitle: const Text(
'Requires 15-word typing challenge before lax changes',
style: TextStyle(color: Colors.white54, fontSize: 13),
style: TextStyle(fontSize: 13),
),
value: settings.requireWordChallenge,
onChanged: (v) => settings.setRequireWordChallenge(v),
@@ -268,8 +239,6 @@ class _DistractionSettingsPage extends StatelessWidget {
}
}
/// Stateful slider tile that shows a friction dialog when the user moves the
/// slider to a value greater than the current persisted value.
class _FrictionSliderTile extends StatefulWidget {
final String title;
final String subtitle;
@@ -320,13 +289,10 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(
widget.title,
style: const TextStyle(color: Colors.white),
),
title: Text(widget.title),
subtitle: Text(
'${_draftValue.toInt()} min',
style: const TextStyle(color: Colors.white70, fontSize: 13),
style: const TextStyle(fontSize: 13),
),
trailing: _pendingConfirm
? Row(
@@ -339,10 +305,7 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> {
_pendingConfirm = false;
});
},
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white38),
),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
@@ -413,19 +376,10 @@ class _ExtrasSettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final settings = context.watch<SettingsService>();
final allTabs = [
'Home',
'Search',
'Create',
'Notifications',
'Reels',
'Profile',
];
final isDark = settings.isDarkMode;
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
title: const Text('Extras', style: TextStyle(fontSize: 17)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
@@ -435,59 +389,48 @@ class _ExtrasSettingsPage extends StatelessWidget {
body: ListView(
children: [
const _SettingsSectionHeader(title: 'EXPERIMENT'),
SwitchListTile(
title: const Text(
'Ghost Mode',
style: TextStyle(color: Colors.white),
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _GhostModeSettingsPage()),
),
subtitle: const Text(
'Hides "typing..." and "seen" status in DMs',
style: TextStyle(color: Colors.white54, fontSize: 13),
leading: Icon(
Icons.visibility_off_outlined,
color: isDark ? Colors.white70 : Colors.black87,
),
value: settings.ghostMode,
onChanged: (v) => settings.setGhostMode(v),
activeThumbColor: Colors.blue,
title: const Text('Ghost Mode'),
subtitle: Text(
settings.anyGhostModeEnabled
? 'Active — some receipts are hidden'
: 'Disabled',
style: TextStyle(
color: settings.anyGhostModeEnabled
? Colors.blue
: (isDark ? Colors.white38 : Colors.black38),
fontSize: 13,
),
),
trailing: const Icon(Icons.chevron_right),
),
SwitchListTile(
title: const Text(
'Enable Text Selection',
style: TextStyle(color: Colors.white),
),
title: const Text('Enable Text Selection'),
subtitle: const Text(
'Allows copying text from posts and captions',
style: TextStyle(color: Colors.white54, fontSize: 13),
style: TextStyle(fontSize: 13),
),
value: settings.enableTextSelection,
onChanged: (v) => settings.setEnableTextSelection(v),
activeThumbColor: Colors.blue,
),
const _SettingsSectionHeader(title: 'BOTTOM BAR'),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Wrap(
spacing: 8,
children: allTabs.map((tab) {
final isEnabled = settings.enabledTabs.contains(tab);
return FilterChip(
label: Text(tab),
selected: isEnabled,
onSelected: (_) => settings.toggleTab(tab),
backgroundColor: Colors.white10,
selectedColor: Colors.blue.withValues(alpha: 0.3),
checkmarkColor: Colors.blue,
labelStyle: TextStyle(
color: isEnabled ? Colors.blue : Colors.white60,
fontSize: 12,
),
);
}).toList(),
),
),
const Padding(
padding: EdgeInsets.fromLTRB(16, 4, 16, 16),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Toggle tabs to customize your navigation bar. At least one tab must be enabled.',
style: TextStyle(color: Colors.white24, fontSize: 11),
'Experimental features: Some features may break if Instagram updates their website.',
style: TextStyle(
color: isDark ? Colors.white24 : Colors.black26,
fontSize: 11,
),
),
),
],
@@ -516,3 +459,243 @@ class _SettingsSectionHeader extends StatelessWidget {
);
}
}
class _GhostModeSettingsPage extends StatelessWidget {
const _GhostModeSettingsPage();
@override
Widget build(BuildContext context) {
final settings = context.watch<SettingsService>();
final isDark = settings.isDarkMode;
return Scaffold(
appBar: AppBar(
title: const Text('Ghost Mode', style: TextStyle(fontSize: 17)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
onPressed: () => Navigator.pop(context),
),
),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Text(
'Control which activity receipts are hidden from other users. ',
style: TextStyle(
color: isDark ? Colors.white38 : Colors.black45,
fontSize: 12,
height: 1.4,
),
),
),
const _SettingsSectionHeader(title: 'MESSAGING'),
SwitchListTile(
secondary: Icon(
Icons.keyboard_outlined,
color: isDark ? Colors.white54 : Colors.black54,
),
title: const Text('Hide typing indicator'),
subtitle: Text(
"Others won't see the 'typing...' status when you write a message",
style: TextStyle(
color: isDark ? Colors.white38 : Colors.black45,
fontSize: 12,
),
),
value: settings.ghostTyping,
onChanged: (v) => settings.setGhostTyping(v),
activeThumbColor: Colors.blue,
),
Stack(
children: [
AbsorbPointer(
child: Opacity(
opacity: 0.5,
child: SwitchListTile(
secondary: Icon(
Icons.done_all_rounded,
color: isDark ? Colors.white54 : Colors.black54,
),
title: const Text('Hide seen status'),
subtitle: Text(
"Others won't see when you've read their DMs",
style: TextStyle(
color: isDark ? Colors.white38 : Colors.black45,
fontSize: 12,
),
),
value: settings.ghostSeen,
onChanged: (v) => settings.setGhostSeen(v),
activeThumbColor: Colors.blue,
),
),
),
Positioned(
right: 16,
top: 0,
bottom: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.blue, width: 0.5),
),
child: const Text(
'COMING SOON',
style: TextStyle(
color: Colors.blue,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
SwitchListTile(
secondary: Icon(
Icons.image_outlined,
color: isDark ? Colors.white54 : Colors.black54,
),
title: const Text('Hide DM photo seen status'),
subtitle: Text(
'Prevents Instagram from marking photos/videos in DMs as viewed',
style: TextStyle(
color: isDark ? Colors.white38 : Colors.black45,
fontSize: 12,
),
),
value: settings.ghostDmPhotos,
onChanged: (v) => settings.setGhostDmPhotos(v),
activeThumbColor: Colors.blue,
),
const _SettingsSectionHeader(title: 'STORIES'),
SwitchListTile(
secondary: Icon(
Icons.auto_stories_outlined,
color: isDark ? Colors.white54 : Colors.black54,
),
title: const Text('Story ghost mode'),
subtitle: Text(
'Watch stories without appearing in the viewer list',
style: TextStyle(
color: isDark ? Colors.white38 : Colors.black45,
fontSize: 12,
),
),
value: settings.ghostStories,
onChanged: (v) => settings.setGhostStories(v),
activeThumbColor: Colors.blue,
),
const SizedBox(height: 32),
],
),
);
}
}
class _NotificationSettingsPage extends StatelessWidget {
const _NotificationSettingsPage();
@override
Widget build(BuildContext context) {
final settings = context.watch<SettingsService>();
final isDark = settings.isDarkMode;
return Scaffold(
appBar: AppBar(
title: const Text('Notifications', style: TextStyle(fontSize: 17)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
onPressed: () => Navigator.pop(context),
),
),
body: ListView(
children: [
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
),
child: Column(
children: [
const Row(
children: [
Icon(
Icons.info_outline,
color: Colors.blueAccent,
size: 20,
),
SizedBox(width: 12),
Text(
'Important Note',
style: TextStyle(
color: Colors.blueAccent,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 10),
Text(
'FocusGram monitors your session locally. For notifications to work, the app must be running in the background (minimized). If you force-close or swipe away the app from your task switcher, notifications will stop until you reopen it.',
style: TextStyle(
color: isDark ? Colors.white70 : Colors.black87,
fontSize: 13,
height: 1.4,
),
),
],
),
),
SwitchListTile(
secondary: const Icon(Icons.mail_outline, color: Colors.blueAccent),
title: const Text('Direct Messages'),
subtitle: const Text(
'Notify when you receive a new DM',
style: TextStyle(fontSize: 13),
),
value: settings.notifyDMs,
onChanged: (v) => settings.setNotifyDMs(v),
activeThumbColor: Colors.blue,
),
SwitchListTile(
secondary: const Icon(
Icons.favorite_border,
color: Colors.blueAccent,
),
title: const Text('General Activity'),
subtitle: const Text(
'Likes, mentions, and other interactions',
style: TextStyle(fontSize: 13),
),
value: settings.notifyActivity,
onChanged: (v) => settings.setNotifyActivity(v),
activeThumbColor: Colors.blue,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Note: Push notifications are generated by the app local service by monitoring the web sessions. This does not rely on Instagram servers sending notifications to your device.',
style: TextStyle(
color: isDark ? Colors.white24 : Colors.black26,
fontSize: 11,
),
),
),
],
),
);
}
}