mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-08-17 11:40:21 +02:00
Feature Pack with bug fixes for V2
This commit is contained in:
@@ -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),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user