feat: settings screen, haptic toggle, rain/lightning toggles, review docs
This commit is contained in:
1 parent
56ea53e73b
commit
04ec8ac5d1
11 files changed
+318
-3
No files matched your search
@@ -102,3 +102,6 @@ hivemind.zip
|
|||||||
XRAY_AUDIT.md
|
XRAY_AUDIT.md
|
||||||
TECHNICAL_DEBT.md
|
TECHNICAL_DEBT.md
|
||||||
DOCUMENTATION-ARCHITECTURE-V2.md
|
DOCUMENTATION-ARCHITECTURE-V2.md
|
||||||
|
config.json
|
||||||
|
RESILIENCE_REVIEW.md
|
||||||
|
CONFIG_REVIEW.md
|
||||||
@@ -37,10 +37,23 @@ Versions follow [semver](https://semver.org/): MAJOR.MINOR.PATCH
|
|||||||
- Reality keypair generated (was placeholder). Throttle `uplinkOnly`/`downlinkOnly`
|
- Reality keypair generated (was placeholder). Throttle `uplinkOnly`/`downlinkOnly`
|
||||||
corrected (seconds, not bytes/sec). Fallback rate limits removed (fingerprint).
|
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
|
### Changed
|
||||||
- **Timer box spinner removed** — the "Connecting…"/"Syncing…" spinner under
|
- **Timer box spinner removed** — the "Connecting…"/"Syncing…" spinner under
|
||||||
the countdown was redundant with the connect button's spinner. Timer card
|
the countdown was redundant with the connect button's spinner. Timer card
|
||||||
is now a clean countdown only (gray '00:00:00' when idle).
|
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
|
### Server
|
||||||
- `api` outbound + 3 routing rules in `xray_config_reality.json` (bootstrap
|
- `api` outbound + 3 routing rules in `xray_config_reality.json` (bootstrap
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:revoltvpn/logic/app_colors.dart';
|
|||||||
import 'package:revoltvpn/logic/vpn_connection.dart';
|
import 'package:revoltvpn/logic/vpn_connection.dart';
|
||||||
import 'package:revoltvpn/logic/session_timer.dart';
|
import 'package:revoltvpn/logic/session_timer.dart';
|
||||||
import 'package:revoltvpn/logic/ad_manager.dart';
|
import 'package:revoltvpn/logic/ad_manager.dart';
|
||||||
|
import 'package:revoltvpn/screens/settings/in_settings/haptic.dart';
|
||||||
|
|
||||||
class ConnectButton extends StatefulWidget {
|
class ConnectButton extends StatefulWidget {
|
||||||
const ConnectButton({super.key});
|
const ConnectButton({super.key});
|
||||||
@@ -56,8 +57,6 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
if (now.difference(_lastTap).inMilliseconds < 1000) return;
|
if (now.difference(_lastTap).inMilliseconds < 1000) return;
|
||||||
_lastTap = now;
|
_lastTap = now;
|
||||||
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
|
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = context.read<VpnConnection>();
|
||||||
final timer = context.read<SessionTimer>();
|
final timer = context.read<SessionTimer>();
|
||||||
final ad = context.read<AdManager>();
|
final ad = context.read<AdManager>();
|
||||||
@@ -67,6 +66,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
_busy = false;
|
_busy = false;
|
||||||
timer.stop();
|
timer.stop();
|
||||||
await vpn.disconnect();
|
await vpn.disconnect();
|
||||||
|
if (hapticEnabled) HapticFeedback.lightImpact();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,10 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ok = await vpn.connect();
|
final ok = await vpn.connect();
|
||||||
if (ok) timer.start();
|
if (ok) {
|
||||||
|
timer.start();
|
||||||
|
if (hapticEnabled) HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _busy = false);
|
if (mounted) setState(() => _busy = false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:revoltvpn/logic/app_colors.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.
|
/// 2D lightning bolt — a jagged yellow line from top to bottom with glow.
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ class _LightningEffectState extends State<LightningEffect>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!lightningEnabled) return const SizedBox.shrink();
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _ctrl,
|
animation: _ctrl,
|
||||||
builder: (_, child) {
|
builder: (_, child) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.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.
|
/// Subtle animated rain effect — sits behind all UI as a full-screen overlay.
|
||||||
///
|
///
|
||||||
@@ -78,6 +79,7 @@ class _RainEffectState extends State<RainEffect>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!rainEnabled) return const SizedBox.shrink();
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: _RainPainter(
|
painter: _RainPainter(
|
||||||
drops: _drops,
|
drops: _drops,
|
||||||
|
|||||||
@@ -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<Offset>(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/info_button.dart';
|
||||||
import 'package:revoltvpn/components/sidebar_contents/discord_link_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/website_button.dart';
|
||||||
|
import 'package:revoltvpn/components/sidebar_contents/settings.dart';
|
||||||
import 'package:revoltvpn/logic/updater.dart';
|
import 'package:revoltvpn/logic/updater.dart';
|
||||||
|
|
||||||
class SidebarDrawer extends StatefulWidget {
|
class SidebarDrawer extends StatefulWidget {
|
||||||
@@ -121,6 +122,15 @@ class _SidebarDrawerState extends State<SidebarDrawer> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
_Row(
|
||||||
|
icon: Icons.settings,
|
||||||
|
label: 'Settings',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
SettingsButton.open(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
_Row(
|
_Row(
|
||||||
icon: Icons.system_update_alt,
|
icon: Icons.system_update_alt,
|
||||||
label: 'Check for updates',
|
label: 'Check for updates',
|
||||||
|
|||||||
@@ -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<HapticFeedbackTile> createState() => _HapticFeedbackTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HapticFeedbackTileState extends State<HapticFeedbackTile> {
|
||||||
|
bool _on = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads persisted state and syncs the top-level bool.
|
||||||
|
Future<void> _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<void> _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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<LightningToggleTile> createState() => _LightningToggleTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LightningToggleTileState extends State<LightningToggleTile> {
|
||||||
|
bool _on = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final value = prefs.getBool('lightning_effect_enabled') ?? true;
|
||||||
|
lightningEnabled = value;
|
||||||
|
if (mounted) setState(() => _on = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RainToggleTile> createState() => _RainToggleTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RainToggleTileState extends State<RainToggleTile> {
|
||||||
|
bool _on = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final value = prefs.getBool('rain_effect_enabled') ?? true;
|
||||||
|
rainEnabled = value;
|
||||||
|
if (mounted) setState(() => _on = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user