Spaces:
Running
Running
| import 'dart:async'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:google_fonts/google_fonts.dart'; | |
| import 'package:sqflite/sqflite.dart'; | |
| import '../services/vpn_service.dart'; | |
| import '../services/storage_service.dart'; | |
| class VpnShieldScreen extends StatefulWidget { | |
| const VpnShieldScreen({super.key}); | |
| State<VpnShieldScreen> createState() => _VpnShieldScreenState(); | |
| } | |
| class _VpnShieldScreenState extends State<VpnShieldScreen> { | |
| bool _isActive = false; | |
| VpnStats? _stats; | |
| int _totalDomains = 0; | |
| final TextEditingController _domainController = TextEditingController(); | |
| Timer? _pollingTimer; | |
| void initState() { | |
| super.initState(); | |
| _checkStatus(); | |
| _pollingTimer = Timer.periodic(const Duration(seconds: 2), (_) => _checkStatus()); | |
| } | |
| void dispose() { | |
| _pollingTimer?.cancel(); | |
| _domainController.dispose(); | |
| super.dispose(); | |
| } | |
| Future<void> _checkStatus() async { | |
| final active = KavachaVpn.isRunning; | |
| final stats = await KavachaVpn.getStats(); | |
| final db = await StorageService().database; | |
| final totalResult = await db.rawQuery('SELECT COUNT(*) as c FROM scam_domains'); | |
| final total = totalResult.isNotEmpty ? totalResult.first['c'] as int : 0; | |
| if (mounted) { | |
| setState(() { | |
| _isActive = active; | |
| _stats = stats; | |
| _totalDomains = total; | |
| }); | |
| } | |
| } | |
| Future<void> _toggleVpn(bool value) async { | |
| if (value) { | |
| await KavachaVpn.start(); | |
| } else { | |
| await KavachaVpn.stop(); | |
| } | |
| _checkStatus(); | |
| } | |
| Future<void> _addDomainManually() async { | |
| final domain = _domainController.text.trim().toLowerCase(); | |
| if (domain.isEmpty) return; | |
| try { | |
| final db = await StorageService().database; | |
| await db.insert('scam_domains', { | |
| 'domain': domain, | |
| 'category': 'User Added', | |
| 'source': 'user', | |
| 'hit_count': 0, | |
| 'created_at': DateTime.now().millisecondsSinceEpoch, | |
| }, conflictAlgorithm: ConflictAlgorithm.ignore); | |
| _domainController.clear(); | |
| await KavachaVpn.reloadBlocklist(); | |
| _checkStatus(); | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar(content: Text('Domain $domain added to blocklist')), | |
| ); | |
| } | |
| } catch (e) { | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar(content: Text('Failed to add domain: $e')), | |
| ); | |
| } | |
| } | |
| } | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Colors.black, | |
| appBar: AppBar( | |
| title: Text('DNS Shield', style: GoogleFonts.outfit(fontWeight: FontWeight.bold, color: Colors.white)), | |
| backgroundColor: Colors.transparent, | |
| iconTheme: const IconThemeData(color: Colors.white), | |
| ), | |
| body: SingleChildScrollView( | |
| padding: const EdgeInsets.all(24), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| _buildHeader(), | |
| const SizedBox(height: 32), | |
| _buildStatsGrid(), | |
| const SizedBox(height: 32), | |
| _buildTopBlocked(), | |
| const SizedBox(height: 32), | |
| _buildManagement(), | |
| const SizedBox(height: 48), | |
| _buildPrivacyNote(), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildHeader() { | |
| return Container( | |
| padding: const EdgeInsets.all(24), | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF1E1E2C), | |
| borderRadius: BorderRadius.circular(20), | |
| border: Border.all(color: _isActive ? Colors.green.withOpacity(0.3) : Colors.white10), | |
| ), | |
| child: Column( | |
| children: [ | |
| Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| Text( | |
| 'VPN Shield', | |
| style: GoogleFonts.outfit(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), | |
| ), | |
| Switch( | |
| value: _isActive, | |
| onChanged: _toggleVpn, | |
| activeColor: Colors.greenAccent, | |
| inactiveThumbColor: Colors.grey, | |
| inactiveTrackColor: Colors.white24, | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 16), | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | |
| decoration: BoxDecoration( | |
| color: _isActive ? Colors.green.withOpacity(0.2) : Colors.grey.withOpacity(0.2), | |
| borderRadius: BorderRadius.circular(20), | |
| ), | |
| child: Text( | |
| _isActive ? 'Active — all DNS protected' : 'Disabled', | |
| style: GoogleFonts.outfit( | |
| color: _isActive ? Colors.greenAccent : Colors.grey, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| Text( | |
| 'Local only — no data sent to any server', | |
| style: GoogleFonts.outfit(color: Colors.white54, fontSize: 14), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildStatsGrid() { | |
| return GridView.count( | |
| crossAxisCount: 2, | |
| shrinkWrap: true, | |
| physics: const NeverScrollableScrollPhysics(), | |
| crossAxisSpacing: 16, | |
| mainAxisSpacing: 16, | |
| childAspectRatio: 1.5, | |
| children: [ | |
| _buildStatCard('Blocked today', '\${_stats?.blockedToday ?? 0}', Colors.orangeAccent), | |
| _buildStatCard('Total domains', '$_totalDomains', Colors.blueAccent), | |
| _buildStatCard('Last blocked', _stats?.lastBlockedDomain ?? 'none', Colors.redAccent, isDomain: true), | |
| _buildStatCard('Status', _isActive ? 'Local TUN' : 'Off', _isActive ? Colors.greenAccent : Colors.grey), | |
| ], | |
| ); | |
| } | |
| Widget _buildStatCard(String title, String value, Color color, {bool isDomain = false}) { | |
| return Container( | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF1E1E2C), | |
| borderRadius: BorderRadius.circular(16), | |
| ), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Text(title, style: GoogleFonts.outfit(color: Colors.white54, fontSize: 13)), | |
| const SizedBox(height: 8), | |
| Text( | |
| value, | |
| style: GoogleFonts.outfit( | |
| color: color, | |
| fontSize: isDomain ? 14 : 24, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| maxLines: 1, | |
| overflow: TextOverflow.ellipsis, | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildTopBlocked() { | |
| final domains = _stats?.topBlockedDomains ?? []; | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text('Top Blocked Domains', style: GoogleFonts.outfit(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), | |
| const SizedBox(height: 16), | |
| if (domains.isEmpty) | |
| Text('No domains blocked yet.', style: GoogleFonts.outfit(color: Colors.white54)), | |
| for (var domain in domains) | |
| Container( | |
| margin: const EdgeInsets.only(bottom: 12), | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF1E1E2C), | |
| borderRadius: BorderRadius.circular(12), | |
| ), | |
| child: Row( | |
| children: [ | |
| const Icon(Icons.block, color: Colors.redAccent, size: 20), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: Text(domain.domain, style: GoogleFonts.outfit(color: Colors.white, fontSize: 16)), | |
| ), | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | |
| decoration: BoxDecoration( | |
| color: Colors.red.withOpacity(0.2), | |
| borderRadius: BorderRadius.circular(12), | |
| ), | |
| child: Text('\${domain.hitCount}', style: GoogleFonts.outfit(color: Colors.redAccent, fontWeight: FontWeight.bold)), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| Widget _buildManagement() { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text('Blocklist Management', style: GoogleFonts.outfit(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), | |
| const SizedBox(height: 16), | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: TextField( | |
| controller: _domainController, | |
| style: const TextStyle(color: Colors.white), | |
| decoration: InputDecoration( | |
| hintText: 'Add domain manually', | |
| hintStyle: const TextStyle(color: Colors.white38), | |
| filled: true, | |
| fillColor: const Color(0xFF1E1E2C), | |
| border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), | |
| contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), | |
| ), | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| ElevatedButton( | |
| onPressed: _addDomainManually, | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: const Color(0xFF6C63FF), | |
| padding: const EdgeInsets.all(14), | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), | |
| ), | |
| child: const Icon(Icons.add, color: Colors.white), | |
| ) | |
| ], | |
| ), | |
| ], | |
| ); | |
| } | |
| Widget _buildPrivacyNote() { | |
| return Container( | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| color: Colors.blue.withOpacity(0.1), | |
| borderRadius: BorderRadius.circular(12), | |
| border: Border.all(color: Colors.blue.withOpacity(0.3)), | |
| ), | |
| child: Row( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Icon(Icons.privacy_tip, color: Colors.blueAccent), | |
| const SizedBox(width: 16), | |
| Expanded( | |
| child: Text( | |
| 'Premithius VPN runs entirely on your device. DNS queries are checked locally and forwarded to Cloudflare 1.1.1.1 for safe domains only. Blocked domains return NXDOMAIN — no data reaches the scam site.', | |
| style: GoogleFonts.outfit(color: Colors.white70, fontSize: 13, height: 1.5), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |