diff --git a/.gitignore b/.gitignore index 1d5aa91..a65f31c 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ technical_debt_*.md server_full_picture_2.txt hivemind_backup.py UI_POSITIONING.md +SERVER_SETUP.md diff --git a/lib/components/clock_display.dart b/lib/components/clock_display.dart index ea41570..8b76934 100644 --- a/lib/components/clock_display.dart +++ b/lib/components/clock_display.dart @@ -3,13 +3,6 @@ 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. -/// -/// Shows a circular quota-progress arc, a glowing countdown timer, and the -/// current transfer speed — all sized to fit within the 260px button. -/// -/// Before the first server sync arrives, a subtle "Syncing…" placeholder -/// is shown instead of a misleading 00:00:00. class ClockDisplay extends StatelessWidget { const ClockDisplay({super.key}); diff --git a/lib/components/connect_button.dart b/lib/components/connect_button.dart index 24eecd0..f61d857 100644 --- a/lib/components/connect_button.dart +++ b/lib/components/connect_button.dart @@ -6,14 +6,6 @@ import 'package:revoltvpn/logic/vpn_connection.dart'; import 'package:revoltvpn/logic/session_timer.dart'; import 'package:revoltvpn/logic/ad_manager.dart'; -/// ConnectButton — circle with brand.png + pulsing glow when connected. -/// -/// Rules: -/// disconnected → connect (ad → config → tunnel) -/// connecting → disconnect (cancel) -/// connected → disconnect -/// error → connect (retry) -/// disconnecting → ignore class ConnectButton extends StatefulWidget { const ConnectButton({super.key}); @@ -59,7 +51,7 @@ class _ConnectButtonState extends State } Future _handleTap() async { - // 1-second cooldown to prevent spam + final now = DateTime.now(); if (now.difference(_lastTap).inMilliseconds < 1000) return; _lastTap = now; @@ -70,7 +62,7 @@ class _ConnectButtonState extends State final timer = context.read(); final ad = context.read(); - // ── Disconnect if connected or connecting ─────────────────────────── + if (vpn.status == VpnStatus.connected || vpn.status == VpnStatus.connecting) { _busy = false; timer.stop(); // MUST stop timer BEFORE disconnect — prevents auto-reconnect @@ -78,10 +70,10 @@ class _ConnectButtonState extends State return; } - // ── Ignore if disconnecting ───────────────────────────────────────── + if (vpn.status == VpnStatus.disconnecting) return; - // ── Connect (or retry) ───────────────────────────────────────────── + if (_busy) return; setState(() => _busy = true); diff --git a/lib/components/online_dot.dart b/lib/components/online_dot.dart index 44d2f76..578a1d1 100644 --- a/lib/components/online_dot.dart +++ b/lib/components/online_dot.dart @@ -1,13 +1,8 @@ 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. class OnlineDot extends StatelessWidget { - /// Whether the server has been confirmed reachable. final bool isOnline; - - /// Optional custom label. Falls back to "Server Online" / "Disconnected". final String? label; const OnlineDot({ diff --git a/lib/components/sidebar_contents/discord_link_button.dart b/lib/components/sidebar_contents/discord_link_button.dart index 5f80021..2991adf 100644 --- a/lib/components/sidebar_contents/discord_link_button.dart +++ b/lib/components/sidebar_contents/discord_link_button.dart @@ -5,7 +5,6 @@ import 'package:revoltvpn/logic/app_colors.dart'; class DiscordLinkButton extends StatelessWidget { const DiscordLinkButton({super.key}); - /// Public entry point for the sidebar or any other widget. static Future launchDiscord() async { final Uri url = Uri.parse('https://discord.gg/revoltvpn'); if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { diff --git a/lib/components/sidebar_contents/gdpr_button.dart b/lib/components/sidebar_contents/gdpr_button.dart index 0fa002f..696ac13 100644 --- a/lib/components/sidebar_contents/gdpr_button.dart +++ b/lib/components/sidebar_contents/gdpr_button.dart @@ -4,17 +4,9 @@ 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. -/// -/// When tapped, it tries to show Google's UMP consent form so the user -/// can adjust their preferences (personalized vs non-personalized ads). -/// If the form isn't available (e.g. user is outside the EEA), it falls -/// back to opening the privacy policy instead. class GdprButton extends StatelessWidget { const GdprButton({super.key}); - /// Public entry point so the sidebar (or any other widget) can trigger - /// the consent flow without embedding an IconButton. static Future showConsentForm() async { try { // If the form is already available, show it directly — no need to diff --git a/lib/components/sidebar_contents/info_button.dart b/lib/components/sidebar_contents/info_button.dart index b5e4d25..9a2f575 100644 --- a/lib/components/sidebar_contents/info_button.dart +++ b/lib/components/sidebar_contents/info_button.dart @@ -5,7 +5,6 @@ import 'package:revoltvpn/logic/app_colors.dart'; class InfoButton extends StatelessWidget { const InfoButton({super.key}); - /// Public entry point for the sidebar or any other widget. static Future launchGitHub() async { final Uri url = Uri.parse('https://github.com/esefxdz/ReVoltVPN'); if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { diff --git a/lib/components/sidebar_contents/privacy_policy_button.dart b/lib/components/sidebar_contents/privacy_policy_button.dart index 3103e25..09b6bd1 100644 --- a/lib/components/sidebar_contents/privacy_policy_button.dart +++ b/lib/components/sidebar_contents/privacy_policy_button.dart @@ -5,7 +5,6 @@ import 'package:revoltvpn/logic/app_colors.dart'; class PrivacyPolicyButton extends StatelessWidget { const PrivacyPolicyButton({super.key}); - /// Public entry point for the sidebar or any other widget. static Future launchPrivacyPolicy() async { final Uri url = Uri.parse('https://github.com/esefxdz/ReVoltVPN/blob/main/PRIVACY_POLICY.md'); if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { diff --git a/lib/components/sidebar_drawer.dart b/lib/components/sidebar_drawer.dart index 2ed3157..ee8caf0 100644 --- a/lib/components/sidebar_drawer.dart +++ b/lib/components/sidebar_drawer.dart @@ -5,8 +5,6 @@ import 'package:revoltvpn/components/sidebar_contents/privacy_policy_button.dart import 'package:revoltvpn/components/sidebar_contents/info_button.dart'; import 'package:revoltvpn/components/sidebar_contents/discord_link_button.dart'; -/// Right-edge drawer. Four fully-tappable rows — tap anywhere on a row -/// to trigger its action and close the drawer. class SidebarDrawer extends StatelessWidget { const SidebarDrawer({super.key}); @@ -19,7 +17,6 @@ class SidebarDrawer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // ── Header ────────────────────────────────────────────────── Container( padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), decoration: const BoxDecoration( @@ -51,7 +48,6 @@ class SidebarDrawer extends StatelessWidget { const SizedBox(height: 8), - // ── Ad Consent (fully tappable) ───────────────────────────── _Row( icon: Icons.admin_panel_settings_outlined, label: 'Ad Consent', @@ -61,7 +57,6 @@ class SidebarDrawer extends StatelessWidget { }, ), - // ── Privacy Policy (fully tappable) ───────────────────────── _Row( icon: Icons.privacy_tip_outlined, label: 'Privacy Policy', @@ -71,7 +66,6 @@ class SidebarDrawer extends StatelessWidget { }, ), - // ── About (fully tappable) ────────────────────────────────── _Row( icon: Icons.help_outline, label: 'About', @@ -81,7 +75,6 @@ class SidebarDrawer extends StatelessWidget { }, ), - // ── Discord (fully tappable) ──────────────────────────────── _Row( icon: Icons.discord, label: 'Discord', diff --git a/lib/components/speed_box.dart b/lib/components/speed_box.dart index 336e316..38bf44b 100644 --- a/lib/components/speed_box.dart +++ b/lib/components/speed_box.dart @@ -3,7 +3,6 @@ import 'package:provider/provider.dart'; import 'package:revoltvpn/logic/app_colors.dart'; import 'package:revoltvpn/logic/session_timer.dart'; -/// SpeedBox — a small card showing current network speed or "Idle". class SpeedBox extends StatelessWidget { const SpeedBox({super.key}); diff --git a/lib/components/status_bar.dart b/lib/components/status_bar.dart index 15cca79..3faaf2f 100644 --- a/lib/components/status_bar.dart +++ b/lib/components/status_bar.dart @@ -5,8 +5,6 @@ import 'package:revoltvpn/logic/vpn_connection.dart'; import 'package:revoltvpn/logic/session_timer.dart'; import 'package:revoltvpn/components/online_dot.dart'; -/// StatusBar — unified bottom card combining the Finland connection badge -/// and the server health indicator in one box. class StatusBar extends StatelessWidget { const StatusBar({super.key}); diff --git a/lib/components/timer_box.dart b/lib/components/timer_box.dart index aa9708c..db6bb54 100644 --- a/lib/components/timer_box.dart +++ b/lib/components/timer_box.dart @@ -3,7 +3,6 @@ import 'package:provider/provider.dart'; import 'package:revoltvpn/logic/app_colors.dart'; import 'package:revoltvpn/logic/session_timer.dart'; -/// TimerBox — a rectangular card showing the session countdown. class TimerBox extends StatelessWidget { const TimerBox({super.key}); diff --git a/lib/logic/app_colors.dart b/lib/logic/app_colors.dart index 997e074..259404b 100644 --- a/lib/logic/app_colors.dart +++ b/lib/logic/app_colors.dart @@ -1,12 +1,5 @@ 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._(); @@ -39,8 +32,7 @@ abstract final class AppColors { 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 + static const Color glassBg = Color(0x22FFFFFF); + static const Color glassBorder = Color(0x33FFFFFF); } diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index 45fdc68..fd577b5 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -7,46 +7,31 @@ import 'package:revoltvpn/logic/vpn_connection.dart'; import 'package:revoltvpn/logic/crypto_service.dart'; import 'package:revoltvpn/components/notification.dart'; -/// SessionTimer is the client-side session authority for the VPN tunnel. -/// -/// Polls the Hivemind API to sync remaining time, data quotas, speed, and -/// throttling status. Rather than guessing the session length optimistically, -/// it waits for the first successful sync before the local countdown begins. class SessionTimer extends ChangeNotifier { Timer? _timer; int _tickCount = 0; final VpnConnection vpnConnection; - // ── Session State ────────────────────────────────────────────────────────── int _remainingSeconds = 0; int _quotaBytes = 0; int _usedBytes = 0; - // ── Sync / Network Health ────────────────────────────────────────────────── bool _hasSyncedOnce = false; int _consecutiveFailures = 0; bool _isDisconnecting = false; - bool _userInitiatedStop = false; // true when user tapped disconnect + bool _userInitiatedStop = false; - /// Maximum consecutive sync failures before the local countdown is paused - /// to avoid showing a false "expired" state while the network is down. static const int _maxConsecutiveFailures = 3; - /// How long we tolerate total silence from the server before forcibly - /// killing the VPN. Prevents the "orphaned tunnel" scenario where the - /// server is gone but the VPN stays up forever. static const int _maxOfflineSeconds = 120; int _offlineSeconds = 0; - /// Seconds between server polls (reduced from 3 to ease server load). static const int _pollIntervalSeconds = 5; - // ── Speed State ──────────────────────────────────────────────────────────── int _lastUsedBytes = 0; - double _currentSpeedKBps = 0.0; // kilobytes per second + double _currentSpeedKBps = 0.0; - // ── Notification dedup ───────────────────────────────────────────────────── String _lastNotifTime = ''; String _lastNotifSpeed = ''; @@ -54,8 +39,6 @@ class SessionTimer extends ChangeNotifier { vpnConnection.addListener(_onVpnConnectionChanged); } - // ── Public Getters ───────────────────────────────────────────────────────── - int get remaining => _remainingSeconds; bool get isRunning => _timer != null && _timer!.isActive; bool get isExpired => _remainingSeconds <= 0 && !isRunning; @@ -94,11 +77,11 @@ class SessionTimer extends ChangeNotifier { return (_quotaBytes - _usedBytes) / _quotaBytes; } - // ── Lifecycle ────────────────────────────────────────────────────────────── + // Lifecycle /// Fires whenever VpnConnection state changes. void _onVpnConnectionChanged() { - // ── Startup restoration: app killed while VPN was running ────────── + if (vpnConnection.status == VpnStatus.connected && !isRunning && vpnConnection.isStartupRestoration) { @@ -106,7 +89,7 @@ class SessionTimer extends ChangeNotifier { return; } - // ── Network-blip reconnect: VPN dropped and came back on its own ── + // The underlying VLESS tunnel may briefly flap. If we weren't // explicitly stopped by the user, resume where we left off. if (vpnConnection.status == VpnStatus.connected && @@ -118,16 +101,12 @@ class SessionTimer extends ChangeNotifier { return; } - // ── Auto-cleanup when the VPN actually goes down ────────────────── + if (vpnConnection.status == VpnStatus.disconnected && isRunning) { _stopInternal(); } } - /// Starts the local countdown and begins server polling. - /// - /// Values are initialised at 0 — the UI shows "Syncing…" until the first - /// successful [_syncWithHivemind] returns real data. Future start(String adType) async { _remainingSeconds = 0; _quotaBytes = 0; @@ -151,7 +130,7 @@ class SessionTimer extends ChangeNotifier { } void _tick(Timer t) { - // ── Local countdown ────────────────────────────────────────────────── + // Decrement the local clock as long as we're not in a network blackout. if (_hasSyncedOnce && _consecutiveFailures < _maxConsecutiveFailures) { if (_remainingSeconds > 0) { @@ -159,7 +138,7 @@ class SessionTimer extends ChangeNotifier { } } - // ── Offline watchdog ───────────────────────────────────────────────── + // If the server has been unreachable for too long, kill the VPN. // Don't let an orphaned tunnel keep running forever. if (_consecutiveFailures >= _maxConsecutiveFailures) { @@ -171,7 +150,7 @@ class SessionTimer extends ChangeNotifier { } } - // ── Local expiry ───────────────────────────────────────────────────── + // When our clock hits zero, disconnect immediately. Don't wait for the // next server poll — the session is over and the user expects the VPN // to drop right now. @@ -186,7 +165,7 @@ class SessionTimer extends ChangeNotifier { _syncWithHivemind(); } - // ── Update notification every tick for smooth countdown ───────────── + // Uses the locally-decremented time so the notification stays in sync // with the in-app display, not gated behind the 5 s server poll. if (_hasSyncedOnce) { @@ -206,8 +185,6 @@ class SessionTimer extends ChangeNotifier { notifyListeners(); } - /// Force-disconnects the VPN and stops the timer with a status message. - /// Used as a local fallback when the server can't be reached. void _forceDisconnect(String reason) { if (_isDisconnecting) return; // Already tearing down debugPrint('[Timer] $reason — forcing disconnect.'); @@ -222,7 +199,7 @@ class SessionTimer extends ChangeNotifier { vpnConnection.disconnect(); } - // ── Server Sync ──────────────────────────────────────────────────────────── + Future _syncWithHivemind() async { // Never sync while the user is explicitly disconnecting. @@ -249,7 +226,7 @@ class SessionTimer extends ChangeNotifier { _quotaBytes = data['quota_bytes'] ?? _quotaBytes; _usedBytes = data['used_bytes'] ?? _usedBytes; - // ── Speed calculation over the polling interval ──────────────────── + final int deltaBytes = _usedBytes - _lastUsedBytes; if (_hasSyncedOnce && deltaBytes > 0) { _currentSpeedKBps = (deltaBytes / _pollIntervalSeconds) / 1000; @@ -258,7 +235,7 @@ class SessionTimer extends ChangeNotifier { } _lastUsedBytes = _usedBytes; - // ── Network is healthy — reset failure counters ──────────────────── + _consecutiveFailures = 0; _offlineSeconds = 0; _hasSyncedOnce = true; @@ -277,8 +254,6 @@ class SessionTimer extends ChangeNotifier { } } - /// Increments the failure counter; pauses the local countdown after - /// [_maxConsecutiveFailures] to avoid displaying a false "expired" clock. void _markSyncFailure() { _consecutiveFailures++; if (_consecutiveFailures >= _maxConsecutiveFailures) { @@ -286,12 +261,8 @@ class SessionTimer extends ChangeNotifier { } } - // ── Stop ─────────────────────────────────────────────────────────────────── - /// Called externally (e.g. from ConnectButton) to begin tearing down. - /// Cancels the timer but keeps the notification alive — - /// [_onVpnConnectionChanged] will do final cleanup when the VPN actually - /// confirms it's disconnected. + void stop() { _userInitiatedStop = true; _isDisconnecting = true; @@ -303,9 +274,6 @@ class SessionTimer extends ChangeNotifier { notifyListeners(); } - /// Restarts the periodic timer without resetting session state. - /// Used when the VPN reconnects after a brief network blip so we don't - /// lose track of remaining time / data. void _resumeTicking() { _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), _tick); @@ -314,8 +282,6 @@ class SessionTimer extends ChangeNotifier { notifyListeners(); } - /// Internal cleanup — called when VpnConnection confirms disconnected. - /// Cancels the persistent notification since the tunnel is truly down. void _stopInternal() { _isDisconnecting = true; _timer?.cancel(); diff --git a/lib/logic/vpn_connection.dart b/lib/logic/vpn_connection.dart index 1676931..87dbda7 100644 --- a/lib/logic/vpn_connection.dart +++ b/lib/logic/vpn_connection.dart @@ -1,13 +1,8 @@ -// ============================================================================= -// vpn_connection.dart — VLESS VPN Tunnel Manager -// ============================================================================= - import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_vless/flutter_vless.dart'; import 'package:revoltvpn/logic/hivemind_service.dart'; -// ── VPN States ──────────────────────────────────────────────────────────────── enum VpnStatus { disconnected, connecting, @@ -99,8 +94,7 @@ class VpnConnection extends ChangeNotifier { } } - // ── Connect ─────────────────────────────────────────────────────────────── - Future connect() async { + Future connect() async { if (_status == VpnStatus.connected || _status == VpnStatus.connecting) { return false; } @@ -121,7 +115,6 @@ class VpnConnection extends ChangeNotifier { return true; } - // ── Step 1: Fetch VLESS URL from Hivemind ────────────────────────── String vlessUrl; try { vlessUrl = await HivemindService.fetchConfigWithPolling( @@ -149,13 +142,11 @@ class VpnConnection extends ChangeNotifier { return false; } - // ── Step 2: Check for cancellation ──────────────────────────────── if (_cancelled) { _setStatus(VpnStatus.disconnected, 'Tap to connect'); return false; } - // ── Step 3: Parse and start VLESS tunnel ─────────────────────────── try { final parsed = FlutterVless.parse(vlessUrl); final config = parsed.getFullConfiguration(); @@ -199,7 +190,6 @@ class VpnConnection extends ChangeNotifier { } } - // ── Disconnect ──────────────────────────────────────────────────────────── Future disconnect() async { _cancelled = true; HivemindService.cancel(); // abort any in-progress polling diff --git a/lib/main.dart b/lib/main.dart index 8c13916..1d11ac8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,63 +10,36 @@ 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. void main() async { - // Ensure that the Flutter engine is fully initialized before using platform channels - // (e.g., for SystemChrome or MobileAds). WidgetsFlutterBinding.ensureInitialized(); - // Lock the application to portrait orientation since the dashboard is a single-screen utility app - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - ]); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - // Configure a stealth system bar layout: transparent status bar with light icons, - // blending seamlessly into the dark background gradient. SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, - systemNavigationBarColor: Color(0xFF0D1117), // Deep space black + systemNavigationBarColor: Color(0xFF0D1117), systemNavigationBarIconBrightness: Brightness.light, )); - // ── GDPR Consent (Google UMP) ────────────────────────────────────────── - // Must run BEFORE MobileAds initialization. Shows consent dialog to - // EEA/UK users; no-op for everyone else. await ConsentManager.requestConsentIfNeeded(); - - // ── Initialize AdMob ─────────────────────────────────────────────────── - // Only after consent is resolved so AdMob respects user preferences. await MobileAds.instance.initialize(); - // Inflate the root widget runApp(const ReVoltApp()); } -/// The root application widget wrapping the app in necessary state providers and themes. class ReVoltApp extends StatelessWidget { const ReVoltApp({super.key}); @override Widget build(BuildContext context) { - // MultiProvider injects global state controllers down the widget tree. return MultiProvider( providers: [ - // Manages the VLESS tunnel lifecycle ChangeNotifierProvider(create: (_) => VpnConnection()), - - // Manages the 1-hour session clock. - // Uses ProxyProvider to automatically inject the VpnConnection, - // allowing the timer to auto-disconnect the VPN when time expires. ChangeNotifierProxyProvider( - create: (ctx) => SessionTimer( - vpnConnection: ctx.read(), - ), - update: (_, vpn, prev) => - prev ?? SessionTimer(vpnConnection: vpn), + create: (ctx) => SessionTimer(vpnConnection: ctx.read()), + update: (_, vpn, prev) => prev ?? SessionTimer(vpnConnection: vpn), ), - - // Manages rewarded ad lifecycle — preloads on startup. ChangeNotifierProvider(create: (_) => AdManager()), ], child: MaterialApp( @@ -78,7 +51,6 @@ class ReVoltApp extends StatelessWidget { ); } - /// Constructs the global dark theme used across the application. ThemeData _buildDarkTheme() { return ThemeData( brightness: Brightness.dark,