This commit is contained in:
Fabian Freund
2026-01-08 08:58:42 +01:00
parent 63712edbc7
commit 6f4654e0be
75 changed files with 654677 additions and 369 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+16
View File
@@ -0,0 +1,16 @@
# flutter_tor_example
Demonstrates how to use the flutter_tor plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,35 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lint/package.yaml
# Uncomment the following section to specify additional rules.
linter:
rules:
unawaited_futures: true
discarded_futures: true
collection_methods_unrelated_type: true
analyzer:
plugins:
- custom_lint
exclude:
- "**.g.dart"
- "**.swagger.dart"
- "**.freezed.dart"
- "**.chopper.dart"
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "eu.weblibre.flutter_tor_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "eu.weblibre.flutter_tor_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="flutter_tor_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package eu.weblibre.flutter_tor_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+476
View File
@@ -0,0 +1,476 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_tor/flutter_tor.dart';
import 'package:socks5_proxy/socks_client.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Tor Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const TorDemoPage(),
);
}
}
class TorDemoPage extends StatefulWidget {
const TorDemoPage({super.key});
@override
State<TorDemoPage> createState() => _TorDemoPageState();
}
class _TorDemoPageState extends State<TorDemoPage> {
final _tor = FlutterTor();
final _logs = <TorLogMessage>[];
TransportType _selectedTransport = TransportType.none;
int? _socksPort;
int _bootstrapProgress = 0;
bool _isRunning = false;
String _entryCountries = '';
String _exitCountries = '';
bool _strictNodes = false;
final _bridgeLinesController = TextEditingController();
// IP test state
String? _currentIp;
bool _isTestingIp = false;
@override
void initState() {
super.initState();
// Listen to logs
_tor.logStream.listen((log) {
setState(() {
_logs.add(log);
if (_logs.length > 100) {
_logs.removeAt(0);
}
});
});
// Listen to status changes
_tor.statusStream.listen((status) {
print(
'DEBUG statusStream: isRunning=${status.isRunning}, socksPort=${status.socksPort}, bootstrap=${status.bootstrapProgress}',
);
setState(() {
_isRunning = status.isRunning;
final newPort = status.socksPort?.toInt();
if (newPort != _socksPort) {
print('DEBUG: Port changed from $_socksPort to $newPort');
}
_socksPort = newPort;
_bootstrapProgress = status.bootstrapProgress.toInt();
});
});
// Listen to bootstrap progress
_tor.bootstrapProgressStream.listen((progress) {
setState(() {
_bootstrapProgress = progress;
});
});
}
Future<void> _startTor() async {
try {
final bridgeLines = _bridgeLinesController.text
.split('\n')
.where((line) => line.trim().isNotEmpty)
.toList();
final config = TorConfiguration(
transport: _selectedTransport,
bridgeLines: bridgeLines,
entryNodeCountries: _entryCountries.isEmpty ? null : _entryCountries,
exitNodeCountries: _exitCountries.isEmpty ? null : _exitCountries,
strictNodes: _strictNodes,
);
final socksPort = await _tor.start(config);
print('DEBUG startTor result: socksPort=${socksPort}');
setState(() {
_socksPort = socksPort.toInt();
print('DEBUG: Set _socksPort to $_socksPort from start result');
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tor started on port ${socksPort}')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to start Tor: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _stopTor() async {
try {
await _tor.stop();
setState(() {
_socksPort = null;
_bootstrapProgress = 0;
});
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Tor stopped')));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to stop Tor: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _requestNewIdentity() async {
try {
await _tor.requestNewIdentity();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Requested new Tor identity')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to request new identity: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _testIpAddress() async {
if (_socksPort == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tor is not running. Start Tor first.'),
backgroundColor: Colors.orange,
),
);
}
return;
}
setState(() {
_isTestingIp = true;
_currentIp = null;
});
// Debug: Show which port we're using
print('DEBUG: _testIpAddress called');
print('DEBUG: Current _socksPort value: $_socksPort');
print('DEBUG: Attempting to connect to SOCKS5 proxy on port $_socksPort');
try {
final portToUse = _socksPort!;
print('DEBUG: About to connect via SOCKS5 proxy at 127.0.0.1:$portToUse');
// Create HttpClient object
final client = HttpClient();
// Assign connection factory
SocksTCPClient.assignToHttpClient(client, [
ProxySettings(InternetAddress.loopbackIPv4, portToUse),
]);
// Connect to ifconfig.me through the SOCKS5 proxy
_currentIp = await client
.getUrl(Uri.parse('https://icanhazip.com/'))
.then((x) => x.close())
.then((x) => utf8.decodeStream(x));
print('DEBUG: Connected to ifconfig.me through SOCKS5 proxy');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Your Tor IP: ${_currentIp}'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 5),
),
);
}
} catch (e) {
print('DEBUG: Error: $e');
setState(() {
_currentIp = 'Error: $e';
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to fetch IP: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
} finally {
setState(() {
_isTestingIp = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Tor Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Column(
children: [
// Status Card
Card(
margin: const EdgeInsets.all(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status: ${_isRunning ? 'Running' : 'Stopped'}',
style: Theme.of(context).textTheme.titleMedium,
),
if (_socksPort != null) Text('SOCKS Port: $_socksPort'),
const SizedBox(height: 8),
LinearProgressIndicator(value: _bootstrapProgress / 100),
Text('Bootstrap: $_bootstrapProgress%'),
],
),
),
),
// Configuration
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<TransportType>(
value: _selectedTransport,
decoration: const InputDecoration(
labelText: 'Transport Type',
),
items: TransportType.values.map((transport) {
return DropdownMenuItem(
value: transport,
child: Text(transport.name),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedTransport = value!;
});
},
),
const SizedBox(height: 8),
TextField(
controller: _bridgeLinesController,
decoration: const InputDecoration(
labelText: 'Bridge Lines (one per line)',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: 'Entry Countries (e.g., de,fr,nl)',
),
onChanged: (value) => _entryCountries = value,
),
TextField(
decoration: const InputDecoration(
labelText: 'Exit Countries (e.g., ch,is,se)',
),
onChanged: (value) => _exitCountries = value,
),
CheckboxListTile(
title: const Text('Strict Nodes'),
value: _strictNodes,
onChanged: (value) {
setState(() {
_strictNodes = value ?? false;
});
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isRunning ? null : _startTor,
icon: const Icon(Icons.play_arrow),
label: const Text('Start Tor'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _isRunning ? _stopTor : null,
icon: const Icon(Icons.stop),
label: const Text('Stop Tor'),
),
),
],
),
if (_isRunning) ...[
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _requestNewIdentity,
icon: const Icon(Icons.refresh),
label: const Text('New Identity'),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _isTestingIp ? null : _testIpAddress,
icon: _isTestingIp
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.public),
label: Text(
_isTestingIp ? 'Testing...' : 'Test IP Address',
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
if (_currentIp != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _currentIp!.startsWith('Error')
? Colors.red.shade100
: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _currentIp!.startsWith('Error')
? Colors.red
: Colors.green,
),
),
child: Row(
children: [
Icon(
_currentIp!.startsWith('Error')
? Icons.error_outline
: Icons.check_circle_outline,
color: _currentIp!.startsWith('Error')
? Colors.red
: Colors.green,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_currentIp!.startsWith('Error')
? _currentIp!
: 'Your Tor IP: $_currentIp',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _currentIp!.startsWith('Error')
? Colors.red.shade900
: Colors.green.shade900,
),
),
),
],
),
),
],
],
const SizedBox(height: 16),
const Divider(),
Text('Logs:', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4),
),
child: ListView.builder(
itemCount: _logs.length,
itemBuilder: (context, index) {
final log = _logs[index];
Color color;
switch (log.severity) {
case 'ERR':
color = Colors.red;
break;
case 'WARN':
color = Colors.orange;
break;
case 'NOTICE':
color = Colors.blue;
break;
default:
color = Colors.black;
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
child: Text(
'[${log.severity}] ${log.message}',
style: TextStyle(
color: color,
fontSize: 12,
fontFamily: 'monospace',
),
),
);
},
),
),
],
),
),
),
],
),
);
}
}
+92
View File
@@ -0,0 +1,92 @@
name: flutter_tor_example
description: "Demonstrates how to use the flutter_tor plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
resolution: workspace
environment:
sdk: ^3.10.4
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_tor:
# When depending on this package from a real application you should use:
# flutter_tor: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# HTTP client for testing Tor connectivity
http: any
# SOCKS5 proxy client for Tor
socks5_proxy: any
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_tor_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text &&
widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}