feat:
Added Extra features like "Ghost Mode" Added Logout Functionality Chagned UA to latest version of iOS. Added app icons. Improved reels player and fixed bugs. Discipline challenge is now compulsory for watching reels. Now the UI doesnt feel like it is in a cheap browser.
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 93 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#000000</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 142 KiB |
@@ -1,9 +1,87 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
class AboutPage extends StatelessWidget {
|
class AboutPage extends StatefulWidget {
|
||||||
const AboutPage({super.key});
|
const AboutPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AboutPage> createState() => _AboutPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AboutPageState extends State<AboutPage> {
|
||||||
|
final String _currentVersion = '0.8.5';
|
||||||
|
bool _isChecking = false;
|
||||||
|
|
||||||
|
Future<void> _checkUpdate() async {
|
||||||
|
setState(() => _isChecking = true);
|
||||||
|
try {
|
||||||
|
final response = await http
|
||||||
|
.get(
|
||||||
|
Uri.parse(
|
||||||
|
'https://api.github.com/repos/Ujwal223/FocusGram/releases/latest',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(seconds: 10));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
final latestVersion = data['tag_name'].toString().replaceAll('v', '');
|
||||||
|
final downloadUrl = data['html_url'];
|
||||||
|
|
||||||
|
if (latestVersion != _currentVersion) {
|
||||||
|
_showUpdateDialog(latestVersion, downloadUrl);
|
||||||
|
} else {
|
||||||
|
_showSnackBar('You are up to date! 🎉');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showSnackBar('Could not check for updates.');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
_showSnackBar('Connectivity issue. Try again later.');
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isChecking = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showUpdateDialog(String version, String url) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: const Color(0xFF1A1A1A),
|
||||||
|
title: const Text(
|
||||||
|
'Update Available!',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
'A new version ($version) is available on GitHub.',
|
||||||
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('Later', style: TextStyle(color: Colors.white38)),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_launchURL(url);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
|
||||||
|
child: const Text('Download'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSnackBar(String msg) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(msg), duration: const Duration(seconds: 2)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -48,9 +126,9 @@ class AboutPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
Text(
|
||||||
'Version 1.1.0',
|
'Version $_currentVersion',
|
||||||
style: TextStyle(color: Colors.white38, fontSize: 13),
|
style: const TextStyle(color: Colors.white38, fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
const Text(
|
const Text(
|
||||||
@@ -67,7 +145,30 @@ class AboutPage extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 40),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _isChecking ? null : _checkUpdate,
|
||||||
|
icon: _isChecking
|
||||||
|
? const SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.update),
|
||||||
|
label: Text(_isChecking ? 'Checking...' : 'Check for Update'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blueAccent.withValues(alpha: 0.2),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(200, 45),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
_launchURL('https://github.com/Ujwal223/FocusGram'),
|
_launchURL('https://github.com/Ujwal223/FocusGram'),
|
||||||
@@ -76,6 +177,7 @@ class AboutPage extends StatelessWidget {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.white10,
|
backgroundColor: Colors.white10,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(200, 45),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@@ -84,7 +186,10 @@ class AboutPage extends StatelessWidget {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
const Text(
|
||||||
'FocusGram is not affiliated with Instagram.',
|
'FocusGram is not affiliated with Instagram.',
|
||||||
style: TextStyle(color: Colors.white12, fontSize: 10),
|
style: TextStyle(
|
||||||
|
color: Color.fromARGB(48, 255, 255, 255),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -98,8 +203,6 @@ class AboutPage extends StatelessWidget {
|
|||||||
if (uri == null) return;
|
if (uri == null) return;
|
||||||
try {
|
try {
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
} catch (_) {
|
} catch (_) {}
|
||||||
// Ignore if cannot launch
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../services/session_manager.dart';
|
|||||||
import '../services/settings_service.dart';
|
import '../services/settings_service.dart';
|
||||||
import '../services/injection_controller.dart';
|
import '../services/injection_controller.dart';
|
||||||
import '../services/navigation_guard.dart';
|
import '../services/navigation_guard.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'session_modal.dart';
|
import 'session_modal.dart';
|
||||||
import 'settings_page.dart';
|
import 'settings_page.dart';
|
||||||
|
|
||||||
@@ -23,14 +24,25 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
late final WebViewController _controller;
|
late final WebViewController _controller;
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
|
||||||
// Cached username for profile navigation
|
|
||||||
String? _cachedUsername;
|
|
||||||
|
|
||||||
// Watchdog for app-session expiry
|
// Watchdog for app-session expiry
|
||||||
Timer? _watchdog;
|
Timer? _watchdog;
|
||||||
bool _extensionDialogShown = false;
|
bool _extensionDialogShown = false;
|
||||||
bool _lastSessionActive = false;
|
bool _lastSessionActive = false;
|
||||||
|
String? _cachedUsername;
|
||||||
|
String _currentUrl = 'https://www.instagram.com/';
|
||||||
|
bool _hasError = false;
|
||||||
|
|
||||||
|
/// Helper to determine if we are on a login/onboarding page.
|
||||||
|
bool get _isOnOnboardingPage {
|
||||||
|
final path = Uri.tryParse(_currentUrl)?.path ?? '';
|
||||||
|
final lowerPath = path.toLowerCase();
|
||||||
|
return lowerPath.contains('/accounts/login') ||
|
||||||
|
lowerPath.contains('/accounts/emailsignup') ||
|
||||||
|
lowerPath.contains('/accounts/signup') ||
|
||||||
|
lowerPath.contains('/legal/') ||
|
||||||
|
lowerPath.contains('/help/') ||
|
||||||
|
_currentUrl.contains('instagram.com/accounts/login');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -39,9 +51,10 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
_initWebView();
|
_initWebView();
|
||||||
_startWatchdog();
|
_startWatchdog();
|
||||||
|
|
||||||
// Listen to session changes to update JS context immediately
|
// Listen to session & settings changes
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<SessionManager>().addListener(_onSessionChanged);
|
context.read<SessionManager>().addListener(_onSessionChanged);
|
||||||
|
context.read<SettingsService>().addListener(_onSettingsChanged);
|
||||||
_lastSessionActive = context.read<SessionManager>().isSessionActive;
|
_lastSessionActive = context.read<SessionManager>().isSessionActive;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -53,6 +66,14 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
_lastSessionActive = sm.isSessionActive;
|
_lastSessionActive = sm.isSessionActive;
|
||||||
_applyInjections();
|
_applyInjections();
|
||||||
}
|
}
|
||||||
|
// Force rebuild for timer updates
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSettingsChanged() {
|
||||||
|
if (!mounted) return;
|
||||||
|
_applyInjections();
|
||||||
|
_controller.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -60,6 +81,7 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_watchdog?.cancel();
|
_watchdog?.cancel();
|
||||||
context.read<SessionManager>().removeListener(_onSessionChanged);
|
context.read<SessionManager>().removeListener(_onSessionChanged);
|
||||||
|
context.read<SettingsService>().removeListener(_onSettingsChanged);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,13 +178,20 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
..setNavigationDelegate(
|
..setNavigationDelegate(
|
||||||
NavigationDelegate(
|
NavigationDelegate(
|
||||||
onPageStarted: (url) {
|
onPageStarted: (url) {
|
||||||
// Only show loading if it's a real page load (not SPA nav)
|
if (mounted) {
|
||||||
if (!url.contains('#')) {
|
setState(() {
|
||||||
if (mounted) setState(() => _isLoading = true);
|
_isLoading = !url.contains('#');
|
||||||
|
_currentUrl = url; // Update immediately to hide/show UI
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPageFinished: (url) {
|
onPageFinished: (url) {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_currentUrl = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
_applyInjections();
|
_applyInjections();
|
||||||
_updateCurrentTab(url);
|
_updateCurrentTab(url);
|
||||||
_cacheUsername();
|
_cacheUsername();
|
||||||
@@ -181,6 +210,16 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
return NavigationDecision.prevent;
|
return NavigationDecision.prevent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Facebook Login Warning
|
||||||
|
if (uri != null &&
|
||||||
|
uri.host.contains('facebook.com') &&
|
||||||
|
_isOnOnboardingPage) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Sorry, Please use Email login')),
|
||||||
|
);
|
||||||
|
return NavigationDecision.prevent;
|
||||||
|
}
|
||||||
|
|
||||||
final decision = NavigationGuard.evaluate(url: request.url);
|
final decision = NavigationGuard.evaluate(url: request.url);
|
||||||
|
|
||||||
if (decision.blocked) {
|
if (decision.blocked) {
|
||||||
@@ -202,24 +241,49 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
..loadRequest(Uri.parse('https://www.instagram.com/'));
|
..loadRequest(Uri.parse('https://www.instagram.com/accounts/login/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applyInjections() {
|
void _applyInjections() {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_isOnOnboardingPage) return; // Restore native login/signup behavior
|
||||||
|
|
||||||
final sessionManager = context.read<SessionManager>();
|
final sessionManager = context.read<SessionManager>();
|
||||||
final settings = context.read<SettingsService>();
|
final settings = context.read<SettingsService>();
|
||||||
final js = InjectionController.buildInjectionJS(
|
final js = InjectionController.buildInjectionJS(
|
||||||
sessionActive: sessionManager.isSessionActive,
|
sessionActive: sessionManager.isSessionActive,
|
||||||
blurExplore: settings.blurExplore,
|
blurExplore: settings.blurExplore,
|
||||||
|
blurReels: settings.blurReels,
|
||||||
|
ghostMode: settings.ghostMode,
|
||||||
|
enableTextSelection: settings.enableTextSelection,
|
||||||
);
|
);
|
||||||
_controller.runJavaScript(js);
|
_controller.runJavaScript(js);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _signOut() async {
|
||||||
|
final manager = WebViewCookieManager();
|
||||||
|
await manager.clearCookies();
|
||||||
|
await _controller.clearCache();
|
||||||
|
// Force immediate state update and navigation
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentIndex = 0;
|
||||||
|
_cachedUsername = null;
|
||||||
|
_isLoading = true; // Show indicator during reload
|
||||||
|
});
|
||||||
|
await _controller.loadRequest(
|
||||||
|
Uri.parse('https://www.instagram.com/accounts/login/'),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('Signed out successfully')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _cacheUsername() async {
|
Future<void> _cacheUsername() async {
|
||||||
if (_cachedUsername != null) return; // Already known
|
|
||||||
try {
|
try {
|
||||||
final result = await _controller.runJavaScriptReturningResult(
|
final result = await _controller.runJavaScriptReturningResult(
|
||||||
InjectionController.getLoggedInUsernameJS,
|
"document.querySelector('header h2')?.innerText || ''",
|
||||||
);
|
);
|
||||||
final raw = result.toString().replaceAll('"', '').replaceAll("'", '');
|
final raw = result.toString().replaceAll('"', '').replaceAll("'", '');
|
||||||
if (raw.isNotEmpty && raw != 'null' && raw != 'undefined') {
|
if (raw.isNotEmpty && raw != 'null' && raw != 'undefined') {
|
||||||
@@ -268,8 +332,10 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onTabTapped(int index) async {
|
Future<void> _onTabTapped(int index) async {
|
||||||
// Don't re-navigate if already on this tab
|
if (index == _currentIndex) {
|
||||||
if (index == _currentIndex) return;
|
await _controller.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() => _currentIndex = index);
|
setState(() => _currentIndex = index);
|
||||||
|
|
||||||
switch (index) {
|
switch (index) {
|
||||||
@@ -284,9 +350,8 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
case 2:
|
case 2:
|
||||||
if (context.read<SessionManager>().isSessionActive) {
|
if (context.read<SessionManager>().isSessionActive) {
|
||||||
await _navigateTo('/reels/');
|
await _navigateTo('/reels/');
|
||||||
} else {
|
|
||||||
_openSessionModal();
|
|
||||||
}
|
}
|
||||||
|
// If not active, do nothing (disabled as requested)
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
await _navigateTo('/direct/inbox/');
|
await _navigateTo('/direct/inbox/');
|
||||||
@@ -326,13 +391,28 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// ── WebView: full screen (behind everything) ────────────────
|
// ── Main Content Layout ────────────────────────────────────
|
||||||
Positioned.fill(child: WebViewWidget(controller: _controller)),
|
SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (!_isOnOnboardingPage) _BrandedTopBar(),
|
||||||
|
Expanded(child: WebViewWidget(controller: _controller)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// ── Thin loading indicator at very top ──────────────────────
|
if (_hasError)
|
||||||
|
_NoInternetScreen(
|
||||||
|
onRetry: () {
|
||||||
|
setState(() => _hasError = false);
|
||||||
|
_controller.reload();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Thin loading indicator (Placed below Top Bar) ──────────
|
||||||
if (_isLoading)
|
if (_isLoading)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 60 + MediaQuery.of(context).padding.top,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: const LinearProgressIndicator(
|
child: const LinearProgressIndicator(
|
||||||
@@ -342,36 +422,26 @@ class _MainWebViewPageState extends State<MainWebViewPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ── The Edge Panel (replaced _StatusBar) ────────────────────
|
// ── The Edge Panel ──────────────────────────────────────────
|
||||||
// No Positioned wrapper here: _EdgePanel returns its own Positioned siblings
|
|
||||||
_EdgePanel(controller: _controller),
|
_EdgePanel(controller: _controller),
|
||||||
|
|
||||||
// ── Our bottom bar overlaid on top of Instagram's bar ───────
|
// ── Our bottom bar ──────────────────────────────────────────
|
||||||
// Making it taller than Instagram's native bar (~50dp) means
|
if (!_isOnOnboardingPage)
|
||||||
// theirs is fully hidden behind ours — no CSS needed as fallback.
|
Positioned(
|
||||||
Positioned(
|
bottom: 0,
|
||||||
bottom: 0,
|
left: 0,
|
||||||
left: 0,
|
right: 0,
|
||||||
right: 0,
|
child: _FocusGramNavBar(
|
||||||
child: _FocusGramNavBar(
|
currentIndex: _currentIndex,
|
||||||
currentIndex: _currentIndex,
|
onTap: _onTabTapped,
|
||||||
onTap: _onTabTapped,
|
height: barHeight * 0.99, // 1% reduction
|
||||||
height: barHeight,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openSessionModal() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (_) => const SessionModal(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -424,7 +494,6 @@ class _EdgePanelState extends State<_EdgePanel> {
|
|||||||
// Hits will pass through the Stack to the WebView except on our children.
|
// Hits will pass through the Stack to the WebView except on our children.
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// ── Tap-to-close Backdrop (only when expanded) ──
|
|
||||||
if (_isExpanded)
|
if (_isExpanded)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -584,7 +653,13 @@ class _EdgePanelState extends State<_EdgePanel> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_formatTime(sm.remainingSessionSeconds),
|
context.read<SessionManager>().isSessionActive
|
||||||
|
? _formatTime(
|
||||||
|
context
|
||||||
|
.read<SessionManager>()
|
||||||
|
.remainingSessionSeconds,
|
||||||
|
)
|
||||||
|
: 'Off',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: barColor,
|
color: barColor,
|
||||||
fontSize: 40,
|
fontSize: 40,
|
||||||
@@ -592,67 +667,58 @@ class _EdgePanelState extends State<_EdgePanel> {
|
|||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 20),
|
||||||
// Daily Remaining
|
_buildStatRow(
|
||||||
const Text(
|
'REEL QUOTA',
|
||||||
'DAILY QUOTA',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white30,
|
|
||||||
fontSize: 11,
|
|
||||||
letterSpacing: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'${sm.dailyRemainingSeconds ~/ 60}m Left',
|
'${sm.dailyRemainingSeconds ~/ 60}m Left',
|
||||||
style: const TextStyle(
|
Icons.timer_outlined,
|
||||||
color: Colors.white70,
|
),
|
||||||
fontSize: 18,
|
_buildStatRow(
|
||||||
fontWeight: FontWeight.w500,
|
'AUTO-CLOSE',
|
||||||
),
|
_formatTime(sm.appSessionRemainingSeconds),
|
||||||
|
Icons.hourglass_empty_rounded,
|
||||||
|
),
|
||||||
|
_buildStatRow(
|
||||||
|
'COOLDOWN',
|
||||||
|
sm.isCooldownActive
|
||||||
|
? _formatTime(sm.cooldownRemainingSeconds)
|
||||||
|
: 'Off',
|
||||||
|
Icons.coffee_rounded,
|
||||||
|
isWarning: sm.isCooldownActive,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
const Divider(color: Colors.white10, height: 1),
|
if (!context
|
||||||
const SizedBox(height: 20),
|
.findAncestorStateOfType<_MainWebViewPageState>()!
|
||||||
// Settings Link
|
._isOnOnboardingPage) ...[
|
||||||
InkWell(
|
const Divider(color: Colors.white10),
|
||||||
onTap: () {
|
const SizedBox(height: 8),
|
||||||
_toggleExpansion();
|
ListTile(
|
||||||
Navigator.push(
|
onTap: () async {
|
||||||
context,
|
_toggleExpansion();
|
||||||
MaterialPageRoute(builder: (_) => const SettingsPage()),
|
final state = context
|
||||||
);
|
.findAncestorStateOfType<_MainWebViewPageState>();
|
||||||
},
|
if (state != null) {
|
||||||
borderRadius: BorderRadius.circular(12),
|
await state._signOut();
|
||||||
child: Padding(
|
}
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
},
|
||||||
child: Row(
|
leading: const Icon(
|
||||||
children: [
|
Icons.logout_rounded,
|
||||||
Container(
|
color: Colors.redAccent,
|
||||||
padding: const EdgeInsets.all(6),
|
size: 20,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.tune_rounded,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 14),
|
|
||||||
const Text(
|
|
||||||
'Preferences',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
title: const Text(
|
||||||
|
'Switch Account',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -662,6 +728,60 @@ class _EdgePanelState extends State<_EdgePanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildStatRow(
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
IconData icon, {
|
||||||
|
bool isWarning = false,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isWarning
|
||||||
|
? Colors.redAccent.withValues(alpha: 0.1)
|
||||||
|
: Colors.white.withValues(alpha: 0.05),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: isWarning ? Colors.redAccent : Colors.white70,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isWarning ? Colors.redAccent : Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String _formatTime(int seconds) {
|
String _formatTime(int seconds) {
|
||||||
final m = seconds ~/ 60;
|
final m = seconds ~/ 60;
|
||||||
final s = seconds % 60;
|
final s = seconds % 60;
|
||||||
@@ -670,9 +790,35 @@ class _EdgePanelState extends State<_EdgePanel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
// Custom Bottom Nav Bar — minimal, Instagram-like
|
// Branded Top Bar — minimal, Instagram-like font
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _BrandedTopBar extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 60,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
border: Border(bottom: BorderSide(color: Colors.white12, width: 0.5)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'FocusGram',
|
||||||
|
style: GoogleFonts.grandHotel(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 32,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _FocusGramNavBar extends StatelessWidget {
|
class _FocusGramNavBar extends StatelessWidget {
|
||||||
final int currentIndex;
|
final int currentIndex;
|
||||||
final Future<void> Function(int) onTap;
|
final Future<void> Function(int) onTap;
|
||||||
@@ -711,6 +857,7 @@ class _FocusGramNavBar extends StatelessWidget {
|
|||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 60,
|
width: 60,
|
||||||
|
height: double.infinity,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isSelected ? filledIcon : outlinedIcon,
|
isSelected ? filledIcon : outlinedIcon,
|
||||||
@@ -727,3 +874,54 @@ class _FocusGramNavBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// No Internet Screen — minimal, branded
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _NoInternetScreen extends StatelessWidget {
|
||||||
|
final VoidCallback onRetry;
|
||||||
|
const _NoInternetScreen({required this.onRetry});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.black,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.wifi_off_rounded, color: Colors.white24, size: 80),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'No Connection',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Please check your internet settings.',
|
||||||
|
style: TextStyle(color: Colors.white38, fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: onRetry,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class _ReelPlayerOverlayState extends State<ReelPlayerOverlay> {
|
|||||||
InjectionController.buildInjectionJS(
|
InjectionController.buildInjectionJS(
|
||||||
sessionActive: true,
|
sessionActive: true,
|
||||||
blurExplore: false,
|
blurExplore: false,
|
||||||
|
blurReels: false,
|
||||||
|
ghostMode: false,
|
||||||
|
enableTextSelection: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ class SettingsPage extends StatelessWidget {
|
|||||||
icon: Icons.visibility_off_outlined,
|
icon: Icons.visibility_off_outlined,
|
||||||
destination: const _DistractionSettingsPage(),
|
destination: const _DistractionSettingsPage(),
|
||||||
),
|
),
|
||||||
|
_buildSettingsTile(
|
||||||
|
context: context,
|
||||||
|
title: 'Extras',
|
||||||
|
subtitle: 'Ghost mode, text selection and experimental features',
|
||||||
|
icon: Icons.extension_outlined,
|
||||||
|
destination: const _ExtrasSettingsPage(),
|
||||||
|
),
|
||||||
_buildSettingsTile(
|
_buildSettingsTile(
|
||||||
context: context,
|
context: context,
|
||||||
title: 'About',
|
title: 'About',
|
||||||
@@ -365,3 +372,53 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ExtrasSettingsPage extends StatelessWidget {
|
||||||
|
const _ExtrasSettingsPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final settings = context.watch<SettingsService>();
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
title: const Text('Extras', style: TextStyle(fontSize: 17)),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Ghost Mode',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Hides "typing..." and "seen" status in DMs',
|
||||||
|
style: TextStyle(color: Colors.white54, fontSize: 13),
|
||||||
|
),
|
||||||
|
value: settings.ghostMode,
|
||||||
|
onChanged: (v) => settings.setGhostMode(v),
|
||||||
|
activeThumbColor: Colors.blue,
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Enable Text Selection',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Allows copying text from posts and captions',
|
||||||
|
style: TextStyle(color: Colors.white54, fontSize: 13),
|
||||||
|
),
|
||||||
|
value: settings.enableTextSelection,
|
||||||
|
onChanged: (v) => settings.setEnableTextSelection(v),
|
||||||
|
activeThumbColor: Colors.blue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,70 +1,116 @@
|
|||||||
/// Generates all CSS and JavaScript injection strings for the WebView.
|
/// Controller for injecting custom JS and CSS into the WebView.
|
||||||
///
|
/// Uses a combination of static strings and dynamic builders to:
|
||||||
/// Strategy:
|
/// - Hide native navigation elements.
|
||||||
/// - Instagram's own bottom nav bar is hidden via both CSS and a periodic JS
|
/// - Inject FocusGram branding into the native header.
|
||||||
/// removal loop, since SPA re-renders can outpace MutationObserver.
|
/// - Implement "Ghost Mode" (stealth features).
|
||||||
/// - Reel elements are hidden/blurred based on settings/session state.
|
/// - Manage Reels/Explore distractions.
|
||||||
/// - A MutationObserver keeps re-applying the rules after SPA re-renders.
|
|
||||||
/// - App-install banners are auto-dismissed.
|
|
||||||
class InjectionController {
|
class InjectionController {
|
||||||
/// iOS Safari user-agent — reduces login friction with Instagram.
|
/// The requested iOS 18.6 User Agent for Instagram App feel.
|
||||||
static const String iOSUserAgent =
|
static const String iOSUserAgent =
|
||||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) '
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/22G86 [FBAN/FBIOS;FBAV/531.0.0.35.77;FBBV/792629356;FBDV/iPhone17,2;FBMD/iPhone;FBSN/iOS;FBSV/18.6;FBSS/3;FBID/phone;FBLC/en_US;FBOP/5;FBRV/0;IABMV/1]';
|
||||||
'AppleWebKit/605.1.15 (KHTML, like Gecko) '
|
|
||||||
'Version/17.0 Mobile/15E148 Safari/604.1';
|
|
||||||
|
|
||||||
// ── CSS injection ───────────────────────────────────────────────────────────
|
// ── CSS & JS injection ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// CSS to fix UI nuances like tap highlights.
|
||||||
|
static const String _globalUIFixesCSS = '''
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
/// CSS to disable text selection globally.
|
||||||
|
static const String _disableSelectionCSS = '''
|
||||||
|
* {
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
/// Ghost Mode JS: Intercepts network calls to block seen/typing receipts.
|
||||||
|
static const String _ghostModeJS = '''
|
||||||
|
(function() {
|
||||||
|
const blockedUrls = [
|
||||||
|
'/api/v1/direct_v2/set_reel_seen/',
|
||||||
|
'/api/v1/direct_v2/threads/set_typing_status/',
|
||||||
|
'/api/v1/stories/reel/seen/',
|
||||||
|
'/api/v1/direct_v2/mark_visual_item_seen/'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Proxy fetch
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
window.fetch = function(url, options) {
|
||||||
|
if (typeof url === 'string') {
|
||||||
|
if (blockedUrls.some(u => url.includes(u))) {
|
||||||
|
return Promise.resolve(new Response(null, { status: 204 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalFetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Proxy XHR
|
||||||
|
const originalOpen = XMLHttpRequest.prototype.open;
|
||||||
|
XMLHttpRequest.prototype.open = function(method, url) {
|
||||||
|
this._blocked = blockedUrls.some(u => url.includes(u));
|
||||||
|
return originalOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
const originalSend = XMLHttpRequest.prototype.send;
|
||||||
|
XMLHttpRequest.prototype.send = function() {
|
||||||
|
if (this._blocked) return;
|
||||||
|
return originalSend.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
''';
|
||||||
|
|
||||||
|
/// Branding JS: Replaces Instagram logo with FocusGram while keeping icons.
|
||||||
|
static const String _brandingJS = '''
|
||||||
|
(function() {
|
||||||
|
function applyBranding() {
|
||||||
|
const igLogo = document.querySelector('svg[aria-label="Instagram"], svg[aria-label="Direct"]');
|
||||||
|
if (igLogo && !igLogo.dataset.focusgrammed) {
|
||||||
|
const container = igLogo.parentElement;
|
||||||
|
if (container) {
|
||||||
|
igLogo.style.display = 'none';
|
||||||
|
igLogo.dataset.focusgrammed = 'true';
|
||||||
|
|
||||||
|
const brandText = document.createElement('span');
|
||||||
|
brandText.innerText = 'FocusGram';
|
||||||
|
brandText.style.fontFamily = '"Grand Hotel", cursive';
|
||||||
|
brandText.style.fontSize = '24px';
|
||||||
|
brandText.style.color = 'white';
|
||||||
|
brandText.style.marginLeft = '8px';
|
||||||
|
brandText.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
|
container.appendChild(brandText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyBranding();
|
||||||
|
const observer = new MutationObserver(applyBranding);
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
|
''';
|
||||||
|
|
||||||
/// Robust CSS that hides Instagram's native bottom nav bar.
|
/// Robust CSS that hides Instagram's native bottom nav bar.
|
||||||
/// Covers all known selector patterns including dynamic class names.
|
|
||||||
static const String _hideInstagramNavCSS = '''
|
static const String _hideInstagramNavCSS = '''
|
||||||
/* ── Instagram bottom navigation bar — hide completely ── */
|
div[role="tablist"], nav[role="navigation"],
|
||||||
/* Role-based selectors */
|
._acbl, ._aa4b, ._aahi, ._ab8s, section nav, footer nav {
|
||||||
div[role="tablist"],
|
|
||||||
nav[role="navigation"],
|
|
||||||
/* Fixed-position bottom bar */
|
|
||||||
div[style*="position: fixed"][style*="bottom"],
|
|
||||||
div[style*="position:fixed"][style*="bottom"],
|
|
||||||
/* Instagram legacy class names */
|
|
||||||
._acbl, ._aa4b, ._aahi, ._ab8s,
|
|
||||||
/* Section nav elements */
|
|
||||||
section nav,
|
|
||||||
/* Any nav inside the main app shell */
|
|
||||||
#react-root nav,
|
|
||||||
/* The outer wrapper of the bottom bar (PWA/mobile web) */
|
|
||||||
[class*="x1n2onr6"][class*="x1vjfegm"] > nav,
|
|
||||||
/* Catch-all: any fixed bottom element containing nav links */
|
|
||||||
footer nav,
|
|
||||||
div[class*="bottom"] nav {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
/* Ensure the body doesn't add bottom padding for the nav */
|
|
||||||
body, #react-root, main {
|
body, #react-root, main {
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// CSS to hide Reel-related elements everywhere (feed, profile, search).
|
/// CSS to hide Reel-related elements everywhere.
|
||||||
/// Used when session is NOT active.
|
|
||||||
static const String _hideReelsCSS = '''
|
static const String _hideReelsCSS = '''
|
||||||
/* Hide reel thumbnails and links */
|
a[href*="/reel/"], a[href*="/reels"], [aria-label*="Reel"], [aria-label*="Reels"],
|
||||||
a[href*="/reel/"],
|
div[data-media-type="2"], [aria-label="Reels"], svg[aria-label="Reels"] {
|
||||||
a[href*="/reels"],
|
|
||||||
[aria-label*="Reel"],
|
|
||||||
[aria-label*="Reels"],
|
|
||||||
div[data-media-type="2"],
|
|
||||||
/* Profile grid reel filter tabs */
|
|
||||||
[aria-label="Reels"],
|
|
||||||
/* Reel indicators on feed thumbnails */
|
|
||||||
svg[aria-label="Reels"],
|
|
||||||
/* Video/reel chips in feed */
|
|
||||||
[class*="reel"],
|
|
||||||
[class*="Reel"] {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
@@ -72,95 +118,49 @@ class InjectionController {
|
|||||||
''';
|
''';
|
||||||
|
|
||||||
/// CSS that adds bottom padding so feed content doesn't hide behind our bar.
|
/// CSS that adds bottom padding so feed content doesn't hide behind our bar.
|
||||||
/// Added more selectors to cover dynamic drawers like Notes and Reactions.
|
|
||||||
static const String _bottomPaddingCSS = '''
|
static const String _bottomPaddingCSS = '''
|
||||||
body, #react-root > div, [role="presentation"] > div {
|
body, #react-root > div, [role="presentation"] > div {
|
||||||
padding-bottom: 72px !important;
|
padding-bottom: 72px !important;
|
||||||
}
|
}
|
||||||
/* Special handling for dynamic bottom drawers */
|
div[style*="bottom: 0px"], div[style*="bottom: 0"], form[method="POST"] {
|
||||||
div[style*="bottom: 0px"], div[style*="bottom: 0"] {
|
|
||||||
padding-bottom: 72px !important;
|
padding-bottom: 72px !important;
|
||||||
}
|
}
|
||||||
''';
|
div[role="main"] div[style*="position: fixed"] {
|
||||||
|
bottom: 72px !important;
|
||||||
/// CSS to push IG content down so it doesn't hide behind our status bar.
|
|
||||||
static const String _topPaddingCSS = '''
|
|
||||||
header, #react-root > div > div > div:first-child {
|
|
||||||
margin-top: 44px !important;
|
|
||||||
}
|
|
||||||
/* Shift fixed headers down */
|
|
||||||
div[style*="position: fixed"][style*="top: 0"] {
|
|
||||||
top: 44px !important;
|
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// CSS to blur Explore feed posts/reels (keeps stories visible).
|
/// CSS to blur Explore feed posts/reels.
|
||||||
static const String _blurExploreCSS = '''
|
static const String _blurExploreCSS = '''
|
||||||
/* Blur Explore grid posts and reel cards (not stories row) */
|
|
||||||
main[role="main"] section > div > div:not(:first-child) a img,
|
main[role="main"] section > div > div:not(:first-child) a img,
|
||||||
main[role="main"] section > div > div:not(:first-child) video,
|
main[role="main"] section > div > div:not(:first-child) video,
|
||||||
main[role="main"] section > div > div:not(:first-child) [class*="x6s0dn4"],
|
main[role="main"] article img, main[role="main"] article video,
|
||||||
main[role="main"] article img,
|
._aagv img, ._aagv video {
|
||||||
main[role="main"] article video,
|
|
||||||
/* Explore page grid */
|
|
||||||
._aagv img,
|
|
||||||
._aagv video {
|
|
||||||
filter: blur(12px) !important;
|
filter: blur(12px) !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
/* Overlay to block tapping blurred content */
|
''';
|
||||||
._aagv::after {
|
|
||||||
content: "";
|
static const String _blurReelsCSS = '''
|
||||||
position: absolute;
|
a[href*="/reel/"] img, a[href*="/reels"] img {
|
||||||
inset: 0;
|
filter: blur(12px) !important;
|
||||||
z-index: 99;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
._aagv {
|
|
||||||
position: relative !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// Auto-dismiss "Open in App" banner that Instagram shows in mobile browsers.
|
/// Auto-dismiss "Open in App" banner.
|
||||||
static const String _dismissAppBannerJS = '''
|
static const String _dismissAppBannerJS = '''
|
||||||
(function dismissBanners() {
|
(function dismissBanners() {
|
||||||
const selectors = [
|
const selectors = ['[id*="app-banner"]', '[class*="app-banner"]', 'div[role="dialog"][aria-label*="app"]'];
|
||||||
'[id*="app-banner"]',
|
selectors.forEach(sel => document.querySelectorAll(sel).forEach(el => el.remove()));
|
||||||
'[class*="app-banner"]',
|
|
||||||
'[data-testid*="app-banner"]',
|
|
||||||
'div[role="dialog"][aria-label*="app"]',
|
|
||||||
'div[role="dialog"][aria-label*="App"]',
|
|
||||||
];
|
|
||||||
selectors.forEach(sel => {
|
|
||||||
document.querySelectorAll(sel).forEach(el => el.remove());
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// Periodic remover: every 500ms force-removes the bottom nav.
|
/// Periodic remover for bottom nav.
|
||||||
/// Complements the MutationObserver for sites that rebuild DOM faster.
|
|
||||||
static const String _periodicNavRemoverJS = '''
|
static const String _periodicNavRemoverJS = '''
|
||||||
(function periodicNavRemove() {
|
(function periodicNavRemove() {
|
||||||
function removeNav() {
|
function removeNav() {
|
||||||
// Target all fixed-bottom elements that could be the nav bar
|
document.querySelectorAll('div[role="tablist"], nav[role="navigation"], footer nav').forEach(el => {
|
||||||
document.querySelectorAll([
|
el.style.cssText += ';display:none!important;height:0!important;';
|
||||||
'div[role="tablist"]',
|
|
||||||
'nav[role="navigation"]',
|
|
||||||
'._acbl', '._aa4b', '._aahi', '._ab8s',
|
|
||||||
'section nav',
|
|
||||||
'footer nav'
|
|
||||||
].join(',')).forEach(function(el) {
|
|
||||||
el.style.cssText += ';display:none!important;height:0!important;overflow:hidden!important;';
|
|
||||||
});
|
|
||||||
// Also hide any element that is fixed at the bottom and contains nav links
|
|
||||||
document.querySelectorAll('div[style]').forEach(function(el) {
|
|
||||||
const s = el.style;
|
|
||||||
if ((s.position === 'fixed' || s.position === 'sticky') &&
|
|
||||||
(s.bottom === '0px' || s.bottom === '0') &&
|
|
||||||
el.querySelector('a,button')) {
|
|
||||||
el.style.cssText += ';display:none!important;';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
removeNav();
|
removeNav();
|
||||||
@@ -168,12 +168,11 @@ class InjectionController {
|
|||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// MutationObserver that continuously re-applies CSS after SPA re-renders.
|
/// MutationObserver that continuously re-applies CSS.
|
||||||
static String _buildMutationObserver(String cssContent) =>
|
static String _buildMutationObserver(String cssContent) =>
|
||||||
'''
|
'''
|
||||||
(function applyFocusGramStyles() {
|
(function applyFocusGramStyles() {
|
||||||
const STYLE_ID = 'focusgram-injected-style';
|
const STYLE_ID = 'focusgram-injected-style';
|
||||||
|
|
||||||
function injectCSS() {
|
function injectCSS() {
|
||||||
let el = document.getElementById(STYLE_ID);
|
let el = document.getElementById(STYLE_ID);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -183,175 +182,88 @@ class InjectionController {
|
|||||||
}
|
}
|
||||||
el.textContent = ${_escapeJsString(cssContent)};
|
el.textContent = ${_escapeJsString(cssContent)};
|
||||||
}
|
}
|
||||||
|
|
||||||
injectCSS();
|
injectCSS();
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
const observer = new MutationObserver(function() {
|
if (!document.getElementById(STYLE_ID)) injectCSS();
|
||||||
if (!document.getElementById(STYLE_ID)) {
|
|
||||||
injectCSS();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
});
|
||||||
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
static String _escapeJsString(String s) {
|
static String _escapeJsString(String s) {
|
||||||
// Wrap in JS template literal backticks; escape any internal backticks.
|
|
||||||
final escaped = s.replaceAll(r'\', r'\\').replaceAll('`', r'\`');
|
final escaped = s.replaceAll(r'\', r'\\').replaceAll('`', r'\`');
|
||||||
return '`$escaped`';
|
return '`$escaped`';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Navigation helpers ──────────────────────────────────────────────────────
|
// ── Navigation helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// JS that soft-navigates Instagram's SPA without a full page reload.
|
|
||||||
/// [path] should start with / e.g. '/direct/inbox/'.
|
|
||||||
static String softNavigateJS(String path) =>
|
static String softNavigateJS(String path) =>
|
||||||
'''
|
'''
|
||||||
(function() {
|
(function() {
|
||||||
const target = ${_escapeJsString(path)};
|
const target = ${_escapeJsString(path)};
|
||||||
// Try React Router / Instagram SPA navigation first (pushState trick)
|
|
||||||
if (window.location.pathname !== target) {
|
if (window.location.pathname !== target) {
|
||||||
window.location.href = target;
|
window.location.href = target;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// JS to click Instagram's native "create post" button.
|
|
||||||
static const String clickCreateButtonJS = '''
|
static const String clickCreateButtonJS = '''
|
||||||
(function() {
|
(function() {
|
||||||
const btn = document.querySelector(
|
const btn = document.querySelector('[aria-label="New post"], [aria-label="Create"]');
|
||||||
'[aria-label="New post"], [aria-label="Create"], svg[aria-label="New post"]'
|
if (btn) btn.closest('a, button') ? btn.closest('a, button').click() : btn.click();
|
||||||
);
|
|
||||||
if (btn) {
|
|
||||||
btn.closest('a, button') ? btn.closest('a, button').click() : btn.click();
|
|
||||||
} else {
|
|
||||||
// Fallback: navigate to home first, create will open as modal
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// JS to get the currently logged-in user's username.
|
/// MutationObserver for Reel scroll locking.
|
||||||
static const String getLoggedInUsernameJS = '''
|
|
||||||
(function() {
|
|
||||||
try {
|
|
||||||
// Try shared data approach
|
|
||||||
const scripts = Array.from(document.querySelectorAll('script[type="application/json"]'));
|
|
||||||
for (const s of scripts) {
|
|
||||||
try {
|
|
||||||
const d = JSON.parse(s.textContent);
|
|
||||||
if (d && d.config && d.config.viewer && d.config.viewer.username) {
|
|
||||||
return d.config.viewer.username;
|
|
||||||
}
|
|
||||||
} catch(e){}
|
|
||||||
}
|
|
||||||
// Try window additionalDataLoaded
|
|
||||||
if (window.__additionalDataLoaded) {
|
|
||||||
const keys = Object.keys(window.__additionalDataLoaded || {});
|
|
||||||
for (const k of keys) {
|
|
||||||
const v = window.__additionalDataLoaded[k];
|
|
||||||
if (v && v.data && v.data.user && v.data.user.username) {
|
|
||||||
return v.data.user.username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: try profile anchor in nav
|
|
||||||
const profileLink = document.querySelector('a[href][aria-label*="rofile"]');
|
|
||||||
if (profileLink) {
|
|
||||||
const href = profileLink.getAttribute('href');
|
|
||||||
if (href) {
|
|
||||||
const parts = href.replace(/^[/]/, "").split("/");
|
|
||||||
if (parts[0] && parts[0].length > 0) return parts[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch(e) { return null; }
|
|
||||||
})();
|
|
||||||
''';
|
|
||||||
|
|
||||||
/// MutationObserver to watch for Reel players and lock their scrolling.
|
|
||||||
static const String reelsMutationObserverJS = '''
|
static const String reelsMutationObserverJS = '''
|
||||||
(function() {
|
(function() {
|
||||||
function lockReelScroll(reelContainer) {
|
function lockReelScroll(reelContainer) {
|
||||||
if (reelContainer.dataset.scrollLocked) return;
|
if (reelContainer.dataset.scrollLocked) return;
|
||||||
|
|
||||||
// If session is active, don't lock
|
|
||||||
if (window.__focusgramSessionActive === true) return;
|
|
||||||
|
|
||||||
reelContainer.dataset.scrollLocked = 'true';
|
reelContainer.dataset.scrollLocked = 'true';
|
||||||
|
|
||||||
let startY = 0;
|
let startY = 0;
|
||||||
|
reelContainer.addEventListener('touchstart', (e) => startY = e.touches[0].clientY, { passive: true });
|
||||||
reelContainer.addEventListener('touchstart', (e) => {
|
|
||||||
startY = e.touches[0].clientY;
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
reelContainer.addEventListener('touchmove', (e) => {
|
reelContainer.addEventListener('touchmove', (e) => {
|
||||||
|
if (window.__focusgramSessionActive === true) return;
|
||||||
const deltaY = e.touches[0].clientY - startY;
|
const deltaY = e.touches[0].clientY - startY;
|
||||||
// Block upward swipe (next reel), allow downward (go back)
|
if (deltaY < -10 && e.cancelable) {
|
||||||
if (deltaY < -10) {
|
e.preventDefault();
|
||||||
if (e.cancelable) {
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for reel player being injected into DOM
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
// Instagram's reel player containers — multiple selectors for resilience
|
document.querySelectorAll('[class*="ReelsVideoPlayer"], video').forEach((el) => {
|
||||||
const reelContainers = document.querySelectorAll(
|
if (el.tagName === 'VIDEO' && el.closest('article')) return;
|
||||||
'[class*="reel"], [class*="Reel"], video'
|
|
||||||
);
|
|
||||||
reelContainers.forEach((el) => {
|
|
||||||
// If it's a video or a reel container, wrap it
|
|
||||||
lockReelScroll(el);
|
lockReelScroll(el);
|
||||||
// Also try parent if it's a video
|
if (el.tagName === 'VIDEO' && el.parentElement) lockReelScroll(el.parentElement);
|
||||||
if (el.tagName === 'VIDEO' && el.parentElement) {
|
|
||||||
lockReelScroll(el.parentElement);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
})();
|
})();
|
||||||
''';
|
''';
|
||||||
|
|
||||||
/// JS to disable swipe-to-next behavior inside the isolated Reel player.
|
|
||||||
static const String disableReelSwipeJS = '''
|
|
||||||
(function disableSwipeNavigation() {
|
|
||||||
if (window.__focusgramSessionActive === true) return;
|
|
||||||
let startX = 0;
|
|
||||||
document.addEventListener('touchstart', e => { startX = e.touches[0].clientX; }, {passive: true});
|
|
||||||
document.addEventListener('touchmove', e => {
|
|
||||||
const dx = Math.abs(e.touches[0].clientX - startX);
|
|
||||||
if (dx > 30) e.preventDefault();
|
|
||||||
}, {passive: false});
|
|
||||||
})();
|
|
||||||
''';
|
|
||||||
|
|
||||||
/// JS to update the session state variable in the page context.
|
|
||||||
static String buildSessionStateJS(bool active) =>
|
static String buildSessionStateJS(bool active) =>
|
||||||
'window.__focusgramSessionActive = $active;';
|
'window.__focusgramSessionActive = $active;';
|
||||||
|
|
||||||
// ── Public API ──────────────────────────────────────────────────────────────
|
/// Full injection JS to run on page load.
|
||||||
|
|
||||||
/// Full injection JS to run on every page load.
|
|
||||||
static String buildInjectionJS({
|
static String buildInjectionJS({
|
||||||
required bool sessionActive,
|
required bool sessionActive,
|
||||||
required bool blurExplore,
|
required bool blurExplore,
|
||||||
|
required bool blurReels,
|
||||||
|
required bool ghostMode,
|
||||||
|
required bool enableTextSelection,
|
||||||
}) {
|
}) {
|
||||||
final StringBuffer css = StringBuffer();
|
final StringBuffer css = StringBuffer();
|
||||||
|
css.writeln(_globalUIFixesCSS);
|
||||||
|
if (!enableTextSelection) css.writeln(_disableSelectionCSS);
|
||||||
css.write(_hideInstagramNavCSS);
|
css.write(_hideInstagramNavCSS);
|
||||||
css.write(_bottomPaddingCSS);
|
css.write(_bottomPaddingCSS);
|
||||||
css.write(_topPaddingCSS);
|
|
||||||
if (!sessionActive) css.write(_hideReelsCSS);
|
if (!sessionActive) {
|
||||||
if (blurExplore) css.write(_blurExploreCSS);
|
css.write(_hideReelsCSS);
|
||||||
|
if (blurExplore) css.write(_blurExploreCSS);
|
||||||
|
if (blurReels) css.write(_blurReelsCSS);
|
||||||
|
}
|
||||||
|
|
||||||
return '''
|
return '''
|
||||||
${buildSessionStateJS(sessionActive)}
|
${buildSessionStateJS(sessionActive)}
|
||||||
@@ -359,6 +271,8 @@ class InjectionController {
|
|||||||
$_periodicNavRemoverJS
|
$_periodicNavRemoverJS
|
||||||
$_dismissAppBannerJS
|
$_dismissAppBannerJS
|
||||||
$reelsMutationObserverJS
|
$reelsMutationObserverJS
|
||||||
|
$_brandingJS
|
||||||
|
${ghostMode ? _ghostModeJS : ''}
|
||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class SessionManager extends ChangeNotifier {
|
|||||||
|
|
||||||
int get perSessionSeconds => _perSessionSeconds;
|
int get perSessionSeconds => _perSessionSeconds;
|
||||||
int get cooldownSeconds => _cooldownSeconds;
|
int get cooldownSeconds => _cooldownSeconds;
|
||||||
|
DateTime? get lastSessionEnd => _lastSessionEnd;
|
||||||
|
|
||||||
// ── Public getters — App session ──────────────────────────
|
// ── Public getters — App session ──────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class SettingsService extends ChangeNotifier {
|
|||||||
static const _keyRequireLongPress = 'set_require_long_press';
|
static const _keyRequireLongPress = 'set_require_long_press';
|
||||||
static const _keyShowBreathGate = 'set_show_breath_gate';
|
static const _keyShowBreathGate = 'set_show_breath_gate';
|
||||||
static const _keyRequireWordChallenge = 'set_require_word_challenge';
|
static const _keyRequireWordChallenge = 'set_require_word_challenge';
|
||||||
|
static const _keyGhostMode = 'set_ghost_mode';
|
||||||
|
static const _keyEnableTextSelection = 'set_enable_text_selection';
|
||||||
|
|
||||||
SharedPreferences? _prefs;
|
SharedPreferences? _prefs;
|
||||||
|
|
||||||
@@ -15,14 +17,17 @@ class SettingsService extends ChangeNotifier {
|
|||||||
bool _blurReels = false; // If false: hide reels in feed (after session ends)
|
bool _blurReels = false; // If false: hide reels in feed (after session ends)
|
||||||
bool _requireLongPress = true; // Long-press FAB to start session
|
bool _requireLongPress = true; // Long-press FAB to start session
|
||||||
bool _showBreathGate = true; // Show breathing gate on every open
|
bool _showBreathGate = true; // Show breathing gate on every open
|
||||||
bool _requireWordChallenge =
|
bool _requireWordChallenge = true;
|
||||||
true; // Random word sequence challenge before changes
|
bool _ghostMode = true; // Default: hide seen/typing
|
||||||
|
bool _enableTextSelection = false; // Default: disabled
|
||||||
|
|
||||||
bool get blurExplore => _blurExplore;
|
bool get blurExplore => _blurExplore;
|
||||||
bool get blurReels => _blurReels;
|
bool get blurReels => _blurReels;
|
||||||
bool get requireLongPress => _requireLongPress;
|
bool get requireLongPress => _requireLongPress;
|
||||||
bool get showBreathGate => _showBreathGate;
|
bool get showBreathGate => _showBreathGate;
|
||||||
bool get requireWordChallenge => _requireWordChallenge;
|
bool get requireWordChallenge => _requireWordChallenge;
|
||||||
|
bool get ghostMode => _ghostMode;
|
||||||
|
bool get enableTextSelection => _enableTextSelection;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
@@ -31,6 +36,8 @@ class SettingsService extends ChangeNotifier {
|
|||||||
_requireLongPress = _prefs!.getBool(_keyRequireLongPress) ?? true;
|
_requireLongPress = _prefs!.getBool(_keyRequireLongPress) ?? true;
|
||||||
_showBreathGate = _prefs!.getBool(_keyShowBreathGate) ?? true;
|
_showBreathGate = _prefs!.getBool(_keyShowBreathGate) ?? true;
|
||||||
_requireWordChallenge = _prefs!.getBool(_keyRequireWordChallenge) ?? true;
|
_requireWordChallenge = _prefs!.getBool(_keyRequireWordChallenge) ?? true;
|
||||||
|
_ghostMode = _prefs!.getBool(_keyGhostMode) ?? true;
|
||||||
|
_enableTextSelection = _prefs!.getBool(_keyEnableTextSelection) ?? false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,4 +70,16 @@ class SettingsService extends ChangeNotifier {
|
|||||||
await _prefs?.setBool(_keyRequireWordChallenge, v);
|
await _prefs?.setBool(_keyRequireWordChallenge, v);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setGhostMode(bool v) async {
|
||||||
|
_ghostMode = v;
|
||||||
|
await _prefs?.setBool(_keyGhostMode, v);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEnableTextSelection(bool v) async {
|
||||||
|
_enableTextSelection = v;
|
||||||
|
await _prefs?.setBool(_keyEnableTextSelection, v);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ class DisciplineChallenge {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Type the following sequence exactly to proceed:',
|
'FocusGram is currently blocked to help you stay focused. Type the following sequence exactly to proceed:',
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.9"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -33,6 +41,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,6 +65,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -49,6 +81,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -86,6 +126,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_launcher_icons
|
||||||
|
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -136,8 +184,32 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
http:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
google_fonts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_fonts
|
||||||
|
sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.2"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
@@ -152,6 +224,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -160,6 +240,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.20.2"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.11.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -192,6 +280,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -216,6 +312,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
|
native_toolchain_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: native_toolchain_c
|
||||||
|
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.4"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -224,6 +328,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.3.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -232,6 +344,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.22"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.0"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -280,6 +416,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -288,6 +432,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
version: "6.1.5+1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -549,6 +701,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "6.6.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.7 <4.0.0"
|
dart: ">=3.10.7 <4.0.0"
|
||||||
flutter: ">=3.38.0"
|
flutter: ">=3.38.4"
|
||||||
|
|||||||
@@ -28,11 +28,28 @@ dependencies:
|
|||||||
|
|
||||||
# URL launcher for About page links — latest stable
|
# URL launcher for About page links — latest stable
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
|
google_fonts: ^8.0.2
|
||||||
|
http: ^1.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/images/focusgram.png
|
||||||
|
- assets/images/focusgram.ico
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: true
|
||||||
|
ios: false
|
||||||
|
image_path: "assets/images/focusgram.png"
|
||||||
|
adaptive_icon_background: "#000000"
|
||||||
|
adaptive_icon_foreground: "assets/images/focusgram.png"
|
||||||
|
min_sdk_android: 21
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:focusgram/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// Widget tests for FocusGram are not yet implemented.
|
|
||||||
// The app requires SharedPreferences and WebView which need mocking.
|
|
||||||
test('placeholder', () {
|
|
||||||
expect(FocusGramApp, isNotNull);
|
|
||||||
});
|
|
||||||
}
|
|
||||||