diff --git a/lib/components/bottom_bar.dart b/lib/components/bottom_bar.dart index c649f81..d9027b2 100644 --- a/lib/components/bottom_bar.dart +++ b/lib/components/bottom_bar.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:paladinvpn/logic/vpn_connection.dart'; import 'package:paladinvpn/components/online_dot.dart'; +import 'package:paladinvpn/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. class BottomBar extends StatelessWidget { const BottomBar({super.key}); - // Dark card background color for high contrast overlay static const Color _cardBg = Color(0xFF1E2533); @override @@ -29,8 +29,11 @@ class BottomBar extends StatelessWidget { ), child: Row( children: [ - // Displays the server online/offline chip + // Server online/offline chip OnlineDot(isOnline: isOnline), + const Spacer(), + // Support button — only visible while connected + if (isOnline) const WatchAdButton(), ], ), ); diff --git a/lib/components/clock_display.dart b/lib/components/clock_display.dart index 4314eae..fc0ad2f 100644 --- a/lib/components/clock_display.dart +++ b/lib/components/clock_display.dart @@ -3,110 +3,98 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:paladinvpn/logic/session_timer.dart'; -/// ClockDisplay shows a large circular progress ring and the session countdown timer. -/// -/// The timer text glows electric cyan when a session is active and dims to slate gray when idle. +/// 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. class ClockDisplay extends StatelessWidget { const ClockDisplay({super.key}); static const Color _cyanGlow = Color(0xFF00E5FF); - static const Color _dimSlate = Color(0xFF4A5568); @override Widget build(BuildContext context) { return Consumer( builder: (context, timer, _) { - final isLive = timer.isRunning; - final color = isLive ? _cyanGlow : _dimSlate; + return SizedBox( + width: 200, + height: 200, + child: Stack( + alignment: Alignment.center, + children: [ + // Background quota track ring + const CustomPaint( + size: Size(200, 200), + painter: _ArcPainter( + progress: 1.0, + color: Color.fromRGBO(74, 85, 104, 0.2), + strokeWidth: 4, + ), + ), - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 220, - height: 220, - child: Stack( - alignment: Alignment.center, + // Live quota progress ring + TweenAnimationBuilder( + tween: Tween(begin: 0, end: timer.progress), + duration: const Duration(milliseconds: 400), + builder: (context, value, _) => CustomPaint( + size: const Size(200, 200), + painter: _ArcPainter( + progress: value, + color: _cyanGlow, + strokeWidth: 4, + glow: true, + ), + ), + ), + + // Center content: countdown + speed + Column( + mainAxisSize: MainAxisSize.min, children: [ - // Background track ring representing the maximum capacity of the countdown - const CustomPaint( - size: Size(220, 220), - painter: _ArcPainter( - progress: 1.0, - color: Color.fromRGBO(74, 85, 104, 0.2), - strokeWidth: 4, + // Glowing countdown timer + Text( + timer.formatted, + style: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: 2, + shadows: [ + Shadow( + color: Color.fromRGBO(0, 229, 255, 0.5), + blurRadius: 16, + ), + ], ), ), - - // Live progress ring showing current remaining time relative to the session limit - if (isLive) - TweenAnimationBuilder( - tween: Tween(begin: 0, end: timer.progress), - duration: const Duration(milliseconds: 400), - builder: (context, value, _) => CustomPaint( - size: const Size(220, 220), - painter: _ArcPainter( - progress: value, - color: _cyanGlow, - strokeWidth: 4, - glow: true, + const SizedBox(height: 6), + + // Speed indicator + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.speed, color: Colors.white54, size: 14), + const SizedBox(width: 4), + Text( + '${timer.currentSpeedKbps.toStringAsFixed(1)} KB/s', + style: const TextStyle( + fontSize: 12, + color: Colors.white54, + fontWeight: FontWeight.w500, ), ), - ), - - // Monospaced digital clock display for session countdown - AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 300), - style: TextStyle( - fontFamily: 'RobotoMono', - fontSize: 48, - fontWeight: FontWeight.w300, - letterSpacing: 4, - color: color, - shadows: isLive - ? const [ - Shadow( - color: Color.fromRGBO(0, 229, 255, 0.6), - blurRadius: 20, - ), - Shadow( - color: Color.fromRGBO(0, 229, 255, 0.3), - blurRadius: 40, - ), - ] - : const [], - ), - child: Text(timer.formatted), + ], ), ], ), - ), - const SizedBox(height: 12), - - // Subtitle state label below the timer - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Text( - isLive ? 'SESSION ACTIVE' : 'STANDBY', - key: ValueKey(isLive), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - letterSpacing: 3, - color: isLive - ? const Color.fromRGBO(0, 229, 255, 0.7) - : const Color.fromRGBO(74, 85, 104, 0.7), - ), - ), - ), - ], + ], + ), ); }, ); } } -/// A custom painter to render a circular progress arc or background track ring. class _ArcPainter extends CustomPainter { final double progress; final Color color; @@ -125,7 +113,6 @@ class _ArcPainter extends CustomPainter { final rect = Rect.fromLTWH(0, 0, size.width, size.height) .deflate(strokeWidth / 2); - // Optional blurred background paint to create an electric neon glow effect if (glow) { final glowPaint = Paint() ..color = const Color.fromRGBO(0, 229, 255, 0.15) @@ -136,7 +123,6 @@ class _ArcPainter extends CustomPainter { canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, glowPaint); } - // Core progress stroke paint final paint = Paint() ..color = color ..style = PaintingStyle.stroke diff --git a/lib/components/connect_button.dart b/lib/components/connect_button.dart index 82cbbe7..5af451d 100644 --- a/lib/components/connect_button.dart +++ b/lib/components/connect_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:paladinvpn/logic/vpn_connection.dart'; import 'package:paladinvpn/logic/session_timer.dart'; +import 'package:paladinvpn/components/clock_display.dart'; class ConnectButton extends StatefulWidget { const ConnectButton({super.key}); @@ -30,26 +31,38 @@ class _ConnectButtonState extends State _pulseAnim = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); + + // Listen to VpnConnection changes to drive the pulse animation + // from outside build(), keeping build() pure. + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().addListener(_onVpnStateChanged); + }); } @override void dispose() { + context.read().removeListener(_onVpnStateChanged); _pulseController.dispose(); super.dispose(); } + void _onVpnStateChanged() { + if (!mounted) return; + final isConnected = context.read().status == VpnStatus.connected; + if (isConnected && !_pulseController.isAnimating) { + _pulseController.repeat(reverse: true); + } else if (!isConnected && _pulseController.isAnimating) { + _pulseController.stop(); + _pulseController.reset(); + } + } + void _handleTap() async { if (_inCooldown) return; - setState(() { - _inCooldown = true; - }); + setState(() => _inCooldown = true); Future.delayed(const Duration(seconds: 5), () { - if (mounted) { - setState(() { - _inCooldown = false; - }); - } + if (mounted) setState(() => _inCooldown = false); }); final vpn = context.read(); @@ -58,14 +71,11 @@ class _ConnectButtonState extends State if (vpn.status == VpnStatus.connected) { vpn.disconnect(); timer.stop(); - _pulseController.stop(); - _pulseController.reset(); } else if (vpn.status == VpnStatus.disconnected || vpn.status == VpnStatus.error) { final success = await vpn.connect(); if (success) { timer.start('main_ad'); - _pulseController.repeat(reverse: true); } } } @@ -77,13 +87,6 @@ class _ConnectButtonState extends State final isConnected = vpn.status == VpnStatus.connected; final isConnecting = vpn.status == VpnStatus.connecting; - if (isConnected && !_pulseController.isAnimating) { - _pulseController.repeat(reverse: true); - } else if (!isConnected && !isConnecting && _pulseController.isAnimating) { - _pulseController.stop(); - _pulseController.reset(); - } - return GestureDetector( onTap: (isConnecting || _inCooldown) ? null : _handleTap, child: AnimatedBuilder( @@ -121,8 +124,8 @@ class _ConnectButtonState extends State : const [], ), child: Center( - child: isConnected - ? _buildConnectedStats(timer) + child: isConnected + ? const ClockDisplay() : _buildDisconnectedIcon(vpn.status, _inCooldown), ), ); @@ -164,44 +167,4 @@ class _ConnectButtonState extends State ], ); } - - Widget _buildConnectedStats(SessionTimer timer) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Countdown Timer - Text( - timer.formatted, - style: const TextStyle( - fontSize: 48, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 2, - ), - ), - const SizedBox(height: 8), - - - - // Speed indicator - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.speed, color: Colors.white70, size: 16), - const SizedBox(width: 4), - Text( - '${timer.currentSpeedKbps.toStringAsFixed(1)} KB/s', - style: const TextStyle( - fontSize: 14, - color: Colors.white70, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - - - ], - ); - } } diff --git a/lib/components/info_button.dart b/lib/components/info_button.dart index 3e4e357..300e146 100644 --- a/lib/components/info_button.dart +++ b/lib/components/info_button.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class InfoButton extends StatelessWidget { - const InfoButton({Key? key}) : super(key: key); + const InfoButton({super.key}); Future _launchGitHub() async { final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN'); diff --git a/lib/components/status_text.dart b/lib/components/status_text.dart index ab0a12a..1146b40 100644 --- a/lib/components/status_text.dart +++ b/lib/components/status_text.dart @@ -52,14 +52,21 @@ class StatusText extends StatelessWidget { ), ), ] else ...[ - // Dynamic status icon (spinning arrows for loading, outlined shield for idle) - Icon( - isConnecting ? Icons.autorenew : Icons.shield_outlined, - size: 18, - color: isConnecting - ? const Color(0xFF00E5FF) - : const Color(0xAA4A5568), - ), + if (isConnecting) + const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: Color(0xFF00E5FF), + ), + ) + else + const Icon( + Icons.shield_outlined, + size: 18, + color: Color(0xAA4A5568), + ), const SizedBox(width: 8), Text( vpn.statusMessage.toUpperCase(), diff --git a/lib/logic/ad_manager.dart b/lib/logic/ad_manager.dart index fd8a786..0fc80f7 100644 --- a/lib/logic/ad_manager.dart +++ b/lib/logic/ad_manager.dart @@ -94,8 +94,8 @@ class AdManager extends ChangeNotifier { final keys = await CryptoService.getOrCreateKeys(); final pubKey = keys['publicKey']!; - // 3. Attach custom data to the ad so Google sends it back to our Hivemind server - _rewardedAd!.serverSideVerificationOptions = ServerSideVerificationOptions( + // 3. Build SSV options to send to our Hivemind server via Google's callback + final ssvOptions = ServerSideVerificationOptions( customData: jsonEncode({ 'device_id': deviceId, 'public_key': pubKey, @@ -130,7 +130,8 @@ class AdManager extends ChangeNotifier { }, ); - // 5. Show the ad + // 5. Show the ad with SSV options + _rewardedAd!.setServerSideOptions(ssvOptions); await _rewardedAd!.show( onUserEarnedReward: (AdWithoutView ad, RewardItem reward) { debugPrint('[AdManager] Reward earned: ${reward.amount} ${reward.type}'); diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index 883fc20..6694945 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -182,6 +182,7 @@ class SessionTimer extends ChangeNotifier { @override void dispose() { _timer?.cancel(); + VpnNotificationManager.cancel(); super.dispose(); } } diff --git a/lib/logic/vpn_connection.dart b/lib/logic/vpn_connection.dart index a3210cf..d312ef8 100644 --- a/lib/logic/vpn_connection.dart +++ b/lib/logic/vpn_connection.dart @@ -156,7 +156,7 @@ class VpnConnection extends ChangeNotifier { return false; } - // ── Step 2: Start the WireGuard tunnel with the fetched config ───────── + // ── Step 3: Start the WireGuard tunnel with the fetched config ───────── try { // Re-initialize to ensure VPN permission is granted before starting await _wireguard.initialize(interfaceName: 'Paladin0'); diff --git a/lib/main.dart b/lib/main.dart index 7bcc1ad..634b9bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import 'package:paladinvpn/logic/vpn_connection.dart'; import 'package:paladinvpn/logic/session_timer.dart'; +import 'package:paladinvpn/logic/ad_manager.dart'; import 'package:paladinvpn/screens/main_screen.dart'; import 'package:paladinvpn/screens/intro.dart'; @@ -53,6 +54,9 @@ class PaladinApp extends StatelessWidget { update: (_, vpn, prev) => prev ?? SessionTimer(vpnConnection: vpn), ), + + // Manages rewarded ad lifecycle — preloads on startup. + ChangeNotifierProvider(create: (_) => AdManager()), ], child: MaterialApp( title: 'Paladin VPN', diff --git a/server_hivemind_5_0.py b/server_hivemind_5_0.py index 7df82a5..7087c28 100644 --- a/server_hivemind_5_0.py +++ b/server_hivemind_5_0.py @@ -447,7 +447,9 @@ def admob_callback(): if not device_id or not client_pubkey: return "Invalid custom_data payload", 400 - success, msg = _start_or_extend_session(device_id, client_pubkey, 'main_ad') + ad_type = custom_data.get('ad_type', 'main_ad') + + success, msg = _start_or_extend_session(device_id, client_pubkey, ad_type) return ("OK", 200) if success else (msg, 500) except Exception as e: