mirror of
https://github.com/Ujwal223/FocusGram.git
synced 2026-05-26 17:07:47 +02:00
831 lines
29 KiB
Dart
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();
|
|
}
|
|
}
|