overall ui bugfixes and hivemind improvements
This commit is contained in:
1 parent
a80a23e008
commit
8f32429e82
10 files changed
+96
-129
No files matched your search
@@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,34 +3,28 @@ 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.
|
||||
/// ClockDisplay renders inside the ConnectButton circle when the VPN is active.
|
||||
///
|
||||
/// The timer text glows electric cyan when a session is active and dims to slate gray when idle.
|
||||
/// 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<SessionTimer>(
|
||||
builder: (context, timer, _) {
|
||||
final isLive = timer.isRunning;
|
||||
final color = isLive ? _cyanGlow : _dimSlate;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 220,
|
||||
height: 220,
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background track ring representing the maximum capacity of the countdown
|
||||
// Background quota track ring
|
||||
const CustomPaint(
|
||||
size: Size(220, 220),
|
||||
size: Size(200, 200),
|
||||
painter: _ArcPainter(
|
||||
progress: 1.0,
|
||||
color: Color.fromRGBO(74, 85, 104, 0.2),
|
||||
@@ -38,13 +32,12 @@ class ClockDisplay extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Live progress ring showing current remaining time relative to the session limit
|
||||
if (isLive)
|
||||
// Live quota progress ring
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: timer.progress),
|
||||
duration: const Duration(milliseconds: 400),
|
||||
builder: (context, value, _) => CustomPaint(
|
||||
size: const Size(220, 220),
|
||||
size: const Size(200, 200),
|
||||
painter: _ArcPainter(
|
||||
progress: value,
|
||||
color: _cyanGlow,
|
||||
@@ -54,59 +47,54 @@ class ClockDisplay extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// 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 [
|
||||
// Center content: countdown + speed
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 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.6),
|
||||
blurRadius: 20,
|
||||
),
|
||||
Shadow(
|
||||
color: Color.fromRGBO(0, 229, 255, 0.3),
|
||||
blurRadius: 40,
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
),
|
||||
child: Text(timer.formatted),
|
||||
color: Color.fromRGBO(0, 229, 255, 0.5),
|
||||
blurRadius: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Subtitle state label below the timer
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
isLive ? 'SESSION ACTIVE' : 'STANDBY',
|
||||
key: ValueKey(isLive),
|
||||
style: TextStyle(
|
||||
// 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,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 3,
|
||||
color: isLive
|
||||
? const Color.fromRGBO(0, 229, 255, 0.7)
|
||||
: const Color.fromRGBO(74, 85, 104, 0.7),
|
||||
),
|
||||
color: Colors.white54,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -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<ConnectButton>
|
||||
_pulseAnim = Tween<double>(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<VpnConnection>().addListener(_onVpnStateChanged);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
context.read<VpnConnection>().removeListener(_onVpnStateChanged);
|
||||
_pulseController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onVpnStateChanged() {
|
||||
if (!mounted) return;
|
||||
final isConnected = context.read<VpnConnection>().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<VpnConnection>();
|
||||
@@ -58,14 +71,11 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
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<ConnectButton>
|
||||
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(
|
||||
@@ -122,7 +125,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
),
|
||||
child: Center(
|
||||
child: isConnected
|
||||
? _buildConnectedStats(timer)
|
||||
? const ClockDisplay()
|
||||
: _buildDisconnectedIcon(vpn.status, _inCooldown),
|
||||
),
|
||||
);
|
||||
@@ -164,44 +167,4 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void> _launchGitHub() async {
|
||||
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN');
|
||||
|
||||
@@ -52,13 +52,20 @@ class StatusText extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Dynamic status icon (spinning arrows for loading, outlined shield for idle)
|
||||
Icon(
|
||||
isConnecting ? Icons.autorenew : Icons.shield_outlined,
|
||||
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: isConnecting
|
||||
? const Color(0xFF00E5FF)
|
||||
: const Color(0xAA4A5568),
|
||||
color: Color(0xAA4A5568),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
|
||||
@@ -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}');
|
||||
|
||||
@@ -182,6 +182,7 @@ class SessionTimer extends ChangeNotifier {
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
VpnNotificationManager.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user