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> _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 _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 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> 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 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>.from( (jsonDecode(schedulesJson) as List).map( (e) => Map.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 setFirstRunCompleted() async { _isFirstRun = false; await _prefs?.setBool(_keyIsFirstRun, false); notifyListeners(); } Future setBlurExplore(bool v) async { _blurExplore = v; await _prefs?.setBool(_keyBlurExplore, v); if (_minimalModeEnabled) { await _checkAndAutoDisableMinimalMode(); } notifyListeners(); } Future 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 setTapToUnblur(bool v) async { _tapToUnblur = v; await _prefs?.setBool(_keyTapToUnblur, v); notifyListeners(); } Future setRequireLongPress(bool v) async { _requireLongPress = v; await _prefs?.setBool(_keyRequireLongPress, v); notifyListeners(); } Future setShowBreathGate(bool v) async { _showBreathGate = v; await _prefs?.setBool(_keyShowBreathGate, v); notifyListeners(); } Future setRequireWordChallenge(bool v) async { _requireWordChallenge = v; await _prefs?.setBool(_keyRequireWordChallenge, v); notifyListeners(); } Future setBreathGateSeconds(int seconds) async { _breathGateSeconds = seconds.clamp(3, 60).toInt(); await _prefs?.setInt(_keyBreathGateSeconds, _breathGateSeconds); notifyListeners(); } Future 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 setEnableTextSelection(bool v) async { _enableTextSelection = v; await _prefs?.setBool(_keyEnableTextSelection, v); notifyListeners(); } Future setShowInstaSettings(bool v) async { _showInstaSettings = v; await _prefs?.setBool(_keyShowInstaSettings, v); notifyListeners(); } Future setBlockAutoplay(bool v) async { _blockAutoplay = v; await _prefs?.setBool(_keyBlockAutoplay, v); notifyListeners(); } // ── Extras (Phase 2) ────────────────────────────────────────────────────── Future setVideoDownloadEnabled(bool v) async { _videoDownloadEnabled = v; await _prefs?.setBool(_keyVideoDownloadEnabled, v); notifyListeners(); } Future setHideSuggestedPosts(bool v) async { _hideSuggestedPosts = v; await _prefs?.setBool(_keyHideSuggestedPosts, v); notifyListeners(); } Future setGrayscaleEnabled(bool v) async { _grayscaleEnabled = v; await _prefs?.setBool(_keyGrayscaleEnabled, v); notifyListeners(); } Future setGrayscaleSchedules( List> schedules, ) async { _grayscaleSchedules = schedules; await _prefs?.setString(_keyGrayscaleSchedules, jsonEncode(schedules)); notifyListeners(); } Future addGrayscaleSchedule(Map schedule) async { _grayscaleSchedules.add(schedule); await _prefs?.setString( _keyGrayscaleSchedules, jsonEncode(_grayscaleSchedules), ); notifyListeners(); } Future updateGrayscaleSchedule( int index, Map schedule, ) async { if (index >= 0 && index < _grayscaleSchedules.length) { _grayscaleSchedules[index] = schedule; await _prefs?.setString( _keyGrayscaleSchedules, jsonEncode(_grayscaleSchedules), ); notifyListeners(); } } Future removeGrayscaleSchedule(int index) async { if (index >= 0 && index < _grayscaleSchedules.length) { _grayscaleSchedules.removeAt(index); await _prefs?.setString( _keyGrayscaleSchedules, jsonEncode(_grayscaleSchedules), ); notifyListeners(); } } Future setHideShopTab(bool v) async { _hideShopTab = v; await _prefs?.setBool(_keyHideShopTab, v); notifyListeners(); } // ── FocusGram v2 overlay setters ────────────────────────────────────────── Future setV2GhostModeEnabled(bool v) async { _v2GhostModeEnabled = v; await _prefs?.setBool(_keyV2GhostModeEnabled, v); notifyListeners(); } Future setV2AdBlockerDomEnabled(bool v) async { _v2AdBlockerDomEnabled = v; await _prefs?.setBool(_keyV2AdBlockerDomEnabled, v); notifyListeners(); } Future setV2ContentHiderEnabled(bool v) async { _v2ContentHiderEnabled = v; await _prefs?.setBool(_keyV2ContentHiderEnabled, v); notifyListeners(); } Future setContentStoriesEnabled(bool v) async { if (v && !_v2ContentHiderEnabled) { _v2ContentHiderEnabled = true; await _prefs?.setBool(_keyV2ContentHiderEnabled, true); } _contentStories = v; await _prefs?.setBool(_keyContentStories, v); notifyListeners(); } Future setContentPostsEnabled(bool v) async { if (v && !_v2ContentHiderEnabled) { _v2ContentHiderEnabled = true; await _prefs?.setBool(_keyV2ContentHiderEnabled, true); } _contentPosts = v; await _prefs?.setBool(_keyContentPosts, v); notifyListeners(); } Future setContentReelsEnabled(bool v) async { if (v && !_v2ContentHiderEnabled) { _v2ContentHiderEnabled = true; await _prefs?.setBool(_keyV2ContentHiderEnabled, true); } _contentReels = v; await _prefs?.setBool(_keyContentReels, v); notifyListeners(); } Future setContentSuggestedEnabled(bool v) async { if (v && !_v2ContentHiderEnabled) { _v2ContentHiderEnabled = true; await _prefs?.setBool(_keyV2ContentHiderEnabled, true); } _contentSuggested = v; await _prefs?.setBool(_keyContentSuggested, v); notifyListeners(); } Future 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 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 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 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 _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 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 setReelsHistoryEnabled(bool v) async { _reelsHistoryEnabled = v; await _prefs?.setBool(_keyReelsHistoryEnabled, v); notifyListeners(); } void setDarkMode(bool dark) { if (_isDarkMode != dark) { _isDarkMode = dark; notifyListeners(); } } Future setSanitizeLinks(bool v) async { _sanitizeLinks = v; await _prefs?.setBool(_keySanitizeLinks, v); notifyListeners(); } Future setNotifyDMs(bool v) async { _notifyDMs = v; await _prefs?.setBool(_keyNotifyDMs, v); if (v) await NotificationService().requestPermissionsNow(); notifyListeners(); } Future setNotifyActivity(bool v) async { _notifyActivity = v; await _prefs?.setBool(_keyNotifyActivity, v); if (v) await NotificationService().requestPermissionsNow(); notifyListeners(); } Future setNotifySessionEnd(bool v) async { _notifySessionEnd = v; await _prefs?.setBool(_keyNotifySessionEnd, v); if (v) await NotificationService().requestPermissionsNow(); notifyListeners(); } Future 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 setGhostMode(bool v) async { _ghostMode = v; await _prefs?.setBool(_keyGhostMode, v); notifyListeners(); } Future setNoAds(bool v) async { _noAds = v; await _prefs?.setBool(_keyNoAds, v); notifyListeners(); } Future setNoStories(bool v) async { _noStories = v; await _prefs?.setBool(_keyNoStories, v); notifyListeners(); } Future setNoReels(bool v) async { _noReels = v; await _prefs?.setBool(_keyNoReels, v); notifyListeners(); } Future setNoAutoplay(bool v) async { _noAutoplay = v; await _prefs?.setBool(_keyNoAutoplay, v); notifyListeners(); } Future setNoDMs(bool v) async { _noDMs = v; await _prefs?.setBool(_keyNoDMs, v); notifyListeners(); } Future 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 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(); } }