mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-05-27 09:22:32 +02:00
Progress SAve- downloader,blur,ghost mode(Partially) works
This commit is contained in:
@@ -27,7 +27,25 @@ class _AppSessionPickerScreenState extends State<AppSessionPickerScreen> {
|
||||
55,
|
||||
60,
|
||||
];
|
||||
int _selectedIndex = 2; // default: 15 min
|
||||
int _selectedIndex = 0; // default: 5 min unless a previous choice exists
|
||||
late final FixedExtentScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final lastMinutes = context.read<SessionManager>().lastAppSessionMinutes;
|
||||
final lastIndex = _minuteOptions.indexOf(lastMinutes);
|
||||
_selectedIndex = lastIndex >= 0 ? lastIndex : 0;
|
||||
_scrollController = FixedExtentScrollController(
|
||||
initialItem: _selectedIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -118,12 +136,10 @@ class _AppSessionPickerScreenState extends State<AppSessionPickerScreen> {
|
||||
perspective: 0.003,
|
||||
squeeze: 1.1,
|
||||
diameterRatio: 2.5,
|
||||
controller: _scrollController,
|
||||
onSelectedItemChanged: (i) {
|
||||
setState(() => _selectedIndex = i);
|
||||
},
|
||||
controller: FixedExtentScrollController(
|
||||
initialItem: _selectedIndex,
|
||||
),
|
||||
childDelegate: ListWheelChildListDelegate(
|
||||
children: _minuteOptions.asMap().entries.map((entry) {
|
||||
final isSelected = entry.key == _selectedIndex;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
/// A mindfulness screen shown before the app opens.
|
||||
/// Forces the user to take a deep 10-second breath.
|
||||
/// A mindfulness screen shown before Instagram opens.
|
||||
class BreathGateScreen extends StatefulWidget {
|
||||
final VoidCallback onFinish;
|
||||
final int durationSeconds;
|
||||
|
||||
const BreathGateScreen({super.key, required this.onFinish});
|
||||
const BreathGateScreen({
|
||||
super.key,
|
||||
required this.onFinish,
|
||||
this.durationSeconds = 10,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BreathGateScreen> createState() => _BreathGateScreenState();
|
||||
@@ -16,15 +20,15 @@ class _BreathGateScreenState extends State<BreathGateScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
int _secondsRemaining = 10;
|
||||
late int _secondsRemaining;
|
||||
Timer? _timer;
|
||||
bool _canContinue = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_secondsRemaining = widget.durationSeconds.clamp(3, 60).toInt();
|
||||
|
||||
// 10-second breathing animation: 5s in, 5s out
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 5),
|
||||
@@ -71,7 +75,7 @@ class _BreathGateScreenState extends State<BreathGateScreen>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Are you sure you want to open FocusGram?',
|
||||
'Are you sure you want to open Instagram?',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
@@ -131,7 +135,7 @@ class _BreathGateScreenState extends State<BreathGateScreen>
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
),
|
||||
child: const Text('Continue to FocusGram'),
|
||||
child: const Text('Continue to Instagram'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../services/settings_service.dart';
|
||||
|
||||
class ExtrasSettingsPage extends StatelessWidget {
|
||||
const ExtrasSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = context.watch<SettingsService>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Extras',
|
||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
||||
),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
const _SectionHeader(title: 'MEDIA'),
|
||||
_SwitchTile(
|
||||
title: 'Download Media (Feed + Reels)',
|
||||
subtitle: 'Adds a download icon on posts and reels',
|
||||
value: settings.videoDownloadEnabled,
|
||||
onChanged: (v) async {
|
||||
await settings.setVideoDownloadEnabled(v);
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
),
|
||||
|
||||
const _SectionHeader(title: 'FOCUS'),
|
||||
_SwitchTile(
|
||||
title: 'GHOST MODE',
|
||||
subtitle: 'Hide seen indicator / read receipts',
|
||||
value: settings.ghostMode,
|
||||
onChanged: (v) async {
|
||||
await settings.setGhostMode(v);
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
),
|
||||
|
||||
const _SectionHeader(title: 'FOCUSGRAM V2'),
|
||||
_SwitchTile(
|
||||
title: 'Ad Blocker',
|
||||
subtitle: 'Removes ads and sponsored posts',
|
||||
value: settings.v2AdBlockerDomEnabled,
|
||||
onChanged: (v) async {
|
||||
await settings.setV2AdBlockerDomEnabled(v);
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
),
|
||||
_SwitchTile(
|
||||
title: 'Block Suggested Posts',
|
||||
subtitle: 'Removes Suggested for you and recommendation units',
|
||||
value: settings.contentSuggested,
|
||||
onChanged: (v) async {
|
||||
await settings.setContentSuggestedEnabled(v);
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SwitchTile extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
const _SwitchTile({
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile(
|
||||
title: Text(title, style: const TextStyle(fontSize: 15)),
|
||||
subtitle: subtitle != null
|
||||
? Text(subtitle ?? '', style: const TextStyle(fontSize: 12))
|
||||
: null,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
const _SectionHeader({required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,11 @@ class _GuardrailsPageState extends State<GuardrailsPage> {
|
||||
Future<void> Function() action,
|
||||
) async {
|
||||
if (sm.isScheduledBlockActive) {
|
||||
final ok = await DisciplineChallenge.show(context, count: 35);
|
||||
final settings = context.read<SettingsService>();
|
||||
final ok = await DisciplineChallenge.show(
|
||||
context,
|
||||
count: settings.resolvedWordChallengeCount(),
|
||||
);
|
||||
if (!context.mounted || !ok) return;
|
||||
}
|
||||
await action();
|
||||
@@ -321,7 +325,8 @@ class _FrictionSliderTileState extends State<_FrictionSliderTile> {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final sm = context.read<SessionManager>();
|
||||
int wordCount = 15;
|
||||
final settings = context.read<SettingsService>();
|
||||
int wordCount = settings.resolvedWordChallengeCount();
|
||||
// If we are at 0 quota, increase difficulty to 35 words
|
||||
if (widget.title.contains('Daily Reel Limit') &&
|
||||
sm.dailyRemainingSeconds <= 0) {
|
||||
|
||||
+848
-252
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
|
||||
// Pages: Welcome, Session Management, Link Handling, Blur Settings, Notifications
|
||||
// Pages: Welcome, Focus controls, Link Handling, Blur Settings, Notifications
|
||||
static const int _kTotalPages = 5;
|
||||
|
||||
static const int _kBlurPage = 3;
|
||||
@@ -32,26 +32,26 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
final List<Widget> slides = [
|
||||
// ── Page 0: Welcome ─────────────────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.auto_awesome,
|
||||
color: Colors.blue,
|
||||
icon: Icons.auto_awesome_rounded,
|
||||
color: const Color(0xFF4F8DFF),
|
||||
title: 'Welcome to FocusGram',
|
||||
description:
|
||||
'The distraction-free way to use Instagram. We help you stay focused by blocking Reels and Explore content.',
|
||||
'Use Instagram with guardrails: timed Reel sessions, calmer feeds, optional media tools, and privacy-first controls that stay on your device.',
|
||||
),
|
||||
|
||||
// ── Page 1: Session Management ───────────────────────────────────────
|
||||
// ── Page 1: Focus controls ───────────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.timer,
|
||||
color: Colors.orange,
|
||||
title: 'Session Management',
|
||||
icon: Icons.timer_outlined,
|
||||
color: const Color(0xFFFFB74D),
|
||||
title: 'Time With Intent',
|
||||
description:
|
||||
'Plan your usage. Set daily limits and use timed sessions to stay in control of your time.',
|
||||
'Set daily limits, cooldowns, scheduled focus hours, and short Reel sessions when you choose to watch.',
|
||||
),
|
||||
|
||||
// ── Page 2: Open links ───────────────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.link,
|
||||
color: Colors.cyan,
|
||||
icon: Icons.link_rounded,
|
||||
color: const Color(0xFF35C2D6),
|
||||
title: 'Open Links in FocusGram',
|
||||
description:
|
||||
'To open Instagram links directly here: Tap "Configure", then "Open by default" → "Add link" and select all.',
|
||||
@@ -63,11 +63,11 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
|
||||
// ── Page 4: Notifications ────────────────────────────────────────────
|
||||
_StaticSlide(
|
||||
icon: Icons.notifications_active,
|
||||
color: Colors.green,
|
||||
title: 'Stay Notified',
|
||||
icon: Icons.notifications_active_outlined,
|
||||
color: const Color(0xFF5DD18A),
|
||||
title: 'Useful Alerts Only',
|
||||
description:
|
||||
'We need notification permissions to alert you when your session is over or a new message arrives.',
|
||||
'Enable notifications only if you want session-end or persistent focus reminders. FocusGram will ask here, not before onboarding.',
|
||||
isPermissionPage: true,
|
||||
permission: Permission.notification,
|
||||
),
|
||||
@@ -108,7 +108,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 28),
|
||||
// CTA button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
@@ -123,14 +123,14 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
final isBlur = _currentPage == _kBlurPage;
|
||||
|
||||
String label;
|
||||
if (isLast) {
|
||||
label = 'Get Started';
|
||||
if (isNotif) {
|
||||
label = 'Allow & Start';
|
||||
} else if (isLink) {
|
||||
label = 'Configure';
|
||||
} else if (isNotif) {
|
||||
label = 'Allow Notifications';
|
||||
} else if (isBlur) {
|
||||
label = 'Save & Continue';
|
||||
} else if (isLast) {
|
||||
label = 'Get Started';
|
||||
} else {
|
||||
label = 'Next';
|
||||
}
|
||||
@@ -143,7 +143,8 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
);
|
||||
} else if (isNotif) {
|
||||
await Permission.notification.request();
|
||||
await NotificationService().init();
|
||||
await NotificationService()
|
||||
.requestPermissionsNow();
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
@@ -178,9 +179,19 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
// Skip button (available on all pages except last)
|
||||
if (_currentPage < _kTotalPages - 1)
|
||||
TextButton(
|
||||
onPressed: () => _finish(context),
|
||||
onPressed: () {
|
||||
if (_currentPage == _kNotifPage) {
|
||||
_finish(context);
|
||||
} else {
|
||||
_pageController.animateToPage(
|
||||
_kTotalPages - 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
'Skip',
|
||||
'Skip setup',
|
||||
style: TextStyle(color: Colors.white38, fontSize: 14),
|
||||
),
|
||||
),
|
||||
@@ -222,18 +233,27 @@ class _StaticSlide extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(40, 40, 40, 160),
|
||||
padding: const EdgeInsets.fromLTRB(28, 40, 28, 160),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 120, color: color),
|
||||
const SizedBox(height: 48),
|
||||
Container(
|
||||
width: 112,
|
||||
height: 112,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.14),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: color.withValues(alpha: 0.28)),
|
||||
),
|
||||
child: Icon(icon, size: 54, color: color),
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -243,10 +263,28 @@ class _StaticSlide extends StatelessWidget {
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
if (isPermissionPage || isAppSettingsPage) ...[
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white10),
|
||||
),
|
||||
child: Text(
|
||||
isPermissionPage
|
||||
? 'Permission is optional and can be changed later.'
|
||||
: 'This opens Android settings; return here when done.',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white54, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -115,6 +115,7 @@ class _ReelPlayerOverlayState extends State<ReelPlayerOverlay> {
|
||||
hideReelsTab: false,
|
||||
hideShopTab: false,
|
||||
disableReelsEntirely: false,
|
||||
blockHomeFeedScroll: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/session_manager.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../utils/discipline_challenge.dart';
|
||||
|
||||
class SessionModal extends StatefulWidget {
|
||||
@@ -63,23 +64,22 @@ class _SessionModalState extends State<SessionModal> {
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [1, 5, 10, 15].map((m) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: (sm.isCooldownActive || sm.isDailyLimitExhausted)
|
||||
? null
|
||||
: () => _start(m),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white12,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text('${m}m'),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [1, 3, 5, 10, 15, 20, 30].map((m) {
|
||||
return SizedBox(
|
||||
width: 72,
|
||||
child: ElevatedButton(
|
||||
onPressed: (sm.isCooldownActive || sm.isDailyLimitExhausted)
|
||||
? null
|
||||
: () => _start(m),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white12,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text('${m}m'),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -92,8 +92,8 @@ class _SessionModalState extends State<SessionModal> {
|
||||
Slider(
|
||||
value: _customMinutes,
|
||||
min: 1,
|
||||
max: 30,
|
||||
divisions: 29,
|
||||
max: 60,
|
||||
divisions: 59,
|
||||
label: '${_customMinutes.toInt()}m',
|
||||
onChanged: (v) => setState(() => _customMinutes = v),
|
||||
),
|
||||
@@ -126,10 +126,15 @@ class _SessionModalState extends State<SessionModal> {
|
||||
|
||||
void _start(int minutes) async {
|
||||
final sm = context.read<SessionManager>();
|
||||
final settings = context.read<SettingsService>();
|
||||
|
||||
// Always require word challenge for reel sessions (User request)
|
||||
final success = await DisciplineChallenge.show(context);
|
||||
if (!success) return;
|
||||
if (settings.requireWordChallenge) {
|
||||
final success = await DisciplineChallenge.show(
|
||||
context,
|
||||
count: settings.resolvedWordChallengeCount(),
|
||||
);
|
||||
if (!success) return;
|
||||
}
|
||||
|
||||
if (sm.startSession(minutes)) {
|
||||
if (mounted) Navigator.pop(context);
|
||||
|
||||
+447
-80
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -8,6 +9,7 @@ import '../services/settings_service.dart';
|
||||
import '../services/focusgram_router.dart';
|
||||
import '../features/screen_time/screen_time_screen.dart';
|
||||
import 'guardrails_page.dart';
|
||||
import 'extras_settings_page.dart';
|
||||
|
||||
// ─── Main Settings Page ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -34,6 +36,7 @@ class SettingsPage extends StatelessWidget {
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
const _DonateTile(),
|
||||
_buildStatsRow(sm),
|
||||
|
||||
const _SectionHeader(title: 'FOCUS & BLOCKING'),
|
||||
@@ -63,6 +66,19 @@ class SettingsPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const _SectionHeader(title: 'EXTRAS'),
|
||||
_SubmoduleTile(
|
||||
icon: Icons.download_rounded,
|
||||
iconColor: Colors.orangeAccent,
|
||||
title: 'Extras',
|
||||
subtitle: 'Download media, Ghost Mode, Ad Blocker',
|
||||
enabled: true,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ExtrasSettingsPage()),
|
||||
),
|
||||
),
|
||||
|
||||
const _SectionHeader(title: 'APPEARANCE'),
|
||||
_SubmoduleTile(
|
||||
icon: Icons.palette_outlined,
|
||||
@@ -264,6 +280,7 @@ class FocusSettingsPage extends StatelessWidget {
|
||||
body: ListView(
|
||||
children: [
|
||||
const _SectionHeader(title: 'BLOCKING'),
|
||||
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
@@ -293,12 +310,23 @@ class FocusSettingsPage extends StatelessWidget {
|
||||
color: Colors.redAccent.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.shield_rounded, color: Colors.redAccent, size: 20),
|
||||
child: const Icon(
|
||||
Icons.shield_rounded,
|
||||
color: Colors.redAccent,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: const Text('Minimal Mode', style: TextStyle(fontSize: 15)),
|
||||
subtitle: Text(
|
||||
settings.minimalModeEnabled ? 'Enabled - tap to customize' : 'Disabled - tap to configure',
|
||||
style: TextStyle(fontSize: 12, color: settings.minimalModeEnabled ? Colors.greenAccent : Colors.grey),
|
||||
settings.minimalModeEnabled
|
||||
? 'Enabled - tap to customize'
|
||||
: 'Disabled - tap to configure',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: settings.minimalModeEnabled
|
||||
? Colors.greenAccent
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.minimalModeEnabled,
|
||||
@@ -307,22 +335,49 @@ class FocusSettingsPage extends StatelessWidget {
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MinimalModeSubmenuPage())),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const MinimalModeSubmenuPage()),
|
||||
),
|
||||
),
|
||||
|
||||
const _SectionHeader(title: 'FRICTION'),
|
||||
_SwitchTile(
|
||||
title: 'Mindfulness Gate',
|
||||
subtitle: 'Breath screen before opening Instagram',
|
||||
subtitle: '${settings.breathGateSeconds}s before opening Instagram',
|
||||
value: settings.showBreathGate,
|
||||
onChanged: (v) => settings.setShowBreathGate(v),
|
||||
),
|
||||
if (settings.showBreathGate)
|
||||
_NumberEditTile(
|
||||
title: 'Gate Duration',
|
||||
label: '${settings.breathGateSeconds} seconds',
|
||||
initialValue: settings.breathGateSeconds,
|
||||
min: 3,
|
||||
max: 60,
|
||||
suffix: 'seconds',
|
||||
onSubmitted: (v) => settings.setBreathGateSeconds(v),
|
||||
),
|
||||
_SwitchTile(
|
||||
title: 'Strict Mode (Word Challenge)',
|
||||
subtitle: 'Must type a phrase before starting a Reel session',
|
||||
title: 'Typing Challenge',
|
||||
subtitle: settings.wordChallengeCount == 0
|
||||
? 'Random: 10-35 words'
|
||||
: '${settings.wordChallengeCount} words',
|
||||
value: settings.requireWordChallenge,
|
||||
onChanged: (v) => settings.setRequireWordChallenge(v),
|
||||
),
|
||||
if (settings.requireWordChallenge)
|
||||
_ChoiceTile<int>(
|
||||
title: 'Typing Words',
|
||||
value: settings.wordChallengeCount,
|
||||
label: settings.wordChallengeCount == 0
|
||||
? 'Random (10-35)'
|
||||
: '${settings.wordChallengeCount} words',
|
||||
options: const [20, 25, 30, 35, 0],
|
||||
optionLabel: (v) => v == 0 ? 'Random (10-35)' : '$v words',
|
||||
onSelected: (v) => settings.setWordChallengeCount(v),
|
||||
),
|
||||
|
||||
const _SectionHeader(title: 'MEDIA'),
|
||||
_SwitchTile(
|
||||
title: 'Block Autoplay Videos',
|
||||
@@ -348,6 +403,48 @@ class FocusSettingsPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const _SectionHeader(title: 'FOCUSGRAM V2 OVERLAY'),
|
||||
|
||||
_SwitchTile(
|
||||
title: 'Content Hider',
|
||||
subtitle: 'Hide stories tray, feed posts, reels, suggested content',
|
||||
value: settings.v2ContentHiderEnabled,
|
||||
onChanged: (v) => settings.setV2ContentHiderEnabled(v),
|
||||
),
|
||||
|
||||
if (settings.v2ContentHiderEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
_SwitchTile(
|
||||
title: 'Hide Stories Tray',
|
||||
subtitle: 'Story bubbles row',
|
||||
value: settings.contentStories,
|
||||
onChanged: (v) => settings.setContentStoriesEnabled(v),
|
||||
),
|
||||
_SwitchTile(
|
||||
title: 'Hide Feed Posts',
|
||||
subtitle: 'Home feed posts',
|
||||
value: settings.contentPosts,
|
||||
onChanged: (v) => settings.setContentPostsEnabled(v),
|
||||
),
|
||||
_SwitchTile(
|
||||
title: 'Hide Reels (Feed)',
|
||||
subtitle: 'Reels shown in the feed',
|
||||
value: settings.contentReels,
|
||||
onChanged: (v) => settings.setContentReelsEnabled(v),
|
||||
),
|
||||
_SwitchTile(
|
||||
title: 'Hide Suggested Content',
|
||||
subtitle: 'Suggested posts and recommendation units',
|
||||
value: settings.contentSuggested,
|
||||
onChanged: (v) => settings.setContentSuggestedEnabled(v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
@@ -355,6 +452,50 @@ class FocusSettingsPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DonateTile extends StatelessWidget {
|
||||
const _DonateTile();
|
||||
|
||||
static final Uri _donateUri = Uri.parse('https://buymemomo.com/ujwal');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.pinkAccent.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.pinkAccent.withValues(alpha: 0.22)),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.pinkAccent.withValues(alpha: 0.14),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.favorite_rounded,
|
||||
color: Colors.pinkAccent,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: const Text(
|
||||
'Please donate to support the development of this project.',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Your support keeps FocusGram free and maintained.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: const Icon(Icons.open_in_new, size: 14),
|
||||
onTap: () =>
|
||||
launchUrl(_donateUri, mode: LaunchMode.externalApplication),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Minimal Mode Submenu ─────────────────────────────────────────────────────
|
||||
|
||||
class MinimalModeSubmenuPage extends StatefulWidget {
|
||||
@@ -368,6 +509,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
late bool _blurExplore;
|
||||
late bool _disableReelsEntirely;
|
||||
late bool _disableExploreEntirely;
|
||||
late bool _blockHomeFeedScroll;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -376,26 +518,51 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
_blurExplore = settings.blurExplore;
|
||||
_disableReelsEntirely = settings.disableReelsEntirely;
|
||||
_disableExploreEntirely = settings.disableExploreEntirely;
|
||||
_blockHomeFeedScroll = settings.blockHomeFeedScroll;
|
||||
}
|
||||
|
||||
void _updateSetting(String key, bool value) {
|
||||
Future<void> _updateSetting(String key, bool value) async {
|
||||
final settings = context.read<SettingsService>();
|
||||
setState(() {
|
||||
switch (key) {
|
||||
case 'blurExplore':
|
||||
_blurExplore = value;
|
||||
settings.setBlurExplore(value);
|
||||
break;
|
||||
case 'disableReelsEntirely':
|
||||
_disableReelsEntirely = value;
|
||||
settings.setDisableReelsEntirelyInternal(value);
|
||||
break;
|
||||
case 'disableExploreEntirely':
|
||||
_disableExploreEntirely = value;
|
||||
settings.setDisableExploreEntirelyInternal(value);
|
||||
break;
|
||||
case 'blockHomeFeedScroll':
|
||||
_blockHomeFeedScroll = value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
switch (key) {
|
||||
case 'blurExplore':
|
||||
await settings.setBlurExplore(value);
|
||||
break;
|
||||
case 'disableReelsEntirely':
|
||||
await settings.setDisableReelsEntirelyInternal(value);
|
||||
break;
|
||||
case 'disableExploreEntirely':
|
||||
await settings.setDisableExploreEntirelyInternal(value);
|
||||
break;
|
||||
case 'blockHomeFeedScroll':
|
||||
await settings.setBlockHomeFeedScrollInternal(value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
final latest = context.read<SettingsService>();
|
||||
setState(() {
|
||||
_blurExplore = latest.blurExplore;
|
||||
_disableReelsEntirely = latest.disableReelsEntirely;
|
||||
_disableExploreEntirely = latest.disableExploreEntirely;
|
||||
_blockHomeFeedScroll = latest.blockHomeFeedScroll;
|
||||
});
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
@@ -406,6 +573,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
_blurExplore = true;
|
||||
_disableReelsEntirely = true;
|
||||
_disableExploreEntirely = true;
|
||||
_blockHomeFeedScroll = true;
|
||||
});
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
@@ -418,6 +586,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
_blurExplore = settings.blurExplore;
|
||||
_disableReelsEntirely = settings.disableReelsEntirely;
|
||||
_disableExploreEntirely = settings.disableExploreEntirely;
|
||||
_blockHomeFeedScroll = settings.blockHomeFeedScroll;
|
||||
});
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
@@ -437,61 +606,88 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isMinimalModeEnabled
|
||||
? [Colors.redAccent.withValues(alpha: 0.2), Colors.red.withValues(alpha: 0.1)]
|
||||
: [Colors.grey.withValues(alpha: 0.1), Colors.grey.withValues(alpha: 0.05)],
|
||||
colors: isMinimalModeEnabled
|
||||
? [
|
||||
Colors.redAccent.withValues(alpha: 0.2),
|
||||
Colors.red.withValues(alpha: 0.1),
|
||||
]
|
||||
: [
|
||||
Colors.grey.withValues(alpha: 0.1),
|
||||
Colors.grey.withValues(alpha: 0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isMinimalModeEnabled ? Colors.redAccent.withValues(alpha: 0.3) : Colors.grey.withValues(alpha: 0.2),
|
||||
color: isMinimalModeEnabled
|
||||
? Colors.redAccent.withValues(alpha: 0.3)
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
isMinimalModeEnabled ? Icons.shield_rounded : Icons.shield_outlined,
|
||||
isMinimalModeEnabled
|
||||
? Icons.shield_rounded
|
||||
: Icons.shield_outlined,
|
||||
color: isMinimalModeEnabled ? Colors.redAccent : Colors.grey,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
isMinimalModeEnabled ? 'Minimal Mode Active' : 'Minimal Mode Disabled',
|
||||
isMinimalModeEnabled
|
||||
? 'Minimal Mode Active'
|
||||
: 'Minimal Mode Disabled',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isMinimalModeEnabled ? Colors.redAccent : Colors.grey,
|
||||
color: isMinimalModeEnabled
|
||||
? Colors.redAccent
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
isMinimalModeEnabled
|
||||
isMinimalModeEnabled
|
||||
? 'Distractions are blocked. Customize which features stay enabled below.'
|
||||
: 'Turn on to block all distractions at once, or customize individual settings below.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13, color: isDark ? Colors.white54 : Colors.black54),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark ? Colors.white54 : Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isMinimalModeEnabled ? _turnOffMinimalMode : _turnOnMinimalMode,
|
||||
onPressed: isMinimalModeEnabled
|
||||
? _turnOffMinimalMode
|
||||
: _turnOnMinimalMode,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isMinimalModeEnabled ? Colors.grey : Colors.redAccent,
|
||||
backgroundColor: isMinimalModeEnabled
|
||||
? Colors.grey
|
||||
: Colors.redAccent,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isMinimalModeEnabled
|
||||
? 'Turn Off Minimal Mode'
|
||||
: 'Turn On Minimal Mode',
|
||||
),
|
||||
child: Text(isMinimalModeEnabled ? 'Turn Off Minimal Mode' : 'Turn On Minimal Mode'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const _SectionHeader(title: 'CUSTOMIZE SETTINGS'),
|
||||
|
||||
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
@@ -502,7 +698,11 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.touch_app_rounded, size: 14, color: Colors.blueAccent),
|
||||
Icon(
|
||||
Icons.touch_app_rounded,
|
||||
size: 14,
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -513,13 +713,19 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
_SwitchTile(
|
||||
title: 'Blur Feed & Explore',
|
||||
subtitle: 'Blurs post thumbnails until tapped',
|
||||
value: _blurExplore,
|
||||
onChanged: (v) => _updateSetting('blurExplore', v),
|
||||
),
|
||||
_SwitchTile(
|
||||
title: 'Block Home Feed Scroll',
|
||||
subtitle: 'Freeze vertical scrolling on the home feed only',
|
||||
value: _blockHomeFeedScroll,
|
||||
onChanged: (v) => _updateSetting('blockHomeFeedScroll', v),
|
||||
),
|
||||
_SwitchTile(
|
||||
title: 'Disable Reels Entirely',
|
||||
subtitle: 'Block all Reels with no session option',
|
||||
@@ -532,7 +738,7 @@ class _MinimalModeSubmenuPageState extends State<MinimalModeSubmenuPage> {
|
||||
value: _disableExploreEntirely,
|
||||
onChanged: (v) => _updateSetting('disableExploreEntirely', v),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
@@ -550,43 +756,52 @@ class AppearancePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppearancePageState extends State<AppearancePage> {
|
||||
Future<void> _addSchedule(BuildContext context, SettingsService settings) async {
|
||||
Future<void> _addSchedule(
|
||||
BuildContext context,
|
||||
SettingsService settings,
|
||||
) async {
|
||||
TimeOfDay? startTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 21, minute: 0),
|
||||
helpText: 'Select start time',
|
||||
);
|
||||
|
||||
|
||||
if (startTime == null || !context.mounted) return;
|
||||
|
||||
|
||||
TimeOfDay? endTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 6, minute: 0),
|
||||
helpText: 'Select end time',
|
||||
);
|
||||
|
||||
|
||||
if (endTime == null || !context.mounted) return;
|
||||
|
||||
|
||||
final newSchedule = {
|
||||
'enabled': true,
|
||||
'startTime': '${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
|
||||
'endTime': '${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
|
||||
'startTime':
|
||||
'${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
|
||||
'endTime':
|
||||
'${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
|
||||
};
|
||||
|
||||
|
||||
await settings.addGrayscaleSchedule(newSchedule);
|
||||
}
|
||||
|
||||
Future<void> _editSchedule(BuildContext context, SettingsService settings, int index) async {
|
||||
Future<void> _editSchedule(
|
||||
BuildContext context,
|
||||
SettingsService settings,
|
||||
int index,
|
||||
) async {
|
||||
final schedules = settings.grayscaleSchedules;
|
||||
if (index >= schedules.length) return;
|
||||
|
||||
|
||||
final current = schedules[index];
|
||||
final startParts = (current['startTime'] as String).split(':');
|
||||
final endParts = (current['endTime'] as String).split(':');
|
||||
|
||||
|
||||
// Capture context before async gap
|
||||
final capturedContext = context;
|
||||
|
||||
|
||||
TimeOfDay? startTime = await showTimePicker(
|
||||
context: capturedContext,
|
||||
initialTime: TimeOfDay(
|
||||
@@ -595,9 +810,9 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
),
|
||||
helpText: 'Select start time',
|
||||
);
|
||||
|
||||
|
||||
if (startTime == null || !capturedContext.mounted) return;
|
||||
|
||||
|
||||
TimeOfDay? endTime = await showTimePicker(
|
||||
context: capturedContext,
|
||||
initialTime: TimeOfDay(
|
||||
@@ -606,27 +821,31 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
),
|
||||
helpText: 'Select end time',
|
||||
);
|
||||
|
||||
|
||||
if (endTime == null || !capturedContext.mounted) return;
|
||||
|
||||
|
||||
final updatedSchedule = {
|
||||
...current,
|
||||
'startTime': '${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
|
||||
'endTime': '${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
|
||||
'startTime':
|
||||
'${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}',
|
||||
'endTime':
|
||||
'${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}',
|
||||
};
|
||||
|
||||
|
||||
await settings.updateGrayscaleSchedule(index, updatedSchedule);
|
||||
}
|
||||
|
||||
Future<void> _toggleSchedule(SettingsService settings, int index) async {
|
||||
final schedules = List<Map<String, dynamic>>.from(settings.grayscaleSchedules);
|
||||
final schedules = List<Map<String, dynamic>>.from(
|
||||
settings.grayscaleSchedules,
|
||||
);
|
||||
if (index >= schedules.length) return;
|
||||
|
||||
|
||||
schedules[index] = {
|
||||
...schedules[index],
|
||||
'enabled': !(schedules[index]['enabled'] as bool),
|
||||
};
|
||||
|
||||
|
||||
await settings.setGrayscaleSchedules(schedules);
|
||||
}
|
||||
|
||||
@@ -648,7 +867,7 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
if (confirmed == true) {
|
||||
await settings.removeGrayscaleSchedule(index);
|
||||
}
|
||||
@@ -669,7 +888,8 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
const _SectionHeader(title: 'DISPLAY'),
|
||||
_SwitchTile(
|
||||
title: 'Grayscale Mode',
|
||||
subtitle: 'Makes Instagram black & white — reduces dopamine response',
|
||||
subtitle:
|
||||
'Makes Instagram black & white — reduces dopamine response',
|
||||
value: settings.grayscaleEnabled,
|
||||
onChanged: (v) => settings.setGrayscaleEnabled(v),
|
||||
),
|
||||
@@ -687,7 +907,7 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
style: TextStyle(fontSize: 12, height: 1.5),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Status indicator
|
||||
if (settings.grayscaleSchedules.isNotEmpty)
|
||||
Padding(
|
||||
@@ -695,26 +915,38 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: settings.isGrayscaleActiveNow ? Colors.green.withValues(alpha: 0.1) : Colors.orange.withValues(alpha: 0.1),
|
||||
color: settings.isGrayscaleActiveNow
|
||||
? Colors.green.withValues(alpha: 0.1)
|
||||
: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: settings.isGrayscaleActiveNow ? Colors.green.withValues(alpha: 0.3) : Colors.orange.withValues(alpha: 0.3)),
|
||||
border: Border.all(
|
||||
color: settings.isGrayscaleActiveNow
|
||||
? Colors.green.withValues(alpha: 0.3)
|
||||
: Colors.orange.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
settings.isGrayscaleActiveNow ? Icons.check_circle : Icons.schedule,
|
||||
color: settings.isGrayscaleActiveNow ? Colors.greenAccent : Colors.orangeAccent,
|
||||
size: 20
|
||||
settings.isGrayscaleActiveNow
|
||||
? Icons.check_circle
|
||||
: Icons.schedule,
|
||||
color: settings.isGrayscaleActiveNow
|
||||
? Colors.greenAccent
|
||||
: Colors.orangeAccent,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
settings.isGrayscaleActiveNow
|
||||
? 'Grayscale is active now'
|
||||
settings.isGrayscaleActiveNow
|
||||
? 'Grayscale is active now'
|
||||
: 'Grayscale is currently inactive',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: settings.isGrayscaleActiveNow ? Colors.greenAccent : Colors.orangeAccent
|
||||
fontSize: 13,
|
||||
color: settings.isGrayscaleActiveNow
|
||||
? Colors.greenAccent
|
||||
: Colors.orangeAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -722,7 +954,7 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Schedule list
|
||||
...List.generate(settings.grayscaleSchedules.length, (index) {
|
||||
final schedule = settings.grayscaleSchedules[index];
|
||||
@@ -732,11 +964,14 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: (isEnabled ? Colors.purpleAccent : Colors.grey).withValues(alpha: 0.12),
|
||||
color: (isEnabled ? Colors.purpleAccent : Colors.grey)
|
||||
.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
isEnabled ? Icons.play_circle_outline : Icons.pause_circle_outline,
|
||||
isEnabled
|
||||
? Icons.play_circle_outline
|
||||
: Icons.pause_circle_outline,
|
||||
color: isEnabled ? Colors.purpleAccent : Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
@@ -750,7 +985,10 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
),
|
||||
subtitle: Text(
|
||||
isEnabled ? 'Active' : 'Disabled',
|
||||
style: TextStyle(fontSize: 12, color: isDark ? Colors.white54 : Colors.black45),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? Colors.white54 : Colors.black45,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -795,7 +1033,7 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
onTap: () => _editSchedule(context, settings, index),
|
||||
);
|
||||
}),
|
||||
|
||||
|
||||
// Add schedule button
|
||||
ListTile(
|
||||
leading: Container(
|
||||
@@ -805,16 +1043,26 @@ class _AppearancePageState extends State<AppearancePage> {
|
||||
color: Colors.green.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.add_circle_outline, color: Colors.green, size: 20),
|
||||
child: const Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: Colors.green,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: const Text(
|
||||
'Add Schedule',
|
||||
style: TextStyle(color: Colors.green),
|
||||
),
|
||||
title: const Text('Add Schedule', style: TextStyle(color: Colors.green)),
|
||||
subtitle: Text(
|
||||
'Add a new grayscale schedule',
|
||||
style: TextStyle(fontSize: 12, color: isDark ? Colors.white54 : Colors.black45),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? Colors.white54 : Colors.black45,
|
||||
),
|
||||
),
|
||||
onTap: () => _addSchedule(context, settings),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
@@ -966,15 +1214,9 @@ class _SwitchTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile(
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
title: Text(title, style: const TextStyle(fontSize: 15)),
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
subtitle ?? '',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
)
|
||||
? Text(subtitle ?? '', style: const TextStyle(fontSize: 12))
|
||||
: null,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
@@ -982,6 +1224,131 @@ class _SwitchTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ChoiceTile<T> extends StatelessWidget {
|
||||
final String title;
|
||||
final T value;
|
||||
final String label;
|
||||
final List<T> options;
|
||||
final String Function(T value) optionLabel;
|
||||
final ValueChanged<T> onSelected;
|
||||
|
||||
const _ChoiceTile({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.options,
|
||||
required this.optionLabel,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(title, style: const TextStyle(fontSize: 15)),
|
||||
subtitle: Text(label, style: const TextStyle(fontSize: 12)),
|
||||
trailing: PopupMenuButton<T>(
|
||||
initialValue: value,
|
||||
onSelected: onSelected,
|
||||
itemBuilder: (context) => options
|
||||
.map(
|
||||
(option) => PopupMenuItem<T>(
|
||||
value: option,
|
||||
child: Text(optionLabel(option)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: const Icon(Icons.expand_more_rounded, size: 22),
|
||||
),
|
||||
onTap: () async {
|
||||
final selected = await showModalBottomSheet<T>(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: options
|
||||
.map(
|
||||
(option) => ListTile(
|
||||
title: Text(optionLabel(option)),
|
||||
trailing: option == value
|
||||
? const Icon(Icons.check_rounded)
|
||||
: null,
|
||||
onTap: () => Navigator.pop(context, option),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (selected != null) onSelected(selected);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NumberEditTile extends StatelessWidget {
|
||||
final String title;
|
||||
final String label;
|
||||
final int initialValue;
|
||||
final int min;
|
||||
final int max;
|
||||
final String suffix;
|
||||
final ValueChanged<int> onSubmitted;
|
||||
|
||||
const _NumberEditTile({
|
||||
required this.title,
|
||||
required this.label,
|
||||
required this.initialValue,
|
||||
required this.min,
|
||||
required this.max,
|
||||
required this.suffix,
|
||||
required this.onSubmitted,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(title, style: const TextStyle(fontSize: 15)),
|
||||
subtitle: Text(label, style: const TextStyle(fontSize: 12)),
|
||||
trailing: const Icon(Icons.edit_outlined, size: 20),
|
||||
onTap: () async {
|
||||
final controller = TextEditingController(text: '$initialValue');
|
||||
final result = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
suffixText: suffix,
|
||||
helperText: '$min-$max $suffix',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final parsed = int.tryParse(controller.text.trim());
|
||||
if (parsed == null) return;
|
||||
Navigator.pop(dialogContext, parsed.clamp(min, max).toInt());
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
if (result != null) onSubmitted(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
const _SectionHeader({required this.title});
|
||||
|
||||
Reference in New Issue
Block a user