v2.0.0: VLESS cleanup — remove bonus ads, throttling, fix Xray compliance, rebrand colors

This commit is contained in:
esefxdz committed 2026-07-25 00:34:14 +03:00
1 parent 10a1450292
commit d106356ca6
20 files changed
+318 -308

No files matched your search

+4 -11
View File
@@ -2,17 +2,13 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.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/logic/app_colors.dart';
import 'package:revoltvpn/components/online_dot.dart';
import 'package:revoltvpn/components/watch_ad_button.dart';
/// BottomBar is the bottom utility panel that surfaces server connectivity status
/// and offers the user an option to support the server when the VPN tunnel is active.
/// BottomBar is the bottom utility panel that surfaces server connectivity status.
class BottomBar extends StatelessWidget {
const BottomBar({super.key});
static const Color _cardBg = Color(0xFF1E2533);
@override
Widget build(BuildContext context) {
return Consumer2<VpnConnection, SessionTimer>(
@@ -26,10 +22,10 @@ class BottomBar extends StatelessWidget {
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: BoxDecoration(
color: _cardBg,
color: AppColors.bgCard,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color.fromRGBO(74, 85, 104, 0.15),
color: AppColors.slate15,
),
),
child: Row(
@@ -43,9 +39,6 @@ class BottomBar extends StatelessWidget {
? 'Server Online'
: 'Disconnected',
),
const Spacer(),
// Support button — only visible while connected AND ads are enabled
if (isConnected && AdManager.adsEnabled) const WatchAdButton(),
],
),
);
+7 -6
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:revoltvpn/logic/app_colors.dart';
import 'package:revoltvpn/logic/session_timer.dart';
/// ClockDisplay renders inside the ConnectButton circle when the VPN is active.
@@ -36,7 +37,7 @@ class ClockDisplay extends StatelessWidget {
height: 32,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color.fromRGBO(0, 229, 255, 0.5),
color: AppColors.cyan50,
),
),
const SizedBox(height: 10),
@@ -45,7 +46,7 @@ class ClockDisplay extends StatelessWidget {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color.fromRGBO(0, 229, 255, 0.6),
color: AppColors.cyan60,
letterSpacing: 1.5,
),
),
@@ -56,11 +57,11 @@ class ClockDisplay extends StatelessWidget {
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.w800,
color: Colors.white,
color: AppColors.textWhite,
letterSpacing: 2,
shadows: [
Shadow(
color: Color.fromRGBO(0, 229, 255, 0.5),
color: AppColors.cyan50,
blurRadius: 16,
),
],
@@ -72,7 +73,7 @@ class ClockDisplay extends StatelessWidget {
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.speed, color: Colors.white54, size: 14),
const Icon(Icons.speed, color: AppColors.textDim, size: 14),
const SizedBox(width: 4),
Text(
timer.currentSpeedKBps > 0.5
@@ -80,7 +81,7 @@ class ClockDisplay extends StatelessWidget {
: 'Idle',
style: const TextStyle(
fontSize: 12,
color: Colors.white54,
color: AppColors.textDim,
fontWeight: FontWeight.w500,
),
),
+28 -17
View File
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
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/components/clock_display.dart';
class ConnectButton extends StatefulWidget {
@@ -17,9 +19,6 @@ class _ConnectButtonState extends State<ConnectButton>
late AnimationController _pulseController;
late Animation<double> _pulseAnim;
static const Color _cyanGlow = Color(0xFF00E5FF);
static const Color _bgDark = Color(0xFF1A202C);
bool _inCooldown = false;
@override
@@ -78,6 +77,18 @@ class _ConnectButtonState extends State<ConnectButton>
} else if (vpn.status == VpnStatus.disconnected ||
vpn.status == VpnStatus.error) {
HapticFeedback.lightImpact();
// ── Show rewarded ad ──
// In production the server creates the session via AdMob SSV callback
// only after a real ad is watched. The debug bypass in HivemindService
// handles session creation when adsEnabled is false.
final adMgr = context.read<AdManager>();
final adWatched = await adMgr.showAd('main_ad');
if (AdManager.adsEnabled && !adWatched) {
// User closed the ad before earning the reward — don't connect.
return;
}
final success = await vpn.connect();
if (success) {
timer.start('main_ad');
@@ -104,9 +115,9 @@ class _ConnectButtonState extends State<ConnectButton>
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1A202C),
backgroundColor: AppColors.bgDark,
title: const Text('VPN Permission Required',
style: TextStyle(color: Colors.white)),
style: TextStyle(color: AppColors.textWhite)),
content: const Text(
'ReVoltVPN needs permission to set up a VPN connection.\n\n'
'Since the permission was denied once, Android won\'t ask again. '
@@ -115,16 +126,16 @@ class _ConnectButtonState extends State<ConnectButton>
'2. Go to Apps → ReVoltVPN\n'
'3. Tap "VPN" and enable it\n'
'4. Return to ReVoltVPN and tap connect',
style: TextStyle(color: Color(0xFFA0AEC0)),
style: TextStyle(color: AppColors.textMuted),
),
actions: [
FilledButton(
onPressed: () => Navigator.of(ctx).pop(),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
backgroundColor: AppColors.cyan,
),
child: const Text('Got it',
style: TextStyle(color: Color(0xFF0D1117))),
style: TextStyle(color: AppColors.bgDeep)),
),
],
),
@@ -153,15 +164,15 @@ class _ConnectButtonState extends State<ConnectButton>
height: 260,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _bgDark,
color: AppColors.bgDark,
border: Border.all(
color: _inCooldown
? Colors.grey.withValues(alpha: 0.5)
? Colors.grey.withValues(alpha: 0.5) // dynamic, OK inline
: isConnected
? _cyanGlow
? AppColors.cyan
: isConnecting
? const Color.fromRGBO(0, 229, 255, 0.5)
: const Color.fromRGBO(74, 85, 104, 0.5),
? AppColors.cyan50
: AppColors.slate50,
width: isConnected && !_inCooldown ? 3 : 2,
),
boxShadow: isConnected
@@ -194,7 +205,7 @@ class _ConnectButtonState extends State<ConnectButton>
height: 48,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Color.fromRGBO(0, 229, 255, 0.7),
color: AppColors.cyan70,
),
);
}
@@ -206,15 +217,15 @@ class _ConnectButtonState extends State<ConnectButton>
if (inCooldown) {
label = 'PLEASE WAIT';
icon = Icons.hourglass_bottom;
tint = Colors.grey;
tint = Colors.grey; // dynamic, OK inline
} else if (status == VpnStatus.error) {
label = 'TRY AGAIN';
icon = Icons.refresh;
tint = const Color(0xFFEF5350); // soft red — something went wrong
tint = AppColors.red; // soft red — something went wrong
} else {
label = 'TAP TO CONNECT';
icon = Icons.power_settings_new;
tint = const Color.fromRGBO(74, 85, 104, 0.7);
tint = AppColors.slate70;
}
return Column(
+2 -1
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:revoltvpn/logic/app_colors.dart';
import 'package:url_launcher/url_launcher.dart';
/// GdprButton lets the user review or change their ad consent choices.
@@ -83,7 +84,7 @@ class GdprButton extends StatelessWidget {
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.admin_panel_settings_outlined,
color: Colors.white, size: 28),
color: AppColors.textWhite, size: 28),
onPressed: _showConsentForm,
tooltip: 'Ad consent & privacy choices',
splashRadius: 24,
+4 -6
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:revoltvpn/logic/app_colors.dart';
/// Header displays the ReVolt brand logo, title, and subtitle.
///
@@ -7,9 +8,6 @@ import 'package:flutter/material.dart';
class Header extends StatelessWidget {
const Header({super.key});
// cyber-revolt cyan glow accent color used as a fallback for the shield icon
static const Color _cyanGlow = Color(0xFF00E5FF);
@override
Widget build(BuildContext context) {
return Column(
@@ -23,7 +21,7 @@ class Header extends StatelessWidget {
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.shield,
size: 120,
color: _cyanGlow,
color: AppColors.cyan,
),
),
const SizedBox(height: 8), // Standard padding between the logo and the title
@@ -35,7 +33,7 @@ class Header extends StatelessWidget {
fontSize: 22,
fontWeight: FontWeight.w800,
letterSpacing: 8,
color: Colors.white,
color: AppColors.textWhite,
),
),
const SizedBox(height: 2), // Close spacing for secondary information
@@ -47,7 +45,7 @@ class Header extends StatelessWidget {
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 4,
color: Color(0xAA4A5568), // Semi-transparent slate gray
color: AppColors.slateAA, // Semi-transparent slate gray
),
),
],
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:revoltvpn/logic/app_colors.dart';
class InfoButton extends StatelessWidget {
const InfoButton({super.key});
@@ -14,7 +15,7 @@ class InfoButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.help_outline, color: Colors.white, size: 28),
icon: const Icon(Icons.help_outline, color: AppColors.textWhite, size: 28),
onPressed: _launchGitHub,
tooltip: 'About ReVoltVPN',
splashRadius: 24,
+5 -4
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:revoltvpn/logic/app_colors.dart';
/// OnlineDot displays a glowing green dot when the server is healthy,
/// and a muted gray dot when disconnected or still syncing.
@@ -30,11 +31,11 @@ class OnlineDot extends StatelessWidget {
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isOnline ? const Color(0xFF00E676) : const Color(0xFF4A5568),
color: isOnline ? AppColors.green : AppColors.slate,
boxShadow: isOnline
? const [
BoxShadow(
color: Color.fromRGBO(0, 230, 118, 0.5),
color: AppColors.green50,
blurRadius: 6,
),
]
@@ -48,8 +49,8 @@ class OnlineDot extends StatelessWidget {
fontSize: 13,
fontWeight: FontWeight.w500,
color: isOnline
? const Color.fromRGBO(255, 255, 255, 0.8)
: const Color(0xFF4A5568),
? AppColors.textWhite80
: AppColors.slate,
letterSpacing: 0.5,
),
),
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:revoltvpn/logic/app_colors.dart';
class PrivacyPolicyButton extends StatelessWidget {
const PrivacyPolicyButton({super.key});
@@ -14,7 +15,7 @@ class PrivacyPolicyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.privacy_tip_outlined, color: Colors.white, size: 28),
icon: const Icon(Icons.privacy_tip_outlined, color: AppColors.textWhite, size: 28),
onPressed: _launchPrivacyPolicy,
tooltip: 'Privacy Policy',
splashRadius: 24,
+9 -8
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:revoltvpn/logic/app_colors.dart';
import 'package:revoltvpn/logic/vpn_connection.dart';
/// StatusText displays the status of the VPN tunnel.
@@ -22,12 +23,12 @@ class StatusText extends StatelessWidget {
key: ValueKey(vpn.status),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0x22FFFFFF), // Subtle glass effect background
color: AppColors.glassBg, // Subtle glass effect background
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isConnected
? const Color(0x8800E5FF) // Glowing cyan border when connected
: const Color(0x33FFFFFF), // Muted translucent border when disconnected/connecting
? AppColors.cyanGlow // Glowing cyan border when connected
: AppColors.glassBorder, // Muted translucent border when disconnected/connecting
width: 1,
),
),
@@ -45,7 +46,7 @@ class StatusText extends StatelessWidget {
const Text(
'Helsinki, Finland',
style: TextStyle(
color: Color(0xFF00E5FF),
color: AppColors.cyan,
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 0.5,
@@ -58,22 +59,22 @@ class StatusText extends StatelessWidget {
height: 14,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: Color(0xFF00E5FF),
color: AppColors.cyan,
),
)
else
const Icon(
Icons.shield_outlined,
size: 18,
color: Color(0xAA4A5568),
color: AppColors.slateAA,
),
const SizedBox(width: 8),
Text(
vpn.statusMessage.toUpperCase(),
style: TextStyle(
color: isConnecting
? const Color(0xFF00E5FF)
: const Color(0xAA4A5568),
? AppColors.cyan
: AppColors.slateAA,
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 1,
-86
View File
@@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:revoltvpn/logic/session_timer.dart';
import 'package:revoltvpn/logic/ad_manager.dart';
/// WatchAdButton is a voluntary option allowing the user to watch a rewarded video ad
/// and extend their active VPN tunnel session by 30 minutes as a reward.
class WatchAdButton extends StatelessWidget {
const WatchAdButton({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () async {
final adWatched = await context.read<AdManager>().showAd('bonus_ad');
if (!adWatched || !context.mounted) return;
// The actual session extension happens server-side via AdMob SSV.
// addBonusTime() syncs the new limits from the server.
final synced = await context.read<SessionTimer>().addBonusTime();
if (!context.mounted) return;
if (synced) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'+30 minutes added!',
style: TextStyle(fontWeight: FontWeight.w600),
),
backgroundColor: const Color(0xFF006064),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
duration: const Duration(seconds: 2),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Could not confirm bonus. Please check your connection.',
style: TextStyle(fontWeight: FontWeight.w600),
),
backgroundColor: Color(0xFF5D4037),
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
),
);
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: const LinearGradient(
colors: [Color(0x1A00E5FF), Color(0x0D00E5FF)],
),
border: Border.all(
color: const Color.fromRGBO(0, 229, 255, 0.25),
),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('❤️', style: TextStyle(fontSize: 14)),
SizedBox(width: 6),
Text(
'Support +30m',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF00E5FF),
letterSpacing: 0.5,
),
),
],
),
),
),
);
}
}
+74
View File
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
// =============================================================================
// app_colors.dart — Single source of truth for every color in ReVoltVPN
// ─────────────────────────────────────────────────────────────────────────────
// Import this file and reference AppColors.* instead of hardcoding hex values
// or Color.fromRGBO() calls anywhere else in the app.
// =============================================================================
abstract final class AppColors {
AppColors._();
// ── Primary: Cyan ──────────────────────────────────────────────────────────
static const Color cyan = Color(0xFF00E5FF);
static const Color cyan15 = Color(0x2600E5FF); // 15 % opacity
static const Color cyan25 = Color(0x4000E5FF); // 25 % opacity
static const Color cyan50 = Color(0x8000E5FF); // 50 % opacity
static const Color cyan60 = Color(0x9900E5FF); // 60 % opacity
static const Color cyan70 = Color(0xB300E5FF); // 70 % opacity
static const Color cyan05 = Color(0x0D00E5FF); // 5 % opacity
static const Color cyanGlow = Color(0x8800E5FF); // glowing border
// ── Backgrounds ────────────────────────────────────────────────────────────
static const Color bgDeep = Color(0xFF0D1117); // intro / scaffold
static const Color bgDark = Color(0xFF1A202C); // button circle
static const Color bgSurface = Color(0xFF151C28); // colorScheme surface
static const Color bgCard = Color(0xFF1E2533); // bottom bar, snackbar
static const Color bgOverlay = Color(0xAA0D1117); // semi-transparent overlay
// ── Slate / Gray ───────────────────────────────────────────────────────────
static const Color slate = Color(0xFF4A5568);
static const Color slate15 = Color(0x264A5568); // 15 % opacity
static const Color slate50 = Color(0x804A5568); // 50 % opacity
static const Color slate70 = Color(0xB34A5568); // 70 % opacity
static const Color slateAA = Color(0xAA4A5568); // ~67 % (legacy usage)
// ── Text ───────────────────────────────────────────────────────────────────
static const Color textWhite = Colors.white;
static const Color textDim = Colors.white54;
static const Color textWhite80 = Color(0xCCFFFFFF); // 80 % white
static const Color textMuted = Color(0xFFA0AEC0); // dialog body text
// ── Semantic ───────────────────────────────────────────────────────────────
static const Color green = Color(0xFF00E676);
static const Color green50 = Color(0x8000E676); // 50 % opacity
static const Color red = Color(0xFFEF5350);
// ── Glass / Borders ────────────────────────────────────────────────────────
static const Color glassBg = Color(0x22FFFFFF); // subtle glass background
static const Color glassBorder = Color(0x33FFFFFF); // muted translucent border
// ── Intro screen snackbars ─────────────────────────────────────────────────
static const Color snackSuccess = Color(0xFF006064);
static const Color snackError = Color(0xFF5D4037);
// ═══════════════════════════════════════════════════════════════════════════
// FUTURE — Yellow palette (not yet wired into the UI)
// ═══════════════════════════════════════════════════════════════════════════
static const Color yellow = Color(0xFFFFD600);
static const Color yellowDark = Color(0xFFFFAB00);
static const Color yellowLight = Color(0xFFFFFF00);
static const Color yellowFaded = Color(0x1AFFD600);
static const Color yellowAmber = Color(0xFFFFC107);
static const Color yellowMustard = Color(0xFFC6A700);
// ═══════════════════════════════════════════════════════════════════════════
// FUTURE — Black palette (not yet wired into the UI)
// ═══════════════════════════════════════════════════════════════════════════
static const Color black = Color(0xFF000000);
static const Color blackOff = Color(0xFF0A0A0A);
static const Color blackFaded = Color(0xAA000000);
static const Color blackWarm = Color(0xFF121212);
static const Color blackCharcoal = Color(0xFF1A1A1A);
}
-1
View File
@@ -17,7 +17,6 @@ abstract final class AppConfig {
// ── VLESS ──────────────────────────────────────────────────────────────
static const int serverPort = 443;
static const String vlessFlow = 'xtls-rprx-vision';
static const String vlessSecurity = 'tls';
static const String vlessType = 'ws';
-2
View File
@@ -42,7 +42,6 @@ class HivemindService {
try {
final customData = jsonEncode({
'device_id': deviceId,
'ad_type': 'main_ad',
'nonce': nonce,
});
final fakeAdmobPingUrl = Uri.parse(
@@ -75,7 +74,6 @@ class HivemindService {
'?path=${Uri.encodeComponent(AppConfig.vlessPath)}'
'&security=${AppConfig.vlessSecurity}'
'&type=${AppConfig.vlessType}'
'&flow=${AppConfig.vlessFlow}'
'#ReVoltVPN';
_expectedNonce = null; // consumed successfully
-24
View File
@@ -22,7 +22,6 @@ class SessionTimer extends ChangeNotifier {
int _remainingSeconds = 0;
int _quotaBytes = 0;
int _usedBytes = 0;
bool _isThrottled = false;
// ── Sync / Network Health ──────────────────────────────────────────────────
bool _hasSyncedOnce = false;
@@ -65,7 +64,6 @@ class SessionTimer extends ChangeNotifier {
int get quotaBytes => _quotaBytes;
int get usedBytes => _usedBytes;
int get remainingBytes => _quotaBytes > _usedBytes ? _quotaBytes - _usedBytes : 0;
bool get isThrottled => _isThrottled;
double get currentSpeedKBps => _currentSpeedKBps;
String get formatted {
@@ -136,7 +134,6 @@ class SessionTimer extends ChangeNotifier {
_usedBytes = 0;
_lastUsedBytes = 0;
_currentSpeedKBps = 0.0;
_isThrottled = false;
_tickCount = 0;
_hasSyncedOnce = false;
_consecutiveFailures = 0;
@@ -251,7 +248,6 @@ class SessionTimer extends ChangeNotifier {
_remainingSeconds = data['expires_in_seconds'] ?? _remainingSeconds;
_quotaBytes = data['quota_bytes'] ?? _quotaBytes;
_usedBytes = data['used_bytes'] ?? _usedBytes;
_isThrottled = data['is_throttled'] ?? false;
// ── Speed calculation over the polling interval ────────────────────
final int deltaBytes = _usedBytes - _lastUsedBytes;
@@ -290,26 +286,6 @@ class SessionTimer extends ChangeNotifier {
}
}
// ── Bonus Time ─────────────────────────────────────────────────────────────
/// Called after the user watches a bonus rewarded ad.
///
/// The actual session extension is handled server-side via the AdMob SSV
/// callback. This method simply re-syncs so the UI picks up the new
/// limits and un-throttled status.
///
/// Returns `true` if the sync succeeded, so the UI can decide whether to
/// show a success snackbar.
Future<bool> addBonusTime() async {
try {
await _syncWithHivemind();
return _hasSyncedOnce;
} catch (e) {
debugPrint('Bonus-time sync error: $e');
return false;
}
}
// ── Stop ───────────────────────────────────────────────────────────────────
/// Called externally (e.g. from ConnectButton) to begin tearing down.
+12
View File
@@ -77,12 +77,16 @@ class VpnConnection extends ChangeNotifier {
switch (status.connectionState) {
case VlessConnectionState.connected:
_setStatus(VpnStatus.connected, 'Secured');
break;
case VlessConnectionState.disconnected:
_setStatus(VpnStatus.disconnected, 'Tap to connect');
break;
case VlessConnectionState.connecting:
_setStatus(VpnStatus.connecting, 'Establishing tunnel…');
break;
case VlessConnectionState.disconnecting:
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
break;
case VlessConnectionState.unknown:
// Native backend emitted an unrecognized state — treat as error
// only if we aren't already in a terminal state.
@@ -90,6 +94,7 @@ class VpnConnection extends ChangeNotifier {
_status != VpnStatus.disconnected) {
_setStatus(VpnStatus.error, 'Connection failed');
}
break;
}
}
@@ -221,6 +226,13 @@ class VpnConnection extends ChangeNotifier {
@override
void dispose() {
if (_status == VpnStatus.connected || _status == VpnStatus.connecting) {
try {
_vless.stopVless();
} catch (_) {
// Best-effort — widget is being destroyed anyway
}
}
super.dispose();
}
}
+7 -9
View File
@@ -7,6 +7,7 @@ import 'package:revoltvpn/logic/vpn_connection.dart';
import 'package:revoltvpn/logic/session_timer.dart';
import 'package:revoltvpn/logic/ad_manager.dart';
import 'package:revoltvpn/logic/consent_manager.dart';
import 'package:revoltvpn/logic/app_colors.dart';
import 'package:revoltvpn/screens/intro.dart';
/// The entry point for the REVOLT VPN application.
@@ -79,21 +80,18 @@ class ReVoltApp extends StatelessWidget {
/// Constructs the global dark theme used across the application.
ThemeData _buildDarkTheme() {
const cyanAccent = Color(0xFF00E5FF);
const bgDark = Color(0xFF0D1117);
return ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: bgDark,
scaffoldBackgroundColor: AppColors.bgDeep,
colorScheme: const ColorScheme.dark(
primary: cyanAccent,
secondary: cyanAccent,
surface: Color(0xFF151C28),
primary: AppColors.cyan,
secondary: AppColors.cyan,
surface: AppColors.bgSurface,
),
fontFamily: 'Roboto',
snackBarTheme: SnackBarThemeData(
backgroundColor: const Color(0xFF1E2533),
contentTextStyle: const TextStyle(color: Colors.white),
backgroundColor: AppColors.bgCard,
contentTextStyle: const TextStyle(color: AppColors.textWhite),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
behavior: SnackBarBehavior.floating,
),
+5 -4
View File
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:revoltvpn/logic/app_colors.dart';
import 'package:revoltvpn/screens/main_screen.dart';
class IntroScreen extends StatefulWidget {
@@ -58,7 +59,7 @@ class _IntroScreenState extends State<IntroScreen> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
backgroundColor: AppColors.bgDeep,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -71,7 +72,7 @@ class _IntroScreenState extends State<IntroScreen> with WidgetsBindingObserver {
return const Icon(
Icons.shield,
size: 140,
color: Color(0xFF00E5FF),
color: AppColors.cyan,
);
},
),
@@ -79,7 +80,7 @@ class _IntroScreenState extends State<IntroScreen> with WidgetsBindingObserver {
const Text(
'REVOLT VPN',
style: TextStyle(
color: Colors.white,
color: AppColors.textWhite,
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: 2.0,
@@ -90,7 +91,7 @@ class _IntroScreenState extends State<IntroScreen> with WidgetsBindingObserver {
width: 40,
height: 40,
child: CircularProgressIndicator(
color: Color(0xFF00E5FF),
color: AppColors.cyan,
strokeWidth: 3.0,
),
),
+4 -6
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:revoltvpn/logic/app_colors.dart';
import 'package:revoltvpn/components/header.dart';
import 'package:revoltvpn/components/status_text.dart';
import 'package:revoltvpn/components/connect_button.dart';
@@ -18,23 +19,20 @@ import 'package:revoltvpn/components/gdpr_button.dart';
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
// Background base color
static const Color _bgTop = Color(0xFF0D1117);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bgTop,
backgroundColor: AppColors.bgDeep,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
color: _bgTop,
color: AppColors.bgDeep,
image: DecorationImage(
image: AssetImage('assets/background.png'),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Color(0xAA0D1117), // Deep dark tint overlay to guarantee readability of overlay elements
AppColors.bgOverlay, // Deep dark tint overlay to guarantee readability of overlay elements
BlendMode.darken,
),
),