ser
This commit is contained in:
1 parent
208da38586
commit
41a2648c6e
17 files changed
+1171
-216
No files matched your search
@@ -7,6 +7,9 @@ import 'package:paladinvpn/logic/session_timer.dart';
|
||||
///
|
||||
/// 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});
|
||||
|
||||
@@ -16,50 +19,76 @@ class ClockDisplay extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SessionTimer>(
|
||||
builder: (context, timer, _) {
|
||||
final bool showSyncing = !timer.hasSyncedOnce;
|
||||
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Center content: countdown + speed
|
||||
// Center content: countdown + speed (or syncing placeholder)
|
||||
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.5),
|
||||
blurRadius: 16,
|
||||
if (showSyncing) ...[
|
||||
// Syncing placeholder — spinner + label
|
||||
const SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Color.fromRGBO(0, 229, 255, 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Syncing…',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color.fromRGBO(0, 229, 255, 0.6),
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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 > 0.5
|
||||
? '${timer.currentSpeedKbps.toStringAsFixed(1)} KB/s'
|
||||
: 'Idle',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white54,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
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/components/clock_display.dart';
|
||||
|
||||
class ConnectButton extends StatefulWidget {
|
||||
@@ -60,30 +62,51 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
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>();
|
||||
final ad = context.read<AdManager>();
|
||||
|
||||
if (vpn.status == VpnStatus.connected) {
|
||||
// Disconnect — apply a short cooldown to prevent rapid toggle spam
|
||||
HapticFeedback.lightImpact();
|
||||
vpn.disconnect();
|
||||
timer.stop();
|
||||
setState(() => _inCooldown = true);
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) setState(() => _inCooldown = false);
|
||||
});
|
||||
} else if (vpn.status == VpnStatus.disconnected ||
|
||||
vpn.status == VpnStatus.error) {
|
||||
// Only attempt connect if an ad is loaded (or we're in dev/web mode)
|
||||
if (!ad.isAdLoaded && !ad.isAdLoading) {
|
||||
ad.preloadAd(); // trigger a reload in background
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Ad is not ready yet. Please wait a moment.'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
HapticFeedback.lightImpact();
|
||||
final success = await vpn.connect();
|
||||
if (success) {
|
||||
timer.start('main_ad');
|
||||
// Only apply cooldown on success — failures clear immediately
|
||||
setState(() => _inCooldown = true);
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) setState(() => _inCooldown = false);
|
||||
});
|
||||
}
|
||||
// On failure the button stays active — the error message in
|
||||
// StatusText tells the user what went wrong.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer2<VpnConnection, SessionTimer>(
|
||||
builder: (context, vpn, timer, _) {
|
||||
return Consumer3<VpnConnection, SessionTimer, AdManager>(
|
||||
builder: (context, vpn, timer, ad, _) {
|
||||
final isConnected = vpn.status == VpnStatus.connected;
|
||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
||||
|
||||
@@ -126,7 +149,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
child: Center(
|
||||
child: isConnected
|
||||
? const ClockDisplay()
|
||||
: _buildDisconnectedIcon(vpn.status, _inCooldown),
|
||||
: _buildDisconnectedIcon(vpn.status, _inCooldown, ad),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -136,7 +159,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisconnectedIcon(VpnStatus status, bool inCooldown) {
|
||||
Widget _buildDisconnectedIcon(VpnStatus status, bool inCooldown, AdManager ad) {
|
||||
if (status == VpnStatus.connecting || status == VpnStatus.disconnecting) {
|
||||
return const SizedBox(
|
||||
width: 48,
|
||||
@@ -147,19 +170,38 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
),
|
||||
);
|
||||
}
|
||||
// Determine the right label for the disconnected state
|
||||
String label;
|
||||
IconData icon;
|
||||
Color tint;
|
||||
|
||||
if (inCooldown) {
|
||||
label = 'PLEASE WAIT';
|
||||
icon = Icons.hourglass_bottom;
|
||||
tint = Colors.grey;
|
||||
} else if (status == VpnStatus.error) {
|
||||
label = 'TRY AGAIN';
|
||||
icon = Icons.refresh;
|
||||
tint = const Color(0xFFEF5350); // soft red — something went wrong
|
||||
} else if (!ad.isAdLoaded && !ad.isAdLoading) {
|
||||
label = 'LOADING AD…';
|
||||
icon = Icons.hourglass_empty;
|
||||
tint = const Color.fromRGBO(74, 85, 104, 0.7);
|
||||
} else {
|
||||
label = 'TAP TO CONNECT';
|
||||
icon = Icons.power_settings_new;
|
||||
tint = const Color.fromRGBO(74, 85, 104, 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),
|
||||
),
|
||||
Icon(icon, size: 64, color: tint),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
inCooldown ? 'PLEASE WAIT' : 'TAP TO CONNECT',
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: inCooldown ? Colors.grey : const Color.fromRGBO(74, 85, 104, 0.9),
|
||||
color: tint,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
|
||||
@@ -26,10 +26,19 @@ class VpnNotificationManager {
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
static String? _lastTimeLeft;
|
||||
static String? _lastSpeedKbps;
|
||||
|
||||
static Future<void> showOrUpdateStatus({
|
||||
required String timeLeft,
|
||||
required String speedKbps,
|
||||
}) async {
|
||||
// Skip update if nothing changed — prevents notification flicker
|
||||
// on Android skins that animate every notification update.
|
||||
if (timeLeft == _lastTimeLeft && speedKbps == _lastSpeedKbps) return;
|
||||
_lastTimeLeft = timeLeft;
|
||||
_lastSpeedKbps = speedKbps;
|
||||
|
||||
if (!_initialized) await init();
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
|
||||
@@ -15,9 +15,15 @@ class WatchAdButton extends StatelessWidget {
|
||||
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
|
||||
final adWatched = await context.read<AdManager>().showAd('bonus_ad');
|
||||
if (!adWatched || !context.mounted) return;
|
||||
|
||||
// The actual session extension happens server-side via AdMob SSV.
|
||||
// addBonusTime() syncs the new limits from the server.
|
||||
final synced = await context.read<SessionTimer>().addBonusTime();
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (synced) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
@@ -32,6 +38,18 @@ class WatchAdButton extends StatelessWidget {
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Could not confirm bonus. Please check your connection.',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: Color(0xFF5D4037),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
|
||||
Reference in new issue
Block a user