mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-07-05 18:57:56 +02:00
RELEASE: moved from beta to First stable release.
Check CHANGELOG.md for full changelog
This commit is contained in:
@@ -1,211 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class AboutPage extends StatefulWidget {
|
||||
const AboutPage({super.key});
|
||||
|
||||
@override
|
||||
State<AboutPage> createState() => _AboutPageState();
|
||||
}
|
||||
|
||||
class _AboutPageState extends State<AboutPage> {
|
||||
final String _currentVersion = '0.9.8-beta.2';
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: const Text(
|
||||
'About FocusGram',
|
||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset(
|
||||
'assets/images/focusgram.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'FocusGram',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Version $_currentVersion',
|
||||
style: const TextStyle(color: Colors.white38, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const Text(
|
||||
'Developed with passion for digital discipline by',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Ujwal Chapagain',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
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(
|
||||
onPressed: () =>
|
||||
_launchURL('https://github.com/Ujwal223/FocusGram'),
|
||||
icon: const Icon(Icons.code),
|
||||
label: const Text('View on GitHub'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white10,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(200, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'FocusGram is not affiliated with Instagram.',
|
||||
style: TextStyle(
|
||||
color: Color.fromARGB(48, 255, 255, 255),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _launchURL(String url) async {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) return;
|
||||
try {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
+811
-273
File diff suppressed because it is too large
Load Diff
@@ -18,58 +18,61 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
|
||||
final List<OnboardingData> _pages = [
|
||||
OnboardingData(
|
||||
title: 'Welcome to FocusGram',
|
||||
description:
|
||||
'The distraction-free way to use Instagram. We help you stay focused by blocking Reels and Explore content.',
|
||||
icon: Icons.auto_awesome,
|
||||
color: Colors.blue,
|
||||
),
|
||||
OnboardingData(
|
||||
title: 'Ghost Mode',
|
||||
description:
|
||||
'Browse with total privacy. We block typing indicators and read receipts automatically.',
|
||||
icon: Icons.visibility_off,
|
||||
color: Colors.purple,
|
||||
),
|
||||
OnboardingData(
|
||||
title: 'Session Management',
|
||||
description:
|
||||
'Plan your usage. Set daily limits and use timed sessions to stay in control of your time.',
|
||||
icon: Icons.timer,
|
||||
color: Colors.orange,
|
||||
),
|
||||
OnboardingData(
|
||||
title: 'Open Links in FocusGram',
|
||||
description:
|
||||
'To open Instagram links directly here: Tap "Configure", then "Open by default" -> "Add link" and select all.',
|
||||
icon: Icons.link,
|
||||
color: Colors.cyan,
|
||||
isAppSettingsPage: true,
|
||||
),
|
||||
OnboardingData(
|
||||
title: 'Upload Content',
|
||||
description:
|
||||
'We need access to your gallery if you want to upload stories or posts directly from FocusGram.',
|
||||
icon: Icons.photo_library,
|
||||
color: Colors.orange,
|
||||
isPermissionPage: true,
|
||||
permission: Permission.photos,
|
||||
),
|
||||
OnboardingData(
|
||||
title: 'Stay Notified',
|
||||
description:
|
||||
'We need notification permissions to alert you when your session is over or a new message arrives.',
|
||||
icon: Icons.notifications_active,
|
||||
color: Colors.green,
|
||||
isPermissionPage: true,
|
||||
permission: Permission.notification,
|
||||
),
|
||||
];
|
||||
// Pages: Welcome, Session Management, Link Handling, Blur Settings, Notifications
|
||||
static const int _kTotalPages = 5;
|
||||
|
||||
static const int _kBlurPage = 3;
|
||||
static const int _kLinkPage = 2;
|
||||
static const int _kNotifPage = 4;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = context.watch<SettingsService>();
|
||||
|
||||
final List<Widget> slides = [
|
||||
// ── Page 0: Welcome ─────────────────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.auto_awesome,
|
||||
color: Colors.blue,
|
||||
title: 'Welcome to FocusGram',
|
||||
description:
|
||||
'The distraction-free way to use Instagram. We help you stay focused by blocking Reels and Explore content.',
|
||||
),
|
||||
|
||||
// ── Page 1: Session Management ───────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.timer,
|
||||
color: Colors.orange,
|
||||
title: 'Session Management',
|
||||
description:
|
||||
'Plan your usage. Set daily limits and use timed sessions to stay in control of your time.',
|
||||
),
|
||||
|
||||
// ── Page 2: Open links ───────────────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.link,
|
||||
color: Colors.cyan,
|
||||
title: 'Open Links in FocusGram',
|
||||
description:
|
||||
'To open Instagram links directly here: Tap "Configure", then "Open by default" → "Add link" and select all.',
|
||||
isAppSettingsPage: true,
|
||||
),
|
||||
|
||||
// ── Page 3: Blur Settings ────────────────────────────────────────────
|
||||
_BlurSettingsSlide(settings: settings),
|
||||
|
||||
// ── Page 4: Notifications ────────────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.notifications_active,
|
||||
color: Colors.green,
|
||||
title: 'Stay Notified',
|
||||
description:
|
||||
'We need notification permissions to alert you when your session is over or a new message arrives.',
|
||||
isPermissionPage: true,
|
||||
permission: Permission.notification,
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
@@ -77,9 +80,8 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (index) => setState(() => _currentPage = index),
|
||||
itemCount: _pages.length,
|
||||
itemBuilder: (context, index) =>
|
||||
_OnboardingSlide(data: _pages[index]),
|
||||
itemCount: _kTotalPages,
|
||||
itemBuilder: (context, index) => slides[index],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 50,
|
||||
@@ -87,11 +89,13 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
right: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
// Dot indicators
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
_pages.length,
|
||||
(index) => Container(
|
||||
_kTotalPages,
|
||||
(index) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: _currentPage == index ? 12 : 8,
|
||||
height: 8,
|
||||
@@ -105,6 +109,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// CTA button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: SizedBox(
|
||||
@@ -112,24 +117,38 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
height: 56,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final data = _pages[_currentPage];
|
||||
final isLast = _currentPage == _kTotalPages - 1;
|
||||
final isLink = _currentPage == _kLinkPage;
|
||||
final isNotif = _currentPage == _kNotifPage;
|
||||
final isBlur = _currentPage == _kBlurPage;
|
||||
|
||||
String label;
|
||||
if (isLast) {
|
||||
label = 'Get Started';
|
||||
} else if (isLink) {
|
||||
label = 'Configure';
|
||||
} else if (isNotif) {
|
||||
label = 'Allow Notifications';
|
||||
} else if (isBlur) {
|
||||
label = 'Save & Continue';
|
||||
} else {
|
||||
label = 'Next';
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (data.isAppSettingsPage) {
|
||||
if (isLink) {
|
||||
await AppSettings.openAppSettings(
|
||||
type: AppSettingsType.settings,
|
||||
);
|
||||
} else if (data.isPermissionPage) {
|
||||
if (data.permission != null) {
|
||||
await data.permission!.request();
|
||||
}
|
||||
if (data.title == 'Stay Notified') {
|
||||
await NotificationService().init();
|
||||
}
|
||||
} else if (isNotif) {
|
||||
await Permission.notification.request();
|
||||
await NotificationService().init();
|
||||
}
|
||||
|
||||
if (_currentPage == _pages.length - 1) {
|
||||
_finish();
|
||||
if (!context.mounted) return;
|
||||
if (isLast) {
|
||||
_finish(context);
|
||||
} else {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -145,11 +164,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_currentPage == _pages.length - 1
|
||||
? 'Get Started'
|
||||
: (data.isAppSettingsPage
|
||||
? 'Configure'
|
||||
: 'Next'),
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -160,6 +175,15 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Skip button (available on all pages except last)
|
||||
if (_currentPage < _kTotalPages - 1)
|
||||
TextButton(
|
||||
onPressed: () => _finish(context),
|
||||
child: const Text(
|
||||
'Skip',
|
||||
style: TextStyle(color: Colors.white38, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -168,48 +192,44 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _finish() {
|
||||
void _finish(BuildContext context) {
|
||||
context.read<SettingsService>().setFirstRunCompleted();
|
||||
widget.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
class OnboardingData {
|
||||
final String title;
|
||||
final String description;
|
||||
// ── Static info slide ──────────────────────────────────────────────────────────
|
||||
|
||||
class _StaticSlide extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String title;
|
||||
final String description;
|
||||
final bool isPermissionPage;
|
||||
final bool isAppSettingsPage;
|
||||
final Permission? permission;
|
||||
|
||||
OnboardingData({
|
||||
required this.title,
|
||||
required this.description,
|
||||
const _StaticSlide({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.isPermissionPage = false,
|
||||
this.isAppSettingsPage = false,
|
||||
this.permission,
|
||||
});
|
||||
}
|
||||
|
||||
class _OnboardingSlide extends StatelessWidget {
|
||||
final OnboardingData data;
|
||||
|
||||
const _OnboardingSlide({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
padding: const EdgeInsets.fromLTRB(40, 40, 40, 160),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(data.icon, size: 120, color: data.color),
|
||||
Icon(icon, size: 120, color: color),
|
||||
const SizedBox(height: 48),
|
||||
Text(
|
||||
data.title,
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
@@ -219,7 +239,7 @@ class _OnboardingSlide extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
data.description,
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
@@ -232,3 +252,147 @@ class _OnboardingSlide extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Blur settings slide ────────────────────────────────────────────────────────
|
||||
|
||||
class _BlurSettingsSlide extends StatelessWidget {
|
||||
final SettingsService settings;
|
||||
|
||||
const _BlurSettingsSlide({required this.settings});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(32, 40, 32, 160),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.blur_on_rounded,
|
||||
size: 90,
|
||||
color: Colors.purpleAccent,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Distraction Shield',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Blur feeds you don\'t want to be tempted by. You can change these anytime in Settings.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white60,
|
||||
fontSize: 16,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Blur Home Feed toggle
|
||||
_BlurToggleTile(
|
||||
icon: Icons.home_rounded,
|
||||
label: 'Blur Home Feed',
|
||||
subtitle: 'Posts in your feed will be blurred until tapped',
|
||||
value: settings.blurReels,
|
||||
onChanged: (v) => settings.setBlurReels(v),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Blur Explore toggle
|
||||
_BlurToggleTile(
|
||||
icon: Icons.explore_rounded,
|
||||
label: 'Blur Explore Feed',
|
||||
subtitle: 'Explore thumbnails stay blurred until you tap',
|
||||
value: settings.blurExplore,
|
||||
onChanged: (v) => settings.setBlurExplore(v),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BlurToggleTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String subtitle;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
const _BlurToggleTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.subtitle,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: value
|
||||
? Colors.purpleAccent.withValues(alpha: 0.12)
|
||||
: Colors.white.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: value
|
||||
? Colors.purpleAccent.withValues(alpha: 0.5)
|
||||
: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: value ? Colors.purpleAccent : Colors.white38,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: value ? Colors.white : Colors.white70,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeThumbColor: Colors.purpleAccent,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import '../services/injection_controller.dart';
|
||||
import '../services/session_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -15,58 +15,12 @@ class ReelPlayerOverlay extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ReelPlayerOverlayState extends State<ReelPlayerOverlay> {
|
||||
late final WebViewController _controller;
|
||||
DateTime? _startTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTime = DateTime.now();
|
||||
_initWebView();
|
||||
}
|
||||
|
||||
void _initWebView() {
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setUserAgent(InjectionController.iOSUserAgent)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageFinished: (url) {
|
||||
// Set isolated player flag to ensure scroll-lock applies even if a session is active globally
|
||||
_controller.runJavaScript(
|
||||
'window.__focusgramIsolatedPlayer = true;',
|
||||
);
|
||||
// Apply scroll-lock via MutationObserver: prevents swiping to next reel
|
||||
_controller.runJavaScript(
|
||||
InjectionController.reelsMutationObserverJS,
|
||||
);
|
||||
// Also hide Instagram's bottom nav inside this overlay
|
||||
_controller.runJavaScript(
|
||||
InjectionController.buildInjectionJS(
|
||||
sessionActive: true,
|
||||
blurExplore: false,
|
||||
blurReels: false,
|
||||
ghostTyping: false,
|
||||
ghostSeen: false,
|
||||
ghostStories: false,
|
||||
ghostDmPhotos: false,
|
||||
enableTextSelection: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
onNavigationRequest: (request) {
|
||||
// Allow only the initial reel URL and instagram.com generally
|
||||
final uri = Uri.tryParse(request.url);
|
||||
if (uri == null) return NavigationDecision.prevent;
|
||||
final host = uri.host;
|
||||
if (!host.contains('instagram.com')) {
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(widget.url));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -114,7 +68,65 @@ class _ReelPlayerOverlayState extends State<ReelPlayerOverlay> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
body: InAppWebView(
|
||||
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
|
||||
initialSettings: InAppWebViewSettings(
|
||||
userAgent: InjectionController.iOSUserAgent,
|
||||
mediaPlaybackRequiresUserGesture: true,
|
||||
useHybridComposition: true,
|
||||
cacheEnabled: true,
|
||||
cacheMode: CacheMode.LOAD_CACHE_ELSE_NETWORK,
|
||||
domStorageEnabled: true,
|
||||
databaseEnabled: true,
|
||||
hardwareAcceleration: true,
|
||||
transparentBackground: true,
|
||||
safeBrowsingEnabled: false,
|
||||
supportZoom: false,
|
||||
allowsInlineMediaPlayback: true,
|
||||
verticalScrollBarEnabled: false,
|
||||
horizontalScrollBarEnabled: false,
|
||||
),
|
||||
onWebViewCreated: (controller) {
|
||||
// Controller is not stored; this overlay is self-contained.
|
||||
},
|
||||
onLoadStop: (controller, url) async {
|
||||
// Set isolated player flag to ensure scroll-lock applies even if a session is active globally
|
||||
await controller.evaluateJavascript(
|
||||
source: 'window.__focusgramIsolatedPlayer = true;',
|
||||
);
|
||||
// Apply scroll-lock via MutationObserver: prevents swiping to next reel
|
||||
await controller.evaluateJavascript(
|
||||
source: InjectionController.reelsMutationObserverJS,
|
||||
);
|
||||
// Also apply FocusGram baseline CSS (hides bottom nav etc.)
|
||||
await controller.evaluateJavascript(
|
||||
source: InjectionController.buildInjectionJS(
|
||||
sessionActive: true,
|
||||
blurExplore: false,
|
||||
blurReels: false,
|
||||
enableTextSelection: true,
|
||||
hideSuggestedPosts: false,
|
||||
hideSponsoredPosts: false,
|
||||
hideLikeCounts: false,
|
||||
hideFollowerCounts: false,
|
||||
hideStoriesBar: false,
|
||||
hideExploreTab: false,
|
||||
hideReelsTab: false,
|
||||
hideShopTab: false,
|
||||
disableReelsEntirely: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
shouldOverrideUrlLoading: (controller, action) async {
|
||||
// Keep this overlay locked to instagram.com pages only
|
||||
final uri = action.request.url;
|
||||
if (uri == null) return NavigationActionPolicy.CANCEL;
|
||||
if (!uri.host.contains('instagram.com')) {
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+453
-549
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user