├── images └── logo.png ├── fonts ├── Inter-Bold.ttf └── Inter-Regular.ttf ├── screenshots ├── flutter_2.png ├── flutter_3.png └── flutter_4.png ├── analysis_options.yaml ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── bug_vpn │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle.kts └── settings.gradle.kts ├── icons_launcher.yaml ├── lib ├── app │ ├── shared │ │ ├── mixins │ │ │ └── notifier_mixin.dart │ │ ├── theme │ │ │ ├── colors.dart │ │ │ └── theme.dart │ │ ├── extension │ │ │ ├── v2ray_extensions.dart │ │ │ └── extensions.dart │ │ ├── routes │ │ │ └── routes.dart │ │ ├── preferences │ │ │ └── preferences.dart │ │ └── utils │ │ │ └── utils.dart │ └── ui │ │ ├── home │ │ └── view │ │ │ ├── providers │ │ │ └── bottom_nav_index_provider.dart │ │ │ ├── pages │ │ │ ├── home.dart │ │ │ ├── splash.dart │ │ │ └── about.dart │ │ │ └── widgets │ │ │ └── lazy_indexed_stack.dart │ │ ├── configs │ │ ├── data │ │ │ ├── source │ │ │ │ └── remote │ │ │ │ │ └── remote_data_source.dart │ │ │ ├── repository │ │ │ │ └── configs_repository.dart │ │ │ └── models │ │ │ │ └── config_model.dart │ │ └── view │ │ │ ├── widgets │ │ │ ├── loading_widget.dart │ │ │ ├── config_error_widget.dart │ │ │ ├── tab_bar_card.dart │ │ │ └── config_card.dart │ │ │ ├── providers │ │ │ └── configs_provider.dart │ │ │ └── pages │ │ │ └── configs.dart │ │ └── vpn │ │ └── view │ │ ├── widgets │ │ ├── connecting_time.dart │ │ ├── connection_status.dart │ │ ├── config_delay.dart │ │ ├── usage_status_card.dart │ │ ├── status_info.dart │ │ ├── usage_status_cards.dart │ │ ├── selected_config_card.dart │ │ └── connection_button.dart │ │ ├── providers │ │ └── v2ray_provider.dart │ │ └── pages │ │ └── vpn.dart └── main.dart ├── README.md ├── pubspec.yaml ├── .gitignore ├── .metadata └── pubspec.lock /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/images/logo.png -------------------------------------------------------------------------------- /fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /screenshots/flutter_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/screenshots/flutter_2.png -------------------------------------------------------------------------------- /screenshots/flutter_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/screenshots/flutter_3.png -------------------------------------------------------------------------------- /screenshots/flutter_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/screenshots/flutter_4.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | - prefer_relative_imports 6 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/H3mnz/bug_vpn/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/bug_vpn/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.bug_vpn 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /icons_launcher.yaml: -------------------------------------------------------------------------------- 1 | # dart run icons_launcher:create 2 | 3 | icons_launcher: 4 | image_path: "images/logo.png" 5 | notification_image: "images/logo.png" 6 | platforms: 7 | android: 8 | enable: true -------------------------------------------------------------------------------- /lib/app/shared/mixins/notifier_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | 3 | mixin NotifierMixin on Notifier { 4 | ValueT update(ValueT Function(ValueT state) cb) => state = cb(state); 5 | } 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip 6 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BUG VPN 2 | 3 | V2Ray client with free configs, developed with flutter. 4 | 5 | 6 |

7 | 8 | 9 | 10 |

11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/app/ui/home/view/providers/bottom_nav_index_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | 3 | final bottomNavIndexProvider = NotifierProvider.autoDispose( 4 | BottomNavIndexNotifier.new, 5 | ); 6 | 7 | class BottomNavIndexNotifier extends Notifier { 8 | @override 9 | int build() => 0; 10 | 11 | update(int index) { 12 | state = index; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/app/shared/theme/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | AppColors._(); 5 | 6 | static const Color amber = Color(0xfff4ac24); 7 | static const Color green = Color(0xff19aba1); 8 | static const Color blue = Color.fromARGB(255, 25, 52, 171); 9 | static const Color red = Color.fromARGB(255, 110, 56, 56); 10 | static const Color light = Color.fromARGB(255, 243, 244, 255); 11 | } 12 | -------------------------------------------------------------------------------- /lib/app/shared/extension/v2ray_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_v2ray_client/flutter_v2ray.dart'; 2 | 3 | extension V2RayStatusExtension on V2RayStatus { 4 | bool get isConnected => state == 'CONNECTED'; 5 | } 6 | 7 | extension FlutterV2rayExtension on V2ray { 8 | Future getDelayWithTimeout(String config, {int seconds = 5}) => 9 | getServerDelay(config: config).timeout( 10 | const Duration(seconds: 5), 11 | onTimeout: () => -1, 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/app/shared/routes/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../ui/home/view/pages/home.dart'; 4 | import '../../ui/home/view/pages/splash.dart'; 5 | 6 | class AppRoutes { 7 | AppRoutes._(); 8 | static const String splashRoute = '/'; 9 | static const String homeRoute = '/home'; 10 | 11 | static Map routes = { 12 | splashRoute: (context) => const SplashPage(), 13 | homeRoute: (context) => const HomePage(), 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = 9 | rootProject.layout.buildDirectory 10 | .dir("../../build") 11 | .get() 12 | rootProject.layout.buildDirectory.value(newBuildDir) 13 | 14 | subprojects { 15 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 16 | project.layout.buildDirectory.value(newSubprojectBuildDir) 17 | } 18 | subprojects { 19 | project.evaluationDependsOn(":app") 20 | } 21 | 22 | tasks.register("clean") { 23 | delete(rootProject.layout.buildDirectory) 24 | } 25 | -------------------------------------------------------------------------------- /lib/app/shared/extension/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | extension Iterables on Iterable { 4 | Map> groupBy(K Function(E) keyFunction) => fold( 5 | >{}, 6 | (Map> map, E element) => 7 | map..putIfAbsent(keyFunction(element), () => []).add(element), 8 | ); 9 | } 10 | 11 | extension IntExt on int { 12 | String formatAsBytes([int decimals = 1]) { 13 | if (this <= 0) return "0 B"; 14 | const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 15 | var i = (log(this) / log(1024)).floor(); 16 | return '${(this / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/app/ui/configs/data/source/remote/remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:http/http.dart' as http; 2 | 3 | import '../../../../../shared/utils/utils.dart'; 4 | 5 | class RemoteDataSource { 6 | Future> getConfigs() async { 7 | try { 8 | final response = await http.get(Uri.parse(AppUtils.sublinksUrl)); 9 | if (response.statusCode != 200) throw Exception('Invalid statusCode'); 10 | final data = response.body 11 | .trim() 12 | .split('\n') 13 | .where( 14 | (line) => line.trim().isNotEmpty, 15 | ) 16 | .toSet(); 17 | return data; 18 | } catch (e) { 19 | throw Exception('$e'); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/app/ui/configs/data/repository/configs_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_v2ray_client/flutter_v2ray.dart'; 2 | 3 | import '../source/remote/remote_data_source.dart'; 4 | 5 | class ConfigsRepository { 6 | final RemoteDataSource source; 7 | 8 | ConfigsRepository(this.source); 9 | 10 | Future> getConfigs() async { 11 | try { 12 | final data = await source.getConfigs(); 13 | return data.where( 14 | (url) { 15 | try { 16 | V2ray.parseFromURL(url); 17 | return true; 18 | } catch (e) { 19 | return false; 20 | } 21 | }, 22 | ).map(V2ray.parseFromURL); 23 | } catch (e) { 24 | rethrow; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/app/shared/preferences/preferences.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | import '../../ui/configs/data/models/config_model.dart'; 4 | 5 | class Preferences { 6 | Preferences._(); 7 | 8 | static final Preferences instance = Preferences._(); 9 | 10 | late SharedPreferences pref; 11 | 12 | Future init() async { 13 | pref = await SharedPreferences.getInstance(); 14 | } 15 | 16 | ConfigModel? getConfig() { 17 | final source = pref.getString('config'); 18 | if (source == null) return null; 19 | 20 | return ConfigModel.fromJson(source); 21 | } 22 | 23 | Future saveConfig(ConfigModel config) async { 24 | return pref.setString('config', config.toJson()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/app/ui/configs/view/widgets/loading_widget.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_animate/flutter_animate.dart'; 5 | 6 | class LoadingWidget extends StatelessWidget { 7 | const LoadingWidget({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Animate( 12 | effects: const [FadeEffect(), ScaleEffect()], 13 | child: const Center( 14 | child: CircularProgressIndicator( 15 | strokeWidth: 4, 16 | backgroundColor: Colors.black12, 17 | strokeAlign: BorderSide.strokeAlignOutside, 18 | strokeCap: StrokeCap.round, 19 | ), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/app/ui/configs/view/providers/configs_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_v2ray_client/url/url.dart'; 3 | 4 | import '../../data/models/config_model.dart'; 5 | import '../../data/repository/configs_repository.dart'; 6 | import '../../data/source/remote/remote_data_source.dart'; 7 | 8 | final configsProvider = FutureProvider>((ref) async { 9 | return ConfigsRepository(RemoteDataSource()).getConfigs(); 10 | }); 11 | 12 | final selectedConfigProvider = NotifierProvider( 13 | SelectedConfigNotifier.new, 14 | ); 15 | 16 | class SelectedConfigNotifier extends Notifier { 17 | @override 18 | ConfigModel? build() => null; 19 | 20 | update(ConfigModel? config) { 21 | state = config; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/connecting_time.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ConnectingTime extends StatelessWidget { 4 | final String value; 5 | const ConnectingTime({ 6 | super.key, 7 | required this.value, 8 | }); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Column( 13 | children: [ 14 | Text( 15 | 'Connecting Time', 16 | style: Theme.of(context).textTheme.titleMedium?.copyWith( 17 | color: Colors.black54, 18 | ), 19 | ), 20 | Text( 21 | value, 22 | textAlign: TextAlign.center, 23 | style: Theme.of(context).textTheme.displayMedium?.copyWith( 24 | fontWeight: FontWeight.bold, 25 | ), 26 | ), 27 | ], 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = 3 | run { 4 | val properties = java.util.Properties() 5 | file("local.properties").inputStream().use { properties.load(it) } 6 | val flutterSdkPath = properties.getProperty("flutter.sdk") 7 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 8 | flutterSdkPath 9 | } 10 | 11 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 22 | id("com.android.application") version "8.9.1" apply false 23 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false 24 | } 25 | 26 | include(":app") 27 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/connection_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ConnectionStatus extends StatelessWidget { 4 | final bool connected; 5 | const ConnectionStatus({ 6 | super.key, 7 | required this.connected, 8 | }); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Column( 13 | crossAxisAlignment: CrossAxisAlignment.center, 14 | children: [ 15 | Text( 16 | 'Status', 17 | style: Theme.of(context).textTheme.titleMedium?.copyWith( 18 | color: Colors.black54, 19 | ), 20 | ), 21 | Text( 22 | connected ? 'Connected' : 'Disconnected', 23 | style: Theme.of(context).textTheme.titleLarge?.copyWith( 24 | fontWeight: FontWeight.bold, 25 | ), 26 | ), 27 | ], 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bug_vpn 2 | description: "V2Ray client with free configs, developed with flutter." 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ^3.10.0 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | # ui and utilities 14 | uicons: 15 | toastification: 16 | loader_overlay: 17 | flutter_animate: 18 | # preferences 19 | shared_preferences: 20 | # network 21 | http: 22 | # state 23 | flutter_riverpod: 24 | # vpn 25 | flutter_v2ray_client: 26 | 27 | 28 | dev_dependencies: 29 | 30 | flutter_test: 31 | sdk: flutter 32 | flutter_lints: ^4.0.0 33 | icons_launcher: 34 | 35 | 36 | flutter: 37 | uses-material-design: true 38 | 39 | assets: 40 | - images/ 41 | 42 | fonts: 43 | - family: Inter 44 | fonts: 45 | - asset: fonts/Inter-Regular.ttf 46 | - asset: fonts/Inter-Bold.ttf 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/.gradle 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 17 | base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 18 | - platform: android 19 | create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 20 | base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/config_delay.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | import '../../../../shared/utils/utils.dart'; 5 | import '../providers/v2ray_provider.dart'; 6 | 7 | class ConfigDelay extends ConsumerWidget { 8 | const ConfigDelay({ 9 | super.key, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final selectedConfigPing = ref.watch(selectedConfigPingProvider); 15 | return AnimatedSwitcher( 16 | duration: const Duration(milliseconds: 250), 17 | child: selectedConfigPing == null 18 | ? const SizedBox.shrink() 19 | : Text( 20 | selectedConfigPing > -1 ? '$selectedConfigPing ms' : '', 21 | key: const ValueKey('ConfigPing'), 22 | style: Theme.of(context).textTheme.titleSmall?.copyWith( 23 | fontWeight: FontWeight.bold, 24 | color: AppUtils.pingColor(selectedConfigPing), 25 | ), 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/app/ui/configs/view/widgets/config_error_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:uicons/uicons.dart'; 3 | 4 | class ConfigErrorWidget extends StatelessWidget { 5 | final VoidCallback onError; 6 | const ConfigErrorWidget({ 7 | super.key, 8 | required this.onError, 9 | }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return SizedBox.expand( 14 | child: Column( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | children: [ 17 | Icon( 18 | UIcons.regularRounded.exclamation, 19 | size: 56, 20 | color: Colors.black54, 21 | ), 22 | const SizedBox(height: 16), 23 | Text( 24 | "You're offline", 25 | style: Theme.of(context).textTheme.titleLarge?.copyWith( 26 | fontWeight: FontWeight.bold, 27 | ), 28 | ), 29 | Text( 30 | "Check your connection", 31 | style: Theme.of(context).textTheme.titleMedium, 32 | ), 33 | const SizedBox(height: 16), 34 | FilledButton( 35 | onPressed: onError, 36 | child: const Text('Try again'), 37 | ), 38 | ], 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/app/shared/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'colors.dart'; 4 | 5 | class AppTheme { 6 | AppTheme._(); 7 | 8 | static ThemeData theme = ThemeData( 9 | fontFamily: 'Inter', 10 | scaffoldBackgroundColor: AppColors.light, 11 | colorScheme: const ColorScheme.light( 12 | primary: AppColors.green, 13 | error: AppColors.red, 14 | ), 15 | filledButtonTheme: FilledButtonThemeData( 16 | style: FilledButton.styleFrom( 17 | padding: const EdgeInsets.symmetric(horizontal: 8), 18 | minimumSize: const Size(kToolbarHeight * 2, kToolbarHeight), 19 | shape: RoundedRectangleBorder( 20 | borderRadius: BorderRadius.circular(12), 21 | ), 22 | ), 23 | ), 24 | cardTheme: CardThemeData( 25 | elevation: 0, 26 | margin: const EdgeInsets.all(0), 27 | color: Colors.white, 28 | shape: RoundedRectangleBorder( 29 | borderRadius: BorderRadius.circular(16), 30 | side: BorderSide(width: 2, color: Colors.grey.shade400), 31 | ), 32 | ), 33 | pageTransitionsTheme: PageTransitionsTheme( 34 | builders: { 35 | for (final platform in TargetPlatform.values) ...{ 36 | platform: const FadeUpwardsPageTransitionsBuilder() 37 | }, 38 | }, 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /lib/app/ui/configs/view/widgets/tab_bar_card.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class TabBarCard extends StatelessWidget { 6 | const TabBarCard({super.key, required this.types}); 7 | 8 | final List types; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Card( 13 | elevation: 0, 14 | clipBehavior: Clip.antiAlias, 15 | shape: RoundedRectangleBorder( 16 | borderRadius: BorderRadius.circular(16), 17 | side: BorderSide(width: 2, color: Colors.grey.shade300), 18 | ), 19 | margin: const EdgeInsets.all(16), 20 | child: TabBar( 21 | tabs: [...types.map((type) => Tab(text: type))], 22 | isScrollable: true, 23 | tabAlignment: TabAlignment.start, 24 | dividerHeight: 0, 25 | padding: const EdgeInsets.all(8), 26 | labelColor: Colors.white, 27 | indicatorSize: TabBarIndicatorSize.tab, 28 | labelPadding: const EdgeInsets.symmetric(horizontal: 16), 29 | splashBorderRadius: BorderRadius.circular(12), 30 | indicator: BoxDecoration( 31 | color: Theme.of(context).primaryColor, 32 | borderRadius: BorderRadius.circular(12), 33 | ), 34 | labelStyle: Theme.of(context).textTheme.titleMedium, 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/app/ui/configs/data/models/config_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_v2ray_client/flutter_v2ray.dart'; 4 | 5 | class ConfigModel { 6 | final String name; 7 | final String type; 8 | final int? ping; 9 | final V2RayURL v2rayURL; 10 | ConfigModel({ 11 | required this.name, 12 | required this.type, 13 | required this.v2rayURL, 14 | this.ping, 15 | }); 16 | 17 | @override 18 | bool operator ==(covariant ConfigModel other) { 19 | if (identical(this, other)) return true; 20 | 21 | return other.name == name && 22 | other.type == type && 23 | other.ping == ping && 24 | other.v2rayURL == v2rayURL; 25 | } 26 | 27 | @override 28 | int get hashCode { 29 | return name.hashCode ^ type.hashCode ^ ping.hashCode ^ v2rayURL.hashCode; 30 | } 31 | 32 | Map toMap() { 33 | return { 34 | 'name': name, 35 | 'type': type, 36 | 'ping': ping, 37 | 'v2rayURL': v2rayURL.url, 38 | }; 39 | } 40 | 41 | factory ConfigModel.fromMap(Map map) { 42 | return ConfigModel( 43 | name: map['name'] as String, 44 | type: map['type'] as String, 45 | ping: map['ping'] != null ? map['ping'] as int : null, 46 | v2rayURL: V2ray.parseFromURL(map['v2rayURL']), 47 | ); 48 | } 49 | 50 | String toJson() => json.encode(toMap()); 51 | 52 | factory ConfigModel.fromJson(String source) => 53 | ConfigModel.fromMap(json.decode(source) as Map); 54 | } 55 | -------------------------------------------------------------------------------- /lib/app/ui/home/view/pages/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:uicons/uicons.dart'; 4 | 5 | import '../../../configs/view/pages/configs.dart'; 6 | import '../../../vpn/view/pages/vpn.dart'; 7 | import '../providers/bottom_nav_index_provider.dart'; 8 | import '../widgets/lazy_indexed_stack.dart'; 9 | 10 | class HomePage extends ConsumerWidget { 11 | const HomePage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | final currentIndex = ref.watch(bottomNavIndexProvider); 16 | return Scaffold( 17 | body: LazyIndexedStack(index: currentIndex, children: const [VpnPage(), ConfigsPage()]), 18 | bottomNavigationBar: BottomAppBar( 19 | elevation: 8, 20 | padding: const EdgeInsets.all(0), 21 | child: BottomNavigationBar( 22 | items: [ 23 | BottomNavigationBarItem( 24 | icon: Icon(UIcons.regularRounded.home), 25 | activeIcon: Icon(UIcons.solidRounded.home), 26 | label: 'Home', 27 | ), 28 | BottomNavigationBarItem( 29 | icon: Icon(UIcons.regularRounded.world), 30 | activeIcon: Icon(UIcons.solidRounded.world), 31 | label: 'All Configs', 32 | ), 33 | ], 34 | showSelectedLabels: false, 35 | showUnselectedLabels: false, 36 | currentIndex: currentIndex, 37 | onTap: (index) => ref.read(bottomNavIndexProvider.notifier).update(index), 38 | ), 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id("dev.flutter.flutter-gradle-plugin") 6 | } 7 | 8 | android { 9 | namespace = "com.example.bug_vpn" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_11.toString() 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.bug_vpn" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | 32 | } 33 | 34 | buildTypes { 35 | release { 36 | // TODO: Add your own signing config for the release build. 37 | // Signing with the debug keys for now, so `flutter run --release` works. 38 | signingConfig = signingConfigs.getByName("debug") 39 | } 40 | } 41 | packagingOptions { 42 | jniLibs { 43 | useLegacyPackaging = true 44 | } 45 | } 46 | } 47 | 48 | flutter { 49 | source = "../.." 50 | } 51 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/providers/v2ray_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_v2ray_client/flutter_v2ray.dart'; 3 | 4 | import '../../../../shared/mixins/notifier_mixin.dart'; 5 | 6 | final v2rayProvider = Provider((ref) { 7 | final v2ray = V2ray( 8 | onStatusChanged: (status) { 9 | ref 10 | .read(v2RayStatusProvider.notifier) 11 | .update( 12 | (_) => V2RayStatus( 13 | download: status.download, 14 | downloadSpeed: status.downloadSpeed, 15 | duration: status.duration, 16 | state: status.state, 17 | upload: status.upload, 18 | uploadSpeed: status.uploadSpeed, 19 | ), 20 | ); 21 | }, 22 | ); 23 | 24 | // ref.read(ccProvider.notifier).update(cb) 25 | 26 | return v2ray..initialize(); 27 | }); 28 | 29 | final v2RayStatusProvider = NotifierProvider( 30 | V2RayStatusNotifier.new, 31 | ); 32 | 33 | class V2RayStatusNotifier extends Notifier with NotifierMixin { 34 | @override 35 | V2RayStatus build() => V2RayStatus(); 36 | } 37 | 38 | final configsPingProvider = NotifierProvider>( 39 | ConfigsPingNotifier.new, 40 | ); 41 | 42 | class ConfigsPingNotifier extends Notifier> with NotifierMixin { 43 | @override 44 | Map build() => {}; 45 | } 46 | 47 | final selectedConfigPingProvider = NotifierProvider( 48 | SelectedConfigPingNotifier.new, 49 | ); 50 | 51 | class SelectedConfigPingNotifier extends Notifier with NotifierMixin { 52 | @override 53 | int? build() => null; 54 | } 55 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/usage_status_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class UsageStatusCard extends StatelessWidget { 4 | final IconData icon; 5 | final String label; 6 | final String value; 7 | final Color? backgroundColor; 8 | final Color? iconColor; 9 | const UsageStatusCard({ 10 | super.key, 11 | required this.icon, 12 | required this.label, 13 | required this.value, 14 | this.backgroundColor, 15 | this.iconColor, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Card( 21 | elevation: 0, 22 | margin: const EdgeInsets.all(0), 23 | color: backgroundColor ?? Colors.white, 24 | shape: RoundedRectangleBorder( 25 | borderRadius: BorderRadius.circular(16), 26 | side: BorderSide(width: 2, color: Colors.grey.shade400), 27 | ), 28 | child: Padding( 29 | padding: const EdgeInsets.all(12), 30 | child: Row( 31 | children: [ 32 | Icon(icon, color: iconColor), 33 | const SizedBox(width: 12), 34 | Expanded( 35 | child: Column( 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | children: [ 38 | Text( 39 | label, 40 | style: Theme.of(context).textTheme.titleSmall?.copyWith( 41 | color: Colors.black54, 42 | ), 43 | ), 44 | const SizedBox(height: 4), 45 | Text( 46 | value, 47 | style: Theme.of(context).textTheme.titleLarge?.copyWith( 48 | fontWeight: FontWeight.bold, 49 | ), 50 | ), 51 | ], 52 | ), 53 | ), 54 | ], 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/app/ui/home/view/pages/splash.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | 4 | import '../../../../shared/routes/routes.dart'; 5 | 6 | class SplashPage extends StatefulWidget { 7 | const SplashPage({super.key}); 8 | 9 | @override 10 | State createState() => _SplashPageState(); 11 | } 12 | 13 | class _SplashPageState extends State { 14 | @override 15 | void initState() { 16 | super.initState(); 17 | 18 | // Future.delayed(const Duration(seconds: 1)).then((value) { 19 | // if (mounted) { 20 | 21 | // } 22 | // }); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | body: Center( 29 | child: Animate( 30 | onComplete: (controller) { 31 | Navigator.of(context).pushReplacementNamed(AppRoutes.homeRoute); 32 | }, 33 | effects: const [ 34 | FadeEffect( 35 | duration: Duration(milliseconds: 200), 36 | ), 37 | SlideEffect( 38 | duration: Duration(milliseconds: 350), 39 | begin: Offset(0, 1), 40 | end: Offset.zero, 41 | ), 42 | ThenEffect(), 43 | ShakeEffect( 44 | duration: Duration(milliseconds: 300), 45 | ), 46 | ThenEffect(), 47 | SlideEffect( 48 | duration: Duration(milliseconds: 350), 49 | begin: Offset.zero, 50 | end: Offset(0, -1), 51 | ), 52 | FadeEffect( 53 | begin: 1, 54 | end: 0, 55 | duration: Duration(milliseconds: 200), 56 | ), 57 | ], 58 | child: SizedBox.square( 59 | dimension: 128, 60 | child: Image.asset('images/logo.png'), 61 | ), 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/status_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | 5 | import '../../../../shared/extension/v2ray_extensions.dart'; 6 | import '../providers/v2ray_provider.dart'; 7 | import 'connecting_time.dart'; 8 | import 'connection_status.dart'; 9 | 10 | class StatusInfo extends ConsumerStatefulWidget { 11 | const StatusInfo({super.key}); 12 | 13 | @override 14 | ConsumerState createState() => _StatusInfoState(); 15 | } 16 | 17 | class _StatusInfoState extends ConsumerState with SingleTickerProviderStateMixin { 18 | late final AnimationController _animationController; 19 | @override 20 | void initState() { 21 | super.initState(); 22 | _animationController = AnimationController(vsync: this, duration: Animate.defaultDuration); 23 | } 24 | 25 | @override 26 | void dispose() { 27 | _animationController.dispose(); 28 | super.dispose(); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final status = ref.watch(v2RayStatusProvider); 34 | ref.listen(v2RayStatusProvider, (previous, next) { 35 | if (previous != next) { 36 | if (next.isConnected) { 37 | _animationController.forward(); 38 | } else { 39 | _animationController.reverse(); 40 | } 41 | } 42 | }); 43 | return Animate( 44 | autoPlay: false, 45 | controller: _animationController, 46 | effects: const [ 47 | FadeEffect(), 48 | SlideEffect(begin: Offset(0, -0.1)), 49 | ], 50 | child: Column( 51 | children: [ 52 | const Spacer(), 53 | ConnectingTime(value: status.duration), 54 | const Spacer(), 55 | ConnectionStatus(connected: status.isConnected), 56 | const SizedBox(height: 16), 57 | ], 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/app/ui/home/view/widgets/lazy_indexed_stack.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// {@template lazy_indexed_stack} 4 | /// A widget that displays a [IndexedStack] with lazy loaded children. 5 | /// {@endtemplate} 6 | class LazyIndexedStack extends StatefulWidget { 7 | /// {@macro lazy_indexed_stack} 8 | const LazyIndexedStack({ 9 | super.key, 10 | this.index = 0, 11 | this.children = const [], 12 | this.alignment = AlignmentDirectional.topStart, 13 | this.textDirection, 14 | this.sizing = StackFit.loose, 15 | }); 16 | 17 | /// The index of the child to display. 18 | final int index; 19 | 20 | /// The list of children that can be displayed. 21 | final List children; 22 | 23 | /// How to align the children in the stack. 24 | final AlignmentGeometry alignment; 25 | 26 | /// The direction to use for resolving [alignment]. 27 | final TextDirection? textDirection; 28 | 29 | /// How to size the non-positioned children in the stack. 30 | final StackFit sizing; 31 | 32 | @override 33 | State createState() => _LazyIndexedStackState(); 34 | } 35 | 36 | class _LazyIndexedStackState extends State { 37 | late final List _activatedChildren; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | _activatedChildren = List.generate( 43 | widget.children.length, 44 | (i) => i == widget.index, 45 | ); 46 | } 47 | 48 | @override 49 | void didUpdateWidget(LazyIndexedStack oldWidget) { 50 | super.didUpdateWidget(oldWidget); 51 | if (oldWidget.index != widget.index) _activateChild(widget.index); 52 | } 53 | 54 | void _activateChild(int? index) { 55 | if (index == null) return; 56 | if (!_activatedChildren[index]) _activatedChildren[index] = true; 57 | } 58 | 59 | List get children { 60 | return List.generate( 61 | widget.children.length, 62 | (i) { 63 | return _activatedChildren[i] ? widget.children[i] : const SizedBox.shrink(); 64 | }, 65 | ); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return IndexedStack( 71 | alignment: widget.alignment, 72 | textDirection: widget.textDirection, 73 | sizing: widget.sizing, 74 | index: widget.index, 75 | children: children, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/usage_status_cards.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:uicons/uicons.dart'; 5 | 6 | import '../../../../shared/extension/extensions.dart'; 7 | import '../../../../shared/extension/v2ray_extensions.dart'; 8 | import '../../../../shared/theme/colors.dart'; 9 | import '../providers/v2ray_provider.dart'; 10 | import 'usage_status_card.dart'; 11 | 12 | class UsageStatusCards extends ConsumerStatefulWidget { 13 | const UsageStatusCards({super.key}); 14 | 15 | @override 16 | ConsumerState createState() => _UsageStatusCardsState(); 17 | } 18 | 19 | class _UsageStatusCardsState extends ConsumerState 20 | with SingleTickerProviderStateMixin { 21 | late final AnimationController _animationController; 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _animationController = AnimationController(vsync: this, duration: Animate.defaultDuration); 26 | } 27 | 28 | @override 29 | void dispose() { 30 | _animationController.dispose(); 31 | super.dispose(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final status = ref.watch(v2RayStatusProvider); 37 | ref.listen( 38 | v2RayStatusProvider, 39 | (previous, next) { 40 | if (previous != next) { 41 | if (next.isConnected) { 42 | _animationController.forward(); 43 | } else { 44 | _animationController.reverse(); 45 | } 46 | } 47 | }, 48 | ); 49 | 50 | return Animate( 51 | autoPlay: false, 52 | controller: _animationController, 53 | effects: const [ 54 | FadeEffect(), 55 | SlideEffect( 56 | begin: Offset(0, 0.1), 57 | ), 58 | ], 59 | child: Row( 60 | children: [ 61 | Expanded( 62 | child: UsageStatusCard( 63 | iconColor: AppColors.blue, 64 | icon: UIcons.regularRounded.chevron_double_down, 65 | label: 'Download', 66 | value: status.download.formatAsBytes(), 67 | ), 68 | ), 69 | const SizedBox(width: 16), 70 | Expanded( 71 | child: UsageStatusCard( 72 | iconColor: AppColors.red, 73 | icon: UIcons.regularRounded.chevron_double_up, 74 | label: 'Upload', 75 | value: status.upload.formatAsBytes(), 76 | ), 77 | ), 78 | ], 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/app/ui/configs/view/widgets/config_card.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_v2ray_client/flutter_v2ray.dart'; 5 | import 'package:uicons/uicons.dart'; 6 | 7 | import '../../../../shared/theme/colors.dart'; 8 | import '../../../../shared/utils/utils.dart'; 9 | 10 | class ConfigCard extends StatelessWidget { 11 | final V2RayURL config; 12 | final bool isSelected; 13 | final int index; 14 | final String type; 15 | final Map configsPing; 16 | final VoidCallback onPing; 17 | final VoidCallback onTap; 18 | const ConfigCard({ 19 | super.key, 20 | required this.config, 21 | required this.isSelected, 22 | required this.index, 23 | required this.type, 24 | required this.configsPing, 25 | required this.onPing, 26 | required this.onTap, 27 | }); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Card( 32 | elevation: 0, 33 | clipBehavior: Clip.antiAlias, 34 | shape: RoundedRectangleBorder( 35 | borderRadius: BorderRadius.circular(16), 36 | side: BorderSide(width: 2, color: isSelected ? AppColors.green : Colors.grey.shade300), 37 | ), 38 | margin: const EdgeInsets.all(0), 39 | child: ListTile( 40 | leading: CircleAvatar( 41 | backgroundColor: isSelected ? Theme.of(context).primaryColor : AppColors.red, 42 | child: Text('${index + 1}'), 43 | ), 44 | title: Text(config.remark, maxLines: 1, overflow: TextOverflow.ellipsis), 45 | subtitle: Text('Network : ${config.network.toUpperCase()}'), 46 | titleTextStyle: Theme.of( 47 | context, 48 | ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), 49 | selected: isSelected, 50 | contentPadding: const EdgeInsets.only(left: 16, right: 8), 51 | trailing: Row( 52 | mainAxisSize: MainAxisSize.min, 53 | children: [ 54 | if (configsPing.containsKey(config.url) && configsPing[config.url]! > 0) 55 | Text( 56 | '${configsPing[config.url]} ms', 57 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 58 | color: AppUtils.pingColor(configsPing[config.url]!), 59 | ), 60 | ), 61 | const SizedBox(width: 8), 62 | IconButton( 63 | tooltip: 'Get config ping', 64 | onPressed: onPing, 65 | icon: Icon(UIcons.regularRounded.tachometer_fastest), 66 | ), 67 | ], 68 | ), 69 | onTap: onTap, 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:loader_overlay/loader_overlay.dart'; 5 | import 'package:toastification/toastification.dart'; 6 | 7 | import 'app/shared/preferences/preferences.dart'; 8 | import 'app/shared/routes/routes.dart'; 9 | import 'app/shared/theme/colors.dart'; 10 | import 'app/shared/theme/theme.dart'; 11 | import 'app/shared/utils/utils.dart'; 12 | import 'app/ui/configs/view/providers/configs_provider.dart'; 13 | 14 | void main() async { 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | SystemChrome.setPreferredOrientations([ 17 | DeviceOrientation.portraitDown, 18 | DeviceOrientation.portraitUp, 19 | ]); 20 | await Preferences.instance.init(); 21 | runApp( 22 | ProviderScope( 23 | overrides: [ 24 | selectedConfigProvider.overrideWithBuild((ref, _) => Preferences.instance.getConfig()), 25 | ], 26 | child: const App(), 27 | ), 28 | ); 29 | } 30 | 31 | class App extends StatelessWidget { 32 | const App({super.key}); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return ToastificationWrapper( 37 | config: ToastificationConfig( 38 | alignment: Alignment.center, 39 | animationDuration: const Duration(milliseconds: 200), 40 | animationBuilder: (context, animation, alignment, child) { 41 | return ScaleTransition( 42 | scale: Tween(begin: 0.5, end: 1).animate(animation), 43 | child: FadeTransition(opacity: animation, child: child), 44 | ); 45 | }, 46 | ), 47 | child: GlobalLoaderOverlay( 48 | overlayColor: Colors.black54, 49 | overlayWidgetBuilder: (_) { 50 | return const Center( 51 | child: Card( 52 | color: Colors.white, 53 | elevation: 0, 54 | child: Padding( 55 | padding: EdgeInsets.all(16), 56 | child: CircularProgressIndicator( 57 | strokeWidth: 4, 58 | backgroundColor: Colors.black12, 59 | strokeAlign: BorderSide.strokeAlignOutside, 60 | strokeCap: StrokeCap.round, 61 | color: AppColors.green, 62 | ), 63 | ), 64 | ), 65 | ); 66 | }, 67 | child: MaterialApp( 68 | title: AppUtils.appLabel, 69 | debugShowCheckedModeBanner: false, 70 | theme: AppTheme.theme, 71 | initialRoute: AppRoutes.splashRoute, 72 | routes: AppRoutes.routes, 73 | scrollBehavior: const MaterialScrollBehavior().copyWith(overscroll: false), 74 | ), 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/app/ui/home/view/pages/about.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:uicons/uicons.dart'; 4 | 5 | import '../../../../shared/utils/utils.dart'; 6 | import '../../../vpn/view/providers/v2ray_provider.dart'; 7 | 8 | class AboutPage extends ConsumerWidget { 9 | const AboutPage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | final v2ray = ref.watch(v2rayProvider); 14 | return Center( 15 | child: SizedBox( 16 | height: MediaQuery.sizeOf(context).height / 2, 17 | child: Padding( 18 | padding: const EdgeInsets.symmetric(horizontal: 24), 19 | child: ClipRRect( 20 | borderRadius: BorderRadius.circular(16), 21 | child: Scaffold( 22 | appBar: AppBar( 23 | automaticallyImplyLeading: false, 24 | title: const Text('About ${AppUtils.appLabel}'), 25 | actions: [ 26 | IconButton( 27 | onPressed: () => Navigator.pop(context), 28 | icon: Icon( 29 | UIcons.regularRounded.x, 30 | size: 16, 31 | ), 32 | ), 33 | const SizedBox(width: 8), 34 | ], 35 | ), 36 | body: SingleChildScrollView( 37 | padding: const EdgeInsets.symmetric(vertical: 16), 38 | child: Column( 39 | children: [ 40 | ListTile( 41 | leading: CircleAvatar( 42 | child: Icon(UIcons.regularRounded.user), 43 | ), 44 | title: const Text('Developer'), 45 | subtitle: const Text('H3mnz'), 46 | ), 47 | ListTile( 48 | leading: CircleAvatar( 49 | child: Icon(UIcons.regularRounded.bug), 50 | ), 51 | title: const Text(AppUtils.appLabel), 52 | subtitle: const Text('Version 1.0'), 53 | ), 54 | ListTile( 55 | leading: CircleAvatar( 56 | child: Icon(UIcons.regularRounded.network), 57 | ), 58 | title: const Text('V2Ray Core'), 59 | subtitle: FutureBuilder( 60 | future: v2ray.getCoreVersion(), 61 | builder: (context, snapshot) => Text(snapshot.data ?? 'Loading...'), 62 | ), 63 | ), 64 | ], 65 | ), 66 | ), 67 | ), 68 | ), 69 | ), 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/app/shared/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:toastification/toastification.dart'; 3 | 4 | import '../theme/colors.dart'; 5 | 6 | class AppUtils { 7 | AppUtils._(); 8 | 9 | static const String appLabel = 'BUG VPN'; 10 | 11 | static const String sublinksUrl = 12 | 'https://raw.githubusercontent.com/SoliSpirit/v2ray-configs/refs/heads/main/Subscriptions/Sub2.txt'; 13 | 14 | static const List subnets = [ 15 | "0.0.0.0/5", 16 | "8.0.0.0/7", 17 | "11.0.0.0/8", 18 | "12.0.0.0/6", 19 | "16.0.0.0/4", 20 | "32.0.0.0/3", 21 | "64.0.0.0/2", 22 | "128.0.0.0/3", 23 | "160.0.0.0/5", 24 | "168.0.0.0/6", 25 | "172.0.0.0/12", 26 | "172.32.0.0/11", 27 | "172.64.0.0/10", 28 | "172.128.0.0/9", 29 | "173.0.0.0/8", 30 | "174.0.0.0/7", 31 | "176.0.0.0/4", 32 | "192.0.0.0/9", 33 | "192.128.0.0/11", 34 | "192.160.0.0/13", 35 | "192.169.0.0/16", 36 | "192.170.0.0/15", 37 | "192.172.0.0/14", 38 | "192.176.0.0/12", 39 | "192.192.0.0/10", 40 | "193.0.0.0/8", 41 | "194.0.0.0/7", 42 | "196.0.0.0/6", 43 | "200.0.0.0/5", 44 | "208.0.0.0/4", 45 | "240.0.0.0/4", 46 | ]; 47 | 48 | static String parseConfigType(String url) { 49 | switch (url.split("://")[0].toLowerCase()) { 50 | case 'vmess': 51 | return 'Vmess'; 52 | case 'vless': 53 | return 'Vless'; 54 | case 'trojan': 55 | return 'Trojan'; 56 | case 'ss': 57 | return 'ShadowSocks'; 58 | case 'socks': 59 | return 'Socks'; 60 | default: 61 | return 'Unknown'; 62 | } 63 | } 64 | 65 | static Color pingColor(int ping) { 66 | if (ping <= 500) return AppColors.green; 67 | if (ping > 500 && ping < 1000) return AppColors.amber; 68 | return const Color.fromARGB(255, 134, 48, 48); 69 | } 70 | 71 | static configNotAvailableToast() { 72 | toastification.dismissAll(); 73 | return toastification.show( 74 | title: const Text('Config not available\n Try another one'), 75 | style: ToastificationStyle.simple, 76 | backgroundColor: AppColors.red, 77 | foregroundColor: Colors.white, 78 | closeOnClick: false, 79 | dragToClose: false, 80 | showIcon: true, 81 | boxShadow: highModeShadow, 82 | autoCloseDuration: const Duration(seconds: 2), 83 | ); 84 | } 85 | 86 | static ToastificationItem selectConfigToast() { 87 | toastification.dismissAll(); 88 | return toastification.show( 89 | title: const Text('Please select config'), 90 | style: ToastificationStyle.simple, 91 | closeOnClick: false, 92 | dragToClose: false, 93 | boxShadow: highModeShadow, 94 | autoCloseDuration: const Duration(seconds: 2), 95 | ); 96 | } 97 | 98 | static ToastificationItem unexpectedErrorToast() { 99 | toastification.dismissAll(); 100 | return toastification.show( 101 | title: const Text('An unexpected error has occurred\n Try again'), 102 | style: ToastificationStyle.simple, 103 | backgroundColor: AppColors.red, 104 | foregroundColor: Colors.white, 105 | closeOnClick: false, 106 | dragToClose: false, 107 | autoCloseDuration: const Duration(seconds: 2), 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/pages/vpn.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:uicons/uicons.dart'; 4 | 5 | import '../../../../shared/utils/utils.dart'; 6 | import '../../../home/view/pages/about.dart'; 7 | import '../widgets/connection_button.dart'; 8 | import '../widgets/selected_config_card.dart'; 9 | import '../widgets/status_info.dart'; 10 | import '../widgets/usage_status_cards.dart'; 11 | 12 | class VpnPage extends ConsumerWidget { 13 | const VpnPage({super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | return Scaffold( 18 | extendBodyBehindAppBar: true, 19 | appBar: AppBar( 20 | elevation: 0, 21 | backgroundColor: Colors.white, 22 | scrolledUnderElevation: 0, 23 | surfaceTintColor: Colors.transparent, 24 | title: const Text(AppUtils.appLabel), 25 | actions: [ 26 | IconButton( 27 | onPressed: () => showDialog( 28 | context: context, 29 | builder: (context) => const AboutPage(), 30 | ), 31 | icon: Icon(UIcons.solidRounded.question), 32 | ), 33 | const SizedBox(width: 8), 34 | ], 35 | ), 36 | body: Stack( 37 | children: [ 38 | Positioned.fill( 39 | child: Transform.scale( 40 | scale: 1.5, 41 | child: Transform.rotate( 42 | angle: 360 / 5, 43 | alignment: Alignment.center, 44 | child: const GridPaper( 45 | color: Colors.white, 46 | ), 47 | ), 48 | ), 49 | ), 50 | Positioned.fill( 51 | child: DecoratedBox( 52 | decoration: BoxDecoration( 53 | gradient: LinearGradient( 54 | colors: [ 55 | Colors.white, 56 | Colors.white.withAlpha(0), 57 | ], 58 | stops: const [0.1, 0.4], 59 | begin: Alignment.topCenter, 60 | end: Alignment.bottomCenter, 61 | ), 62 | ), 63 | ), 64 | ), 65 | Positioned.fill( 66 | top: kToolbarHeight + MediaQuery.paddingOf(context).top, 67 | child: const Padding( 68 | padding: EdgeInsets.all(16), 69 | child: Column( 70 | children: [ 71 | Expanded( 72 | flex: 1, 73 | child: StatusInfo(), 74 | ), 75 | Expanded( 76 | flex: 1, 77 | child: ConnectionButton(), 78 | ), 79 | Expanded( 80 | child: Column( 81 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 82 | children: [ 83 | Spacer(flex: 2), 84 | UsageStatusCards(), 85 | Spacer(flex: 2), 86 | SelectedConfigCard(), 87 | Spacer(), 88 | ], 89 | ), 90 | ), 91 | ], 92 | ), 93 | ), 94 | ), 95 | ], 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/selected_config_card.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:loader_overlay/loader_overlay.dart'; 6 | import 'package:uicons/uicons.dart'; 7 | 8 | import '../../../../shared/extension/v2ray_extensions.dart'; 9 | import '../../../configs/view/providers/configs_provider.dart'; 10 | import '../../../home/view/providers/bottom_nav_index_provider.dart'; 11 | import '../providers/v2ray_provider.dart'; 12 | import 'config_delay.dart'; 13 | 14 | class SelectedConfigCard extends ConsumerWidget { 15 | const SelectedConfigCard({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | final v2ray = ref.read(v2rayProvider); 20 | final status = ref.watch(v2RayStatusProvider); 21 | final selectedConfig = ref.watch(selectedConfigProvider); 22 | Widget? child; 23 | if (selectedConfig == null) { 24 | child = OutlinedButton.icon( 25 | onPressed: () => ref.read(bottomNavIndexProvider.notifier).update(1), 26 | style: OutlinedButton.styleFrom( 27 | side: BorderSide(width: 2, color: Theme.of(context).primaryColor), 28 | padding: const EdgeInsets.only(left: 16, right: 12, top: 12, bottom: 12), 29 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), 30 | textStyle: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), 31 | ), 32 | iconAlignment: IconAlignment.end, 33 | label: const Text('Begin by Selecting a config'), 34 | icon: Icon(UIcons.regularRounded.angle_small_right), 35 | ); 36 | } else { 37 | child = Card( 38 | key: const ValueKey('SelectedConfigCard'), 39 | child: ListTile( 40 | onTap: () => ref.read(bottomNavIndexProvider.notifier).update(1), 41 | leading: CircleAvatar(child: Icon(UIcons.solidRounded.shield_interrogation)), 42 | title: Text(selectedConfig.name, maxLines: 1, overflow: TextOverflow.ellipsis), 43 | titleTextStyle: Theme.of( 44 | context, 45 | ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), 46 | subtitle: Text('Network : ${selectedConfig.v2rayURL.network.toUpperCase()}'), 47 | contentPadding: const EdgeInsets.only(left: 16, right: 8), 48 | trailing: Row( 49 | mainAxisSize: MainAxisSize.min, 50 | children: [ 51 | const ConfigDelay(), 52 | const SizedBox(width: 8), 53 | IconButton( 54 | tooltip: 'Get config ping', 55 | onPressed: () async { 56 | if (status.isConnected) return; 57 | context.loaderOverlay.show(); 58 | try { 59 | final delay = await v2ray.getDelayWithTimeout( 60 | selectedConfig.v2rayURL.getFullConfiguration(), 61 | ); 62 | ref.read(selectedConfigPingProvider.notifier).update((_) => delay); 63 | } catch (e) { 64 | log(e.toString()); 65 | } 66 | if (context.mounted) context.loaderOverlay.hide(); 67 | }, 68 | icon: Icon(UIcons.regularRounded.tachometer_alt_fastest), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ); 74 | } 75 | 76 | return AnimatedSwitcher(duration: const Duration(milliseconds: 300), child: child); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/app/ui/configs/view/pages/configs.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:developer'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_animate/flutter_animate.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | import 'package:flutter_v2ray_client/flutter_v2ray.dart'; 8 | import 'package:loader_overlay/loader_overlay.dart'; 9 | import 'package:uicons/uicons.dart'; 10 | 11 | import '../../../../shared/extension/extensions.dart'; 12 | import '../../../../shared/extension/v2ray_extensions.dart'; 13 | import '../../../../shared/preferences/preferences.dart'; 14 | import '../../../../shared/utils/utils.dart'; 15 | import '../../../vpn/view/providers/v2ray_provider.dart'; 16 | import '../../data/models/config_model.dart'; 17 | import '../providers/configs_provider.dart'; 18 | import '../widgets/config_card.dart'; 19 | import '../widgets/config_error_widget.dart'; 20 | import '../widgets/loading_widget.dart'; 21 | import '../widgets/tab_bar_card.dart'; 22 | 23 | class ConfigsPage extends ConsumerWidget { 24 | const ConfigsPage({super.key}); 25 | 26 | @override 27 | Widget build(BuildContext context, WidgetRef ref) { 28 | final v2ray = ref.watch(v2rayProvider); 29 | final provider = ref.watch(configsProvider); 30 | final configsPing = ref.watch(configsPingProvider); 31 | final selectedConfig = ref.watch(selectedConfigProvider); 32 | 33 | return Scaffold( 34 | appBar: AppBar( 35 | scrolledUnderElevation: 0, 36 | surfaceTintColor: Colors.transparent, 37 | title: const Text('All Configs'), 38 | actions: [ 39 | IconButton( 40 | onPressed: () => ref.invalidate(configsProvider), 41 | icon: Icon(UIcons.regularRounded.refresh), 42 | ), 43 | const SizedBox(width: 16), 44 | ], 45 | ), 46 | body: provider.when( 47 | skipError: false, 48 | skipLoadingOnRefresh: false, 49 | skipLoadingOnReload: false, 50 | data: (configs) { 51 | final collections = configs.groupBy((config) => AppUtils.parseConfigType(config.url)); 52 | final types = collections.keys.toList()..sort(); 53 | return DefaultTabController( 54 | length: types.length, 55 | child: Column( 56 | children: [ 57 | Animate( 58 | effects: const [ 59 | FadeEffect(), 60 | SlideEffect(begin: Offset(0, -0.1)), 61 | ], 62 | child: Row( 63 | children: [Expanded(child: TabBarCard(types: types))], 64 | ), 65 | ), 66 | Expanded( 67 | child: Animate( 68 | effects: const [ 69 | FadeEffect(), 70 | SlideEffect(begin: Offset(0, 0.1)), 71 | ], 72 | child: TabBarView( 73 | children: [ 74 | ...types.map( 75 | (type) => ListView.separated( 76 | itemCount: collections[type]!.length, 77 | padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), 78 | separatorBuilder: (context, index) => const SizedBox(height: 8), 79 | itemBuilder: (context, index) { 80 | final config = collections[type]!.elementAt(index); 81 | final isSelected = selectedConfig?.v2rayURL.url == config.url; 82 | return ConfigCard( 83 | config: config, 84 | isSelected: isSelected, 85 | index: index, 86 | type: type, 87 | configsPing: configsPing, 88 | onPing: () => _getDelay(context, v2ray, config, ref), 89 | onTap: () async { 90 | await v2ray.stopV2Ray(); 91 | if (!context.mounted) return; 92 | await _getDelay(context, v2ray, config, ref); 93 | ref 94 | .read(selectedConfigPingProvider.notifier) 95 | .update((_) => configsPing[config.url]); 96 | ref 97 | .read(selectedConfigProvider.notifier) 98 | .update( 99 | ConfigModel( 100 | ping: configsPing[config.url], 101 | name: '${type.toUpperCase()} ${index + 1}', 102 | type: type, 103 | v2rayURL: config, 104 | ), 105 | ); 106 | Preferences.instance.saveConfig( 107 | ConfigModel( 108 | ping: configsPing[config.url], 109 | name: '${type.toUpperCase()} ${index + 1}', 110 | type: type, 111 | v2rayURL: config, 112 | ), 113 | ); 114 | }, 115 | ); 116 | }, 117 | ), 118 | ), 119 | ], 120 | ), 121 | ), 122 | ), 123 | ], 124 | ), 125 | ); 126 | }, 127 | error: (error, stackTrace) => 128 | ConfigErrorWidget(onError: () => ref.invalidate(configsProvider)), 129 | loading: () => const LoadingWidget(), 130 | ), 131 | ); 132 | } 133 | 134 | Future _getDelay(BuildContext context, V2ray v2ray, V2RayURL config, WidgetRef ref) async { 135 | context.loaderOverlay.show(); 136 | try { 137 | final ping = await v2ray.getDelayWithTimeout(config.getFullConfiguration()); 138 | 139 | ref 140 | .read(configsPingProvider.notifier) 141 | .update( 142 | (state) => { 143 | ...state, 144 | ...{config.url: ping}, 145 | }, 146 | ); 147 | } catch (e) { 148 | log(e.toString()); 149 | AppUtils.configNotAvailableToast(); 150 | } 151 | if (context.mounted) context.loaderOverlay.hide(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/app/ui/vpn/view/widgets/connection_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'dart:math' as math; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_animate/flutter_animate.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | import 'package:flutter_v2ray_client/flutter_v2ray.dart'; 8 | import 'package:uicons/uicons.dart'; 9 | 10 | import '../../../../shared/extension/v2ray_extensions.dart'; 11 | import '../../../../shared/theme/colors.dart'; 12 | import '../../../../shared/utils/utils.dart'; 13 | import '../../../configs/data/models/config_model.dart'; 14 | import '../../../configs/view/providers/configs_provider.dart'; 15 | import '../providers/v2ray_provider.dart'; 16 | 17 | class ConnectionButton extends ConsumerStatefulWidget { 18 | const ConnectionButton({super.key}); 19 | 20 | @override 21 | ConsumerState createState() => _ConnectionButtonState(); 22 | } 23 | 24 | class _ConnectionButtonState extends ConsumerState with TickerProviderStateMixin { 25 | late AnimationController animationController; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | animationController = AnimationController(vsync: this, duration: const Duration(seconds: 2)); 31 | 32 | animationController.reverse(); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | animationController.dispose(); 38 | 39 | super.dispose(); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | final status = ref.watch(v2RayStatusProvider); 45 | final v2ray = ref.read(v2rayProvider); 46 | final selectedConfig = ref.watch(selectedConfigProvider); 47 | 48 | return LayoutBuilder( 49 | builder: (context, constraints) { 50 | final h = constraints.maxHeight; 51 | return Center( 52 | child: SizedBox.square( 53 | dimension: h, 54 | child: TweenAnimationBuilder( 55 | tween: Tween(end: status.isConnected ? 1 : 0), 56 | duration: const Duration(milliseconds: 300), 57 | builder: (context, t, child) { 58 | return ClipRRect( 59 | borderRadius: BorderRadius.circular(100), 60 | child: InkWell( 61 | borderRadius: BorderRadius.circular(100), 62 | onTap: () async { 63 | if (animationController.isAnimating) return; 64 | _handleOnTap(status, v2ray, selectedConfig); 65 | }, 66 | child: Center( 67 | child: Stack( 68 | clipBehavior: Clip.none, 69 | children: [ 70 | Positioned.fill( 71 | child: Animate( 72 | controller: animationController, 73 | autoPlay: false, 74 | effects: const [ 75 | ScaleEffect(begin: Offset(1, 1), end: Offset(0.8, 0.8)), 76 | ], 77 | child: Card( 78 | elevation: 24, 79 | margin: const EdgeInsets.all(0), 80 | color: Color.lerp( 81 | AppColors.amber, 82 | AppColors.green, 83 | t, 84 | )?.withAlpha(50), 85 | shadowColor: Color.lerp( 86 | AppColors.amber, 87 | AppColors.green, 88 | t, 89 | )?.withAlpha(50), 90 | clipBehavior: Clip.antiAlias, 91 | shape: const StadiumBorder(), 92 | ), 93 | ), 94 | ), 95 | 96 | // 97 | Positioned.fill( 98 | child: Padding( 99 | padding: const EdgeInsets.all(2), 100 | child: Animate( 101 | controller: animationController, 102 | autoPlay: false, 103 | effects: const [ 104 | ScaleEffect(begin: Offset(1, 1), end: Offset(0.5, 0.5)), 105 | ], 106 | child: CustomPaint( 107 | painter: _ArcPainter( 108 | color: Color.lerp(AppColors.amber, AppColors.green, t)!, 109 | arcCount: 3, 110 | strokeWidth: 4, 111 | ), 112 | ), 113 | ), 114 | ), 115 | ), 116 | // 117 | Positioned.fill( 118 | child: Padding( 119 | padding: const EdgeInsets.all(2), 120 | child: Animate( 121 | autoPlay: false, 122 | controller: animationController, 123 | effects: const [ 124 | ScaleEffect(begin: Offset(1, 1), end: Offset(0, 0)), 125 | ], 126 | child: CustomPaint( 127 | painter: _ArcPainter( 128 | color: Color.lerp(AppColors.amber, AppColors.green, t)!, 129 | arcCount: 3, 130 | reverse: true, 131 | strokeWidth: 20, 132 | ), 133 | ), 134 | ), 135 | ), 136 | ), 137 | 138 | // 139 | Positioned.fill( 140 | child: Card( 141 | elevation: 0, 142 | shape: const StadiumBorder(), 143 | margin: const EdgeInsets.all(32), 144 | color: Color.lerp(AppColors.amber, AppColors.green, t), 145 | child: SizedBox.expand( 146 | child: Icon( 147 | UIcons.solidRounded.bug, 148 | size: h * 0.3, 149 | color: Colors.white, 150 | shadows: const [Shadow(blurRadius: 4, color: Colors.black12)], 151 | ), 152 | ), 153 | ), 154 | ), 155 | ], 156 | ), 157 | ), 158 | ), 159 | ); 160 | }, 161 | ), 162 | ), 163 | ); 164 | }, 165 | ); 166 | } 167 | 168 | Future _handleOnTap(V2RayStatus status, V2ray v2ray, ConfigModel? selectedConfig) async { 169 | if (status.isConnected) { 170 | animationController.repeat(); 171 | 172 | await Future.delayed(const Duration(seconds: 1)); 173 | await v2ray.stopV2Ray(); 174 | ref.read(selectedConfigPingProvider.notifier).update((_) => null); 175 | animationController.reverse(); 176 | } else { 177 | if (selectedConfig is ConfigModel) { 178 | if (await v2ray.requestPermission()) { 179 | try { 180 | animationController.repeat(); 181 | 182 | final delay = await v2ray.getDelayWithTimeout( 183 | selectedConfig.v2rayURL.getFullConfiguration(), 184 | ); 185 | await Future.delayed(const Duration(seconds: 1)); 186 | if (delay > 0) { 187 | await v2ray.startV2Ray( 188 | remark: '${AppUtils.appLabel} is Running...', 189 | bypassSubnets: AppUtils.subnets, 190 | // proxyOnly: true, 191 | config: selectedConfig.v2rayURL.getFullConfiguration(), 192 | ); 193 | ref.read(selectedConfigPingProvider.notifier).update((_) => delay); 194 | } else { 195 | AppUtils.configNotAvailableToast(); 196 | } 197 | } catch (e) { 198 | log(e.toString()); 199 | AppUtils.unexpectedErrorToast(); 200 | } 201 | animationController.reverse(); 202 | } 203 | } else { 204 | AppUtils.selectConfigToast(); 205 | } 206 | } 207 | } 208 | } 209 | 210 | class _ArcPainter extends CustomPainter { 211 | final Color color; 212 | final int arcCount; 213 | final bool reverse; 214 | final double strokeWidth; 215 | _ArcPainter({ 216 | required this.color, 217 | required this.arcCount, 218 | this.reverse = false, 219 | this.strokeWidth = 2, 220 | }); 221 | @override 222 | void paint(Canvas canvas, Size size) { 223 | final p = Paint() 224 | ..color = color 225 | ..strokeWidth = strokeWidth 226 | ..style = PaintingStyle.stroke 227 | ..strokeCap = StrokeCap.round; 228 | final h = size.height; 229 | final w = size.width; 230 | 231 | double sweepAngle = reverse ? -math.pi / arcCount : math.pi / arcCount; 232 | double startAngle = 0; 233 | final arcsRect = Rect.fromLTWH(0, 0, w, h); 234 | 235 | for (int i = 0; i < arcCount; i++) { 236 | canvas.drawArc(arcsRect, startAngle, sweepAngle, false, p); 237 | startAngle = startAngle + sweepAngle * 2; 238 | } 239 | } 240 | 241 | @override 242 | bool shouldRepaint(covariant CustomPainter oldDelegate) { 243 | return this != oldDelegate; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "91.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "8.4.1" 20 | archive: 21 | dependency: transitive 22 | description: 23 | name: archive 24 | sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "4.0.7" 28 | args: 29 | dependency: transitive 30 | description: 31 | name: args 32 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.7.0" 36 | async: 37 | dependency: transitive 38 | description: 39 | name: async 40 | sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.13.0" 44 | back_button_interceptor: 45 | dependency: transitive 46 | description: 47 | name: back_button_interceptor 48 | sha256: b85977faabf4aeb95164b3b8bf81784bed4c54ea1aef90a036ab6927ecf80c5a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "8.0.4" 52 | boolean_selector: 53 | dependency: transitive 54 | description: 55 | name: boolean_selector 56 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.1.2" 60 | characters: 61 | dependency: transitive 62 | description: 63 | name: characters 64 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.4.0" 68 | cli_config: 69 | dependency: transitive 70 | description: 71 | name: cli_config 72 | sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "0.2.0" 76 | clock: 77 | dependency: transitive 78 | description: 79 | name: clock 80 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.1.2" 84 | collection: 85 | dependency: transitive 86 | description: 87 | name: collection 88 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.19.1" 92 | convert: 93 | dependency: transitive 94 | description: 95 | name: convert 96 | sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "3.1.2" 100 | coverage: 101 | dependency: transitive 102 | description: 103 | name: coverage 104 | sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "1.15.0" 108 | crypto: 109 | dependency: transitive 110 | description: 111 | name: crypto 112 | sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "3.0.7" 116 | equatable: 117 | dependency: transitive 118 | description: 119 | name: equatable 120 | sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "2.0.7" 124 | fake_async: 125 | dependency: transitive 126 | description: 127 | name: fake_async 128 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.3.3" 132 | ffi: 133 | dependency: transitive 134 | description: 135 | name: ffi 136 | sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.1.4" 140 | file: 141 | dependency: transitive 142 | description: 143 | name: file 144 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "7.0.1" 148 | fixnum: 149 | dependency: transitive 150 | description: 151 | name: fixnum 152 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.1.1" 156 | flutter: 157 | dependency: "direct main" 158 | description: flutter 159 | source: sdk 160 | version: "0.0.0" 161 | flutter_animate: 162 | dependency: "direct main" 163 | description: 164 | name: flutter_animate 165 | sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" 166 | url: "https://pub.dev" 167 | source: hosted 168 | version: "4.5.2" 169 | flutter_lints: 170 | dependency: "direct dev" 171 | description: 172 | name: flutter_lints 173 | sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" 174 | url: "https://pub.dev" 175 | source: hosted 176 | version: "4.0.0" 177 | flutter_riverpod: 178 | dependency: "direct main" 179 | description: 180 | name: flutter_riverpod 181 | sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde" 182 | url: "https://pub.dev" 183 | source: hosted 184 | version: "3.0.3" 185 | flutter_shaders: 186 | dependency: transitive 187 | description: 188 | name: flutter_shaders 189 | sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" 190 | url: "https://pub.dev" 191 | source: hosted 192 | version: "0.1.3" 193 | flutter_test: 194 | dependency: "direct dev" 195 | description: flutter 196 | source: sdk 197 | version: "0.0.0" 198 | flutter_v2ray_client: 199 | dependency: "direct main" 200 | description: 201 | name: flutter_v2ray_client 202 | sha256: ea424e45567784f44967839b2054355b893721c0edea331a69a9d5ffd9482769 203 | url: "https://pub.dev" 204 | source: hosted 205 | version: "3.0.1" 206 | flutter_web_plugins: 207 | dependency: transitive 208 | description: flutter 209 | source: sdk 210 | version: "0.0.0" 211 | frontend_server_client: 212 | dependency: transitive 213 | description: 214 | name: frontend_server_client 215 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "4.0.0" 219 | glob: 220 | dependency: transitive 221 | description: 222 | name: glob 223 | sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de 224 | url: "https://pub.dev" 225 | source: hosted 226 | version: "2.1.3" 227 | http: 228 | dependency: "direct main" 229 | description: 230 | name: http 231 | sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "1.6.0" 235 | http_multi_server: 236 | dependency: transitive 237 | description: 238 | name: http_multi_server 239 | sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 240 | url: "https://pub.dev" 241 | source: hosted 242 | version: "3.2.2" 243 | http_parser: 244 | dependency: transitive 245 | description: 246 | name: http_parser 247 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" 248 | url: "https://pub.dev" 249 | source: hosted 250 | version: "4.1.2" 251 | icons_launcher: 252 | dependency: "direct dev" 253 | description: 254 | name: icons_launcher 255 | sha256: "6317d56a73ee528f1dd570d7cd7be120ce58014e0fe635d141ada3d88782f58d" 256 | url: "https://pub.dev" 257 | source: hosted 258 | version: "3.0.3" 259 | iconsax_flutter: 260 | dependency: transitive 261 | description: 262 | name: iconsax_flutter 263 | sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564 264 | url: "https://pub.dev" 265 | source: hosted 266 | version: "1.0.1" 267 | image: 268 | dependency: transitive 269 | description: 270 | name: image 271 | sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" 272 | url: "https://pub.dev" 273 | source: hosted 274 | version: "4.5.4" 275 | io: 276 | dependency: transitive 277 | description: 278 | name: io 279 | sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "1.0.5" 283 | js: 284 | dependency: transitive 285 | description: 286 | name: js 287 | sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "0.7.2" 291 | leak_tracker: 292 | dependency: transitive 293 | description: 294 | name: leak_tracker 295 | sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "11.0.2" 299 | leak_tracker_flutter_testing: 300 | dependency: transitive 301 | description: 302 | name: leak_tracker_flutter_testing 303 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "3.0.10" 307 | leak_tracker_testing: 308 | dependency: transitive 309 | description: 310 | name: leak_tracker_testing 311 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "3.0.2" 315 | lints: 316 | dependency: transitive 317 | description: 318 | name: lints 319 | sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "4.0.0" 323 | loader_overlay: 324 | dependency: "direct main" 325 | description: 326 | name: loader_overlay 327 | sha256: "285c9ccab9a42a392ba948bd0b14376fd0ee9ddd7b63e3018bcd54460fd3e021" 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "5.0.0" 331 | logging: 332 | dependency: transitive 333 | description: 334 | name: logging 335 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "1.3.0" 339 | matcher: 340 | dependency: transitive 341 | description: 342 | name: matcher 343 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "0.12.17" 347 | material_color_utilities: 348 | dependency: transitive 349 | description: 350 | name: material_color_utilities 351 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "0.11.1" 355 | meta: 356 | dependency: transitive 357 | description: 358 | name: meta 359 | sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" 360 | url: "https://pub.dev" 361 | source: hosted 362 | version: "1.17.0" 363 | mime: 364 | dependency: transitive 365 | description: 366 | name: mime 367 | sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" 368 | url: "https://pub.dev" 369 | source: hosted 370 | version: "2.0.0" 371 | node_preamble: 372 | dependency: transitive 373 | description: 374 | name: node_preamble 375 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 376 | url: "https://pub.dev" 377 | source: hosted 378 | version: "2.0.2" 379 | package_config: 380 | dependency: transitive 381 | description: 382 | name: package_config 383 | sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc 384 | url: "https://pub.dev" 385 | source: hosted 386 | version: "2.2.0" 387 | path: 388 | dependency: transitive 389 | description: 390 | name: path 391 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 392 | url: "https://pub.dev" 393 | source: hosted 394 | version: "1.9.1" 395 | path_provider_linux: 396 | dependency: transitive 397 | description: 398 | name: path_provider_linux 399 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 400 | url: "https://pub.dev" 401 | source: hosted 402 | version: "2.2.1" 403 | path_provider_platform_interface: 404 | dependency: transitive 405 | description: 406 | name: path_provider_platform_interface 407 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 408 | url: "https://pub.dev" 409 | source: hosted 410 | version: "2.1.2" 411 | path_provider_windows: 412 | dependency: transitive 413 | description: 414 | name: path_provider_windows 415 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 416 | url: "https://pub.dev" 417 | source: hosted 418 | version: "2.3.0" 419 | pausable_timer: 420 | dependency: transitive 421 | description: 422 | name: pausable_timer 423 | sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" 424 | url: "https://pub.dev" 425 | source: hosted 426 | version: "3.1.0+3" 427 | petitparser: 428 | dependency: transitive 429 | description: 430 | name: petitparser 431 | sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" 432 | url: "https://pub.dev" 433 | source: hosted 434 | version: "7.0.1" 435 | platform: 436 | dependency: transitive 437 | description: 438 | name: platform 439 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 440 | url: "https://pub.dev" 441 | source: hosted 442 | version: "3.1.6" 443 | plugin_platform_interface: 444 | dependency: transitive 445 | description: 446 | name: plugin_platform_interface 447 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 448 | url: "https://pub.dev" 449 | source: hosted 450 | version: "2.1.8" 451 | pool: 452 | dependency: transitive 453 | description: 454 | name: pool 455 | sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" 456 | url: "https://pub.dev" 457 | source: hosted 458 | version: "1.5.2" 459 | posix: 460 | dependency: transitive 461 | description: 462 | name: posix 463 | sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" 464 | url: "https://pub.dev" 465 | source: hosted 466 | version: "6.0.3" 467 | pub_semver: 468 | dependency: transitive 469 | description: 470 | name: pub_semver 471 | sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" 472 | url: "https://pub.dev" 473 | source: hosted 474 | version: "2.2.0" 475 | riverpod: 476 | dependency: transitive 477 | description: 478 | name: riverpod 479 | sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59 480 | url: "https://pub.dev" 481 | source: hosted 482 | version: "3.0.3" 483 | shared_preferences: 484 | dependency: "direct main" 485 | description: 486 | name: shared_preferences 487 | sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" 488 | url: "https://pub.dev" 489 | source: hosted 490 | version: "2.5.3" 491 | shared_preferences_android: 492 | dependency: transitive 493 | description: 494 | name: shared_preferences_android 495 | sha256: "07d552dbe8e71ed720e5205e760438ff4ecfb76ec3b32ea664350e2ca4b0c43b" 496 | url: "https://pub.dev" 497 | source: hosted 498 | version: "2.4.16" 499 | shared_preferences_foundation: 500 | dependency: transitive 501 | description: 502 | name: shared_preferences_foundation 503 | sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" 504 | url: "https://pub.dev" 505 | source: hosted 506 | version: "2.5.6" 507 | shared_preferences_linux: 508 | dependency: transitive 509 | description: 510 | name: shared_preferences_linux 511 | sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 512 | url: "https://pub.dev" 513 | source: hosted 514 | version: "2.4.1" 515 | shared_preferences_platform_interface: 516 | dependency: transitive 517 | description: 518 | name: shared_preferences_platform_interface 519 | sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 520 | url: "https://pub.dev" 521 | source: hosted 522 | version: "2.4.1" 523 | shared_preferences_web: 524 | dependency: transitive 525 | description: 526 | name: shared_preferences_web 527 | sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 528 | url: "https://pub.dev" 529 | source: hosted 530 | version: "2.4.3" 531 | shared_preferences_windows: 532 | dependency: transitive 533 | description: 534 | name: shared_preferences_windows 535 | sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 536 | url: "https://pub.dev" 537 | source: hosted 538 | version: "2.4.1" 539 | shelf: 540 | dependency: transitive 541 | description: 542 | name: shelf 543 | sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 544 | url: "https://pub.dev" 545 | source: hosted 546 | version: "1.4.2" 547 | shelf_packages_handler: 548 | dependency: transitive 549 | description: 550 | name: shelf_packages_handler 551 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 552 | url: "https://pub.dev" 553 | source: hosted 554 | version: "3.0.2" 555 | shelf_static: 556 | dependency: transitive 557 | description: 558 | name: shelf_static 559 | sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 560 | url: "https://pub.dev" 561 | source: hosted 562 | version: "1.1.3" 563 | shelf_web_socket: 564 | dependency: transitive 565 | description: 566 | name: shelf_web_socket 567 | sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" 568 | url: "https://pub.dev" 569 | source: hosted 570 | version: "3.0.0" 571 | sky_engine: 572 | dependency: transitive 573 | description: flutter 574 | source: sdk 575 | version: "0.0.0" 576 | source_map_stack_trace: 577 | dependency: transitive 578 | description: 579 | name: source_map_stack_trace 580 | sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b 581 | url: "https://pub.dev" 582 | source: hosted 583 | version: "2.1.2" 584 | source_maps: 585 | dependency: transitive 586 | description: 587 | name: source_maps 588 | sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" 589 | url: "https://pub.dev" 590 | source: hosted 591 | version: "0.10.13" 592 | source_span: 593 | dependency: transitive 594 | description: 595 | name: source_span 596 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 597 | url: "https://pub.dev" 598 | source: hosted 599 | version: "1.10.1" 600 | stack_trace: 601 | dependency: transitive 602 | description: 603 | name: stack_trace 604 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 605 | url: "https://pub.dev" 606 | source: hosted 607 | version: "1.12.1" 608 | state_notifier: 609 | dependency: transitive 610 | description: 611 | name: state_notifier 612 | sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb 613 | url: "https://pub.dev" 614 | source: hosted 615 | version: "1.0.0" 616 | stream_channel: 617 | dependency: transitive 618 | description: 619 | name: stream_channel 620 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 621 | url: "https://pub.dev" 622 | source: hosted 623 | version: "2.1.4" 624 | string_scanner: 625 | dependency: transitive 626 | description: 627 | name: string_scanner 628 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 629 | url: "https://pub.dev" 630 | source: hosted 631 | version: "1.4.1" 632 | term_glyph: 633 | dependency: transitive 634 | description: 635 | name: term_glyph 636 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 637 | url: "https://pub.dev" 638 | source: hosted 639 | version: "1.2.2" 640 | test: 641 | dependency: transitive 642 | description: 643 | name: test 644 | sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" 645 | url: "https://pub.dev" 646 | source: hosted 647 | version: "1.26.3" 648 | test_api: 649 | dependency: transitive 650 | description: 651 | name: test_api 652 | sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 653 | url: "https://pub.dev" 654 | source: hosted 655 | version: "0.7.7" 656 | test_core: 657 | dependency: transitive 658 | description: 659 | name: test_core 660 | sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" 661 | url: "https://pub.dev" 662 | source: hosted 663 | version: "0.6.12" 664 | toastification: 665 | dependency: "direct main" 666 | description: 667 | name: toastification 668 | sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112" 669 | url: "https://pub.dev" 670 | source: hosted 671 | version: "3.0.3" 672 | typed_data: 673 | dependency: transitive 674 | description: 675 | name: typed_data 676 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 677 | url: "https://pub.dev" 678 | source: hosted 679 | version: "1.4.0" 680 | uicons: 681 | dependency: "direct main" 682 | description: 683 | name: uicons 684 | sha256: "07c9d221c76462b6f6f7e41bb36db3826a9da4ad166167f7a5f9d629b0136483" 685 | url: "https://pub.dev" 686 | source: hosted 687 | version: "1.0.1" 688 | uicons_bold_rounded: 689 | dependency: transitive 690 | description: 691 | name: uicons_bold_rounded 692 | sha256: d0cb77efa94e6737a8e25a385c9164d5099e543373a415313eb457f9b7e5d94e 693 | url: "https://pub.dev" 694 | source: hosted 695 | version: "1.0.0" 696 | uicons_bold_straight: 697 | dependency: transitive 698 | description: 699 | name: uicons_bold_straight 700 | sha256: f2d6649748744b424a689082868374636d9bea4cea1db15da6ba288b5e9a0b8e 701 | url: "https://pub.dev" 702 | source: hosted 703 | version: "1.0.0" 704 | uicons_brands: 705 | dependency: transitive 706 | description: 707 | name: uicons_brands 708 | sha256: "4bffb4681e55cfb43c11a773c1976b9a3930723883ae67bafa9d92fba7ab7c5f" 709 | url: "https://pub.dev" 710 | source: hosted 711 | version: "1.0.0" 712 | uicons_regular_rounded: 713 | dependency: transitive 714 | description: 715 | name: uicons_regular_rounded 716 | sha256: "80bf870676524fb48698a2e2037e76de6e81d5d10999191362a89de946fad541" 717 | url: "https://pub.dev" 718 | source: hosted 719 | version: "1.0.0" 720 | uicons_regular_straight: 721 | dependency: transitive 722 | description: 723 | name: uicons_regular_straight 724 | sha256: "967cbcbc75f2b5247b7c6018a8594a6e36745e37146531bc726ab5ffde4e9e4f" 725 | url: "https://pub.dev" 726 | source: hosted 727 | version: "1.0.0" 728 | uicons_solid_rounded: 729 | dependency: transitive 730 | description: 731 | name: uicons_solid_rounded 732 | sha256: "2a8ee5654941f9777bddf109acc691b60211bdef6d0249851968ce9aa220473f" 733 | url: "https://pub.dev" 734 | source: hosted 735 | version: "1.0.0" 736 | uicons_solid_straight: 737 | dependency: transitive 738 | description: 739 | name: uicons_solid_straight 740 | sha256: a751ac73eda538907e672951f80b7a9e3a117084afa50f5b6baae2edf285cb04 741 | url: "https://pub.dev" 742 | source: hosted 743 | version: "1.0.0" 744 | universal_io: 745 | dependency: transitive 746 | description: 747 | name: universal_io 748 | sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 749 | url: "https://pub.dev" 750 | source: hosted 751 | version: "2.3.1" 752 | uuid: 753 | dependency: transitive 754 | description: 755 | name: uuid 756 | sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 757 | url: "https://pub.dev" 758 | source: hosted 759 | version: "4.5.2" 760 | vector_math: 761 | dependency: transitive 762 | description: 763 | name: vector_math 764 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 765 | url: "https://pub.dev" 766 | source: hosted 767 | version: "2.2.0" 768 | vm_service: 769 | dependency: transitive 770 | description: 771 | name: vm_service 772 | sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" 773 | url: "https://pub.dev" 774 | source: hosted 775 | version: "15.0.2" 776 | watcher: 777 | dependency: transitive 778 | description: 779 | name: watcher 780 | sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" 781 | url: "https://pub.dev" 782 | source: hosted 783 | version: "1.1.4" 784 | web: 785 | dependency: transitive 786 | description: 787 | name: web 788 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 789 | url: "https://pub.dev" 790 | source: hosted 791 | version: "1.1.1" 792 | web_socket: 793 | dependency: transitive 794 | description: 795 | name: web_socket 796 | sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" 797 | url: "https://pub.dev" 798 | source: hosted 799 | version: "1.0.1" 800 | web_socket_channel: 801 | dependency: transitive 802 | description: 803 | name: web_socket_channel 804 | sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 805 | url: "https://pub.dev" 806 | source: hosted 807 | version: "3.0.3" 808 | webkit_inspection_protocol: 809 | dependency: transitive 810 | description: 811 | name: webkit_inspection_protocol 812 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 813 | url: "https://pub.dev" 814 | source: hosted 815 | version: "1.2.1" 816 | xdg_directories: 817 | dependency: transitive 818 | description: 819 | name: xdg_directories 820 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 821 | url: "https://pub.dev" 822 | source: hosted 823 | version: "1.1.0" 824 | xml: 825 | dependency: transitive 826 | description: 827 | name: xml 828 | sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" 829 | url: "https://pub.dev" 830 | source: hosted 831 | version: "6.6.1" 832 | yaml: 833 | dependency: transitive 834 | description: 835 | name: yaml 836 | sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce 837 | url: "https://pub.dev" 838 | source: hosted 839 | version: "3.1.3" 840 | sdks: 841 | dart: ">=3.10.0 <4.0.0" 842 | flutter: ">=3.35.0" 843 | --------------------------------------------------------------------------------