Files
FocusGram-Android/lib/services/settings_service.dart
T
2026-05-25 22:12:38 +05:45

831 lines
29 KiB
Dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'notification_service.dart';
/// Stores and retrieves all user-configurable app settings.
class SettingsService extends ChangeNotifier {
static const _keyBlurExplore = 'set_blur_explore';
static const _keyBlurReels = 'set_blur_reels';
static const _keyTapToUnblur = 'set_tap_to_unblur';
static const _keyShowBreathGate = 'set_show_breath_gate';
static const _keyRequireWordChallenge = 'set_require_word_challenge';
static const _keyRequireLongPress = 'set_require_long_press';
static const _keyBreathGateSeconds = 'breath_gate_seconds';
static const _keyWordChallengeCount = 'word_challenge_count';
static const _keyEnableTextSelection = 'set_enable_text_selection';
static const _keyEnabledTabs = 'set_enabled_tabs';
static const _keyShowInstaSettings = 'set_show_insta_settings';
static const _keyIsFirstRun = 'set_is_first_run';
// Focus / playback
static const _keyBlockAutoplay = 'block_autoplay';
// Extras (Phase 2)
static const _keyVideoDownloadEnabled = 'video_download_enabled';
static const _keyHideSuggestedPosts = 'hide_suggested_posts';
// ── FocusGram v2 overlay toggles ─────────────────────────────────────────
static const _keyV2GhostModeEnabled = 'v2_ghost_mode_enabled';
static const _keyV2AdBlockerDomEnabled = 'v2_adblock_dom_enabled';
static const _keyV2ContentHiderEnabled = 'v2_content_hider_enabled';
// Content hider flags (consumed by v2/content_hider.js via prefs keys)
static const _keyContentStories = 'content_stories';
static const _keyContentPosts = 'content_posts';
static const _keyContentReels = 'content_reels';
static const _keyContentSuggested = 'content_suggested';
// Grayscale mode - now supports multiple schedules
static const _keyGrayscaleEnabled = 'grayscale_enabled';
static const _keyGrayscaleSchedules = 'grayscale_schedules';
// Content filtering / UI hiding
static const _keyHideLikeCounts = 'hide_like_counts';
static const _keyHideFollowerCounts = 'hide_follower_counts';
static const _keyHideShopTab = 'hide_shop_tab';
// Minimal mode
static const _keyMinimalModeEnabled = 'minimal_mode_enabled';
// Minimal mode state tracking for smart restore
static const _keyMinimalModePrevDisableReels =
'minimal_mode_prev_disable_reels';
static const _keyMinimalModePrevDisableExplore =
'minimal_mode_prev_disable_explore';
static const _keyMinimalModePrevBlurExplore =
'minimal_mode_prev_blur_explore';
static const _keyMinimalModePrevBlockHomeFeedScroll =
'minimal_mode_prev_block_home_feed_scroll';
// Reels History
static const _keyReelsHistoryEnabled = 'reels_history_enabled';
// Privacy keys
static const _keySanitizeLinks = 'set_sanitize_links';
static const _keyNotifyDMs = 'set_notify_dms';
static const _keyNotifyActivity = 'set_notify_activity';
static const _keyNotifySessionEnd = 'set_notify_session_end';
static const _keyNotifyPersistent = 'set_notify_persistent';
// Focus mode settings
static const _keyGhostMode = 'ghost_mode';
static const _keyNoAds = 'no_ads';
static const _keyNoStories = 'no_stories';
static const _keyNoReels = 'no_reels';
static const _keyNoAutoplay = 'no_autoplay';
static const _keyNoDMs = 'no_dms';
SharedPreferences? _prefs;
bool _blurExplore = true;
bool _blurReels = false;
bool _tapToUnblur = true;
bool _requireLongPress = true;
bool _showBreathGate = true;
bool _requireWordChallenge = true;
int _breathGateSeconds = 10;
int _wordChallengeCount = 30;
bool _enableTextSelection = false;
bool _showInstaSettings = true;
bool _isDarkMode = true; // Default to dark as per existing app theme
bool _blockAutoplay = true;
bool _videoDownloadEnabled = false;
bool _hideSuggestedPosts = false;
// ── FocusGram v2 overlay toggles ─────────────────────────────────────────
bool _v2GhostModeEnabled = false;
bool _v2AdBlockerDomEnabled = false;
bool _v2ContentHiderEnabled = false;
// Content hider flags (consumed by v2/content_hider.js via prefs keys)
bool _contentStories = false;
bool _contentPosts = false;
bool _contentReels = false;
bool _contentSuggested = false;
// Grayscale mode - now supports multiple schedules
bool _grayscaleEnabled = false;
List<Map<String, dynamic>> _grayscaleSchedules = [];
// Content filtering / UI hiding
bool _hideLikeCounts = false;
bool _hideFollowerCounts = false;
bool _hideShopTab = false;
// These are now controlled internally by minimal mode
bool _disableReelsEntirely = false;
bool _disableExploreEntirely = false;
bool _blockHomeFeedScroll = false;
bool _minimalModeEnabled = false;
// Tracking for smart restore
bool _prevDisableReels = false;
bool _prevDisableExplore = false;
bool _prevBlurExplore = false;
bool _prevBlockHomeFeedScroll = false;
bool _reelsHistoryEnabled = true;
// Privacy defaults - notifications OFF by default
bool _sanitizeLinks = true;
bool _notifyDMs = false;
bool _notifyActivity = false;
bool _notifySessionEnd = false;
bool _notifyPersistent = false;
// Focus mode settings
bool _ghostMode = false;
bool _noAds = false;
bool _noStories = false;
bool _noReels = false;
bool _noAutoplay = false;
bool _noDMs = false;
List<String> _enabledTabs = [
'Home',
'Search',
'Reels',
'Messages',
'Profile',
];
bool _isFirstRun = true;
bool get blurExplore => _blurExplore;
bool get blurReels => _blurReels;
bool get tapToUnblur => _tapToUnblur;
bool get requireLongPress => _requireLongPress;
bool get showBreathGate => _showBreathGate;
bool get requireWordChallenge => _requireWordChallenge;
int get breathGateSeconds => _breathGateSeconds;
int get wordChallengeCount => _wordChallengeCount;
bool get enableTextSelection => _enableTextSelection;
bool get showInstaSettings => _showInstaSettings;
List<String> get enabledTabs => _enabledTabs;
bool get isFirstRun => _isFirstRun;
bool get isDarkMode => _isDarkMode;
bool get blockAutoplay => _blockAutoplay;
// Extras (Phase 2)
bool get videoDownloadEnabled => _videoDownloadEnabled;
bool get hideSuggestedPosts => _hideSuggestedPosts;
// ── FocusGram v2 overlay toggles ─────────────────────────────────────────
bool get v2GhostModeEnabled => _v2GhostModeEnabled;
bool get v2AdBlockerDomEnabled => _v2AdBlockerDomEnabled;
bool get v2ContentHiderEnabled => _v2ContentHiderEnabled;
bool get contentStories => _contentStories;
bool get contentPosts => _contentPosts;
bool get contentReels => _contentReels;
bool get contentSuggested => _contentSuggested;
bool get notifyDMs => _notifyDMs;
bool get notifyActivity => _notifyActivity;
bool get notifySessionEnd => _notifySessionEnd;
bool get notifyPersistent => _notifyPersistent;
bool get grayscaleEnabled => _grayscaleEnabled;
List<Map<String, dynamic>> get grayscaleSchedules => _grayscaleSchedules;
bool get hideLikeCounts => _hideLikeCounts;
bool get hideFollowerCounts => _hideFollowerCounts;
bool get hideShopTab => _hideShopTab;
// Focus mode settings
bool get ghostMode => _ghostMode;
bool get noAds => _noAds;
bool get noStories => _noStories;
bool get noReels => _noReels;
bool get noAutoplay => _noAutoplay;
bool get noDMs => _noDMs;
// These are now controlled by minimal mode only
bool get disableReelsEntirely => _disableReelsEntirely;
bool get disableExploreEntirely => _disableExploreEntirely;
bool get blockHomeFeedScroll => _blockHomeFeedScroll;
bool get minimalModeEnabled => _minimalModeEnabled;
bool get reelsHistoryEnabled => _reelsHistoryEnabled;
/// True if grayscale should currently be applied, considering the manual
/// toggle and the optional schedules.
bool get isGrayscaleActiveNow {
if (_grayscaleEnabled) return true;
if (_grayscaleSchedules.isEmpty) return false;
final now = DateTime.now();
final currentMinutes = now.hour * 60 + now.minute;
for (final schedule in _grayscaleSchedules) {
if (schedule['enabled'] != true) continue;
try {
final startParts = (schedule['startTime'] as String).split(':');
final endParts = (schedule['endTime'] as String).split(':');
if (startParts.length != 2 || endParts.length != 2) continue;
final startMinutes =
int.parse(startParts[0]) * 60 + int.parse(startParts[1]);
final endMinutes = int.parse(endParts[0]) * 60 + int.parse(endParts[1]);
// Handle overnight schedules (e.g., 21:00 to 06:00)
if (endMinutes < startMinutes) {
// Overnight: active if current time is >= start OR < end
if (currentMinutes >= startMinutes || currentMinutes < endMinutes) {
return true;
}
} else {
// Same day: active if current time is between start and end
if (currentMinutes >= startMinutes && currentMinutes < endMinutes) {
return true;
}
}
} catch (_) {
continue;
}
}
return false;
}
// Privacy getters
bool get sanitizeLinks => _sanitizeLinks;
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
_blurExplore = _prefs!.getBool(_keyBlurExplore) ?? true;
_blurReels = _prefs!.getBool(_keyBlurReels) ?? false;
_tapToUnblur = _prefs!.getBool(_keyTapToUnblur) ?? true;
_requireLongPress = _prefs!.getBool(_keyRequireLongPress) ?? true;
_showBreathGate = _prefs!.getBool(_keyShowBreathGate) ?? true;
_requireWordChallenge = _prefs!.getBool(_keyRequireWordChallenge) ?? true;
_breathGateSeconds = (_prefs!.getInt(_keyBreathGateSeconds) ?? 10)
.clamp(3, 60)
.toInt();
_wordChallengeCount = _normaliseWordChallengeCount(
_prefs!.getInt(_keyWordChallengeCount) ?? 30,
);
_enableTextSelection = _prefs!.getBool(_keyEnableTextSelection) ?? false;
_showInstaSettings = _prefs!.getBool(_keyShowInstaSettings) ?? true;
_blockAutoplay = _prefs!.getBool(_keyBlockAutoplay) ?? true;
// Extras (Phase 2) - defaults OFF for safety/non-invasive behavior
_videoDownloadEnabled = _prefs!.getBool(_keyVideoDownloadEnabled) ?? false;
_hideSuggestedPosts = _prefs!.getBool(_keyHideSuggestedPosts) ?? false;
// ── FocusGram v2 overlay toggles ─────────────────────────────────────────
_v2GhostModeEnabled = _prefs!.getBool(_keyV2GhostModeEnabled) ?? false;
_v2AdBlockerDomEnabled =
_prefs!.getBool(_keyV2AdBlockerDomEnabled) ?? false;
_v2ContentHiderEnabled =
_prefs!.getBool(_keyV2ContentHiderEnabled) ?? false;
_contentStories = _prefs!.getBool(_keyContentStories) ?? false;
_contentPosts = _prefs!.getBool(_keyContentPosts) ?? false;
_contentReels = _prefs!.getBool(_keyContentReels) ?? false;
_contentSuggested = _prefs!.getBool(_keyContentSuggested) ?? false;
_hideSuggestedPosts = _prefs!.getBool(_keyHideSuggestedPosts) ?? false;
// Load grayscale schedules
final schedulesJson = _prefs!.getString(_keyGrayscaleSchedules);
if (schedulesJson != null) {
try {
_grayscaleSchedules = List<Map<String, dynamic>>.from(
(jsonDecode(schedulesJson) as List).map(
(e) => Map<String, dynamic>.from(e),
),
);
} catch (_) {
_grayscaleSchedules = [];
}
}
_hideLikeCounts = _prefs!.getBool(_keyHideLikeCounts) ?? false;
_hideFollowerCounts = _prefs!.getBool(_keyHideFollowerCounts) ?? false;
_hideShopTab = _prefs!.getBool(_keyHideShopTab) ?? false;
// Load minimal mode
_minimalModeEnabled = _prefs!.getBool(_keyMinimalModeEnabled) ?? false;
// Load previous states for smart restore
_prevDisableReels =
_prefs!.getBool(_keyMinimalModePrevDisableReels) ?? false;
_prevDisableExplore =
_prefs!.getBool(_keyMinimalModePrevDisableExplore) ?? false;
_prevBlurExplore = _prefs!.getBool(_keyMinimalModePrevBlurExplore) ?? false;
_prevBlockHomeFeedScroll =
_prefs!.getBool(_keyMinimalModePrevBlockHomeFeedScroll) ?? false;
// These are now internal states, not user-facing settings
_disableReelsEntirely =
_prefs!.getBool('internal_disable_reels_entirely') ?? false;
_disableExploreEntirely =
_prefs!.getBool('internal_disable_explore_entirely') ?? false;
_blockHomeFeedScroll =
_prefs!.getBool('internal_block_home_feed_scroll') ?? false;
_reelsHistoryEnabled = _prefs!.getBool(_keyReelsHistoryEnabled) ?? true;
// Focus mode settings
_ghostMode = _prefs!.getBool(_keyGhostMode) ?? false;
_noAds = _prefs!.getBool(_keyNoAds) ?? false;
_noStories = _prefs!.getBool(_keyNoStories) ?? false;
_noReels = _prefs!.getBool(_keyNoReels) ?? false;
_noAutoplay = _prefs!.getBool(_keyNoAutoplay) ?? false;
_noDMs = _prefs!.getBool(_keyNoDMs) ?? false;
_sanitizeLinks = _prefs!.getBool(_keySanitizeLinks) ?? true;
_notifyDMs = _prefs!.getBool(_keyNotifyDMs) ?? false;
_notifyActivity = _prefs!.getBool(_keyNotifyActivity) ?? false;
_notifySessionEnd = _prefs!.getBool(_keyNotifySessionEnd) ?? false;
_notifyPersistent = _prefs!.getBool(_keyNotifyPersistent) ?? false;
_enabledTabs =
(_prefs!.getStringList(_keyEnabledTabs) ??
['Home', 'Search', 'Reels', 'Messages', 'Profile'])
..remove('Create');
if (!_enabledTabs.contains('Messages') && _enabledTabs.length < 5) {
// Migration: add Messages if missing
_enabledTabs.insert(3, 'Messages');
}
_isFirstRun = _prefs!.getBool(_keyIsFirstRun) ?? true;
notifyListeners();
}
Future<void> setFirstRunCompleted() async {
_isFirstRun = false;
await _prefs?.setBool(_keyIsFirstRun, false);
notifyListeners();
}
Future<void> setBlurExplore(bool v) async {
_blurExplore = v;
await _prefs?.setBool(_keyBlurExplore, v);
if (_minimalModeEnabled) {
await _checkAndAutoDisableMinimalMode();
}
notifyListeners();
}
Future<void> setBlurReels(bool v) async {
_blurReels = v;
// Sync blur reels with blur explore - enabling one enables the other
if (v && !_blurExplore) {
_blurExplore = true;
await _prefs?.setBool(_keyBlurExplore, true);
}
await _prefs?.setBool(_keyBlurReels, v);
notifyListeners();
}
Future<void> setTapToUnblur(bool v) async {
_tapToUnblur = v;
await _prefs?.setBool(_keyTapToUnblur, v);
notifyListeners();
}
Future<void> setRequireLongPress(bool v) async {
_requireLongPress = v;
await _prefs?.setBool(_keyRequireLongPress, v);
notifyListeners();
}
Future<void> setShowBreathGate(bool v) async {
_showBreathGate = v;
await _prefs?.setBool(_keyShowBreathGate, v);
notifyListeners();
}
Future<void> setRequireWordChallenge(bool v) async {
_requireWordChallenge = v;
await _prefs?.setBool(_keyRequireWordChallenge, v);
notifyListeners();
}
Future<void> setBreathGateSeconds(int seconds) async {
final clamped = seconds.clamp(3, 60);
_breathGateSeconds = clamped.toInt();
await _prefs?.setInt(_keyBreathGateSeconds, _breathGateSeconds);
// Defer notifyListeners to next microtask to avoid rebuild conflicts
Future.microtask(notifyListeners);
}
Future<void> setWordChallengeCount(int count) async {
_wordChallengeCount = _normaliseWordChallengeCount(count);
await _prefs?.setInt(_keyWordChallengeCount, _wordChallengeCount);
notifyListeners();
}
int resolvedWordChallengeCount() {
if (_wordChallengeCount != 0) return _wordChallengeCount;
final now = DateTime.now().microsecondsSinceEpoch;
return 10 + (now % 26);
}
static int _normaliseWordChallengeCount(int count) {
if (count == 0) return 0;
const allowed = [20, 25, 30, 35];
return allowed.contains(count) ? count : 30;
}
Future<void> setEnableTextSelection(bool v) async {
_enableTextSelection = v;
await _prefs?.setBool(_keyEnableTextSelection, v);
notifyListeners();
}
Future<void> setShowInstaSettings(bool v) async {
_showInstaSettings = v;
await _prefs?.setBool(_keyShowInstaSettings, v);
notifyListeners();
}
Future<void> setBlockAutoplay(bool v) async {
_blockAutoplay = v;
await _prefs?.setBool(_keyBlockAutoplay, v);
notifyListeners();
}
// ── Extras (Phase 2) ──────────────────────────────────────────────────────
Future<void> setVideoDownloadEnabled(bool v) async {
_videoDownloadEnabled = v;
await _prefs?.setBool(_keyVideoDownloadEnabled, v);
notifyListeners();
}
Future<void> setHideSuggestedPosts(bool v) async {
_hideSuggestedPosts = v;
await _prefs?.setBool(_keyHideSuggestedPosts, v);
notifyListeners();
}
Future<void> setGrayscaleEnabled(bool v) async {
_grayscaleEnabled = v;
await _prefs?.setBool(_keyGrayscaleEnabled, v);
notifyListeners();
}
Future<void> setGrayscaleSchedules(
List<Map<String, dynamic>> schedules,
) async {
_grayscaleSchedules = schedules;
await _prefs?.setString(_keyGrayscaleSchedules, jsonEncode(schedules));
notifyListeners();
}
Future<void> addGrayscaleSchedule(Map<String, dynamic> schedule) async {
_grayscaleSchedules.add(schedule);
await _prefs?.setString(
_keyGrayscaleSchedules,
jsonEncode(_grayscaleSchedules),
);
notifyListeners();
}
Future<void> updateGrayscaleSchedule(
int index,
Map<String, dynamic> schedule,
) async {
if (index >= 0 && index < _grayscaleSchedules.length) {
_grayscaleSchedules[index] = schedule;
await _prefs?.setString(
_keyGrayscaleSchedules,
jsonEncode(_grayscaleSchedules),
);
notifyListeners();
}
}
Future<void> removeGrayscaleSchedule(int index) async {
if (index >= 0 && index < _grayscaleSchedules.length) {
_grayscaleSchedules.removeAt(index);
await _prefs?.setString(
_keyGrayscaleSchedules,
jsonEncode(_grayscaleSchedules),
);
notifyListeners();
}
}
Future<void> setHideShopTab(bool v) async {
_hideShopTab = v;
await _prefs?.setBool(_keyHideShopTab, v);
notifyListeners();
}
// ── FocusGram v2 overlay setters ──────────────────────────────────────────
Future<void> setV2GhostModeEnabled(bool v) async {
_v2GhostModeEnabled = v;
await _prefs?.setBool(_keyV2GhostModeEnabled, v);
notifyListeners();
}
Future<void> setV2AdBlockerDomEnabled(bool v) async {
_v2AdBlockerDomEnabled = v;
await _prefs?.setBool(_keyV2AdBlockerDomEnabled, v);
notifyListeners();
}
Future<void> setV2ContentHiderEnabled(bool v) async {
_v2ContentHiderEnabled = v;
await _prefs?.setBool(_keyV2ContentHiderEnabled, v);
notifyListeners();
}
Future<void> setContentStoriesEnabled(bool v) async {
if (v && !_v2ContentHiderEnabled) {
_v2ContentHiderEnabled = true;
await _prefs?.setBool(_keyV2ContentHiderEnabled, true);
}
_contentStories = v;
await _prefs?.setBool(_keyContentStories, v);
notifyListeners();
}
Future<void> setContentPostsEnabled(bool v) async {
if (v && !_v2ContentHiderEnabled) {
_v2ContentHiderEnabled = true;
await _prefs?.setBool(_keyV2ContentHiderEnabled, true);
}
_contentPosts = v;
await _prefs?.setBool(_keyContentPosts, v);
notifyListeners();
}
Future<void> setContentReelsEnabled(bool v) async {
if (v && !_v2ContentHiderEnabled) {
_v2ContentHiderEnabled = true;
await _prefs?.setBool(_keyV2ContentHiderEnabled, true);
}
_contentReels = v;
await _prefs?.setBool(_keyContentReels, v);
notifyListeners();
}
Future<void> setContentSuggestedEnabled(bool v) async {
if (v && !_v2ContentHiderEnabled) {
_v2ContentHiderEnabled = true;
await _prefs?.setBool(_keyV2ContentHiderEnabled, true);
}
_contentSuggested = v;
await _prefs?.setBool(_keyContentSuggested, v);
notifyListeners();
}
Future<void> setHideFollowerCounts(bool v) async {
_hideFollowerCounts = v;
await _prefs?.setBool(_keyHideFollowerCounts, v);
notifyListeners();
}
/// Setter for internal disable reels state (used by minimal mode submenu)
/// Auto-disables minimal mode if all features are turned off
Future<void> setDisableReelsEntirelyInternal(bool v) async {
_disableReelsEntirely = v;
await _prefs?.setBool('internal_disable_reels_entirely', v);
// Check if minimal mode should auto-disable
await _checkAndAutoDisableMinimalMode();
notifyListeners();
}
/// Setter for internal disable explore state (used by minimal mode submenu)
/// Auto-disables minimal mode if all features are turned off
Future<void> setDisableExploreEntirelyInternal(bool v) async {
_disableExploreEntirely = v;
await _prefs?.setBool('internal_disable_explore_entirely', v);
// Check if minimal mode should auto-disable
await _checkAndAutoDisableMinimalMode();
notifyListeners();
}
/// Setter for home feed scroll blocking state (used by minimal mode submenu).
Future<void> setBlockHomeFeedScrollInternal(bool v) async {
_blockHomeFeedScroll = v;
await _prefs?.setBool('internal_block_home_feed_scroll', v);
await _checkAndAutoDisableMinimalMode();
notifyListeners();
}
/// Helper: Auto-disable minimal mode if all its features are disabled
/// This ensures minimal mode auto-turns-off when user disables all sub-features
///
/// NOTE: We must check the RAW state variables here, NOT the public getters
/// (disableReelsEntirely/disableExploreEntirely), because those getters
/// unconditionally return true when _minimalModeEnabled is true, which would
/// make the "all disabled" condition impossible to reach.
Future<void> _checkAndAutoDisableMinimalMode() async {
if (!_minimalModeEnabled) return;
// Check the RAW saved state, not the getters
final rawReels =
_prefs?.getBool('internal_disable_reels_entirely') ??
_disableReelsEntirely;
final rawExplore =
_prefs?.getBool('internal_disable_explore_entirely') ??
_disableExploreEntirely;
final rawHomeFeedScroll =
_prefs?.getBool('internal_block_home_feed_scroll') ??
_blockHomeFeedScroll;
final allDisabled =
!rawReels && !rawExplore && !rawHomeFeedScroll && !_blurExplore;
if (allDisabled) {
_minimalModeEnabled = false;
await _prefs?.setBool(_keyMinimalModeEnabled, false);
}
}
/// Smart minimal mode toggle with state preservation
Future<void> setMinimalModeEnabled(bool v) async {
if (v) {
// ── Turning ON ──────────────────────────────────────────────────────────
// Save current pre-minimal-mode states so we can restore them later
_prevDisableReels = _disableReelsEntirely;
_prevDisableExplore = _disableExploreEntirely;
_prevBlurExplore = _blurExplore;
_prevBlockHomeFeedScroll = _blockHomeFeedScroll;
await _prefs?.setBool(_keyMinimalModePrevDisableReels, _prevDisableReels);
await _prefs?.setBool(
_keyMinimalModePrevDisableExplore,
_prevDisableExplore,
);
await _prefs?.setBool(_keyMinimalModePrevBlurExplore, _prevBlurExplore);
await _prefs?.setBool(
_keyMinimalModePrevBlockHomeFeedScroll,
_prevBlockHomeFeedScroll,
);
_minimalModeEnabled = true;
_disableReelsEntirely = true;
_disableExploreEntirely = true;
_blockHomeFeedScroll = true;
_blurExplore = true; // blurExplore is controlled by minimal mode while ON
await _prefs?.setBool(_keyMinimalModeEnabled, true);
await _prefs?.setBool('internal_disable_reels_entirely', true);
await _prefs?.setBool('internal_disable_explore_entirely', true);
await _prefs?.setBool('internal_block_home_feed_scroll', true);
await _prefs?.setBool(_keyBlurExplore, true);
} else {
// ── Turning OFF ─────────────────────────────────────────────────────────
// Restore states that were saved BEFORE minimal mode was enabled.
// _prevDisableReels/Explore were saved at the moment minimal mode turned ON.
_minimalModeEnabled = false;
_disableReelsEntirely = _prevDisableReels;
_disableExploreEntirely = _prevDisableExplore;
_blockHomeFeedScroll = _prevBlockHomeFeedScroll;
// For blurExplore: use _prevBlurExplore if it was saved, otherwise fall back
// to the saved prefs value (covers the case where no prev was saved).
_blurExplore = _prevBlurExplore;
await _prefs?.setBool(_keyMinimalModeEnabled, false);
await _prefs?.setBool(
'internal_disable_reels_entirely',
_disableReelsEntirely,
);
await _prefs?.setBool(
'internal_disable_explore_entirely',
_disableExploreEntirely,
);
await _prefs?.setBool(
'internal_block_home_feed_scroll',
_blockHomeFeedScroll,
);
await _prefs?.setBool(_keyBlurExplore, _blurExplore);
// After restoring, check whether the user had ALL minimal features OFF
// already — if so, minimal mode should stay off (no-op).
if (!_disableReelsEntirely &&
!_disableExploreEntirely &&
!_blockHomeFeedScroll &&
!_blurExplore) {
// All features are off — minimal mode correctly stays off. No action needed.
}
}
notifyListeners();
}
Future<void> setReelsHistoryEnabled(bool v) async {
_reelsHistoryEnabled = v;
await _prefs?.setBool(_keyReelsHistoryEnabled, v);
notifyListeners();
}
void setDarkMode(bool dark) {
if (_isDarkMode != dark) {
_isDarkMode = dark;
notifyListeners();
}
}
Future<void> setSanitizeLinks(bool v) async {
_sanitizeLinks = v;
await _prefs?.setBool(_keySanitizeLinks, v);
notifyListeners();
}
Future<void> setNotifyDMs(bool v) async {
_notifyDMs = v;
await _prefs?.setBool(_keyNotifyDMs, v);
if (v) await NotificationService().requestPermissionsNow();
notifyListeners();
}
Future<void> setNotifyActivity(bool v) async {
_notifyActivity = v;
await _prefs?.setBool(_keyNotifyActivity, v);
if (v) await NotificationService().requestPermissionsNow();
notifyListeners();
}
Future<void> setNotifySessionEnd(bool v) async {
_notifySessionEnd = v;
await _prefs?.setBool(_keyNotifySessionEnd, v);
if (v) await NotificationService().requestPermissionsNow();
notifyListeners();
}
Future<void> setNotifyPersistent(bool v) async {
_notifyPersistent = v;
await _prefs?.setBool(_keyNotifyPersistent, v);
if (v) {
await NotificationService().requestPermissionsNow();
} else {
await NotificationService().cancelPersistentNotification(id: 5001);
}
notifyListeners();
}
// ── Focus mode settings ──────────────────────────────────────────────────────
Future<void> setGhostMode(bool v) async {
_ghostMode = v;
await _prefs?.setBool(_keyGhostMode, v);
notifyListeners();
}
Future<void> setNoAds(bool v) async {
_noAds = v;
await _prefs?.setBool(_keyNoAds, v);
notifyListeners();
}
Future<void> setNoStories(bool v) async {
_noStories = v;
await _prefs?.setBool(_keyNoStories, v);
notifyListeners();
}
Future<void> setNoReels(bool v) async {
_noReels = v;
await _prefs?.setBool(_keyNoReels, v);
notifyListeners();
}
Future<void> setNoAutoplay(bool v) async {
_noAutoplay = v;
await _prefs?.setBool(_keyNoAutoplay, v);
notifyListeners();
}
Future<void> setNoDMs(bool v) async {
_noDMs = v;
await _prefs?.setBool(_keyNoDMs, v);
notifyListeners();
}
Future<void> toggleTab(String tab) async {
if (_enabledTabs.contains(tab)) {
if (_enabledTabs.length > 1) {
_enabledTabs.remove(tab);
}
} else {
_enabledTabs.add(tab);
}
await _prefs?.setStringList(_keyEnabledTabs, _enabledTabs);
notifyListeners();
}
Future<void> reorderTab(int oldIndex, int newIndex) async {
if (newIndex > oldIndex) newIndex -= 1;
final String item = _enabledTabs.removeAt(oldIndex);
_enabledTabs.insert(newIndex, item);
await _prefs?.setStringList(_keyEnabledTabs, _enabledTabs);
notifyListeners();
}
}