refactor(app): debloat 3.3.1 and fix startup restoration

Maintainer changes on top of PR #2 (Naveax). Reduces the release to two
deliberate connection modes and resolves the issues found in review:

- Lock-screen privacy: move VISIBILITY_PRIVATE and a generic publicVersion
  into MainActivity.updateVpnNotification. The PR's fix lived in an
  unreferenced class and never reached the live notification.
- Haptics: restore the default to off, and reduce HapticSettings to a
  single success() on connect/disconnect via the native channel.
- SOCKS5: make it a real proxyOnly local-proxy mode rather than a TUN
  connection under a different label. TUN stays the default.
- Health probe: restore the idle-only guard so /health is no longer polled
  every 30s while connected.
- Recovery: add a _connectionEpoch counter so connect() aborts an
  in-flight Extreme recovery.
- Startup restoration: rebuild the recovery snapshot from the still-active
  session after an app-process restart (TUN), and surface an honest
  orphaned state with a Reconnect action in proxy mode.

Drops per-app routing entirely. It only ever functioned in TUN --
addDisallowedApplication lives inside the VPN-interface setup that
proxyOnly skips -- and its allowlist half had no Dart caller.

Also removes flutter_local_notifications, the global tap-haptic surface,
and unused runtime telemetry.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
esefxdzandClaude Opus 5 committed 2026-08-31 23:31:51 +03:00
1 parent a71161e7e5
commit 6fcde9dd7b
18 files changed
+271 -1027

No files matched your search

-65
View File
@@ -1,65 +0,0 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:revoltvpn/logic/haptic_settings.dart';
/// Gives short touch taps the same haptic feedback across the app.
/// Scroll gestures are ignored by checking travel distance and touch duration.
class AppHapticSurface extends StatefulWidget {
final Widget child;
const AppHapticSurface({super.key, required this.child});
@override
State<AppHapticSurface> createState() => _AppHapticSurfaceState();
}
class _PointerStart {
final Offset position;
final DateTime time;
const _PointerStart(this.position, this.time);
}
class _AppHapticSurfaceState extends State<AppHapticSurface> {
static const double _maxTapTravel = 18;
static const Duration _maxTapDuration = Duration(milliseconds: 700);
final Map<int, _PointerStart> _starts = <int, _PointerStart>{};
bool _supportsHaptics(PointerDeviceKind kind) {
return kind == PointerDeviceKind.touch ||
kind == PointerDeviceKind.stylus ||
kind == PointerDeviceKind.invertedStylus;
}
void _onDown(PointerDownEvent event) {
if (!_supportsHaptics(event.kind)) return;
_starts[event.pointer] = _PointerStart(event.position, DateTime.now());
}
void _onCancel(PointerCancelEvent event) {
_starts.remove(event.pointer);
}
void _onUp(PointerUpEvent event) {
final start = _starts.remove(event.pointer);
if (start == null || !_supportsHaptics(event.kind)) return;
final travelled = (event.position - start.position).distance;
final elapsed = DateTime.now().difference(start.time);
if (travelled <= _maxTapTravel && elapsed <= _maxTapDuration) {
HapticSettings.tap();
}
}
@override
Widget build(BuildContext context) {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: _onDown,
onPointerUp: _onUp,
onPointerCancel: _onCancel,
child: widget.child,
);
}
}
+6 -1
View File
@@ -4,6 +4,7 @@ import 'package:revoltvpn/logic/app_colors.dart';
import 'package:revoltvpn/logic/vpn_connection.dart';
import 'package:revoltvpn/logic/session_timer.dart';
import 'package:revoltvpn/logic/ad_manager.dart';
import 'package:revoltvpn/logic/haptic_settings.dart';
class ConnectButton extends StatefulWidget {
const ConnectButton({super.key});
@@ -82,6 +83,7 @@ class _ConnectButtonState extends State<ConnectButton>
if (vpn.status == VpnStatus.connected || vpn.status == VpnStatus.connecting) {
_busy = false;
await timer.disconnect();
HapticSettings.success();
return;
}
@@ -93,7 +95,10 @@ class _ConnectButtonState extends State<ConnectButton>
if (!adWatched && AdManager.adsEnabled) return;
final ok = await vpn.connect();
if (ok) await timer.start();
if (ok) {
await timer.start();
HapticSettings.success();
}
} finally {
if (mounted) setState(() => _busy = false);
}
-95
View File
@@ -1,95 +0,0 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class VpnNotificationManager {
static final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
static bool _initialized = false;
static String? _lastTimeLeft;
static String? _lastSpeedKbps;
// Android channels are immutable — bump this if settings change.
static const _channelId = 'revolt_vpn_v4';
static const _channelName = 'Revolt VPN';
static const _notifId = 888;
static Future<void> init() async {
if (_initialized) return;
const androidSettings =
AndroidInitializationSettings('@drawable/notification_icon');
const initSettings = InitializationSettings(android: androidSettings);
await _plugin.initialize(initSettings);
if (Platform.isAndroid) {
final androidPlugin = _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
await androidPlugin?.requestNotificationsPermission();
}
_initialized = true;
}
static Future<void> showOrUpdateStatus({
required String timeLeft,
required String speedKbps,
}) async {
if (timeLeft == _lastTimeLeft && speedKbps == _lastSpeedKbps) return;
_lastTimeLeft = timeLeft;
_lastSpeedKbps = speedKbps;
if (!_initialized) await init();
final bodyText = 'Time: $timeLeft • $speedKbps';
final bigPictureStyle = BigPictureStyleInformation(
const DrawableResourceAndroidBitmap('notification_banner'),
largeIcon: const DrawableResourceAndroidBitmap('notification_icon'),
contentTitle: 'Revolt VPN',
summaryText: bodyText,
htmlFormatContentTitle: false,
htmlFormatSummaryText: false,
);
final androidDetails = AndroidNotificationDetails(
_channelId,
_channelName,
channelDescription: 'Session timer and speed',
importance: Importance.low,
priority: Priority.low,
ongoing: true,
autoCancel: false,
playSound: false,
enableVibration: false,
showWhen: false,
// Session time/speed is usage metadata. Keep it out of public lock-screen
// previews while leaving the ongoing VPN status notification intact.
visibility: NotificationVisibility.private,
usesChronometer: false,
category: AndroidNotificationCategory.service,
colorized: true,
color: const ui.Color(0xFF0D1117),
styleInformation: bigPictureStyle,
);
final details = NotificationDetails(android: androidDetails);
try {
await _plugin.show(_notifId, _channelName, bodyText, details);
} catch (e) {
_initialized = false;
try {
await init();
await _plugin.show(_notifId, _channelName, bodyText, details);
} catch (_) {}
}
}
static Future<void> cancel() async {
if (!_initialized) return;
await _plugin.cancel(_notifId);
}
}
+12
View File
@@ -104,6 +104,18 @@ class StatusBar extends StatelessWidget {
label: statusLabel,
),
const Spacer(),
if (vpn.networkTransport != 'unknown' &&
vpn.networkTransport != 'none')
Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
vpn.networkTransport,
style: const TextStyle(
color: AppColors.textDim,
fontSize: 11,
),
),
),
if (isConnected)
const Icon(
Icons.lock,