import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/theme/theme_extensions.dart'; import 'package:hiddify/core/widget/animated_text.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/connection/widget/experimental_feature_notice.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/alerts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class ConnectionButton extends HookConsumerWidget { const ConnectionButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); final connectionStatus = ref.watch(connectionNotifierProvider); final activeProxy = ref.watch(activeProxyNotifierProvider); final delay = activeProxy.valueOrNull?.urlTestDelay ?? 0; final requiresReconnect = ref.watch(configOptionNotifierProvider).valueOrNull; final today = DateTime.now(); ref.listen( connectionNotifierProvider, (_, next) { if (next case AsyncError(:final error)) { CustomAlertDialog.fromErr(t.presentError(error)).show(context); } if (next case AsyncData(value: Disconnected(:final connectionFailure?))) { CustomAlertDialog.fromErr(t.presentError(connectionFailure)).show(context); } }, ); final buttonTheme = Theme.of(context).extension()!; Future showExperimentalNotice() async { final hasExperimental = ref.read(ConfigOptions.hasExperimentalFeatures); final canShowNotice = !ref.read(disableExperimentalFeatureNoticeProvider); if (hasExperimental && canShowNotice && context.mounted) { return await const ExperimentalFeatureNoticeDialog().show(context) ?? false; } return true; } return _ConnectionButton( onTap: switch (connectionStatus) { AsyncData(value: Disconnected()) || AsyncError() => () async { if (await showExperimentalNotice()) { return await ref.read(connectionNotifierProvider.notifier).toggleConnection(); } }, AsyncData(value: Connected()) => () async { if (requiresReconnect == true && await showExperimentalNotice()) { return await ref.read(connectionNotifierProvider.notifier).reconnect(await ref.read(activeProfileProvider.future)); } return await ref.read(connectionNotifierProvider.notifier).toggleConnection(); }, _ => () {}, }, enabled: switch (connectionStatus) { AsyncData(value: Connected()) || AsyncData(value: Disconnected()) || AsyncError() => true, _ => false, }, label: switch (connectionStatus) { AsyncData(value: Connected()) when requiresReconnect == true => t.connection.reconnect, AsyncData(value: Connected()) when delay <= 0 || delay >= 65000 => t.connection.connecting, AsyncData(value: final status) => status.present(t), _ => "", }, buttonColor: switch (connectionStatus) { AsyncData(value: Connected()) when requiresReconnect == true => Colors.teal, AsyncData(value: Connected()) when delay <= 0 || delay >= 65000 => const Color.fromARGB(255, 185, 176, 103), AsyncData(value: Connected()) => buttonTheme.connectedColor!, AsyncData(value: _) => buttonTheme.idleColor!, _ => Colors.red, }, image: switch (connectionStatus) { AsyncData(value: Connected()) when requiresReconnect == true => Assets.images.disconnectNorouz, AsyncData(value: Connected()) => Assets.images.connectNorouz, AsyncData(value: _) => Assets.images.disconnectNorouz, _ => Assets.images.disconnectNorouz, }, useImage: true, // Всегда показывать картинки isConnected: connectionStatus is AsyncData && (connectionStatus as AsyncData).value is Connected, ); } } class _ConnectionButton extends StatefulWidget { const _ConnectionButton({ required this.onTap, required this.enabled, required this.label, required this.buttonColor, required this.image, required this.useImage, required this.isConnected, }); final VoidCallback onTap; final bool enabled; final String label; final Color buttonColor; final AssetGenImage image; final bool useImage; final bool isConnected; @override State<_ConnectionButton> createState() => _ConnectionButtonState(); } class _ConnectionButtonState extends State<_ConnectionButton> with SingleTickerProviderStateMixin { late AnimationController _pulseController; bool _isPressed = false; @override void initState() { super.initState(); _pulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); if (widget.isConnected) { _pulseController.repeat(); } } @override void didUpdateWidget(_ConnectionButton oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isConnected != oldWidget.isConnected) { if (widget.isConnected) { _pulseController.repeat(); } else { _pulseController.stop(); _pulseController.reset(); } } } @override void dispose() { _pulseController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Semantics( button: true, enabled: widget.enabled, label: widget.label, child: GestureDetector( onTapDown: widget.enabled ? (_) { HapticFeedback.lightImpact(); setState(() => _isPressed = true); } : null, onTapUp: widget.enabled ? (_) { setState(() => _isPressed = false); HapticFeedback.mediumImpact(); widget.onTap(); } : null, onTapCancel: () { setState(() => _isPressed = false); }, child: AnimatedScale( scale: _isPressed ? 0.95 : 1.0, duration: const Duration(milliseconds: 100), curve: Curves.easeOut, child: Container( width: 200, height: 200, decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ // Основная тень BoxShadow( blurRadius: 32, spreadRadius: 0, color: widget.buttonColor.withOpacity(0.4), offset: const Offset(0, 8), ), // Внутренняя подсветка if (widget.isConnected) BoxShadow( blurRadius: 40, spreadRadius: -8, color: widget.buttonColor.withOpacity(0.6), offset: const Offset(0, 0), ), ], ), child: Stack( alignment: Alignment.center, children: [ // Pulse эффект при подключении if (widget.isConnected) AnimatedBuilder( animation: _pulseController, builder: (context, child) { return Container( width: 200 + (_pulseController.value * 20), height: 200 + (_pulseController.value * 20), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: widget.buttonColor.withOpacity( 0.3 * (1 - _pulseController.value), ), width: 3, ), ), ); }, ), // Внешний круг с neomorphism эффектом Container( width: 200, height: 200, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isDark ? [ const Color(0xFF2C2C2C), const Color(0xFF1A1A1A), ] : [ const Color(0xFFF5F5F5), const Color(0xFFE0E0E0), ], ), boxShadow: [ // Внешняя тень BoxShadow( color: isDark ? Colors.black.withOpacity(0.5) : Colors.grey.withOpacity(0.3), offset: const Offset(8, 8), blurRadius: 16, ), // Внутренняя подсветка BoxShadow( color: isDark ? Colors.white.withOpacity(0.05) : Colors.white.withOpacity(0.8), offset: const Offset(-8, -8), blurRadius: 16, ), ], ), ), // Внутренний круг с градиентом Container( width: 160, height: 160, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ widget.buttonColor.withOpacity(0.95), widget.buttonColor, widget.buttonColor.withOpacity(1.0), ], stops: const [0.0, 0.7, 1.0], ), boxShadow: [ BoxShadow( color: widget.buttonColor.withOpacity(0.5), blurRadius: 20, spreadRadius: -5, ), ], ), child: Material( key: const ValueKey("home_connection_button"), shape: const CircleBorder(), color: Colors.transparent, child: InkWell( customBorder: const CircleBorder(), onTap: null, // Handled by GestureDetector splashColor: Colors.white.withOpacity(0.2), highlightColor: Colors.white.withOpacity(0.1), child: Center( child: TweenAnimationBuilder( tween: Tween( begin: 0.0, end: widget.isConnected ? 1.0 : 0.0, ), duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, builder: (context, value, child) { return Transform.rotate( angle: value * 0.5, // Легкий поворот при подключении child: Icon( Icons.power_settings_new_rounded, color: Colors.white, size: 72, shadows: [ Shadow( color: Colors.black.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), ); }, ), ), ), ), ), // Glossy overlay эффект Positioned( top: 30, child: Container( width: 100, height: 50, decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.white.withOpacity(0.3), Colors.white.withOpacity(0.0), ], ), ), ), ), ], ), ).animate(target: widget.enabled ? 0 : 1).blurXY(end: 2, curve: Curves.easeOut).animate(target: widget.enabled ? 0 : 1).scaleXY(end: 0.92, curve: Curves.easeInOut), ), ), ), const Gap(28), ExcludeSemantics( child: AnimatedText( widget.label, style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), ), ], ); } }