mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-07-02 17:35:46 +02:00
Feature Pack with bug fixes for V2
This commit is contained in:
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -339,4 +339,4 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) ?? '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user