Files
umbrix/lib/features/home/widget/connection_button.dart

379 lines
15 KiB
Dart
Raw Normal View History

2023-07-06 17:18:41 +03:30
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
2023-07-06 17:18:41 +03:30
import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/model/failures.dart';
import 'package:umbrix/core/theme/theme_extensions.dart';
import 'package:umbrix/core/widget/animated_text.dart';
import 'package:umbrix/features/config_option/data/config_option_repository.dart';
import 'package:umbrix/features/config_option/notifier/config_option_notifier.dart';
import 'package:umbrix/features/connection/model/connection_status.dart';
import 'package:umbrix/features/connection/notifier/connection_notifier.dart';
import 'package:umbrix/features/connection/widget/experimental_feature_notice.dart';
import 'package:umbrix/features/profile/notifier/active_profile_notifier.dart';
import 'package:umbrix/features/proxy/active/active_proxy_notifier.dart';
import 'package:umbrix/gen/assets.gen.dart';
import 'package:umbrix/utils/alerts.dart';
2023-07-06 17:18:41 +03:30
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 delay = ref.watch(
activeProxyNotifierProvider.select((v) => v.valueOrNull?.urlTestDelay ?? 0),
);
final requiresReconnect = ref.watch(
configOptionNotifierProvider.select((v) => v.valueOrNull == true),
);
2023-07-06 17:18:41 +03:30
2023-08-19 22:27:23 +03:30
ref.listen(
2023-12-01 12:56:24 +03:30
connectionNotifierProvider,
2023-08-19 22:27:23 +03:30
(_, next) {
if (next case AsyncError(:final error)) {
2023-08-26 17:01:51 +03:30
CustomAlertDialog.fromErr(t.presentError(error)).show(context);
2023-08-19 22:27:23 +03:30
}
if (next case AsyncData(value: Disconnected(:final connectionFailure?))) {
CustomAlertDialog.fromErr(t.presentError(connectionFailure)).show(context);
2023-08-19 22:27:23 +03:30
}
},
);
2023-09-06 12:56:30 +03:30
final buttonTheme = Theme.of(context).extension<ConnectionButtonTheme>()!;
Future<bool> 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;
}
2023-12-31 10:28:52 +03:30
return _ConnectionButton(
onTap: switch (connectionStatus) {
AsyncData(value: Disconnected()) || AsyncError() => () async {
if (await showExperimentalNotice()) {
return await ref.read(connectionNotifierProvider.notifier).toggleConnection();
2023-12-31 10:28:52 +03:30
}
},
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,
},
2024-03-17 14:45:15 +01:00
image: switch (connectionStatus) {
AsyncData(value: Connected()) when requiresReconnect == true => Assets.images.disconnectNorouz,
2024-03-17 01:54:05 +03:30
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,
);
2023-08-19 22:27:23 +03:30
}
}
2023-07-06 17:18:41 +03:30
class _ConnectionButton extends StatefulWidget {
2023-08-19 22:27:23 +03:30
const _ConnectionButton({
required this.onTap,
required this.enabled,
required this.label,
required this.buttonColor,
2024-03-17 01:54:05 +03:30
required this.image,
required this.useImage,
required this.isConnected,
2023-08-19 22:27:23 +03:30
});
final VoidCallback onTap;
final bool enabled;
final String label;
final Color buttonColor;
2024-03-17 01:54:05 +03:30
final AssetGenImage image;
final bool useImage;
final bool isConnected;
2023-08-19 22:27:23 +03:30
@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();
}
2023-08-19 22:27:23 +03:30
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
2023-07-06 17:18:41 +03:30
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
2023-09-02 21:09:22 +03:30
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),
),
],
2023-09-02 21:09:22 +03:30
),
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<double>(
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),
],
),
),
),
),
],
2023-07-06 17:18:41 +03:30
),
).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),
),
),
2023-09-02 21:09:22 +03:30
),
const Gap(28),
2024-02-13 18:49:58 +03:30
ExcludeSemantics(
2024-03-08 15:07:45 +03:30
child: AnimatedText(
widget.label,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
2024-02-13 18:49:58 +03:30
),
2023-07-06 17:18:41 +03:30
),
],
);
}
}