Feature Pack with bug fixes for V2

This commit is contained in:
Ujwal223
2026-06-13 13:06:25 +05:45
parent 39b6545e4a
commit b7c8120496
41 changed files with 453 additions and 414 deletions
@@ -5,7 +5,6 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import '../../scripts/spa_navigation_monitor.dart';
import '../../scripts/native_feel.dart';
import '../../scripts/focus_scripts.dart';
import '../../scripts/reel_metadata_extractor.dart';
class InstagramPreloader {
static HeadlessInAppWebView? _headlessWebView;
@@ -8,7 +8,7 @@ class ReelsHistoryEntry {
final String title;
final String thumbnailUrl;
final DateTime visitedAt;
final int durationSeconds; // How long the session lasted
final int durationSeconds; // How long the session lasted
final int adsWatchedInSession; // How many ads watched during this session
const ReelsHistoryEntry({
@@ -123,7 +123,9 @@ class ReelsHistoryService {
final now = DateTime.now();
final sevenDaysAgo = now.subtract(const Duration(days: 7));
final recent = entries.where((e) => e.visitedAt.isAfter(sevenDaysAgo)).toList();
final recent = entries
.where((e) => e.visitedAt.isAfter(sevenDaysAgo))
.toList();
if (recent.isEmpty) return 0;
return recent.length / 7.0;
@@ -138,7 +140,8 @@ class ReelsHistoryService {
final Map<String, int> counts = {};
for (final entry in recent) {
final dayKey = '${entry.visitedAt.year}-'
final dayKey =
'${entry.visitedAt.year}-'
'${entry.visitedAt.month.toString().padLeft(2, '0')}-'
'${entry.visitedAt.day.toString().padLeft(2, '0')}';
counts[dayKey] = (counts[dayKey] ?? 0) + 1;
+8 -4
View File
@@ -128,6 +128,7 @@ class _InitialRouteHandlerState extends State<InitialRouteHandler>
bool _breathCompleted = false;
bool _appSessionStarted = false;
bool _onboardingCompleted = false;
bool _lockScreenDismissed = false;
late AppLinks _appLinks;
@override
@@ -162,11 +163,14 @@ class _InitialRouteHandlerState extends State<InitialRouteHandler>
}
}
void _showLockScreen() {
Navigator.push(
Future<void> _showLockScreen() async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const AppLockScreen(forAppWide: true)),
);
if (result == true && mounted) {
setState(() => _lockScreenDismissed = true);
}
}
Future<void> _initDeepLinks() async {
@@ -190,8 +194,8 @@ class _InitialRouteHandlerState extends State<InitialRouteHandler>
final settings = context.watch<SettingsService>();
final appLock = context.watch<AppLockService>();
// Step 0: App-wide lock (shows before everything)
if (appLock.needsUnlockOnStart && !_appSessionStarted) {
// Step 0: App-wide lock (shows before everything, once per cold start)
if (appLock.needsUnlockOnStart && !_lockScreenDismissed) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!appLock.isShowingLock) {
appLock.onLockScreenShown();
+38 -20
View File
@@ -152,14 +152,19 @@ class _AdsterraAdScreenState extends State<AdsterraAdScreen> {
children: [
const Icon(Icons.videocam, color: Colors.white54, size: 18),
const SizedBox(width: 8),
const Text('Sponsored',
style: TextStyle(color: Colors.white54, fontSize: 13)),
const Text(
'Sponsored',
style: TextStyle(color: Colors.white54, fontSize: 13),
),
const Spacer(),
Text('${_elapsed.clamp(0, widget.requiredSeconds)}s / ${widget.requiredSeconds}s',
style: TextStyle(
color: done ? Colors.greenAccent : Colors.white54,
fontSize: 13,
fontWeight: FontWeight.w600)),
Text(
'${_elapsed.clamp(0, widget.requiredSeconds)}s / ${widget.requiredSeconds}s',
style: TextStyle(
color: done ? Colors.greenAccent : Colors.white54,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
),
@@ -171,7 +176,8 @@ class _AdsterraAdScreenState extends State<AdsterraAdScreen> {
minHeight: 3,
backgroundColor: Colors.white12,
valueColor: AlwaysStoppedAnimation<Color>(
done ? Colors.greenAccent : Colors.blueAccent),
done ? Colors.greenAccent : Colors.blueAccent,
),
),
),
// Hint text
@@ -212,8 +218,10 @@ class _AdsterraAdScreenState extends State<AdsterraAdScreen> {
!url.startsWith('about:')) {
if (_adsClicked < 2) _adsClicked++;
if (mounted) setState(() {});
await launchUrl(Uri.parse(url),
mode: LaunchMode.externalApplication);
await launchUrl(
Uri.parse(url),
mode: LaunchMode.externalApplication,
);
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
@@ -232,18 +240,24 @@ class _AdsterraAdScreenState extends State<AdsterraAdScreen> {
child: ElevatedButton.icon(
onPressed: buttonEnabled ? buttonAction : null,
style: ElevatedButton.styleFrom(
backgroundColor: done ? Colors.greenAccent : Colors.grey,
backgroundColor: done
? Colors.greenAccent
: Colors.grey,
foregroundColor: done ? Colors.black : Colors.white38,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
borderRadius: BorderRadius.circular(14),
),
),
icon: Icon(
done ? Icons.check_circle : Icons.timer_outlined,
size: 22),
done ? Icons.check_circle : Icons.timer_outlined,
size: 22,
),
label: Text(
buttonText,
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 16),
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
@@ -261,11 +275,14 @@ class _AdsterraAdScreenState extends State<AdsterraAdScreen> {
color: Colors.orangeAccent.withValues(alpha: 0.4),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
borderRadius: BorderRadius.circular(12),
),
),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Retry — Reload Ads',
style: TextStyle(fontWeight: FontWeight.w600)),
label: const Text(
'Retry — Reload Ads',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
const SizedBox(height: 4),
@@ -274,8 +291,9 @@ class _AdsterraAdScreenState extends State<AdsterraAdScreen> {
child: Text(
'Skip (no reward)',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.3),
fontSize: 13),
color: Colors.white.withValues(alpha: 0.3),
fontSize: 13,
),
),
),
],
+11 -48
View File
@@ -5,7 +5,7 @@ import '../services/app_lock_service.dart';
/// The lock screen shown when FocusGram is locked.
///
/// Supports PIN entry with optional scrambled keypad and biometric fallback.
/// Supports PIN entry with optional scrambled keypad.
/// [forAppWide] controls which PIN to verify: true = app-wide, false = messages.
/// [title] lets the screen show context (e.g. "Messages Locked").
class AppLockScreen extends StatefulWidget {
@@ -73,10 +73,7 @@ class _AppLockScreenState extends State<AppLockScreen> {
// Title
Text(
widget.title ?? 'FocusGram is Locked',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
@@ -126,21 +123,6 @@ class _AppLockScreenState extends State<AppLockScreen> {
const Spacer(),
// Biometrics button
if (appLock.biometricsEnabled)
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: IconButton(
icon: Icon(
Icons.fingerprint,
color: Colors.blueAccent.withValues(alpha: 0.8),
size: 40,
),
onPressed: _authenticateBiometric,
tooltip: 'Use fingerprint',
),
),
// Keypad
_buildKeypad(appLock),
],
@@ -219,11 +201,7 @@ class _AppLockScreenState extends State<AppLockScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_KeypadButton(
label: '',
onTap: _onDelete,
isFunction: true,
),
_KeypadButton(label: '', onTap: _onDelete, isFunction: true),
_KeypadButton(
label: digitLabels[0],
onTap: () => _onDigit(digitLabels[0]),
@@ -257,14 +235,19 @@ class _AppLockScreenState extends State<AppLockScreen> {
void _onDelete() {
if (_enteredPin.isEmpty) return;
setState(() => _enteredPin = _enteredPin.substring(0, _enteredPin.length - 1));
setState(
() => _enteredPin = _enteredPin.substring(0, _enteredPin.length - 1),
);
}
Future<void> _verifyPin() async {
setState(() => _isVerifying = true);
final appLock = context.read<AppLockService>();
final valid = await appLock.verifyPin(_enteredPin, forAppWide: widget.forAppWide);
final valid = await appLock.verifyPin(
_enteredPin,
forAppWide: widget.forAppWide,
);
if (!mounted) return;
@@ -283,27 +266,7 @@ class _AppLockScreenState extends State<AppLockScreen> {
}
}
Future<void> _authenticateBiometric() async {
final appLock = context.read<AppLockService>();
final available = await appLock.isBiometricsAvailable();
if (!available) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Biometrics not available on this device')),
);
}
return;
}
final success = await appLock.authenticateWithBiometrics();
if (success && mounted) {
appLock.onUnlocked();
Navigator.of(context).pop(true);
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Biometric authentication failed')),
);
}
}
}
class _KeypadButton extends StatelessWidget {
+34 -19
View File
@@ -32,8 +32,10 @@ class _AppLockSettingsPageState extends State<AppLockSettingsPage> {
return Scaffold(
appBar: AppBar(
title: const Text('App Lock',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
title: const Text(
'App Lock',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
@@ -49,9 +51,16 @@ class _AppLockSettingsPageState extends State<AppLockSettingsPage> {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: anythingOn
? [Colors.blueAccent.withValues(alpha: 0.15), Colors.blue.withValues(alpha: 0.05)]
: [Colors.grey.withValues(alpha: 0.1), Colors.grey.withValues(alpha: 0.05)],
begin: Alignment.topLeft, end: Alignment.bottomRight,
? [
Colors.blueAccent.withValues(alpha: 0.15),
Colors.blue.withValues(alpha: 0.05),
]
: [
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(
@@ -71,7 +80,8 @@ class _AppLockSettingsPageState extends State<AppLockSettingsPage> {
Text(
anythingOn ? 'Lock Active' : 'No Lock',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold,
fontSize: 20,
fontWeight: FontWeight.bold,
color: anythingOn ? Colors.blueAccent : Colors.grey,
),
),
@@ -92,9 +102,7 @@ class _AppLockSettingsPageState extends State<AppLockSettingsPage> {
// ── App-wide lock ────────────────────────────────────
SwitchListTile(
title: const Text('Lock Entire App'),
subtitle: const Text(
'Require PIN when opening FocusGram.',
),
subtitle: const Text('Require PIN when opening FocusGram.'),
value: a.lockAppWide,
onChanged: (v) async {
if (v && !a.hasPin) {
@@ -133,9 +141,9 @@ class _AppLockSettingsPageState extends State<AppLockSettingsPage> {
MaterialPageRoute(builder: (_) => const AppLockSetupScreen()),
);
if (ok == true && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PIN updated')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('PIN updated')));
}
},
),
@@ -163,7 +171,11 @@ class _AppLockSettingsPageState extends State<AppLockSettingsPage> {
),
child: const Row(
children: [
Icon(Icons.info_outline, size: 16, color: Colors.blueAccent),
Icon(
Icons.info_outline,
size: 16,
color: Colors.blueAccent,
),
SizedBox(width: 8),
Expanded(
child: Text(
@@ -199,12 +211,15 @@ class _SectionHeader extends StatelessWidget {
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)),
child: Text(
title,
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
);
}
}
+1 -4
View File
@@ -29,10 +29,7 @@ class _AppLockSetupScreenState extends State<AppLockSetupScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Set App Lock PIN'),
centerTitle: true,
),
appBar: AppBar(title: const Text('Set App Lock PIN'), centerTitle: true),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
+1 -3
View File
@@ -95,9 +95,7 @@ class _BaitMeButtonState extends State<BaitMeButton>
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: baitEngine.isOnCooldown
? null
: _onBaitMe,
onTap: baitEngine.isOnCooldown ? null : _onBaitMe,
child: Center(
child: Icon(
Icons.casino_rounded,
+25 -13
View File
@@ -68,7 +68,9 @@ class _BaitMeFullScreenState extends State<BaitMeFullScreen>
const SizedBox(height: 8),
Text(
_done
? BaitEngine.outcomeSubtext(_lastOutcome ?? BaitOutcome.addTenMinutes)
? BaitEngine.outcomeSubtext(
_lastOutcome ?? BaitOutcome.addTenMinutes,
)
: 'Tap the button to test your luck!',
textAlign: TextAlign.center,
style: TextStyle(
@@ -83,7 +85,9 @@ class _BaitMeFullScreenState extends State<BaitMeFullScreen>
animation: _spinAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _isSpinning ? _spinAnimation.value * 2 * pi * 5 : 0,
angle: _isSpinning
? _spinAnimation.value * 2 * pi * 5
: 0,
child: child,
);
},
@@ -155,8 +159,9 @@ class _BaitMeFullScreenState extends State<BaitMeFullScreen>
child: ElevatedButton.icon(
onPressed: _isSpinning ? null : _onBaitMe,
style: ElevatedButton.styleFrom(
backgroundColor:
_done ? Colors.greenAccent : Colors.purpleAccent,
backgroundColor: _done
? Colors.greenAccent
: Colors.purpleAccent,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@@ -167,16 +172,16 @@ class _BaitMeFullScreenState extends State<BaitMeFullScreen>
_isSpinning
? Icons.hourglass_top
: _done
? Icons.check_circle
: Icons.casino_rounded,
? Icons.check_circle
: Icons.casino_rounded,
size: 24,
),
label: Text(
_isSpinning
? 'Rolling…'
: _done
? 'Done — Close'
: '🎲 Spin the Wheel!',
? 'Done — Close'
: '🎲 Spin the Wheel!',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
@@ -190,9 +195,12 @@ class _BaitMeFullScreenState extends State<BaitMeFullScreen>
padding: const EdgeInsets.only(top: 12),
child: TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Not now',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.3))),
child: Text(
'Not now',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.3),
),
),
),
),
@@ -220,13 +228,17 @@ class _BaitMeFullScreenState extends State<BaitMeFullScreen>
baitEngine.onAddMinutes = (m) => creditStore.addBonusMinutes(m);
baitEngine.onResetSession = () => creditStore.resetBalances();
baitEngine.onReduceSessionTime = (m) {
for (var i = 0; i < m; i++) creditStore.drainReelsMinute();
for (var i = 0; i < m; i++) {
creditStore.drainReelsMinute();
}
};
baitEngine.onEndReelSession = () => sessionManager.endSession();
baitEngine.onEndAppSession = () => sessionManager.endAppSession();
baitEngine.onOpenUrl = (url) async {
final uri = Uri.tryParse(url);
if (uri != null) await launchUrl(uri, mode: LaunchMode.externalApplication);
if (uri != null) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
};
final outcome = await baitEngine.activate();
+1 -1
View File
@@ -339,4 +339,4 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
}
}
}
*/
*/
+5 -9
View File
@@ -35,8 +35,9 @@ class _EffortFrictionGateState extends State<EffortFrictionGate> {
Widget build(BuildContext context) {
final creditStore = context.watch<CreditStore>();
final isReels = widget.sessionType == 'reels';
final credits =
isReels ? creditStore.reelsMinutes : creditStore.instaMinutes;
final credits = isReels
? creditStore.reelsMinutes
: creditStore.instaMinutes;
return Scaffold(
backgroundColor: Colors.black,
@@ -55,10 +56,7 @@ class _EffortFrictionGateState extends State<EffortFrictionGate> {
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.orange.shade800,
Colors.orange.shade500,
],
colors: [Colors.orange.shade800, Colors.orange.shade500],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
@@ -231,9 +229,7 @@ class _EffortFrictionGateState extends State<EffortFrictionGate> {
onPressed: widget.onCancel ?? () => Navigator.pop(context),
child: Text(
credits > 0 ? 'Skip for now' : 'Not now',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.4),
),
style: TextStyle(color: Colors.white.withValues(alpha: 0.4)),
),
),
+2 -2
View File
@@ -77,7 +77,7 @@ class ExtrasSettingsPage extends StatelessWidget {
}
String _ghostSubtitle(SettingsService s) {
if (s.ghostMode) return 'DM Ghost active';
if (s.ghostMode) return 'DM Ghost active — works inside chat only';
return 'Tap to configure ghost modes';
}
@@ -102,7 +102,7 @@ class _LaunchPagePicker extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: settings.startupPage,
initialValue: settings.startupPage,
decoration: const InputDecoration(
labelText: 'Launch Page',
border: OutlineInputBorder(),
+2 -3
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../services/settings_service.dart';
@@ -28,7 +27,7 @@ class GhostModeSubmenuPage extends StatelessWidget {
_GhostCard(
icon: Icons.visibility_off_rounded,
title: 'DM Ghost',
subtitle: 'Read messages without the person knowing',
subtitle: 'Read messages without the person knowing (works inside chat interface — first entry only)',
value: s.ghostMode,
warning:
'When DM Ghost is enabled, you can\'t send messages or react to any, you can just receive messages. You can turn ghost mode off anytime from topbar button.',
@@ -117,7 +116,7 @@ class _GhostCard extends StatelessWidget {
),
Switch(
value: value,
activeColor: danger ? Colors.redAccent : null,
activeThumbColor: danger ? Colors.redAccent : null,
onChanged: onChanged,
),
],
+58 -37
View File
@@ -43,8 +43,10 @@ class LevelPanelScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: _levelColors(levelService.level, isDark)[0]
.withValues(alpha: 0.3),
color: _levelColors(
levelService.level,
isDark,
)[0].withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
@@ -116,12 +118,14 @@ class LevelPanelScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black)
.withValues(alpha: 0.05),
color: (isDark ? Colors.white : Colors.black).withValues(
alpha: 0.05,
),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: (isDark ? Colors.white : Colors.black)
.withValues(alpha: 0.1),
color: (isDark ? Colors.white : Colors.black).withValues(
alpha: 0.1,
),
),
),
child: Row(
@@ -183,19 +187,18 @@ class LevelPanelScreen extends StatelessWidget {
final unlocked = levelService.isFeatureUnlocked(feature);
return Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black)
.withValues(alpha: unlocked ? 0.04 : 0.02),
color: (isDark ? Colors.white : Colors.black).withValues(
alpha: unlocked ? 0.04 : 0.02,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: unlocked
? Colors.greenAccent.withValues(alpha: 0.2)
: (isDark ? Colors.white : Colors.black)
.withValues(alpha: 0.08),
: (isDark ? Colors.white : Colors.black).withValues(
alpha: 0.08,
),
),
),
child: Row(
@@ -211,12 +214,10 @@ class LevelPanelScreen extends StatelessWidget {
feature.name,
style: TextStyle(
fontSize: 14,
fontWeight:
unlocked ? FontWeight.w600 : FontWeight.normal,
color:
unlocked
? null
: Colors.grey,
fontWeight: unlocked
? FontWeight.w600
: FontWeight.normal,
color: unlocked ? null : Colors.grey,
),
),
),
@@ -314,8 +315,9 @@ class LevelPanelScreen extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black)
.withValues(alpha: 0.04),
color: (isDark ? Colors.white : Colors.black).withValues(
alpha: 0.04,
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
@@ -386,8 +388,11 @@ class LevelPanelScreen extends StatelessWidget {
children: [
Row(
children: [
Icon(Icons.warning_amber_rounded,
color: Colors.redAccent, size: 18),
Icon(
Icons.warning_amber_rounded,
color: Colors.redAccent,
size: 18,
),
SizedBox(width: 8),
Text(
'XP decays if you backslide',
@@ -404,7 +409,11 @@ class LevelPanelScreen extends StatelessWidget {
'• Watching more reels than your weekly average deducts XP\n'
'• Exceeding limits for 3 consecutive days drops a level\n'
'• Levels are preserved on monthly reset, but XP resets',
style: TextStyle(fontSize: 12, color: Colors.grey, height: 1.5),
style: TextStyle(
fontSize: 12,
color: Colors.grey,
height: 1.5,
),
),
],
),
@@ -417,12 +426,18 @@ class LevelPanelScreen extends StatelessWidget {
Color _levelColor(int level) {
switch (level) {
case 1: return Colors.grey;
case 2: return Colors.blue;
case 3: return Colors.purple;
case 4: return Colors.orange;
case 5: return Colors.amber;
default: return Colors.grey;
case 1:
return Colors.grey;
case 2:
return Colors.blue;
case 3:
return Colors.purple;
case 4:
return Colors.orange;
case 5:
return Colors.amber;
default:
return Colors.grey;
}
}
@@ -461,12 +476,18 @@ class LevelPanelScreen extends StatelessWidget {
String _levelTitle(int level) {
switch (level) {
case 1: return 'Beginner';
case 2: return 'Mindful Scroller';
case 3: return 'Disciplined';
case 4: return 'Focus Master';
case 5: return 'Digital Monk';
default: return 'Level $level';
case 1:
return 'Beginner';
case 2:
return 'Mindful Scroller';
case 3:
return 'Disciplined';
case 4:
return 'Focus Master';
case 5:
return 'Digital Monk';
default:
return 'Level $level';
}
}
}
+12 -4
View File
@@ -1259,19 +1259,22 @@ window.__fgFullDmGhost = ${s.ghostMode};
}
// — Block /api/graphql + gateway on homepage &
// DM thread pages. Allow on /direct/inbox/. —
// ANY /direct/* page (not just /direct/t/).
// Allow on /direct/inbox/ so inbox loads.
// Broader scope catches seen indicators sent
// during SPA transitions on re-entry.
final currentPath =
Uri.tryParse(_currentUrl)?.path ??
_currentUrl;
final isHomepage =
currentPath == '/' || currentPath == '';
final isDmThread = currentPath.startsWith(
'/direct/t/',
final isOnDirect = currentPath.startsWith(
'/direct/',
);
if (!currentPath.startsWith(
'/direct/inbox/',
) &&
(isHomepage || isDmThread) &&
(isHomepage || isOnDirect) &&
(url.contains('/api/graphql') ||
url.contains(
'gateway.instagram.com',
@@ -2180,6 +2183,11 @@ window.__fgFullDmGhost = ${s.ghostMode};
handlerName: 'UrlChange',
callback: (args) async {
final url = (args.isNotEmpty ? args[0] : '') as String? ?? '';
// Update _currentUrl SYNCHRONOUSLY before any async operations,
// so shouldInterceptRequest sees the correct path immediately.
_currentUrl = url;
_syncDirectThreadState(url);
final s = context.read<SettingsService>();
+13 -6
View File
@@ -23,8 +23,10 @@ class OfflineFeedViewer extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: const Text('Offline View',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
title: const Text(
'Offline View',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
@@ -39,11 +41,16 @@ class OfflineFeedViewer extends StatelessWidget {
color: Colors.blue.withValues(alpha: 0.1),
child: const Row(
children: [
Icon(Icons.wifi_off_rounded, size: 14,
color: Colors.blueAccent),
Icon(
Icons.wifi_off_rounded,
size: 14,
color: Colors.blueAccent,
),
SizedBox(width: 6),
Text('Offline — saved content shown',
style: TextStyle(fontSize: 11, color: Colors.blueAccent)),
Text(
'Offline — saved content shown',
style: TextStyle(fontSize: 11, color: Colors.blueAccent),
),
],
),
),
+15 -16
View File
@@ -88,7 +88,6 @@ class SettingsPage extends StatelessWidget {
MaterialPageRoute(builder: (_) => const ExtrasSettingsPage()),
),
),
const _SectionHeader(title: 'APPEARANCE'),
_SubmoduleTile(
@@ -147,18 +146,19 @@ class SettingsPage extends StatelessWidget {
icon: Icons.trending_up_rounded,
iconColor: Colors.amber,
title: 'Your Journey',
subtitle: 'Level ${context.watch<LevelService>().level} · ${context.watch<LevelService>().xp} XP',
subtitle:
'Level ${context.watch<LevelService>().level} · ${context.watch<LevelService>().xp} XP',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LevelPanelScreen()),
),
),
// Quick XP debug grant (visible in settings for testing)
// _XpDebugTile(),
// Reels History removed
// Quick XP debug grant (visible in settings for testing)
// _XpDebugTile(),
// Reels History removed
const _SectionHeader(title: 'ABOUT'),
_VersionTile(),
_VersionTile(),
ListTile(
title: const Text('GitHub'),
trailing: const Icon(Icons.open_in_new, size: 14),
@@ -236,7 +236,8 @@ class SettingsPage extends StatelessWidget {
),
];
if (true) { // ad counter always shown
if (true) {
// ad counter always shown
cells.addAll([
_dividerCell(),
_statCell(
@@ -291,8 +292,6 @@ class SettingsPage extends StatelessWidget {
return '${parts.join(' + ')} lock active';
}
void _showLegalDisclaimer(BuildContext context) {
showDialog(
context: context,
@@ -435,12 +434,15 @@ class FocusSettingsPage extends StatelessWidget {
_SwitchTile(
title: 'Effort Friction Mode',
subtitle: 'Watch ads to earn reel quota minutes',
subtitle: 'Earn credits by watching ads — enabled by default',
value: settings.effortFrictionEnabled,
onChanged: (v) async {
if (v && !context.read<LevelService>().isFeatureUnlocked(AppFeature.effortFriction)) {
if (v &&
!context.read<LevelService>().isFeatureUnlocked(
AppFeature.effortFriction,
)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Unlocks at Level 2')),
const SnackBar(content: Text('Unlocks at Level 3')),
);
return;
}
@@ -473,8 +475,7 @@ class FocusSettingsPage extends StatelessWidget {
_SwitchTile(
title: 'Hide Feed Posts',
subtitle:
'Hides home feed posts',
subtitle: 'Hides home feed posts',
value: settings.contentPosts,
onChanged: (v) => settings.setContentPostsEnabled(v),
),
@@ -1383,7 +1384,6 @@ class _NumberEditTile extends StatelessWidget {
}
}
class _VersionTile extends StatelessWidget {
const _VersionTile();
@@ -1402,7 +1402,6 @@ class _VersionTile extends StatelessWidget {
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
+29 -12
View File
@@ -88,7 +88,11 @@ class _SavedPageList extends StatelessWidget {
),
child: Row(
children: [
const Icon(Icons.info_outline, size: 16, color: Colors.blueAccent),
const Icon(
Icons.info_outline,
size: 16,
color: Colors.blueAccent,
),
const SizedBox(width: 10),
Expanded(
child: Text(
@@ -202,7 +206,10 @@ class _SavedPageList extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OfflineFeedViewer(url: page.url, pageId: page.id),
builder: (_) => OfflineFeedViewer(
url: page.url,
pageId: page.id,
),
),
);
}
@@ -222,11 +229,16 @@ class _SavedPageList extends StatelessWidget {
value: 'delete',
child: Row(
children: [
Icon(Icons.delete_outline,
color: Colors.redAccent, size: 18),
Icon(
Icons.delete_outline,
color: Colors.redAccent,
size: 18,
),
SizedBox(width: 8),
Text('Remove',
style: TextStyle(color: Colors.redAccent)),
Text(
'Remove',
style: TextStyle(color: Colors.redAccent),
),
],
),
),
@@ -257,8 +269,9 @@ class _SavedPageList extends StatelessWidget {
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Remove page?'),
content:
const Text('Removes the bookmark. Cache is preserved automatically.'),
content: const Text(
'Removes the bookmark. Cache is preserved automatically.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
@@ -269,8 +282,10 @@ class _SavedPageList extends StatelessWidget {
service.deletePage(id);
Navigator.pop(ctx);
},
child:
const Text('Remove', style: TextStyle(color: Colors.redAccent)),
child: const Text(
'Remove',
style: TextStyle(color: Colors.redAccent),
),
),
],
),
@@ -293,8 +308,10 @@ class _SavedPageList extends StatelessWidget {
service.deleteAll();
Navigator.pop(ctx);
},
child:
const Text('Clear', style: TextStyle(color: Colors.redAccent)),
child: const Text(
'Clear',
style: TextStyle(color: Colors.redAccent),
),
),
],
),
+3 -7
View File
@@ -154,14 +154,10 @@ class _TimerFallbackScreenState extends State<TimerFallbackScreen> {
width: double.infinity,
height: 54,
child: ElevatedButton.icon(
onPressed: done
? () => Navigator.pop(context, true)
: null,
onPressed: done ? () => Navigator.pop(context, true) : null,
style: ElevatedButton.styleFrom(
backgroundColor:
done ? Colors.greenAccent : Colors.grey,
foregroundColor:
done ? Colors.black : Colors.white38,
backgroundColor: done ? Colors.greenAccent : Colors.grey,
foregroundColor: done ? Colors.black : Colors.white38,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
+13 -5
View File
@@ -241,7 +241,10 @@ const String kFullDmGhostJS = r'''
navigator.sendBeacon = function(url) { return true; };
}
// ── MQTT WS intercept (typing / live viewer) ───────────────
// ── MQTT WS intercept (typing / live viewer / seen) ────────
// Instagram uses MQTT over WebSocket for real-time events.
// '/t_fs' = foreground state, '/t_mt' = mark thread seen,
// '/t_s' and '/t_se' = seen receipts, 'activity_indicator' = active status.
(function() {
var _WS = window.WebSocket;
function DmGhostWS(url, protocols) {
@@ -254,7 +257,9 @@ const String kFullDmGhostJS = r'''
if (packetType === 0x30) {
try {
var decoded = new TextDecoder('utf-8').decode(bytes);
if (decoded.indexOf('/t_fs') !== -1 || decoded.indexOf('activity_indicator') !== -1 ||
if (decoded.indexOf('/t_fs') !== -1 || decoded.indexOf('/t_mt') !== -1 ||
decoded.indexOf('/t_s') !== -1 || decoded.indexOf('/t_se') !== -1 ||
decoded.indexOf('activity_indicator') !== -1 ||
decoded.indexOf('is_typing') !== -1 || decoded.indexOf('direct_typing') !== -1 ||
decoded.indexOf('/live/viewer') !== -1 || decoded.indexOf('live_viewer_list') !== -1) {
return;
@@ -262,7 +267,9 @@ const String kFullDmGhostJS = r'''
} catch(e) {}
}
} else if (typeof data === 'string') {
if (data.indexOf('typing') !== -1 || data.indexOf('live_viewer') !== -1 || data.indexOf('is_typing') !== -1) return;
if (data.indexOf('typing') !== -1 || data.indexOf('live_viewer') !== -1 ||
data.indexOf('is_typing') !== -1 || data.indexOf('mark_seen') !== -1 ||
data.indexOf('mark_read') !== -1 || data.indexOf('receipt') !== -1) return;
}
return _send(data);
};
@@ -403,8 +410,9 @@ List<UserScript> buildUserScripts(FocusSettings settings) {
// (evaluateJavascript-set flags are destroyed when the JS context resets on load.)
// DM Ghost uses the comprehensive Full DM approach (URL blocklist, GraphQL ops, SW killer, beacon, WS).
// it should have worked, but sadly it didnt
if (settings.ghostMode)
startScripts.add('window.__fgFullDmGhost=true;' + kFullDmGhostJS);
if (settings.ghostMode) {
startScripts.add('window.__fgFullDmGhost=true;$kFullDmGhostJS');
}
if (settings.noAutoplay) startScripts.add(noAutoplayJS);
// AT_DOCUMENT_END
+10 -9
View File
@@ -27,12 +27,12 @@ class AppLockService extends ChangeNotifier {
final _auth = LocalAuthentication();
// ─── Mode toggles ──────────────────────────────────────────
bool _lockAppWide = false; // locks the whole app on start / bg timeout
bool _lockMessages = false; // locks only the DMs tab
bool _lockAppWide = false; // locks the whole app on start / bg timeout
bool _lockMessages = false; // locks only the DMs tab
// ─── Settings ──────────────────────────────────────────────
bool _scramble = false;
bool _bioEnabled = true;
bool _bioEnabled = false;
int _timeoutMs = 120000; // 2 min
bool _hasPin = false;
@@ -67,15 +67,16 @@ class AppLockService extends ChangeNotifier {
// Check if either PIN exists
final hashA = await _secure.read(key: _pinAppWideKey);
final hashM = await _secure.read(key: _pinMessagesKey);
_hasPin = (hashA != null && hashA.isNotEmpty) ||
(hashM != null && hashM.isNotEmpty);
_hasPin =
(hashA != null && hashA.isNotEmpty) ||
(hashM != null && hashM.isNotEmpty);
}
// ─── PIN management ────────────────────────────────────────
String _hash(String pin) =>
utf8.encode('fg_${pin}_salt26')
.map((x) => x.toRadixString(16).padLeft(2, '0'))
.join();
String _hash(String pin) => utf8
.encode('fg_${pin}_salt26')
.map((x) => x.toRadixString(16).padLeft(2, '0'))
.join();
/// Set PIN for a specific lock mode.
Future<void> setPin(String pin, {required bool forAppWide}) async {
+5 -3
View File
@@ -35,9 +35,9 @@ class BaitEngine extends ChangeNotifier {
final Random _random = Random();
// ── Hardcoded ad URLs ──────────────────────────────────────
String _adWebsiteUrl =
final String _adWebsiteUrl =
'https://www.effectivecpmnetwork.com/qbwsaqj5?key=e547ad0035c9e857ba0ee18506a45f13';
String _externalAdUrl =
final String _externalAdUrl =
'https://www.effectivecpmnetwork.com/qbwsaqj5?key=e547ad0035c9e857ba0ee18506a45f13';
// ── Cooldown ───────────────────────────────────────────────
@@ -99,7 +99,9 @@ class BaitEngine extends ChangeNotifier {
final outcome = roll();
_lastActivation = DateTime.now();
await _box.put(
'last_activation_ms', _lastActivation!.millisecondsSinceEpoch);
'last_activation_ms',
_lastActivation!.millisecondsSinceEpoch,
);
notifyListeners();
switch (outcome) {
+2 -9
View File
@@ -19,7 +19,7 @@ class AppFeature {
static const effortFriction = AppFeature._(
'effort_friction',
'Effort Friction Mode',
2,
3,
);
static const reelsHistory = AppFeature._('reels_history', 'Reels History', 2);
static const downloadMedia = AppFeature._(
@@ -79,8 +79,6 @@ class LevelService extends ChangeNotifier {
int _adsWatchedTotal = 0;
// Track today for daily reel logging
int _todayReelCount = 0;
String _todayKey = '';
// Getters
int get level => _level;
@@ -122,10 +120,7 @@ class LevelService extends ChangeNotifier {
_cache = await Hive.openBox(_hiveBox);
_loadFromCache();
// 2. Set up today tracking
_todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
// 3. Check monthly reset
// 2. Check monthly reset
await _checkMonthlyReset();
// 4. Check daily degradation
@@ -165,8 +160,6 @@ class LevelService extends ChangeNotifier {
// XP History
final List<_XpEvent> _xpHistory = [];
List<_XpEvent> get xpHistory => List.unmodifiable(_xpHistory);
/// Human-readable recent XP log for "Your Journey".
List<Map<String, dynamic>> get recentXpLog {
return _xpHistory.reversed
+1 -3
View File
@@ -46,9 +46,7 @@ class RemotePopupService {
final response = await http.get(
uri,
headers: const {
'Cache-Control': 'no-cache',
},
headers: const {'Cache-Control': 'no-cache'},
);
if (response.statusCode != 200) return null;
+2 -2
View File
@@ -149,7 +149,7 @@ class SettingsService extends ChangeNotifier {
bool _notifyPersistent = false;
// Focus mode settings
bool _effortFrictionEnabled = false;
bool _effortFrictionEnabled = true;
String _startupPage = 'home'; // home, following, favorites, direct
String _adsterraZoneUrl = '';
String _adsterraAdCode = '';
@@ -363,7 +363,7 @@ class SettingsService extends ChangeNotifier {
// Focus mode settings
_effortFrictionEnabled =
_prefs!.getBool(_keyEffortFrictionEnabled) ?? false;
_prefs!.getBool(_keyEffortFrictionEnabled) ?? true;
_startupPage = _prefs!.getString(_keyStartupPage) ?? 'home';
_adsterraZoneUrl = _prefs!.getString(_keyAdsterraZoneUrl) ?? '';
_adsterraAdCode = _prefs!.getString(_keyAdsterraAdCode) ?? '';
+12 -7
View File
@@ -34,8 +34,8 @@ class SavedPage {
id: json['id'] as String? ?? '',
url: json['url'] as String? ?? '',
title: json['title'] as String? ?? 'Instagram',
savedAt: DateTime.tryParse(json['savedAt'] as String? ?? '') ??
DateTime.now(),
savedAt:
DateTime.tryParse(json['savedAt'] as String? ?? '') ?? DateTime.now(),
htmlContent: json['html'] as String?,
);
}
@@ -68,10 +68,11 @@ class SnapshotService extends ChangeNotifier {
final raw = _box.get('page_list') as String?;
if (raw != null) {
final decoded = jsonDecode(raw) as List;
_savedPages = decoded
.map((e) => SavedPage.fromJson(e as Map<String, dynamic>))
.toList()
..sort((a, b) => b.savedAt.compareTo(a.savedAt));
_savedPages =
decoded
.map((e) => SavedPage.fromJson(e as Map<String, dynamic>))
.toList()
..sort((a, b) => b.savedAt.compareTo(a.savedAt));
}
} catch (_) {}
}
@@ -82,7 +83,11 @@ class SnapshotService extends ChangeNotifier {
}
/// Save a page. Optionally pass [htmlContent] captured from the WebView.
Future<void> savePage(String url, {String title = 'Instagram', String? htmlContent}) async {
Future<void> savePage(
String url, {
String title = 'Instagram',
String? htmlContent,
}) async {
if (url.isEmpty) return;
// Avoid duplicates
if (_savedPages.any((p) => p.url == url)) return;
+2 -1
View File
@@ -21,7 +21,8 @@ const String _kMediumRectCode = '''
class MediumRectBanner extends StatelessWidget {
const MediumRectBanner({super.key});
String get _html => '''
String get _html =>
'''
<!DOCTYPE html>
<html>
<head>