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
+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),
),