Initial commit (Security Hardened Slate)

This commit is contained in:
esefxdz committed 2026-05-29 14:42:24 +03:00
commit 157f60aad2
65 files changed
+3735

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
import 'package:paladinvpn/components/online_dot.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
Widget build(BuildContext context) {
return Consumer<VpnConnection>(
builder: (context, vpn, _) {
final isOnline = vpn.status == VpnStatus.connected;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: BoxDecoration(
color: _cardBg,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color.fromRGBO(74, 85, 104, 0.15),
),
),
child: Row(
children: [
// Displays the server online/offline chip
OnlineDot(isOnline: isOnline),
],
),
);
},
);
}
}
+152
View File
@@ -0,0 +1,152 @@
import 'dart:math';
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.
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,
child: Stack(
alignment: Alignment.center,
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,
),
),
// Live progress ring showing current remaining time relative to the session limit
if (isLive)
TweenAnimationBuilder<double>(
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,
),
),
),
// 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;
final double strokeWidth;
final bool glow;
const _ArcPainter({
required this.progress,
required this.color,
this.strokeWidth = 4,
this.glow = false,
});
@override
void paint(Canvas canvas, Size size) {
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)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth + 8
..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8);
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, glowPaint);
}
// Core progress stroke paint
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, paint);
}
@override
bool shouldRepaint(covariant _ArcPainter old) =>
old.progress != progress || old.color != color;
}
+207
View File
@@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
import 'package:paladinvpn/logic/session_timer.dart';
class ConnectButton extends StatefulWidget {
const ConnectButton({super.key});
@override
State<ConnectButton> createState() => _ConnectButtonState();
}
class _ConnectButtonState extends State<ConnectButton>
with SingleTickerProviderStateMixin {
late AnimationController _pulseController;
late Animation<double> _pulseAnim;
static const Color _cyanGlow = Color(0xFF00E5FF);
static const Color _bgDark = Color(0xFF1A202C);
bool _inCooldown = false;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
_pulseAnim = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_pulseController.dispose();
super.dispose();
}
void _handleTap() async {
if (_inCooldown) return;
setState(() {
_inCooldown = true;
});
Future.delayed(const Duration(seconds: 5), () {
if (mounted) {
setState(() {
_inCooldown = false;
});
}
});
final vpn = context.read<VpnConnection>();
final timer = context.read<SessionTimer>();
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);
}
}
}
@override
Widget build(BuildContext context) {
return Consumer2<VpnConnection, SessionTimer>(
builder: (context, vpn, timer, _) {
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(
animation: _pulseAnim,
builder: (context, child) {
final glowRadius = isConnected ? 20.0 + (_pulseAnim.value * 15.0) : 0.0;
final glowAlpha = isConnected
? ((0.3 + _pulseAnim.value * 0.2) * 255).round()
: 0;
return Container(
width: 260,
height: 260,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _bgDark,
border: Border.all(
color: _inCooldown
? Colors.grey.withValues(alpha: 0.5)
: isConnected
? _cyanGlow
: isConnecting
? const Color.fromRGBO(0, 229, 255, 0.5)
: const Color.fromRGBO(74, 85, 104, 0.5),
width: isConnected && !_inCooldown ? 3 : 2,
),
boxShadow: isConnected
? [
BoxShadow(
color: Color.fromARGB(glowAlpha, 0, 229, 255),
blurRadius: glowRadius,
spreadRadius: glowRadius * 0.3,
),
]
: const [],
),
child: Center(
child: isConnected
? _buildConnectedStats(timer)
: _buildDisconnectedIcon(vpn.status, _inCooldown),
),
);
},
),
);
},
);
}
Widget _buildDisconnectedIcon(VpnStatus status, bool inCooldown) {
if (status == VpnStatus.connecting || status == VpnStatus.disconnecting) {
return const SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Color.fromRGBO(0, 229, 255, 0.7),
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
inCooldown ? Icons.hourglass_bottom : Icons.power_settings_new,
size: 64,
color: inCooldown ? Colors.grey : const Color.fromRGBO(74, 85, 104, 0.7),
),
const SizedBox(height: 12),
Text(
inCooldown ? 'PLEASE WAIT' : 'TAP TO CONNECT',
style: TextStyle(
color: inCooldown ? Colors.grey : const Color.fromRGBO(74, 85, 104, 0.9),
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
],
);
}
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,
),
),
],
),
],
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
/// Header displays the Paladin brand logo, title, and subtitle.
///
/// It represents the top branding portion of the main dashboard and is designed
/// with clean proportions, consistent typography, and a modern cyberpunk-inspired color scheme.
class Header extends StatelessWidget {
const Header({super.key});
// Cyber-paladin 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(
mainAxisSize: MainAxisSize.min,
children: [
// Brand logo image with a fallback Icon in case the asset is missing
Image.asset(
'assets/logo.png',
width: 120, // Enlarged logo to stand out visually
height: 120,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.shield,
size: 120,
color: _cyanGlow,
),
),
const SizedBox(height: 8), // Standard padding between the logo and the title
// Brand primary name
const Text(
'Paladin',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
letterSpacing: 8,
color: Colors.white,
),
),
const SizedBox(height: 2), // Close spacing for secondary information
// Brand secondary label / purpose
const Text(
'SECURE TUNNEL',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 4,
color: Color(0xAA4A5568), // Semi-transparent slate gray
),
),
],
);
}
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class InfoButton extends StatelessWidget {
const InfoButton({Key? key}) : super(key: key);
Future<void> _launchGitHub() async {
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN');
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
debugPrint('Could not launch $url');
}
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.help_outline, color: Colors.white, size: 28),
onPressed: _launchGitHub,
tooltip: 'About PaladinVPN',
splashRadius: 24,
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
/// OnlineDot displays a glowing green dot when the server is online/connected,
/// and a muted gray dot when disconnected.
class OnlineDot extends StatelessWidget {
/// Represents if the VPN tunnel session is active (online).
final bool isOnline;
const OnlineDot({
super.key,
required this.isOnline,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// Glowing status dot: Green when active, muted gray when inactive.
AnimatedContainer(
duration: const Duration(milliseconds: 400),
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isOnline ? const Color(0xFF00E676) : const Color(0xFF4A5568),
boxShadow: isOnline
? const [
BoxShadow(
color: Color.fromRGBO(0, 230, 118, 0.5),
blurRadius: 6,
),
]
: const [],
),
),
const SizedBox(width: 10),
// Dynamic status text label
Text(
isOnline ? 'Server Online' : 'Disconnected',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: isOnline
? const Color.fromRGBO(255, 255, 255, 0.8)
: const Color(0xFF4A5568),
letterSpacing: 0.5,
),
),
],
);
}
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class PrivacyPolicyButton extends StatelessWidget {
const PrivacyPolicyButton({super.key});
Future<void> _launchPrivacyPolicy() async {
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN/blob/main/PRIVACY_POLICY.md');
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
debugPrint('Could not launch $url');
}
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.privacy_tip_outlined, color: Colors.white, size: 28),
onPressed: _launchPrivacyPolicy,
tooltip: 'Privacy Policy',
splashRadius: 24,
);
}
}
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
/// StatusText displays the status of the VPN tunnel.
///
/// When connected, it shows the Finnish flag along with the tunnel location ("Helsinki, Finland").
/// When connecting or disconnected, it shows status messages with clean micro-animations.
class StatusText extends StatelessWidget {
const StatusText({super.key});
@override
Widget build(BuildContext context) {
return Consumer<VpnConnection>(
builder: (context, vpn, _) {
final isConnected = vpn.status == VpnStatus.connected;
final isConnecting = vpn.status == VpnStatus.connecting;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Container(
key: ValueKey(vpn.status),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0x22FFFFFF), // 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
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isConnected) ...[
// Circular Finnish flag icon representing the active tunnel endpoint
Image.asset(
'assets/finland_flag_256.png',
width: 20,
height: 20,
),
const SizedBox(width: 8),
const Text(
'Helsinki, Finland',
style: TextStyle(
color: Color(0xFF00E5FF),
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 0.5,
),
),
] 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),
),
const SizedBox(width: 8),
Text(
vpn.statusMessage.toUpperCase(),
style: TextStyle(
color: isConnecting
? const Color(0xFF00E5FF)
: const Color(0xAA4A5568),
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 1,
),
),
],
],
),
),
);
},
);
}
}
+68
View File
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/session_timer.dart';
import 'package:paladinvpn/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 success = await context.read<AdManager>().showAd('bonus_ad');
if (success && context.mounted) {
context.read<SessionTimer>().addBonusTime(); // Calls Hivemind API for 30m + 1GB
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),
),
);
}
},
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,
),
),
],
),
),
),
);
}
}