refactor(app): remove unsupported runtime and UI state
This commit is contained in:
1 parent
9ac236a981
commit
0ed7bf27b5
19 files changed
+154
-362
No files matched your search
@@ -19,6 +19,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
DateTime _lastTap = DateTime.fromMillisecondsSinceEpoch(0);
|
DateTime _lastTap = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
AnimationController? _pulse;
|
AnimationController? _pulse;
|
||||||
Animation<double>? _pulseAnim;
|
Animation<double>? _pulseAnim;
|
||||||
|
VpnConnection? _vpn;
|
||||||
|
|
||||||
double get _pulseValue => _pulseAnim?.value ?? 0.0;
|
double get _pulseValue => _pulseAnim?.value ?? 0.0;
|
||||||
|
|
||||||
@@ -35,20 +36,25 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
);
|
);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) context.read<VpnConnection>().addListener(_onVpnChanged);
|
if (!mounted || _vpn != null) return;
|
||||||
|
final vpn = context.read<VpnConnection>();
|
||||||
|
_vpn = vpn;
|
||||||
|
vpn.addListener(_onVpnChanged);
|
||||||
|
_onVpnChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
context.read<VpnConnection>().removeListener(_onVpnChanged);
|
_vpn?.removeListener(_onVpnChanged);
|
||||||
_pulse?.dispose();
|
_pulse?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onVpnChanged() {
|
void _onVpnChanged() {
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = _vpn;
|
||||||
|
if (vpn == null) return;
|
||||||
if (vpn.status == VpnStatus.connected) {
|
if (vpn.status == VpnStatus.connected) {
|
||||||
_pulse?.repeat(reverse: true);
|
_pulse?.repeat(reverse: true);
|
||||||
} else {
|
} else {
|
||||||
@@ -60,14 +66,12 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (state == AppLifecycleState.paused ||
|
if (state != AppLifecycleState.resumed) {
|
||||||
state == AppLifecycleState.detached) {
|
|
||||||
_pulse?.stop();
|
_pulse?.stop();
|
||||||
} else if (state == AppLifecycleState.resumed) {
|
return;
|
||||||
final vpn = context.read<VpnConnection>();
|
|
||||||
if (vpn.status == VpnStatus.connected) {
|
|
||||||
_pulse?.repeat(reverse: true);
|
|
||||||
}
|
}
|
||||||
|
if (_vpn?.status == VpnStatus.connected) {
|
||||||
|
_pulse?.repeat(reverse: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,28 @@ class DataDisclosureDialog {
|
|||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
await showDialog(
|
final accepted = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false, // Must tap Continue — no tap-outside dismiss.
|
barrierDismissible: false, // Must tap Continue — no tap-outside dismiss.
|
||||||
builder: (_) => _DisclosureDialog(
|
builder: (_) => const _DisclosureDialog(),
|
||||||
onContinue: () {
|
|
||||||
prefs.setBool(_key, true);
|
|
||||||
Navigator.of(context).pop(); // Dismiss the dialog.
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (accepted != true) return;
|
||||||
|
|
||||||
|
// Persist only after the user action is complete. If storage fails, do not
|
||||||
|
// invent an accepted state; the disclosure will simply reappear later.
|
||||||
|
try {
|
||||||
|
final saved = await prefs.setBool(_key, true);
|
||||||
|
if (!saved) {
|
||||||
|
debugPrint('[Disclosure] Preference write was rejected');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[Disclosure] Failed to persist acknowledgement: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DisclosureDialog extends StatelessWidget {
|
class _DisclosureDialog extends StatelessWidget {
|
||||||
final VoidCallback onContinue;
|
const _DisclosureDialog();
|
||||||
|
|
||||||
const _DisclosureDialog({required this.onContinue});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -49,7 +54,6 @@ class _DisclosureDialog extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Title
|
|
||||||
const Text(
|
const Text(
|
||||||
'Before you connect',
|
'Before you connect',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -59,8 +63,6 @@ class _DisclosureDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Body
|
|
||||||
const Text(
|
const Text(
|
||||||
'Revolt VPN creates a secure VPN tunnel to protect your '
|
'Revolt VPN creates a secure VPN tunnel to protect your '
|
||||||
'traffic. To enforce your data/time quota, we log your '
|
'traffic. To enforce your data/time quota, we log your '
|
||||||
@@ -76,12 +78,10 @@ class _DisclosureDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Continue button — full-width, accent
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: onContinue,
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.accent,
|
backgroundColor: AppColors.accent,
|
||||||
foregroundColor: AppColors.bgDeep,
|
foregroundColor: AppColors.bgDeep,
|
||||||
|
|||||||
@@ -5,13 +5,9 @@ import 'package:revoltvpn/logic/app_colors.dart';
|
|||||||
import 'package:revoltvpn/screens/settings/in_settings/lightning.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.
|
||||||
|
|
||||||
class LightningEffect extends StatefulWidget {
|
class LightningEffect extends StatefulWidget {
|
||||||
|
|
||||||
final int minIntervalSec;
|
final int minIntervalSec;
|
||||||
|
|
||||||
final int maxIntervalSec;
|
final int maxIntervalSec;
|
||||||
|
|
||||||
final double doubleChance;
|
final double doubleChance;
|
||||||
|
|
||||||
const LightningEffect({
|
const LightningEffect({
|
||||||
@@ -26,26 +22,49 @@ class LightningEffect extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LightningEffectState extends State<LightningEffect>
|
class _LightningEffectState extends State<LightningEffect>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||||
late final AnimationController _ctrl;
|
late final AnimationController _ctrl;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
final _random = Random();
|
final _random = Random();
|
||||||
|
bool _appActive = true;
|
||||||
|
|
||||||
LightningBolt? _bolt;
|
LightningBolt? _bolt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
|
_ctrl = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
final lifecycle = WidgetsBinding.instance.lifecycleState;
|
||||||
|
_appActive = lifecycle == null || lifecycle == AppLifecycleState.resumed;
|
||||||
lightningEnabled.addListener(_onEnabledChanged);
|
lightningEnabled.addListener(_onEnabledChanged);
|
||||||
|
if (_appActive && lightningEnabled.value) _scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
_appActive = state == AppLifecycleState.resumed;
|
||||||
|
if (!_appActive) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
_ctrl.stop();
|
||||||
|
_bolt = null;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (lightningEnabled.value) _scheduleNext();
|
if (lightningEnabled.value) _scheduleNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEnabledChanged() {
|
void _onEnabledChanged() {
|
||||||
if (lightningEnabled.value) {
|
if (lightningEnabled.value && _appActive) {
|
||||||
_scheduleNext();
|
_scheduleNext();
|
||||||
} else {
|
} else {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
_ctrl.stop();
|
_ctrl.stop();
|
||||||
_bolt = null;
|
_bolt = null;
|
||||||
}
|
}
|
||||||
@@ -54,6 +73,7 @@ class _LightningEffectState extends State<LightningEffect>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
lightningEnabled.removeListener(_onEnabledChanged);
|
lightningEnabled.removeListener(_onEnabledChanged);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_ctrl.dispose();
|
_ctrl.dispose();
|
||||||
@@ -61,19 +81,23 @@ class _LightningEffectState extends State<LightningEffect>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleNext() {
|
void _scheduleNext() {
|
||||||
|
_timer?.cancel();
|
||||||
|
if (!mounted || !_appActive || !lightningEnabled.value) return;
|
||||||
|
|
||||||
final secs = widget.minIntervalSec +
|
final secs = widget.minIntervalSec +
|
||||||
_random.nextInt(widget.maxIntervalSec - widget.minIntervalSec + 1);
|
_random.nextInt(widget.maxIntervalSec - widget.minIntervalSec + 1);
|
||||||
_timer = Timer(Duration(seconds: secs), _strike);
|
_timer = Timer(Duration(seconds: secs), _strike);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _strike() {
|
void _strike() {
|
||||||
if (!mounted || !lightningEnabled.value) return;
|
_timer = null;
|
||||||
|
if (!mounted || !_appActive || !lightningEnabled.value) return;
|
||||||
_bolt = LightningBolt.random(_random);
|
_bolt = LightningBolt.random(_random);
|
||||||
_ctrl.forward(from: 0.0);
|
_ctrl.forward(from: 0.0);
|
||||||
|
|
||||||
if (_random.nextDouble() < widget.doubleChance) {
|
if (_random.nextDouble() < widget.doubleChance) {
|
||||||
Future.delayed(const Duration(milliseconds: 120), () {
|
Future.delayed(const Duration(milliseconds: 120), () {
|
||||||
if (mounted && lightningEnabled.value) {
|
if (mounted && _appActive && lightningEnabled.value) {
|
||||||
_bolt = LightningBolt.random(_random);
|
_bolt = LightningBolt.random(_random);
|
||||||
_ctrl.forward(from: 0.0);
|
_ctrl.forward(from: 0.0);
|
||||||
}
|
}
|
||||||
@@ -85,8 +109,8 @@ class _LightningEffectState extends State<LightningEffect>
|
|||||||
|
|
||||||
/// Sharp flash curve: quick on, quick off. No slow fade.
|
/// Sharp flash curve: quick on, quick off. No slow fade.
|
||||||
double _opacity(double t) {
|
double _opacity(double t) {
|
||||||
if (t < 0.08) return t / 0.08; // fast ramp
|
if (t < 0.08) return t / 0.08;
|
||||||
return 1.0 - ((t - 0.08) / 0.92); // linear decay
|
return 1.0 - ((t - 0.08) / 0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -117,11 +141,10 @@ class _LightningEffectState extends State<LightningEffect>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A single bolt defined by a list of 2D points (0.0–1.0 in width×height space).
|
/// A single bolt defined by a list of 2D points (0.0–1.0 in width×height space).
|
||||||
class LightningBolt {
|
class LightningBolt {
|
||||||
final List<Offset> mainPath;
|
final List<Offset> mainPath;
|
||||||
final List<List<Offset>> branches; // 0–2 forks off the main path
|
final List<List<Offset>> branches;
|
||||||
|
|
||||||
const LightningBolt({required this.mainPath, this.branches = const []});
|
const LightningBolt({required this.mainPath, this.branches = const []});
|
||||||
|
|
||||||
@@ -130,24 +153,19 @@ class LightningBolt {
|
|||||||
factory LightningBolt.random(Random rng) {
|
factory LightningBolt.random(Random rng) {
|
||||||
final points = <Offset>[];
|
final points = <Offset>[];
|
||||||
|
|
||||||
// Start: top, slight horizontal offset
|
|
||||||
double x = 0.5 + (rng.nextDouble() - 0.5) * 0.15;
|
double x = 0.5 + (rng.nextDouble() - 0.5) * 0.15;
|
||||||
points.add(Offset(x, 0.0));
|
points.add(Offset(x, 0.0));
|
||||||
|
|
||||||
// Zigzag down — 6–10 segments
|
|
||||||
final segments = 6 + rng.nextInt(5);
|
final segments = 6 + rng.nextInt(5);
|
||||||
for (int i = 1; i < segments; i++) {
|
for (int i = 1; i < segments; i++) {
|
||||||
final y = i / segments;
|
final y = i / segments;
|
||||||
// Random horizontal offset — each segment adds jitter
|
|
||||||
x += (rng.nextDouble() - 0.5) * 0.25;
|
x += (rng.nextDouble() - 0.5) * 0.25;
|
||||||
x = x.clamp(0.05, 0.95); // stay on screen
|
x = x.clamp(0.05, 0.95);
|
||||||
points.add(Offset(x, y));
|
points.add(Offset(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
// End: bottom
|
|
||||||
points.add(Offset(x.clamp(0.1, 0.9), 1.0));
|
points.add(Offset(x.clamp(0.1, 0.9), 1.0));
|
||||||
|
|
||||||
// Branches — 0–2 forks from random points along the main path
|
|
||||||
final branches = <List<Offset>>[];
|
final branches = <List<Offset>>[];
|
||||||
final branchCount = rng.nextInt(3);
|
final branchCount = rng.nextInt(3);
|
||||||
for (int b = 0; b < branchCount; b++) {
|
for (int b = 0; b < branchCount; b++) {
|
||||||
@@ -160,7 +178,7 @@ class LightningBolt {
|
|||||||
double by = start.dy;
|
double by = start.dy;
|
||||||
final branchLen = 2 + rng.nextInt(3);
|
final branchLen = 2 + rng.nextInt(3);
|
||||||
for (int j = 0; j < branchLen; j++) {
|
for (int j = 0; j < branchLen; j++) {
|
||||||
by += (0.05 + rng.nextDouble() * 0.12);
|
by += 0.05 + rng.nextDouble() * 0.12;
|
||||||
bx += (rng.nextDouble() - 0.5) * 0.3;
|
bx += (rng.nextDouble() - 0.5) * 0.3;
|
||||||
bx = bx.clamp(0.02, 0.98);
|
bx = bx.clamp(0.02, 0.98);
|
||||||
by = by.clamp(0.0, 1.0);
|
by = by.clamp(0.0, 1.0);
|
||||||
@@ -173,7 +191,6 @@ class LightningBolt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _LightningPainter extends CustomPainter {
|
class _LightningPainter extends CustomPainter {
|
||||||
final LightningBolt bolt;
|
final LightningBolt bolt;
|
||||||
final double opacity;
|
final double opacity;
|
||||||
@@ -182,7 +199,8 @@ class _LightningPainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final toScreen = (Offset p) => Offset(p.dx * size.width, p.dy * size.height);
|
Offset toScreen(Offset p) => Offset(p.dx * size.width, p.dy * size.height);
|
||||||
|
|
||||||
final scaledMain = bolt.mainPath.map(toScreen).toList();
|
final scaledMain = bolt.mainPath.map(toScreen).toList();
|
||||||
final scaledBranches = bolt.branches
|
final scaledBranches = bolt.branches
|
||||||
.map((b) => b.map(toScreen).toList())
|
.map((b) => b.map(toScreen).toList())
|
||||||
@@ -191,7 +209,6 @@ class _LightningPainter extends CustomPainter {
|
|||||||
final baseColor = AppColors.accent.withAlpha((opacity * 255).round());
|
final baseColor = AppColors.accent.withAlpha((opacity * 255).round());
|
||||||
final glowColor = AppColors.accent.withAlpha((opacity * 0.35 * 255).round());
|
final glowColor = AppColors.accent.withAlpha((opacity * 0.35 * 255).round());
|
||||||
|
|
||||||
// Glow
|
|
||||||
final glowPaint = Paint()
|
final glowPaint = Paint()
|
||||||
..color = glowColor
|
..color = glowColor
|
||||||
..strokeWidth = 18
|
..strokeWidth = 18
|
||||||
@@ -202,7 +219,6 @@ class _LightningPainter extends CustomPainter {
|
|||||||
|
|
||||||
_drawPath(canvas, scaledMain, scaledBranches, glowPaint);
|
_drawPath(canvas, scaledMain, scaledBranches, glowPaint);
|
||||||
|
|
||||||
// Core
|
|
||||||
final corePaint = Paint()
|
final corePaint = Paint()
|
||||||
..color = baseColor
|
..color = baseColor
|
||||||
..strokeWidth = 5
|
..strokeWidth = 5
|
||||||
@@ -212,7 +228,6 @@ class _LightningPainter extends CustomPainter {
|
|||||||
|
|
||||||
_drawPath(canvas, scaledMain, scaledBranches, corePaint);
|
_drawPath(canvas, scaledMain, scaledBranches, corePaint);
|
||||||
|
|
||||||
// Hot center
|
|
||||||
if (opacity > 0.6) {
|
if (opacity > 0.6) {
|
||||||
final hotPaint = Paint()
|
final hotPaint = Paint()
|
||||||
..color = Colors.white.withAlpha((opacity * 180).round())
|
..color = Colors.white.withAlpha((opacity * 180).round())
|
||||||
@@ -225,8 +240,12 @@ class _LightningPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawPath(Canvas canvas, List<Offset> main,
|
void _drawPath(
|
||||||
List<List<Offset>> branches, Paint paint) {
|
Canvas canvas,
|
||||||
|
List<Offset> main,
|
||||||
|
List<List<Offset>> branches,
|
||||||
|
Paint paint,
|
||||||
|
) {
|
||||||
final mainPath = Path()..moveTo(main.first.dx, main.first.dy);
|
final mainPath = Path()..moveTo(main.first.dx, main.first.dy);
|
||||||
for (int i = 1; i < main.length; i++) {
|
for (int i = 1; i < main.length; i++) {
|
||||||
mainPath.lineTo(main[i].dx, main[i].dy);
|
mainPath.lineTo(main[i].dx, main[i].dy);
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class DiscordLinkButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return const IconButton(
|
||||||
icon: const Icon(Icons.discord, color: AppColors.textWhite, size: 28),
|
icon: Icon(Icons.discord, color: AppColors.textWhite, size: 28),
|
||||||
onPressed: launchDiscord,
|
onPressed: launchDiscord,
|
||||||
tooltip: 'Join Discord',
|
tooltip: 'Join Discord',
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ class GdprButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return const IconButton(
|
||||||
icon: const Icon(Icons.admin_panel_settings_outlined,
|
icon: Icon(Icons.admin_panel_settings_outlined,
|
||||||
color: AppColors.textWhite, size: 28),
|
color: AppColors.textWhite, size: 28),
|
||||||
onPressed: showConsentForm,
|
onPressed: showConsentForm,
|
||||||
tooltip: 'Ad consent & privacy choices',
|
tooltip: 'Ad consent & privacy choices',
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class InfoButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return const IconButton(
|
||||||
icon: const Icon(Icons.help_outline, color: AppColors.textWhite, size: 28),
|
icon: Icon(Icons.help_outline, color: AppColors.textWhite, size: 28),
|
||||||
onPressed: launchGitHub,
|
onPressed: launchGitHub,
|
||||||
tooltip: 'About ReVoltVPN',
|
tooltip: 'About ReVoltVPN',
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ class PrivacyPolicyButton extends StatelessWidget {
|
|||||||
const PrivacyPolicyButton({super.key});
|
const PrivacyPolicyButton({super.key});
|
||||||
|
|
||||||
static Future<void> launchPrivacyPolicy() async {
|
static Future<void> launchPrivacyPolicy() async {
|
||||||
final Uri url = Uri.parse('https://github.com/esefxdz/ReVoltVPN/blob/main/PRIVACY_POLICY.md');
|
final Uri url = Uri.parse(
|
||||||
|
'https://github.com/esefxdz/ReVoltVPN/blob/main/PRIVACY_POLICY.md',
|
||||||
|
);
|
||||||
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||||||
debugPrint('Could not launch $url');
|
debugPrint('Could not launch $url');
|
||||||
}
|
}
|
||||||
@@ -14,8 +16,12 @@ class PrivacyPolicyButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return const IconButton(
|
||||||
icon: const Icon(Icons.privacy_tip_outlined, color: AppColors.textWhite, size: 28),
|
icon: Icon(
|
||||||
|
Icons.privacy_tip_outlined,
|
||||||
|
color: AppColors.textWhite,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
onPressed: launchPrivacyPolicy,
|
onPressed: launchPrivacyPolicy,
|
||||||
tooltip: 'Privacy Policy',
|
tooltip: 'Privacy Policy',
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class WebsiteButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return const IconButton(
|
||||||
icon: const Icon(Icons.language, color: AppColors.textWhite, size: 28),
|
icon: Icon(Icons.language, color: AppColors.textWhite, size: 28),
|
||||||
onPressed: launchWebsite,
|
onPressed: launchWebsite,
|
||||||
tooltip: 'Website',
|
tooltip: 'Website',
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
|
|||||||
@@ -1,62 +1,31 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:revoltvpn/components/online_dot.dart';
|
||||||
import 'package:revoltvpn/logic/app_colors.dart';
|
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/server_list.dart';
|
|
||||||
import 'package:revoltvpn/components/online_dot.dart';
|
|
||||||
import 'package:revoltvpn/screens/server_selector/selection_screen.dart';
|
|
||||||
|
|
||||||
class StatusBar extends StatelessWidget {
|
class StatusBar extends StatelessWidget {
|
||||||
const StatusBar({super.key});
|
const StatusBar({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer2<VpnConnection, SessionTimer>(
|
return Consumer<VpnConnection>(
|
||||||
builder: (context, vpn, timer, _) {
|
builder: (context, vpn, _) {
|
||||||
final isConnected = vpn.status == VpnStatus.connected;
|
final isConnected = vpn.status == VpnStatus.connected;
|
||||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
final statusLabel = switch (vpn.status) {
|
||||||
final serverHealthy =
|
VpnStatus.connected => 'Connected',
|
||||||
vpn.serverReachable || (isConnected && timer.hasSyncedOnce);
|
VpnStatus.connecting => 'Connecting…',
|
||||||
final sessionOrTunnelHealthy = timer.hasSyncedOnce || vpn.serverReachable;
|
VpnStatus.disconnecting => 'Disconnecting…',
|
||||||
|
VpnStatus.error => 'Error',
|
||||||
final statusLabel = isConnected
|
VpnStatus.disconnected => vpn.serverReachable ? 'Ready' : 'Offline',
|
||||||
? (sessionOrTunnelHealthy ? 'Online' : 'Syncing…')
|
};
|
||||||
: (isConnecting
|
|
||||||
? 'Connecting…'
|
|
||||||
: (vpn.serverReachable ? 'Ready' : 'Offline'));
|
|
||||||
|
|
||||||
final server = context.watch<ServerList>().selected;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
Container(
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
PageRouteBuilder(
|
|
||||||
transitionDuration: const Duration(milliseconds: 300),
|
|
||||||
pageBuilder: (_, __, ___) => const SelectionScreen(),
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -66,12 +35,16 @@ class StatusBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Image.asset(server.flagAsset, width: 20, height: 20),
|
Image.asset(
|
||||||
|
'assets/finland_flag_256.png',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
const Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
server.name,
|
'Helsinki, Finland',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.accent,
|
color: AppColors.accent,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -79,15 +52,9 @@ class StatusBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(
|
|
||||||
Icons.chevron_right,
|
|
||||||
color: AppColors.textDim,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding:
|
||||||
@@ -100,7 +67,7 @@ class StatusBar extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
OnlineDot(
|
OnlineDot(
|
||||||
isOnline: serverHealthy,
|
isOnline: isConnected,
|
||||||
label: statusLabel,
|
label: statusLabel,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|||||||
@@ -18,23 +18,32 @@ abstract final class ConnectionSettings {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final storedMode = prefs.getString(_modeKey);
|
final storedMode = prefs.getString(_modeKey);
|
||||||
|
|
||||||
// Any unrecognized value (including the pre-V3 'auto') is TUN. 'proxy' is
|
// Only the explicit current proxy value restores SOCKS5. Unknown and
|
||||||
// the only persisted value that selects the SOCKS5 mode.
|
// obsolete values, including the former 'auto'/'ass' labels, fail closed
|
||||||
|
// to full-tunnel TUN instead of silently selecting a weaker route.
|
||||||
_mode = storedMode == ConnectionMode.proxy.name
|
_mode = storedMode == ConnectionMode.proxy.name
|
||||||
? ConnectionMode.proxy
|
? ConnectionMode.proxy
|
||||||
: ConnectionMode.tun;
|
: ConnectionMode.tun;
|
||||||
|
|
||||||
if (storedMode != _mode.name) {
|
if (storedMode != _mode.name) {
|
||||||
await prefs.setString(_modeKey, _mode.name);
|
final migrated = await prefs.setString(_modeKey, _mode.name);
|
||||||
|
if (!migrated) {
|
||||||
|
throw StateError('Could not persist migrated connection mode.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setMode(ConnectionMode mode) async {
|
static Future<bool> setMode(ConnectionMode mode) async {
|
||||||
|
if (_initialized && _mode == mode) return true;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final saved = await prefs.setString(_modeKey, mode.name);
|
||||||
|
if (!saved) return false;
|
||||||
|
|
||||||
_mode = mode;
|
_mode = mode;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
return true;
|
||||||
await prefs.setString(_modeKey, mode.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// Background-survival controls.
|
/// Android background-survival controls that ReVolt can actually verify.
|
||||||
///
|
|
||||||
/// Android stops background services of apps that are not exempt from Doze and
|
|
||||||
/// App Standby, and it tears down the whole process group when a task is swiped
|
|
||||||
/// away. Those are OS behaviours, not app bugs; the battery-optimisation
|
|
||||||
/// exemption is the one sanctioned way an app can ask for around them, and it
|
|
||||||
/// still needs one user tap.
|
|
||||||
abstract final class PowerSettings {
|
abstract final class PowerSettings {
|
||||||
PowerSettings._();
|
PowerSettings._();
|
||||||
|
|
||||||
@@ -15,7 +9,6 @@ abstract final class PowerSettings {
|
|||||||
'com.revoltvpn.app/power',
|
'com.revoltvpn.app/power',
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Whether the OS has exempted this app from Doze / App Standby.
|
|
||||||
static Future<bool> isBatteryOptimisationDisabled() async {
|
static Future<bool> isBatteryOptimisationDisabled() async {
|
||||||
if (kIsWeb) return true;
|
if (kIsWeb) return true;
|
||||||
try {
|
try {
|
||||||
@@ -28,8 +21,6 @@ abstract final class PowerSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the system exemption dialog. Returns false only when no system UI
|
|
||||||
/// could be opened at all — a user declining still returns true.
|
|
||||||
static Future<bool> requestDisableBatteryOptimisation() async {
|
static Future<bool> requestDisableBatteryOptimisation() async {
|
||||||
if (kIsWeb) return true;
|
if (kIsWeb) return true;
|
||||||
try {
|
try {
|
||||||
@@ -41,53 +32,4 @@ abstract final class PowerSettings {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the VPN runtime is genuinely alive.
|
|
||||||
///
|
|
||||||
/// The Xray service runs in a separate process, so nothing it stores in
|
|
||||||
/// statics is visible to us. This asks the OS instead: is our service process
|
|
||||||
/// running, and (for TUN) is there an active VPN transport?
|
|
||||||
static Future<VpnRuntimeState> runtimeState() async {
|
|
||||||
if (kIsWeb) return const VpnRuntimeState(processAlive: false, vpnTransport: false);
|
|
||||||
try {
|
|
||||||
final result = await _channel.invokeMapMethod<String, dynamic>(
|
|
||||||
'isVpnRuntimeAlive',
|
|
||||||
);
|
|
||||||
return VpnRuntimeState(
|
|
||||||
processAlive: result?['processAlive'] == true,
|
|
||||||
vpnTransport: result?['vpnTransport'] == true,
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
return const VpnRuntimeState(processAlive: false, vpnTransport: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pushes the absolute session-expiry timestamp (epoch ms) to the VPN
|
|
||||||
/// service so it can tear the tunnel down even when the UI process is gone.
|
|
||||||
static Future<void> setSessionExpiry(int expiresAtMs) async {
|
|
||||||
if (kIsWeb) return;
|
|
||||||
try {
|
|
||||||
await _channel.invokeMethod<void>('setSessionExpiry', {
|
|
||||||
'expiresAtMs': expiresAtMs,
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
// Best-effort. The daemon also keeps its last persisted expiry.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class VpnRuntimeState {
|
|
||||||
final bool processAlive;
|
|
||||||
final bool vpnTransport;
|
|
||||||
|
|
||||||
const VpnRuntimeState({
|
|
||||||
required this.processAlive,
|
|
||||||
required this.vpnTransport,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// TUN needs both a live service and a real VPN interface. Proxy mode has no
|
|
||||||
/// VPN interface at all, so the service process is the whole signal.
|
|
||||||
bool aliveFor({required bool tunMode}) =>
|
|
||||||
processAlive && (!tunMode || vpnTransport);
|
|
||||||
}
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
// ── lib/logic/server_list.dart ──────────────────────────────────────────────
|
|
||||||
// Server data model and ChangeNotifier — shared by StatusBar (shows current
|
|
||||||
// server name/flag) and SelectionScreen (lets user pick a server).
|
|
||||||
//
|
|
||||||
// Each server is defined by its tunnel IP/port. Reality credentials (public
|
|
||||||
// key, shortId) come from /session/status per session; only the transport
|
|
||||||
// parameters live in AppConfig.
|
|
||||||
//
|
|
||||||
// When multi-server ships:
|
|
||||||
// - _all becomes a fetched list (from /api/servers or bundled config)
|
|
||||||
// - SelectionScreen shows multiple rows
|
|
||||||
// - select() triggers a disconnect/reconnect cycle if the tunnel is active
|
|
||||||
|
|
||||||
class ServerInfo {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String flagAsset;
|
|
||||||
final String ip;
|
|
||||||
final int port;
|
|
||||||
|
|
||||||
const ServerInfo({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.flagAsset,
|
|
||||||
required this.ip,
|
|
||||||
required this.port,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerList extends ChangeNotifier {
|
|
||||||
static const _all = [
|
|
||||||
ServerInfo(
|
|
||||||
id: 'finland',
|
|
||||||
name: 'Helsinki, Finland',
|
|
||||||
flagAsset: 'assets/finland_flag_256.png',
|
|
||||||
ip: '204.168.246.88',
|
|
||||||
port: 443,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
int _selectedIndex = 0;
|
|
||||||
|
|
||||||
List<ServerInfo> get all => _all;
|
|
||||||
int get selectedIndex => _selectedIndex;
|
|
||||||
ServerInfo get selected => _all[_selectedIndex];
|
|
||||||
|
|
||||||
// Loads persisted selection from SharedPreferences.
|
|
||||||
// Fire-and-forget — UI starts with default, updates when prefs load.
|
|
||||||
Future<void> init() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final saved = prefs.getInt('selected_server_index') ?? 0;
|
|
||||||
if (saved >= 0 && saved < _all.length && saved != _selectedIndex) {
|
|
||||||
_selectedIndex = saved;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switches to a different server. Persists the choice.
|
|
||||||
Future<void> select(int index) async {
|
|
||||||
if (index == _selectedIndex || index < 0 || index >= _all.length) return;
|
|
||||||
_selectedIndex = index;
|
|
||||||
notifyListeners();
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setInt('selected_server_index', index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-12
@@ -7,7 +7,6 @@ import 'package:revoltvpn/logic/ad_manager.dart';
|
|||||||
import 'package:revoltvpn/logic/app_colors.dart';
|
import 'package:revoltvpn/logic/app_colors.dart';
|
||||||
import 'package:revoltvpn/logic/connection_settings.dart';
|
import 'package:revoltvpn/logic/connection_settings.dart';
|
||||||
import 'package:revoltvpn/logic/haptic_settings.dart';
|
import 'package:revoltvpn/logic/haptic_settings.dart';
|
||||||
import 'package:revoltvpn/logic/server_list.dart';
|
|
||||||
import 'package:revoltvpn/logic/session_timer.dart';
|
import 'package:revoltvpn/logic/session_timer.dart';
|
||||||
import 'package:revoltvpn/logic/vpn_connection.dart';
|
import 'package:revoltvpn/logic/vpn_connection.dart';
|
||||||
import 'package:revoltvpn/screens/intro.dart';
|
import 'package:revoltvpn/screens/intro.dart';
|
||||||
@@ -26,13 +25,10 @@ Future<void> main() async {
|
|||||||
systemNavigationBarIconBrightness: Brightness.light,
|
systemNavigationBarIconBrightness: Brightness.light,
|
||||||
));
|
));
|
||||||
|
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = FlutterError.presentError;
|
||||||
debugPrint('[FlutterError] ${details.exception}');
|
|
||||||
};
|
|
||||||
|
|
||||||
PlatformDispatcher.instance.onError = (error, stack) {
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
debugPrint('[PlatformError] $error\n$stack');
|
debugPrint('[PlatformError] $error\n$stack');
|
||||||
return true;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
runApp(const ReVoltApp());
|
runApp(const ReVoltApp());
|
||||||
@@ -47,8 +43,6 @@ class ReVoltApp extends StatelessWidget {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => VpnConnection(), lazy: false),
|
ChangeNotifierProvider(create: (_) => VpnConnection(), lazy: false),
|
||||||
ChangeNotifierProxyProvider<VpnConnection, SessionTimer>(
|
ChangeNotifierProxyProvider<VpnConnection, SessionTimer>(
|
||||||
// Eager, like VpnConnection: the timer has to be listening before the
|
|
||||||
// engine reports an already-running tunnel, not after the intro.
|
|
||||||
lazy: false,
|
lazy: false,
|
||||||
create: (ctx) =>
|
create: (ctx) =>
|
||||||
SessionTimer(vpnConnection: ctx.read<VpnConnection>()),
|
SessionTimer(vpnConnection: ctx.read<VpnConnection>()),
|
||||||
@@ -56,10 +50,6 @@ class ReVoltApp extends StatelessWidget {
|
|||||||
prev ?? SessionTimer(vpnConnection: vpn),
|
prev ?? SessionTimer(vpnConnection: vpn),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(create: (_) => AdManager()),
|
ChangeNotifierProvider(create: (_) => AdManager()),
|
||||||
ChangeNotifierProvider(
|
|
||||||
create: (_) => ServerList()..init(),
|
|
||||||
lazy: false,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'ReVolt VPN',
|
title: 'ReVolt VPN',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:revoltvpn/logic/app_colors.dart';
|
import 'package:revoltvpn/logic/app_colors.dart';
|
||||||
import 'package:revoltvpn/logic/haptic_settings.dart';
|
|
||||||
import 'package:revoltvpn/logic/vpn_connection.dart';
|
import 'package:revoltvpn/logic/vpn_connection.dart';
|
||||||
import 'package:revoltvpn/screens/main_screen.dart';
|
import 'package:revoltvpn/screens/main_screen.dart';
|
||||||
import 'package:revoltvpn/screens/settings/in_settings/rain.dart';
|
import 'package:revoltvpn/screens/settings/in_settings/rain.dart';
|
||||||
@@ -45,11 +44,10 @@ class _IntroScreenState extends State<IntroScreen> with WidgetsBindingObserver {
|
|||||||
final elapsed = Stopwatch()..start();
|
final elapsed = Stopwatch()..start();
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = context.read<VpnConnection>();
|
||||||
|
|
||||||
// The overlays and haptic layer read these before MainScreen mounts.
|
// Haptics is initialized before runApp. Only visual preferences remain.
|
||||||
final settingsPrefs = Future.wait([
|
final settingsPrefs = Future.wait([
|
||||||
loadRainPref(),
|
loadRainPref(),
|
||||||
loadLightningPref(),
|
loadLightningPref(),
|
||||||
HapticSettings.initialize(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -80,11 +78,10 @@ class _IntroScreenState extends State<IntroScreen> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
void _navigateIfReady() {
|
void _navigateIfReady() {
|
||||||
if (!mounted || _navigated || !_bootComplete) return;
|
if (!mounted || _navigated || !_bootComplete) return;
|
||||||
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.paused) {
|
final lifecycle = WidgetsBinding.instance.lifecycleState;
|
||||||
return;
|
if (lifecycle != null && lifecycle != AppLifecycleState.resumed) return;
|
||||||
}
|
|
||||||
_navigated = true;
|
|
||||||
|
|
||||||
|
_navigated = true;
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
transitionDuration: const Duration(milliseconds: 400),
|
transitionDuration: const Duration(milliseconds: 400),
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:revoltvpn/logic/app_colors.dart';
|
|
||||||
import 'package:revoltvpn/logic/server_list.dart';
|
|
||||||
|
|
||||||
// ── lib/screens/server_selector/selection_screen.dart ──────────────────────
|
|
||||||
// Server picker — reads the server list from the ServerList provider.
|
|
||||||
// Each row shows a flag + city name. Tapping a row persists the choice
|
|
||||||
// and pops back. No latency measurement — the app runs inside a Reality
|
|
||||||
// tunnel and must not emit separate probe traffic.
|
|
||||||
|
|
||||||
class SelectionScreen extends StatelessWidget {
|
|
||||||
const SelectionScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final serverList = context.watch<ServerList>();
|
|
||||||
|
|
||||||
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(
|
|
||||||
'Select Server',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.textWhite,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: ListView.builder(
|
|
||||||
itemCount: serverList.all.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final server = serverList.all[index];
|
|
||||||
final isSelected = index == serverList.selectedIndex;
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: Image.asset(server.flagAsset, width: 28, height: 28),
|
|
||||||
title: Text(server.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color: isSelected ? AppColors.accent : AppColors.textWhite,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
)),
|
|
||||||
trailing: isSelected
|
|
||||||
? const Icon(Icons.check, color: AppColors.accent)
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
serverList.select(index);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
contentPadding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:revoltvpn/logic/app_colors.dart';
|
import 'package:revoltvpn/logic/app_colors.dart';
|
||||||
import 'package:revoltvpn/logic/power_settings.dart';
|
import 'package:revoltvpn/logic/power_settings.dart';
|
||||||
|
|
||||||
/// Keeping the tunnel alive is an OS concern, not an app one.
|
/// Android may restrict background work when battery optimisation is active.
|
||||||
///
|
/// Surface the OS setting we can actually detect and request; do not promise
|
||||||
/// Android stops background services of apps that are not exempt from Doze, and
|
/// reboot/Always-on restoration that the per-session runtime cannot guarantee.
|
||||||
/// removes the whole process group when its task is swiped away. There are
|
|
||||||
/// exactly two sanctioned ways around that, both requiring one user tap, and
|
|
||||||
/// neither can be enabled programmatically. This tile surfaces both.
|
|
||||||
class BackgroundReliabilityTile extends StatefulWidget {
|
class BackgroundReliabilityTile extends StatefulWidget {
|
||||||
const BackgroundReliabilityTile({super.key});
|
const BackgroundReliabilityTile({super.key});
|
||||||
|
|
||||||
@@ -35,8 +32,6 @@ class _BackgroundReliabilityTileState extends State<BackgroundReliabilityTile>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
// The exemption is granted in a system dialog, so the only reliable moment
|
|
||||||
// to re-read it is when we come back to the foreground.
|
|
||||||
if (state == AppLifecycleState.resumed) _refresh();
|
if (state == AppLifecycleState.resumed) _refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +63,8 @@ class _BackgroundReliabilityTileState extends State<BackgroundReliabilityTile>
|
|||||||
style: TextStyle(color: AppColors.textWhite, fontSize: 15),
|
style: TextStyle(color: AppColors.textWhite, fontSize: 15),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'Android stops background apps to save power. Disabling battery '
|
'Android may restrict background work to save power. ReVolt only '
|
||||||
'optimisation is what keeps the VPN running when the app is closed.',
|
'shows the system control it can verify.',
|
||||||
style: TextStyle(color: AppColors.textDim, fontSize: 12),
|
style: TextStyle(color: AppColors.textDim, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -83,8 +78,8 @@ class _BackgroundReliabilityTileState extends State<BackgroundReliabilityTile>
|
|||||||
? 'Battery optimisation: checking…'
|
? 'Battery optimisation: checking…'
|
||||||
: exempt
|
: exempt
|
||||||
? 'Battery optimisation: disabled for Revolt VPN ✓'
|
? 'Battery optimisation: disabled for Revolt VPN ✓'
|
||||||
: 'Battery optimisation: still active — the system may '
|
: 'Battery optimisation: active — Android may stop '
|
||||||
'stop the VPN in the background.',
|
'background VPN work.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: exempt == true ? AppColors.accent : AppColors.textMuted,
|
color: exempt == true ? AppColors.accent : AppColors.textMuted,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
@@ -51,8 +51,14 @@ class _ConnectionModeTileState extends State<ConnectionModeTile> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ConnectionSettings.setMode(next);
|
final saved = await ConnectionSettings.setMode(next);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
if (!saved) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Could not save connection mode.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_mode = next;
|
_mode = next;
|
||||||
@@ -61,10 +67,6 @@ class _ConnectionModeTileState extends State<ConnectionModeTile> {
|
|||||||
widget.onChanged?.call(next);
|
widget.onChanged?.call(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recovers from an orphaned proxy: tear the runtime down and start a fresh
|
|
||||||
/// session so a readable set of credentials exists again. No rewarded ad —
|
|
||||||
/// the user is repairing a connection they already paid for, not opening a
|
|
||||||
/// new one.
|
|
||||||
Future<void> _reconnect() async {
|
Future<void> _reconnect() async {
|
||||||
if (_reconnecting) return;
|
if (_reconnecting) return;
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = context.read<VpnConnection>();
|
||||||
@@ -168,11 +170,6 @@ class _ConnectionModeTileState extends State<ConnectionModeTile> {
|
|||||||
builder: (context, vpn, _) {
|
builder: (context, vpn, _) {
|
||||||
final session = vpn.activeSocksSession;
|
final session = vpn.activeSocksSession;
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
// The proxy can be running while this session is unknown:
|
|
||||||
// Android killed the app process, the foreground service kept
|
|
||||||
// the listener alive, and its credentials existed only in the
|
|
||||||
// memory that died with it. Saying "Connect" there would be a
|
|
||||||
// lie — the user is already connected.
|
|
||||||
final orphaned = vpn.status == VpnStatus.connected &&
|
final orphaned = vpn.status == VpnStatus.connected &&
|
||||||
vpn.adoptedRunningRuntime;
|
vpn.adoptedRunningRuntime;
|
||||||
if (orphaned) {
|
if (orphaned) {
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:revoltvpn/logic/app_colors.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.
|
|
||||||
|
|
||||||
final ValueNotifier<bool> lightningEnabled = ValueNotifier(true);
|
final ValueNotifier<bool> lightningEnabled = ValueNotifier(true);
|
||||||
|
|
||||||
Future<void> loadLightningPref() async {
|
Future<void> loadLightningPref() async {
|
||||||
@@ -35,7 +32,9 @@ class _LightningToggleTileState extends State<LightningToggleTile> {
|
|||||||
|
|
||||||
Future<void> _toggle(bool value) async {
|
Future<void> _toggle(bool value) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool('lightning_effect_enabled', value);
|
final saved = await prefs.setBool('lightning_effect_enabled', value);
|
||||||
|
if (!saved || !mounted) return;
|
||||||
|
|
||||||
lightningEnabled.value = value;
|
lightningEnabled.value = value;
|
||||||
setState(() => _on = value);
|
setState(() => _on = value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:revoltvpn/logic/app_colors.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.
|
|
||||||
|
|
||||||
final ValueNotifier<bool> rainEnabled = ValueNotifier(true);
|
final ValueNotifier<bool> rainEnabled = ValueNotifier(true);
|
||||||
|
|
||||||
Future<void> loadRainPref() async {
|
Future<void> loadRainPref() async {
|
||||||
@@ -35,7 +32,9 @@ class _RainToggleTileState extends State<RainToggleTile> {
|
|||||||
|
|
||||||
Future<void> _toggle(bool value) async {
|
Future<void> _toggle(bool value) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool('rain_effect_enabled', value);
|
final saved = await prefs.setBool('rain_effect_enabled', value);
|
||||||
|
if (!saved || !mounted) return;
|
||||||
|
|
||||||
rainEnabled.value = value;
|
rainEnabled.value = value;
|
||||||
setState(() => _on = value);
|
setState(() => _on = value);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user