From 04ec8ac5d1d014a5bddf93a490eb3527f118e57d Mon Sep 17 00:00:00 2001 From: esefxdz Date: Thu, 6 Aug 2026 23:08:26 +0300 Subject: [PATCH] feat: settings screen, haptic toggle, rain/lightning toggles, review docs --- .gitignore | 3 + CHANGELOG.md | 13 ++++ lib/components/connect_button.dart | 9 ++- lib/components/lightning.dart | 2 + lib/components/rain.dart | 2 + lib/components/sidebar_contents/settings.dart | 46 +++++++++++++ lib/components/sidebar_drawer.dart | 10 +++ lib/screens/settings/in_settings/haptic.dart | 64 +++++++++++++++++++ .../settings/in_settings/lightning.dart | 58 +++++++++++++++++ lib/screens/settings/in_settings/rain.dart | 58 +++++++++++++++++ lib/screens/settings/settings_screen.dart | 56 ++++++++++++++++ 11 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 lib/components/sidebar_contents/settings.dart create mode 100644 lib/screens/settings/in_settings/haptic.dart create mode 100644 lib/screens/settings/in_settings/lightning.dart create mode 100644 lib/screens/settings/in_settings/rain.dart create mode 100644 lib/screens/settings/settings_screen.dart diff --git a/.gitignore b/.gitignore index 523286f..c0dc50d 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ hivemind.zip XRAY_AUDIT.md TECHNICAL_DEBT.md DOCUMENTATION-ARCHITECTURE-V2.md +config.json +RESILIENCE_REVIEW.md +CONFIG_REVIEW.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a88de5b..6cbff7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,10 +37,23 @@ Versions follow [semver](https://semver.org/): MAJOR.MINOR.PATCH - Reality keypair generated (was placeholder). Throttle `uplinkOnly`/`downlinkOnly` corrected (seconds, not bytes/sec). Fallback rate limits removed (fingerprint). +### Added +- **Settings screen** — reached from a new sidebar row (between Website and + Check for updates). Slide-from-right navigation via `PageRouteBuilder`. + Folder: `lib/screens/settings/` with one file per feature in `in_settings/`. +- **Haptic feedback toggle** — on/off switch in Settings for `lightImpact()` + vibration on successful connect/disconnect. Default: off. Uses sync-bool + pattern (no await on tap, no SharedPreferences in connect button). +- **Rain/Lightning effect toggles** — two switches in Settings to control + the animated background effects. Default: on. Effects detect the toggle + within one frame (no Provider/notifier needed). + ### Changed - **Timer box spinner removed** — the "Connecting…"/"Syncing…" spinner under the countdown was redundant with the connect button's spinner. Timer card is now a clean countdown only (gray '00:00:00' when idle). +- **Sidebar** now has 7 rows (added Settings). `FRONTEND_POLISH_REVIEW.md` + fully triaged: 4 items fixed, 11 skipped. ### Server - `api` outbound + 3 routing rules in `xray_config_reality.json` (bootstrap diff --git a/lib/components/connect_button.dart b/lib/components/connect_button.dart index 9f625bf..05ed96c 100644 --- a/lib/components/connect_button.dart +++ b/lib/components/connect_button.dart @@ -5,6 +5,7 @@ import 'package:revoltvpn/logic/app_colors.dart'; import 'package:revoltvpn/logic/vpn_connection.dart'; import 'package:revoltvpn/logic/session_timer.dart'; import 'package:revoltvpn/logic/ad_manager.dart'; +import 'package:revoltvpn/screens/settings/in_settings/haptic.dart'; class ConnectButton extends StatefulWidget { const ConnectButton({super.key}); @@ -56,8 +57,6 @@ class _ConnectButtonState extends State if (now.difference(_lastTap).inMilliseconds < 1000) return; _lastTap = now; - HapticFeedback.lightImpact(); - final vpn = context.read(); final timer = context.read(); final ad = context.read(); @@ -67,6 +66,7 @@ class _ConnectButtonState extends State _busy = false; timer.stop(); await vpn.disconnect(); + if (hapticEnabled) HapticFeedback.lightImpact(); return; } @@ -84,7 +84,10 @@ class _ConnectButtonState extends State return; } final ok = await vpn.connect(); - if (ok) timer.start(); + if (ok) { + timer.start(); + if (hapticEnabled) HapticFeedback.lightImpact(); + } } finally { if (mounted) setState(() => _busy = false); } diff --git a/lib/components/lightning.dart b/lib/components/lightning.dart index 9a1d343..2bb1b8b 100644 --- a/lib/components/lightning.dart +++ b/lib/components/lightning.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:revoltvpn/logic/app_colors.dart'; +import 'package:revoltvpn/screens/settings/in_settings/lightning.dart'; /// 2D lightning bolt — a jagged yellow line from top to bottom with glow. @@ -77,6 +78,7 @@ class _LightningEffectState extends State @override Widget build(BuildContext context) { + if (!lightningEnabled) return const SizedBox.shrink(); return AnimatedBuilder( animation: _ctrl, builder: (_, child) { diff --git a/lib/components/rain.dart b/lib/components/rain.dart index 72fc73b..8584d9e 100644 --- a/lib/components/rain.dart +++ b/lib/components/rain.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:revoltvpn/screens/settings/in_settings/rain.dart'; /// Subtle animated rain effect — sits behind all UI as a full-screen overlay. /// @@ -78,6 +79,7 @@ class _RainEffectState extends State @override Widget build(BuildContext context) { + if (!rainEnabled) return const SizedBox.shrink(); return CustomPaint( painter: _RainPainter( drops: _drops, diff --git a/lib/components/sidebar_contents/settings.dart b/lib/components/sidebar_contents/settings.dart new file mode 100644 index 0000000..8ddcac9 --- /dev/null +++ b/lib/components/sidebar_contents/settings.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:revoltvpn/logic/app_colors.dart'; +import 'package:revoltvpn/screens/settings/settings_screen.dart'; + +// ── lib/components/sidebar_contents/settings.dart ──────────────────────────── +// Thin sidebar row — taps navigate to the full SettingsScreen. +// The actual settings UI lives in lib/screens/settings/ (separate folder, +// one file per feature). + +class SettingsButton extends StatelessWidget { + const SettingsButton({super.key}); + + // Opens the settings page sliding in from the right — matches the + // sidebar end-drawer animation so it feels like one continuous motion. + static void open(BuildContext context) { + Navigator.of(context).push( + PageRouteBuilder( + transitionDuration: const Duration(milliseconds: 300), + reverseTransitionDuration: const Duration(milliseconds: 250), + pageBuilder: (_, __, ___) => const SettingsScreen(), + transitionsBuilder: (_, animation, __, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1.0, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeOut, + )), + child: child, + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return IconButton( + icon: const Icon(Icons.settings, color: AppColors.textWhite, size: 28), + onPressed: () => open(context), + tooltip: 'Settings', + splashRadius: 24, + ); + } +} diff --git a/lib/components/sidebar_drawer.dart b/lib/components/sidebar_drawer.dart index 9c7da76..e305d22 100644 --- a/lib/components/sidebar_drawer.dart +++ b/lib/components/sidebar_drawer.dart @@ -6,6 +6,7 @@ import 'package:revoltvpn/components/sidebar_contents/privacy_policy_button.dart import 'package:revoltvpn/components/sidebar_contents/info_button.dart'; import 'package:revoltvpn/components/sidebar_contents/discord_link_button.dart'; import 'package:revoltvpn/components/sidebar_contents/website_button.dart'; +import 'package:revoltvpn/components/sidebar_contents/settings.dart'; import 'package:revoltvpn/logic/updater.dart'; class SidebarDrawer extends StatefulWidget { @@ -121,6 +122,15 @@ class _SidebarDrawerState extends State { }, ), + _Row( + icon: Icons.settings, + label: 'Settings', + onTap: () { + Navigator.of(context).pop(); + SettingsButton.open(context); + }, + ), + _Row( icon: Icons.system_update_alt, label: 'Check for updates', diff --git a/lib/screens/settings/in_settings/haptic.dart b/lib/screens/settings/in_settings/haptic.dart new file mode 100644 index 0000000..cf72522 --- /dev/null +++ b/lib/screens/settings/in_settings/haptic.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:revoltvpn/logic/app_colors.dart'; + +// ── lib/screens/settings/in_settings/haptic.dart ──────────────────────────── +// Toggle for light haptic feedback on successful connect/disconnect. +// +// Top-level bool (hapticEnabled) is read synchronously by connect_button.dart — +// no await, no SharedPreferences lookup on every tap. The tile loads the +// persisted value once and keeps the global in sync on every toggle. +// +// SharedPreferences key: haptic_feedback_enabled (default: false — off). + +// lib/components/connect_button.dart reads this directly. Keep it exported. +bool hapticEnabled = false; + +class HapticFeedbackTile extends StatefulWidget { + const HapticFeedbackTile({super.key}); + + @override + State createState() => _HapticFeedbackTileState(); +} + +class _HapticFeedbackTileState extends State { + bool _on = false; + + @override + void initState() { + super.initState(); + _load(); + } + + // Loads persisted state and syncs the top-level bool. + Future _load() async { + final prefs = await SharedPreferences.getInstance(); + final value = prefs.getBool('haptic_feedback_enabled') ?? false; + hapticEnabled = value; + if (mounted) setState(() => _on = value); + } + + // Writes to SharedPreferences AND updates the live bool so the connect + // button sees the change immediately — no restart needed. + Future _toggle(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('haptic_feedback_enabled', value); + hapticEnabled = value; + setState(() => _on = value); + } + + @override + Widget build(BuildContext context) { + return SwitchListTile( + title: const Text( + 'Haptic feedback', + style: TextStyle(color: AppColors.textWhite, fontSize: 15), + ), + value: _on, + onChanged: _toggle, + activeTrackColor: AppColors.accent.withAlpha(80), + activeThumbColor: AppColors.accent, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), + ); + } +} diff --git a/lib/screens/settings/in_settings/lightning.dart b/lib/screens/settings/in_settings/lightning.dart new file mode 100644 index 0000000..cfba408 --- /dev/null +++ b/lib/screens/settings/in_settings/lightning.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:revoltvpn/logic/app_colors.dart'; + +// ── lib/screens/settings/in_settings/lightning.dart ───────────────────────── +// Toggle for the lightning strike overlay on the main screen. +// +// Same architecture as haptic.dart: exports a top-level bool read synchronously +// by lib/components/lightning.dart. Default: true (lightning is on — opt-out). + +// lib/components/lightning.dart reads this in its build method. +bool lightningEnabled = true; + +class LightningToggleTile extends StatefulWidget { + const LightningToggleTile({super.key}); + + @override + State createState() => _LightningToggleTileState(); +} + +class _LightningToggleTileState extends State { + bool _on = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final prefs = await SharedPreferences.getInstance(); + final value = prefs.getBool('lightning_effect_enabled') ?? true; + lightningEnabled = value; + if (mounted) setState(() => _on = value); + } + + Future _toggle(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('lightning_effect_enabled', value); + lightningEnabled = value; + setState(() => _on = value); + } + + @override + Widget build(BuildContext context) { + return SwitchListTile( + title: const Text( + 'Lightning effect', + style: TextStyle(color: AppColors.textWhite, fontSize: 15), + ), + value: _on, + onChanged: _toggle, + activeTrackColor: AppColors.accent.withAlpha(80), + activeThumbColor: AppColors.accent, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), + ); + } +} diff --git a/lib/screens/settings/in_settings/rain.dart b/lib/screens/settings/in_settings/rain.dart new file mode 100644 index 0000000..6b93856 --- /dev/null +++ b/lib/screens/settings/in_settings/rain.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:revoltvpn/logic/app_colors.dart'; + +// ── lib/screens/settings/in_settings/rain.dart ────────────────────────────── +// Toggle for the animated rain overlay on the main screen. +// +// Same architecture as haptic.dart: exports a top-level bool read synchronously +// by lib/components/rain.dart. Default: true (rain is on — opt-out). + +// lib/components/rain.dart reads this in its build method. +bool rainEnabled = true; + +class RainToggleTile extends StatefulWidget { + const RainToggleTile({super.key}); + + @override + State createState() => _RainToggleTileState(); +} + +class _RainToggleTileState extends State { + bool _on = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final prefs = await SharedPreferences.getInstance(); + final value = prefs.getBool('rain_effect_enabled') ?? true; + rainEnabled = value; + if (mounted) setState(() => _on = value); + } + + Future _toggle(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('rain_effect_enabled', value); + rainEnabled = value; + setState(() => _on = value); + } + + @override + Widget build(BuildContext context) { + return SwitchListTile( + title: const Text( + 'Rain effect', + style: TextStyle(color: AppColors.textWhite, fontSize: 15), + ), + value: _on, + onChanged: _toggle, + activeTrackColor: AppColors.accent.withAlpha(80), + activeThumbColor: AppColors.accent, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), + ); + } +} diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart new file mode 100644 index 0000000..5301160 --- /dev/null +++ b/lib/screens/settings/settings_screen.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:revoltvpn/logic/app_colors.dart'; +import 'package:revoltvpn/screens/settings/in_settings/haptic.dart'; +import 'package:revoltvpn/screens/settings/in_settings/rain.dart'; +import 'package:revoltvpn/screens/settings/in_settings/lightning.dart'; + +// ── lib/screens/settings/settings_screen.dart ──────────────────────────────── +// Settings shell — full-screen page reached from the sidebar. +// +// Folder layout: +// lib/screens/settings/ ← this folder: settings family +// settings_screen.dart ← shell: Scaffold + AppBar + ListView (this file) +// *_tile.dart ← one file per setting feature (add below) +// lib/components/sidebar_contents/ +// settings.dart ← thin sidebar row that navigates here +// +// To add a setting: +// 1. Create a new file in this folder, e.g. appearance_tile.dart +// 2. Import it below +// 3. Append it to the children list + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgDeep, + appBar: AppBar( + backgroundColor: AppColors.bgSurface, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: AppColors.textWhite), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text( + 'Settings', + style: TextStyle( + color: AppColors.textWhite, + fontWeight: FontWeight.w600, + ), + ), + ), + body: ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: const [ + HapticFeedbackTile(), + RainToggleTile(), + LightningToggleTile(), + // ── Add setting tiles here ──────────────────────────────────────── + // Each tile imports from its own file in this folder. + ], + ), + ); + } +}