This commit is contained in:
Fabian Freund
2025-02-16 23:21:44 +01:00
parent ddb0f14059
commit 6368e30479
81 changed files with 20308 additions and 88 deletions
@@ -0,0 +1,85 @@
import 'dart:async';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:lensai/features/tor/utils/tor_entrypoint.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
part 'tor_proxy.g.dart';
class _TorService {
final service = FlutterBackgroundService();
final _portSubject = BehaviorSubject<int?>.seeded(null);
ValueStream<int?> get portStream => _portSubject.stream;
Future<void> start() async {
await service.startService();
}
Future<void> stop() async {
service.invoke("stop");
}
Future<void> initializeService() async {
await service.configure(
iosConfiguration: IosConfiguration(
autoStart: false,
),
androidConfiguration: AndroidConfiguration(
autoStart: false,
onStart: onStart,
isForegroundMode: true,
autoStartOnBoot: false,
),
);
service.on('portUpdate').listen((data) {
final port = data!['port'] as int;
_portSubject.add((port > -1) ? port : null);
});
}
void dispose() {
unawaited(stop());
unawaited(_portSubject.close());
}
}
@Riverpod(keepAlive: true)
class TorProxyService extends _$TorProxyService {
final _tor = _TorService();
Future<void> connect({bool forceReconnect = false}) async {
final currentPort = _tor.portStream.valueOrNull;
if (currentPort == null || forceReconnect) {
state = const AsyncLoading();
await _tor.start();
}
}
Future<void> disconnect() async {
state = const AsyncLoading();
await _tor.stop();
}
@override
Future<int?> build() async {
final portSub = _tor.portStream.distinct().listen(
(port) {
state = AsyncData(port);
},
);
await _tor.initializeService();
ref.onDispose(() {
unawaited(portSub.cancel());
_tor.dispose();
});
return _tor.portStream.valueOrNull;
}
}
@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tor_proxy.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$torProxyServiceHash() => r'85ed8e3b49007aa56e6b82ff802a3e0ce567a0c8';
/// See also [TorProxyService].
@ProviderFor(TorProxyService)
final torProxyServiceProvider =
AsyncNotifierProvider<TorProxyService, int?>.internal(
TorProxyService.new,
name: r'torProxyServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$torProxyServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TorProxyService = AsyncNotifier<int?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart';
import 'package:lensai/features/tor/domain/services/tor_proxy.dart';
import 'package:lensai/features/tor/presentation/widgets/tor_dialog.dart';
import 'package:lensai/features/tor/presentation/widgets/tor_notification.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'start_tor_proxy.g.dart';
@Riverpod()
class StartProxyController extends _$StartProxyController {
// ignore: avoid_build_context_in_providers
Future<void> maybeStartProxy(BuildContext context) async {
final torProxyRunning = ref.read(torProxyServiceProvider);
if (torProxyRunning.valueOrNull == null) {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return TorDialog();
},
);
if (result == true) {
final connection = ref.read(torProxyServiceProvider.notifier).connect();
ref.read(overlayDialogControllerProvider.notifier).show(
Positioned(
top: 0,
left: 0,
child: TorNotification(),
),
);
await connection;
}
}
}
@override
void build() {}
}
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'start_tor_proxy.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$startProxyControllerHash() =>
r'c49d4f75bb499b3e695f1c4d08dcc095cfee7143';
/// See also [StartProxyController].
@ProviderFor(StartProxyController)
final startProxyControllerProvider =
AutoDisposeNotifierProvider<StartProxyController, void>.internal(
StartProxyController.new,
name: r'startProxyControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$startProxyControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$StartProxyController = AutoDisposeNotifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/tor/domain/services/tor_proxy.dart';
class TorProxyScreen extends HookConsumerWidget {
const TorProxyScreen();
@override
Widget build(BuildContext context, WidgetRef ref) {
final torProxyPort = ref.watch(torProxyServiceProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Tor Proxy'),
),
body: ColoredBox(
color: const Color(0xFF7D4698),
child: Column(
children: [
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SvgPicture.asset(
'assets/images/tor_white.svg',
width: 200,
),
const SizedBox(
height: 16,
),
ListTileTheme(
iconColor: Colors.white,
textColor: Colors.white,
child: SwitchListTile.adaptive(
inactiveThumbColor: Colors.white,
activeColor: const Color(0xFF68B030),
trackColor: WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) {
if (states.isEmpty) {
return const Color(0xFF333A41);
}
return null; // Use the default color.
}),
trackOutlineColor:
WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) {
if (states.isEmpty) {
return Colors.white;
}
return null; // Use the default color.
}),
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return const Icon(MdiIcons.axisArrowLock);
}
return null; // Use the default color.
}),
value: torProxyPort.valueOrNull != null,
title: const Text('Tor Proxy'),
secondary: const Icon(MdiIcons.power),
onChanged: torProxyPort.isLoading
? null
: (value) async {
if (value) {
await ref
.read(torProxyServiceProvider.notifier)
.connect();
} else {
await ref
.read(torProxyServiceProvider.notifier)
.disconnect();
}
},
),
),
],
),
),
Flexible(
child: Column(
children: [
if (torProxyPort.isLoading)
const Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(
backgroundColor: Color(0xFF333A41),
color: Color(0xFF68B030),
),
SizedBox(
height: 8,
),
Text(
'Establishing connection...',
style: TextStyle(color: Colors.white),
),
],
),
if (torProxyPort.valueOrNull != null)
Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
MdiIcons.shieldLockOutline,
color: Color(0xFF68B030),
size: 32,
),
const SizedBox(width: 12),
Text(
'Connected to the Tor Network',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: const Color(0xFF68B030)),
),
],
),
),
],
),
),
],
),
),
);
}
}
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lensai/presentation/icons/tor_icons.dart';
class TorDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
icon: const Icon(
TorIcons.onionAlt,
color: Color(0xFF7D4698),
),
title: const Text('Tor Proxy'),
content: const Text(
'This container requires a Tor proxy for secure connections, which is not currently running.',
),
actions: [
TextButton(
onPressed: () {
context.pop(false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
context.pop(true);
},
child: const Text('Enable'),
),
],
);
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart';
import 'package:lensai/features/tor/domain/services/tor_proxy.dart';
import 'package:lensai/presentation/icons/tor_icons.dart';
import 'package:lensai/presentation/widgets/animate_gradient_shader.dart';
class TorNotification extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(
torProxyServiceProvider,
(previous, next) {
if (!next.isLoading) {
ref.read(overlayDialogControllerProvider.notifier).dismiss();
}
},
);
return SafeArea(
child: ColoredBox(
color: const Color(0xFF7D4698),
child: SizedBox(
height: 56,
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
AnimateGradientShader(
duration: const Duration(milliseconds: 500),
primaryEnd: Alignment.bottomLeft,
secondaryEnd: Alignment.topRight,
primaryColors: const [Color(0xFF68B030), Color(0xFF68B030)],
secondaryColors: const [Colors.white, Colors.white],
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Icon(TorIcons.onionAlt),
),
),
Expanded(
child: Text(
'Tor Proxy is connecting...',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white),
),
),
IconButton(
onPressed: () {
ref
.read(overlayDialogControllerProvider.notifier)
.dismiss();
},
icon: const Icon(
Icons.close,
color: Colors.white,
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,22 @@
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:tor/tor.dart';
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
await Tor.init();
final portSub = Tor.instance.events.stream.listen((port) {
service.invoke('portUpdate', {'port': port});
});
await Tor.instance.start();
service.on("stop").listen((event) async {
Tor.instance.stop();
service.invoke('portUpdate', {'port': -1});
await portSub.cancel();
await service.stopSelf();
});
}