Progress SAve- downloader,blur,ghost mode(Partially) works

This commit is contained in:
Ujwal223
2026-05-25 18:00:57 +05:45
parent 4f63e784ac
commit 2d33dcb889
66 changed files with 6373 additions and 909 deletions
+20 -4
View File
@@ -27,7 +27,25 @@ class _AppSessionPickerScreenState extends State<AppSessionPickerScreen> {
55,
60,
];
int _selectedIndex = 2; // default: 15 min
int _selectedIndex = 0; // default: 5 min unless a previous choice exists
late final FixedExtentScrollController _scrollController;
@override
void initState() {
super.initState();
final lastMinutes = context.read<SessionManager>().lastAppSessionMinutes;
final lastIndex = _minuteOptions.indexOf(lastMinutes);
_selectedIndex = lastIndex >= 0 ? lastIndex : 0;
_scrollController = FixedExtentScrollController(
initialItem: _selectedIndex,
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -118,12 +136,10 @@ class _AppSessionPickerScreenState extends State<AppSessionPickerScreen> {
perspective: 0.003,
squeeze: 1.1,
diameterRatio: 2.5,
controller: _scrollController,
onSelectedItemChanged: (i) {
setState(() => _selectedIndex = i);
},
controller: FixedExtentScrollController(
initialItem: _selectedIndex,
),
childDelegate: ListWheelChildListDelegate(
children: _minuteOptions.asMap().entries.map((entry) {
final isSelected = entry.key == _selectedIndex;
+11 -7
View File
@@ -1,12 +1,16 @@
import 'package:flutter/material.dart';
import 'dart:async';
/// A mindfulness screen shown before the app opens.
/// Forces the user to take a deep 10-second breath.
/// A mindfulness screen shown before Instagram opens.
class BreathGateScreen extends StatefulWidget {
final VoidCallback onFinish;
final int durationSeconds;
const BreathGateScreen({super.key, required this.onFinish});
const BreathGateScreen({
super.key,
required this.onFinish,
this.durationSeconds = 10,
});
@override
State<BreathGateScreen> createState() => _BreathGateScreenState();
@@ -16,15 +20,15 @@ class _BreathGateScreenState extends State<BreathGateScreen>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
int _secondsRemaining = 10;
late int _secondsRemaining;
Timer? _timer;
bool _canContinue = false;
@override
void initState() {
super.initState();
_secondsRemaining = widget.durationSeconds.clamp(3, 60).toInt();
// 10-second breathing animation: 5s in, 5s out
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
@@ -71,7 +75,7 @@ class _BreathGateScreenState extends State<BreathGateScreen>
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Are you sure you want to open FocusGram?',
'Are you sure you want to open Instagram?',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
@@ -131,7 +135,7 @@ class _BreathGateScreenState extends State<BreathGateScreen>
borderRadius: BorderRadius.circular(25),
),
),
child: const Text('Continue to FocusGram'),
child: const Text('Continue to Instagram'),
),
),
],
+122
View File
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../services/settings_service.dart';
class ExtrasSettingsPage extends StatelessWidget {
const ExtrasSettingsPage({super.key});
@override
Widget build(BuildContext context) {
final settings = context.watch<SettingsService>();
return Scaffold(
appBar: AppBar(
title: const Text(
'Extras',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
onPressed: () => Navigator.pop(context),
),
),
body: ListView(
children: [
const _SectionHeader(title: 'MEDIA'),
_SwitchTile(
title: 'Download Media (Feed + Reels)',
subtitle: 'Adds a download icon on posts and reels',
value: settings.videoDownloadEnabled,
onChanged: (v) async {
await settings.setVideoDownloadEnabled(v);
HapticFeedback.selectionClick();
},
),
const _SectionHeader(title: 'FOCUS'),
_SwitchTile(
title: 'GHOST MODE',
subtitle: 'Hide seen indicator / read receipts',
value: settings.ghostMode,
onChanged: (v) async {
await settings.setGhostMode(v);
HapticFeedback.selectionClick();
},
),
const _SectionHeader(title: 'FOCUSGRAM V2'),
_SwitchTile(
title: 'Ad Blocker',
subtitle: 'Removes ads and sponsored posts',
value: settings.v2AdBlockerDomEnabled,
onChanged: (v) async {
await settings.setV2AdBlockerDomEnabled(v);
HapticFeedback.selectionClick();
},
),
_SwitchTile(
title: 'Block Suggested Posts',
subtitle: 'Removes Suggested for you and recommendation units',
value: settings.contentSuggested,
onChanged: (v) async {
await settings.setContentSuggestedEnabled(v);
HapticFeedback.selectionClick();
},
),
const SizedBox(height: 40),
],
),
);
}
}
class _SwitchTile extends StatelessWidget {
final String title;
final String? subtitle;
final bool value;
final ValueChanged<bool> onChanged;
const _SwitchTile({
required this.title,
this.subtitle,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: Text(title, style: const TextStyle(fontSize: 15)),
subtitle: subtitle != null
? Text(subtitle ?? '', style: const TextStyle(fontSize: 12))
: null,
value: value,
onChanged: onChanged,
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
);
}
}
+7 -2
View File
@@ -18,7 +18,11 @@ class _GuardrailsPageState extends State<GuardrailsPage> {
Future<void> Function() action,
) async {
if (sm.isScheduledBlockActive) {
final ok = await DisciplineChallenge.show(context, count: 35);
final settings = context.read<SettingsService>();
final ok = await DisciplineChallenge.show(
context,
count: settings.resolvedWordChallengeCount(),
);
if (!context.mounted || !ok) return;
}
await action();
@@ -321,7 +325,8 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> {
ElevatedButton(
onPressed: () async {
final sm = context.read<SessionManager>();
int wordCount = 15;
final settings = context.read<SettingsService>();
int wordCount = settings.resolvedWordChallengeCount();
// If we are at 0 quota, increase difficulty to 35 words
if (widget.title.contains('Daily Reel Limit') &&
sm.dailyRemainingSeconds <= 0) {
File diff suppressed because it is too large Load Diff
+66 -28
View File
@@ -18,7 +18,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
final PageController _pageController = PageController();
int _currentPage = 0;
// Pages: Welcome, Session Management, Link Handling, Blur Settings, Notifications
// Pages: Welcome, Focus controls, Link Handling, Blur Settings, Notifications
static const int _kTotalPages = 5;
static const int _kBlurPage = 3;
@@ -32,26 +32,26 @@ class _OnboardingPageState extends State<OnboardingPage> {
final List<Widget> slides = [
// Page 0: Welcome
_StaticSlide(
icon: Icons.auto_awesome,
color: Colors.blue,
icon: Icons.auto_awesome_rounded,
color: const Color(0xFF4F8DFF),
title: 'Welcome to FocusGram',
description:
'The distraction-free way to use Instagram. We help you stay focused by blocking Reels and Explore content.',
'Use Instagram with guardrails: timed Reel sessions, calmer feeds, optional media tools, and privacy-first controls that stay on your device.',
),
// Page 1: Session Management
// Page 1: Focus controls
_StaticSlide(
icon: Icons.timer,
color: Colors.orange,
title: 'Session Management',
icon: Icons.timer_outlined,
color: const Color(0xFFFFB74D),
title: 'Time With Intent',
description:
'Plan your usage. Set daily limits and use timed sessions to stay in control of your time.',
'Set daily limits, cooldowns, scheduled focus hours, and short Reel sessions when you choose to watch.',
),
// Page 2: Open links
_StaticSlide(
icon: Icons.link,
color: Colors.cyan,
icon: Icons.link_rounded,
color: const Color(0xFF35C2D6),
title: 'Open Links in FocusGram',
description:
'To open Instagram links directly here: Tap "Configure", then "Open by default" → "Add link" and select all.',
@@ -63,11 +63,11 @@ class _OnboardingPageState extends State<OnboardingPage> {
// Page 4: Notifications
_StaticSlide(
icon: Icons.notifications_active,
color: Colors.green,
title: 'Stay Notified',
icon: Icons.notifications_active_outlined,
color: const Color(0xFF5DD18A),
title: 'Useful Alerts Only',
description:
'We need notification permissions to alert you when your session is over or a new message arrives.',
'Enable notifications only if you want session-end or persistent focus reminders. FocusGram will ask here, not before onboarding.',
isPermissionPage: true,
permission: Permission.notification,
),
@@ -108,7 +108,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
),
),
),
const SizedBox(height: 32),
const SizedBox(height: 28),
// CTA button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
@@ -123,14 +123,14 @@ class _OnboardingPageState extends State<OnboardingPage> {
final isBlur = _currentPage == _kBlurPage;
String label;
if (isLast) {
label = 'Get Started';
if (isNotif) {
label = 'Allow & Start';
} else if (isLink) {
label = 'Configure';
} else if (isNotif) {
label = 'Allow Notifications';
} else if (isBlur) {
label = 'Save & Continue';
} else if (isLast) {
label = 'Get Started';
} else {
label = 'Next';
}
@@ -143,7 +143,8 @@ class _OnboardingPageState extends State<OnboardingPage> {
);
} else if (isNotif) {
await Permission.notification.request();
await NotificationService().init();
await NotificationService()
.requestPermissionsNow();
}
if (!context.mounted) return;
@@ -178,9 +179,19 @@ class _OnboardingPageState extends State<OnboardingPage> {
// Skip button (available on all pages except last)
if (_currentPage < _kTotalPages - 1)
TextButton(
onPressed: () => _finish(context),
onPressed: () {
if (_currentPage == _kNotifPage) {
_finish(context);
} else {
_pageController.animateToPage(
_kTotalPages - 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
child: const Text(
'Skip',
'Skip setup',
style: TextStyle(color: Colors.white38, fontSize: 14),
),
),
@@ -222,18 +233,27 @@ class _StaticSlide extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(40, 40, 40, 160),
padding: const EdgeInsets.fromLTRB(28, 40, 28, 160),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 120, color: color),
const SizedBox(height: 48),
Container(
width: 112,
height: 112,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(28),
border: Border.all(color: color.withValues(alpha: 0.28)),
),
child: Icon(icon, size: 54, color: color),
),
const SizedBox(height: 36),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
@@ -243,10 +263,28 @@ class _StaticSlide extends StatelessWidget {
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white70,
fontSize: 18,
fontSize: 16,
height: 1.5,
),
),
if (isPermissionPage || isAppSettingsPage) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
),
child: Text(
isPermissionPage
? 'Permission is optional and can be changed later.'
: 'This opens Android settings; return here when done.',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white54, fontSize: 12),
),
),
],
],
),
);
+1
View File
@@ -115,6 +115,7 @@ class _ReelPlayerOverlayState extends State<ReelPlayerOverlay> {
hideReelsTab: false,
hideShopTab: false,
disableReelsEntirely: false,
blockHomeFeedScroll: false,
),
);
},
+26 -21
View File
@@ -1,6 +1,7 @@
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 SessionModal extends StatefulWidget {
@@ -63,23 +64,22 @@ class _SessionModalState extends State<SessionModal> {
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [1, 5, 10, 15].map((m) {
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ElevatedButton(
onPressed: (sm.isCooldownActive || sm.isDailyLimitExhausted)
? null
: () => _start(m),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white12,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Text('${m}m'),
Wrap(
spacing: 8,
runSpacing: 8,
children: [1, 3, 5, 10, 15, 20, 30].map((m) {
return SizedBox(
width: 72,
child: ElevatedButton(
onPressed: (sm.isCooldownActive || sm.isDailyLimitExhausted)
? null
: () => _start(m),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white12,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Text('${m}m'),
),
);
}).toList(),
@@ -92,8 +92,8 @@ class _SessionModalState extends State<SessionModal> {
Slider(
value: _customMinutes,
min: 1,
max: 30,
divisions: 29,
max: 60,
divisions: 59,
label: '${_customMinutes.toInt()}m',
onChanged: (v) => setState(() => _customMinutes = v),
),
@@ -126,10 +126,15 @@ class _SessionModalState extends State<SessionModal> {
void _start(int minutes) async {
final sm = context.read<SessionManager>();
final settings = context.read<SettingsService>();
// Always require word challenge for reel sessions (User request)
final success = await DisciplineChallenge.show(context);
if (!success) return;
if (settings.requireWordChallenge) {
final success = await DisciplineChallenge.show(
context,
count: settings.resolvedWordChallengeCount(),
);
if (!success) return;
}
if (sm.startSession(minutes)) {
if (mounted) Navigator.pop(context);
+447 -80
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -8,6 +9,7 @@ import '../services/settings_service.dart';
import '../services/focusgram_router.dart';
import '../features/screen_time/screen_time_screen.dart';
import 'guardrails_page.dart';
import 'extras_settings_page.dart';
// Main Settings Page
@@ -34,6 +36,7 @@ class SettingsPage extends StatelessWidget {
),
body: ListView(
children: [
const _DonateTile(),
_buildStatsRow(sm),
const _SectionHeader(title: 'FOCUS & BLOCKING'),
@@ -63,6 +66,19 @@ class SettingsPage extends StatelessWidget {
),
),
const _SectionHeader(title: 'EXTRAS'),
_SubmoduleTile(
icon: Icons.download_rounded,
iconColor: Colors.orangeAccent,
title: 'Extras',
subtitle: 'Download media, Ghost Mode, Ad Blocker',
enabled: true,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ExtrasSettingsPage()),
),
),
const _SectionHeader(title: 'APPEARANCE'),
_SubmoduleTile(
icon: Icons.palette_outlined,
@@ -264,6 +280,7 @@ class FocusSettingsPage extends StatelessWidget {
body: ListView(
children: [
const _SectionHeader(title: 'BLOCKING'),
Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -293,12 +310,23 @@ class FocusSettingsPage extends StatelessWidget {
color: Colors.redAccent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.shield_rounded, color: Colors.redAccent, size: 20),
child: const Icon(
Icons.shield_rounded,
color: Colors.redAccent,
size: 20,
),
),
title: const Text('Minimal Mode', style: TextStyle(fontSize: 15)),
subtitle: Text(
settings.minimalModeEnabled ? 'Enabled - tap to customize' : 'Disabled - tap to configure',
style: TextStyle(fontSize: 12, color: settings.minimalModeEnabled ? Colors.greenAccent : Colors.grey),
settings.minimalModeEnabled
? 'Enabled - tap to customize'
: 'Disabled - tap to configure',
style: TextStyle(
fontSize: 12,
color: settings.minimalModeEnabled
? Colors.greenAccent
: Colors.grey,
),
),
trailing: Switch(
value: settings.minimalModeEnabled,
@@ -307,22 +335,49 @@ class FocusSettingsPage extends StatelessWidget {
HapticFeedback.selectionClick();
},
),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MinimalModeSubmenuPage())),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const MinimalModeSubmenuPage()),
),
),
const _SectionHeader(title: 'FRICTION'),
_SwitchTile(
title: 'Mindfulness Gate',
subtitle: 'Breath screen before opening Instagram',
subtitle: '${settings.breathGateSeconds}s before opening Instagram',
value: settings.showBreathGate,
onChanged: (v) => settings.setShowBreathGate(v),
),
if (settings.showBreathGate)
_NumberEditTile(
title: 'Gate Duration',
label: '${settings.breathGateSeconds} seconds',
initialValue: settings.breathGateSeconds,
min: 3,
max: 60,
suffix: 'seconds',
onSubmitted: (v) => settings.setBreathGateSeconds(v),
),
_SwitchTile(
title: 'Strict Mode (Word Challenge)',
subtitle: 'Must type a phrase before starting a Reel session',
title: 'Typing Challenge',
subtitle: settings.wordChallengeCount == 0
? 'Random: 10-35 words'
: '${settings.wordChallengeCount} words',
value: settings.requireWordChallenge,
onChanged: (v) => settings.setRequireWordChallenge(v),
),
if (settings.requireWordChallenge)
_ChoiceTile<int>(
title: 'Typing Words',
value: settings.wordChallengeCount,
label: settings.wordChallengeCount == 0
? 'Random (10-35)'
: '${settings.wordChallengeCount} words',
options: const [20, 25, 30, 35, 0],
optionLabel: (v) => v == 0 ? 'Random (10-35)' : '$v words',
onSelected: (v) => settings.setWordChallengeCount(v),
),
const _SectionHeader(title: 'MEDIA'),
_SwitchTile(
title: 'Block Autoplay Videos',
@@ -348,6 +403,48 @@ class FocusSettingsPage extends StatelessWidget {
),
),
const _SectionHeader(title: 'FOCUSGRAM V2 OVERLAY'),
_SwitchTile(
title: 'Content Hider',
subtitle: 'Hide stories tray, feed posts, reels, suggested content',
value: settings.v2ContentHiderEnabled,
onChanged: (v) => settings.setV2ContentHiderEnabled(v),
),
if (settings.v2ContentHiderEnabled)
Padding(
padding: const EdgeInsets.only(left: 32),
child: Column(
children: [
_SwitchTile(
title: 'Hide Stories Tray',
subtitle: 'Story bubbles row',
value: settings.contentStories,
onChanged: (v) => settings.setContentStoriesEnabled(v),
),
_SwitchTile(
title: 'Hide Feed Posts',
subtitle: 'Home feed posts',
value: settings.contentPosts,
onChanged: (v) => settings.setContentPostsEnabled(v),
),
_SwitchTile(
title: 'Hide Reels (Feed)',
subtitle: 'Reels shown in the feed',
value: settings.contentReels,
onChanged: (v) => settings.setContentReelsEnabled(v),
),
_SwitchTile(
title: 'Hide Suggested Content',
subtitle: 'Suggested posts and recommendation units',
value: settings.contentSuggested,
onChanged: (v) => settings.setContentSuggestedEnabled(v),
),
],
),
),
const SizedBox(height: 40),
],
),
@@ -355,6 +452,50 @@ class FocusSettingsPage extends StatelessWidget {
}
}
class _DonateTile extends StatelessWidget {
const _DonateTile();
static final Uri _donateUri = Uri.parse('https://buymemomo.com/ujwal');
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 4),
decoration: BoxDecoration(
color: Colors.pinkAccent.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.pinkAccent.withValues(alpha: 0.22)),
),
child: ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.pinkAccent.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.favorite_rounded,
color: Colors.pinkAccent,
size: 20,
),
),
title: const Text(
'Please donate to support the development of this project.',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
subtitle: const Text(
'Your support keeps FocusGram free and maintained.',
style: TextStyle(fontSize: 12),
),
trailing: const Icon(Icons.open_in_new, size: 14),
onTap: () =>
launchUrl(_donateUri, mode: LaunchMode.externalApplication),
),
);
}
}
// Minimal Mode Submenu
class MinimalModeSubmenuPage extends StatefulWidget {
@@ -368,6 +509,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
late bool _blurExplore;
late bool _disableReelsEntirely;
late bool _disableExploreEntirely;
late bool _blockHomeFeedScroll;
@override
void initState() {
@@ -376,26 +518,51 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
_blurExplore = settings.blurExplore;
_disableReelsEntirely = settings.disableReelsEntirely;
_disableExploreEntirely = settings.disableExploreEntirely;
_blockHomeFeedScroll = settings.blockHomeFeedScroll;
}
void _updateSetting(String key, bool value) {
Future<void> _updateSetting(String key, bool value) async {
final settings = context.read<SettingsService>();
setState(() {
switch (key) {
case 'blurExplore':
_blurExplore = value;
settings.setBlurExplore(value);
break;
case 'disableReelsEntirely':
_disableReelsEntirely = value;
settings.setDisableReelsEntirelyInternal(value);
break;
case 'disableExploreEntirely':
_disableExploreEntirely = value;
settings.setDisableExploreEntirelyInternal(value);
break;
case 'blockHomeFeedScroll':
_blockHomeFeedScroll = value;
break;
}
});
switch (key) {
case 'blurExplore':
await settings.setBlurExplore(value);
break;
case 'disableReelsEntirely':
await settings.setDisableReelsEntirelyInternal(value);
break;
case 'disableExploreEntirely':
await settings.setDisableExploreEntirelyInternal(value);
break;
case 'blockHomeFeedScroll':
await settings.setBlockHomeFeedScrollInternal(value);
break;
}
if (!mounted) return;
final latest = context.read<SettingsService>();
setState(() {
_blurExplore = latest.blurExplore;
_disableReelsEntirely = latest.disableReelsEntirely;
_disableExploreEntirely = latest.disableExploreEntirely;
_blockHomeFeedScroll = latest.blockHomeFeedScroll;
});
HapticFeedback.selectionClick();
}
@@ -406,6 +573,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
_blurExplore = true;
_disableReelsEntirely = true;
_disableExploreEntirely = true;
_blockHomeFeedScroll = true;
});
HapticFeedback.mediumImpact();
}
@@ -418,6 +586,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
_blurExplore = settings.blurExplore;
_disableReelsEntirely = settings.disableReelsEntirely;
_disableExploreEntirely = settings.disableExploreEntirely;
_blockHomeFeedScroll = settings.blockHomeFeedScroll;
});
HapticFeedback.mediumImpact();
}
@@ -437,61 +606,88 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isMinimalModeEnabled
? [Colors.redAccent.withValues(alpha: 0.2), Colors.red.withValues(alpha: 0.1)]
: [Colors.grey.withValues(alpha: 0.1), Colors.grey.withValues(alpha: 0.05)],
colors: isMinimalModeEnabled
? [
Colors.redAccent.withValues(alpha: 0.2),
Colors.red.withValues(alpha: 0.1),
]
: [
Colors.grey.withValues(alpha: 0.1),
Colors.grey.withValues(alpha: 0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isMinimalModeEnabled ? Colors.redAccent.withValues(alpha: 0.3) : Colors.grey.withValues(alpha: 0.2),
color: isMinimalModeEnabled
? Colors.redAccent.withValues(alpha: 0.3)
: Colors.grey.withValues(alpha: 0.2),
),
),
child: Column(
children: [
Icon(
isMinimalModeEnabled ? Icons.shield_rounded : Icons.shield_outlined,
isMinimalModeEnabled
? Icons.shield_rounded
: Icons.shield_outlined,
color: isMinimalModeEnabled ? Colors.redAccent : Colors.grey,
size: 48,
),
const SizedBox(height: 12),
Text(
isMinimalModeEnabled ? 'Minimal Mode Active' : 'Minimal Mode Disabled',
isMinimalModeEnabled
? 'Minimal Mode Active'
: 'Minimal Mode Disabled',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isMinimalModeEnabled ? Colors.redAccent : Colors.grey,
color: isMinimalModeEnabled
? Colors.redAccent
: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
isMinimalModeEnabled
isMinimalModeEnabled
? 'Distractions are blocked. Customize which features stay enabled below.'
: 'Turn on to block all distractions at once, or customize individual settings below.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: isDark ? Colors.white54 : Colors.black54),
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.white54 : Colors.black54,
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isMinimalModeEnabled ? _turnOffMinimalMode : _turnOnMinimalMode,
onPressed: isMinimalModeEnabled
? _turnOffMinimalMode
: _turnOnMinimalMode,
style: ElevatedButton.styleFrom(
backgroundColor: isMinimalModeEnabled ? Colors.grey : Colors.redAccent,
backgroundColor: isMinimalModeEnabled
? Colors.grey
: Colors.redAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
isMinimalModeEnabled
? 'Turn Off Minimal Mode'
: 'Turn On Minimal Mode',
),
child: Text(isMinimalModeEnabled ? 'Turn Off Minimal Mode' : 'Turn On Minimal Mode'),
),
),
],
),
),
const _SectionHeader(title: 'CUSTOMIZE SETTINGS'),
Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -502,7 +698,11 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
),
child: const Row(
children: [
Icon(Icons.touch_app_rounded, size: 14, color: Colors.blueAccent),
Icon(
Icons.touch_app_rounded,
size: 14,
color: Colors.blueAccent,
),
SizedBox(width: 8),
Expanded(
child: Text(
@@ -513,13 +713,19 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
],
),
),
_SwitchTile(
title: 'Blur Feed & Explore',
subtitle: 'Blurs post thumbnails until tapped',
value: _blurExplore,
onChanged: (v) => _updateSetting('blurExplore', v),
),
_SwitchTile(
title: 'Block Home Feed Scroll',
subtitle: 'Freeze vertical scrolling on the home feed only',
value: _blockHomeFeedScroll,
onChanged: (v) => _updateSetting('blockHomeFeedScroll', v),
),
_SwitchTile(
title: 'Disable Reels Entirely',
subtitle: 'Block all Reels with no session option',
@@ -532,7 +738,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
value: _disableExploreEntirely,
onChanged: (v) => _updateSetting('disableExploreEntirely', v),
),
const SizedBox(height: 40),
],
),
@@ -550,43 +756,52 @@ class AppearancePage extends StatefulWidget {
}
class _AppearancePageState extends State<AppearancePage> {
Future<void> _addSchedule(BuildContext context, SettingsService settings) async {
Future<void> _addSchedule(
BuildContext context,
SettingsService settings,
) async {
TimeOfDay? startTime = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 21, minute: 0),
helpText: 'Select start time',
);
if (startTime == null || !context.mounted) return;
TimeOfDay? endTime = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 6, minute: 0),
helpText: 'Select end time',
);
if (endTime == null || !context.mounted) return;
final newSchedule = {
'enabled': true,
'startTime': '${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
'endTime': '${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
'startTime':
'${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
'endTime':
'${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
};
await settings.addGrayscaleSchedule(newSchedule);
}
Future<void> _editSchedule(BuildContext context, SettingsService settings, int index) async {
Future<void> _editSchedule(
BuildContext context,
SettingsService settings,
int index,
) async {
final schedules = settings.grayscaleSchedules;
if (index >= schedules.length) return;
final current = schedules[index];
final startParts = (current['startTime'] as String).split(':');
final endParts = (current['endTime'] as String).split(':');
// Capture context before async gap
final capturedContext = context;
TimeOfDay? startTime = await showTimePicker(
context: capturedContext,
initialTime: TimeOfDay(
@@ -595,9 +810,9 @@ class _AppearancePageState extends State<AppearancePage> {
),
helpText: 'Select start time',
);
if (startTime == null || !capturedContext.mounted) return;
TimeOfDay? endTime = await showTimePicker(
context: capturedContext,
initialTime: TimeOfDay(
@@ -606,27 +821,31 @@ class _AppearancePageState extends State<AppearancePage> {
),
helpText: 'Select end time',
);
if (endTime == null || !capturedContext.mounted) return;
final updatedSchedule = {
...current,
'startTime': '${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
'endTime': '${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
'startTime':
'${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
'endTime':
'${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
};
await settings.updateGrayscaleSchedule(index, updatedSchedule);
}
Future<void> _toggleSchedule(SettingsService settings, int index) async {
final schedules = List<Map<String, dynamic>>.from(settings.grayscaleSchedules);
final schedules = List<Map<String, dynamic>>.from(
settings.grayscaleSchedules,
);
if (index >= schedules.length) return;
schedules[index] = {
...schedules[index],
'enabled': !(schedules[index]['enabled'] as bool),
};
await settings.setGrayscaleSchedules(schedules);
}
@@ -648,7 +867,7 @@ class _AppearancePageState extends State<AppearancePage> {
],
),
);
if (confirmed == true) {
await settings.removeGrayscaleSchedule(index);
}
@@ -669,7 +888,8 @@ class _AppearancePageState extends State<AppearancePage> {
const _SectionHeader(title: 'DISPLAY'),
_SwitchTile(
title: 'Grayscale Mode',
subtitle: 'Makes Instagram black & white — reduces dopamine response',
subtitle:
'Makes Instagram black & white — reduces dopamine response',
value: settings.grayscaleEnabled,
onChanged: (v) => settings.setGrayscaleEnabled(v),
),
@@ -687,7 +907,7 @@ class _AppearancePageState extends State<AppearancePage> {
style: TextStyle(fontSize: 12, height: 1.5),
),
),
// Status indicator
if (settings.grayscaleSchedules.isNotEmpty)
Padding(
@@ -695,26 +915,38 @@ class _AppearancePageState extends State<AppearancePage> {
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: settings.isGrayscaleActiveNow ? Colors.green.withValues(alpha: 0.1) : Colors.orange.withValues(alpha: 0.1),
color: settings.isGrayscaleActiveNow
? Colors.green.withValues(alpha: 0.1)
: Colors.orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: settings.isGrayscaleActiveNow ? Colors.green.withValues(alpha: 0.3) : Colors.orange.withValues(alpha: 0.3)),
border: Border.all(
color: settings.isGrayscaleActiveNow
? Colors.green.withValues(alpha: 0.3)
: Colors.orange.withValues(alpha: 0.3),
),
),
child: Row(
children: [
Icon(
settings.isGrayscaleActiveNow ? Icons.check_circle : Icons.schedule,
color: settings.isGrayscaleActiveNow ? Colors.greenAccent : Colors.orangeAccent,
size: 20
settings.isGrayscaleActiveNow
? Icons.check_circle
: Icons.schedule,
color: settings.isGrayscaleActiveNow
? Colors.greenAccent
: Colors.orangeAccent,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
settings.isGrayscaleActiveNow
? 'Grayscale is active now'
settings.isGrayscaleActiveNow
? 'Grayscale is active now'
: 'Grayscale is currently inactive',
style: TextStyle(
fontSize: 13,
color: settings.isGrayscaleActiveNow ? Colors.greenAccent : Colors.orangeAccent
fontSize: 13,
color: settings.isGrayscaleActiveNow
? Colors.greenAccent
: Colors.orangeAccent,
),
),
),
@@ -722,7 +954,7 @@ class _AppearancePageState extends State<AppearancePage> {
),
),
),
// Schedule list
...List.generate(settings.grayscaleSchedules.length, (index) {
final schedule = settings.grayscaleSchedules[index];
@@ -732,11 +964,14 @@ class _AppearancePageState extends State<AppearancePage> {
width: 36,
height: 36,
decoration: BoxDecoration(
color: (isEnabled ? Colors.purpleAccent : Colors.grey).withValues(alpha: 0.12),
color: (isEnabled ? Colors.purpleAccent : Colors.grey)
.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
isEnabled ? Icons.play_circle_outline : Icons.pause_circle_outline,
isEnabled
? Icons.play_circle_outline
: Icons.pause_circle_outline,
color: isEnabled ? Colors.purpleAccent : Colors.grey,
size: 20,
),
@@ -750,7 +985,10 @@ class _AppearancePageState extends State<AppearancePage> {
),
subtitle: Text(
isEnabled ? 'Active' : 'Disabled',
style: TextStyle(fontSize: 12, color: isDark ? Colors.white54 : Colors.black45),
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.white54 : Colors.black45,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
@@ -795,7 +1033,7 @@ class _AppearancePageState extends State<AppearancePage> {
onTap: () => _editSchedule(context, settings, index),
);
}),
// Add schedule button
ListTile(
leading: Container(
@@ -805,16 +1043,26 @@ class _AppearancePageState extends State<AppearancePage> {
color: Colors.green.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.add_circle_outline, color: Colors.green, size: 20),
child: const Icon(
Icons.add_circle_outline,
color: Colors.green,
size: 20,
),
),
title: const Text(
'Add Schedule',
style: TextStyle(color: Colors.green),
),
title: const Text('Add Schedule', style: TextStyle(color: Colors.green)),
subtitle: Text(
'Add a new grayscale schedule',
style: TextStyle(fontSize: 12, color: isDark ? Colors.white54 : Colors.black45),
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.white54 : Colors.black45,
),
),
onTap: () => _addSchedule(context, settings),
),
const SizedBox(height: 40),
],
),
@@ -966,15 +1214,9 @@ class _SwitchTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: Text(
title,
style: const TextStyle(fontSize: 15),
),
title: Text(title, style: const TextStyle(fontSize: 15)),
subtitle: subtitle != null
? Text(
subtitle ?? '',
style: const TextStyle(fontSize: 12),
)
? Text(subtitle ?? '', style: const TextStyle(fontSize: 12))
: null,
value: value,
onChanged: onChanged,
@@ -982,6 +1224,131 @@ class _SwitchTile extends StatelessWidget {
}
}
class _ChoiceTile<T> extends StatelessWidget {
final String title;
final T value;
final String label;
final List<T> options;
final String Function(T value) optionLabel;
final ValueChanged<T> onSelected;
const _ChoiceTile({
required this.title,
required this.value,
required this.label,
required this.options,
required this.optionLabel,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title, style: const TextStyle(fontSize: 15)),
subtitle: Text(label, style: const TextStyle(fontSize: 12)),
trailing: PopupMenuButton<T>(
initialValue: value,
onSelected: onSelected,
itemBuilder: (context) => options
.map(
(option) => PopupMenuItem<T>(
value: option,
child: Text(optionLabel(option)),
),
)
.toList(),
child: const Icon(Icons.expand_more_rounded, size: 22),
),
onTap: () async {
final selected = await showModalBottomSheet<T>(
context: context,
builder: (context) => SafeArea(
child: ListView(
shrinkWrap: true,
children: options
.map(
(option) => ListTile(
title: Text(optionLabel(option)),
trailing: option == value
? const Icon(Icons.check_rounded)
: null,
onTap: () => Navigator.pop(context, option),
),
)
.toList(),
),
),
);
if (selected != null) onSelected(selected);
},
);
}
}
class _NumberEditTile extends StatelessWidget {
final String title;
final String label;
final int initialValue;
final int min;
final int max;
final String suffix;
final ValueChanged<int> onSubmitted;
const _NumberEditTile({
required this.title,
required this.label,
required this.initialValue,
required this.min,
required this.max,
required this.suffix,
required this.onSubmitted,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title, style: const TextStyle(fontSize: 15)),
subtitle: Text(label, style: const TextStyle(fontSize: 12)),
trailing: const Icon(Icons.edit_outlined, size: 20),
onTap: () async {
final controller = TextEditingController(text: '$initialValue');
final result = await showDialog<int>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(title),
content: TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
suffixText: suffix,
helperText: '$min-$max $suffix',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
final parsed = int.tryParse(controller.text.trim());
if (parsed == null) return;
Navigator.pop(dialogContext, parsed.clamp(min, max).toInt());
},
child: const Text('Save'),
),
],
),
);
controller.dispose();
if (result != null) onSubmitted(result);
},
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});