Files
umbrix/lib/features/profile/add/add_profile_modal.dart
2025-12-26 03:42:40 +03:00

477 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:combine/combine.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/notification/in_app_notification_controller.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/common/qr_code_scanner_screen.dart';
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart';
import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart';
import 'package:hiddify/features/profile/notifier/profile_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class AddProfileModal extends HookConsumerWidget {
const AddProfileModal({
super.key,
this.url,
this.scrollController,
});
static const warpConsentGiven = "warp_consent_given";
final String? url;
final ScrollController? scrollController;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final addProfileState = ref.watch(addProfileProvider);
ref.listen(
addProfileProvider,
(previous, next) {
if (next case AsyncData(value: final _?)) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (context.mounted && context.canPop()) context.pop();
},
);
}
},
);
useMemoized(() async {
await Future.delayed(const Duration(milliseconds: 200));
if (url != null && context.mounted) {
if (addProfileState.isLoading) return;
ref.read(addProfileProvider.notifier).add(url!);
}
});
final theme = Theme.of(context);
const buttonsPadding = 24.0;
const buttonsGap = 16.0;
return SingleChildScrollView(
controller: scrollController,
child: AnimatedSize(
duration: const Duration(milliseconds: 250),
child: Builder(
builder: (context) {
// Fixed button width instead of using LayoutBuilder
final buttonWidth = (MediaQuery.of(context).size.width / 2) - (buttonsPadding + (buttonsGap / 2));
return AnimatedCrossFade(
firstChild: SizedBox(
height: buttonWidth.clamp(0, 168),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 64),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
t.profile.add.addingProfileMsg,
style: theme.textTheme.bodySmall,
),
const Gap(8),
const LinearProgressIndicator(
backgroundColor: Colors.transparent,
),
const Gap(8),
TextButton(
onPressed: () {
ref.invalidate(addProfileProvider);
},
child: Text(
MaterialLocalizations.of(context).cancelButtonLabel,
),
),
],
),
),
),
secondChild: Column(
children: [
// Заголовок
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
t.profile.add.buttonText,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
// Основные кнопки в виде больших карточек
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_ModernButton(
key: const ValueKey("add_from_clipboard_button"),
label: t.profile.add.fromClipboard,
subtitle: "Paste from clipboard",
icon: FluentIcons.clipboard_paste_24_filled,
gradient: LinearGradient(
colors: [
theme.colorScheme.primaryContainer,
theme.colorScheme.secondaryContainer,
],
),
onTap: () async {
final captureResult = await Clipboard.getData(Clipboard.kTextPlain).then((value) => value?.text ?? '');
if (addProfileState.isLoading) return;
ref.read(addProfileProvider.notifier).add(captureResult);
},
),
const Gap(12),
if (!PlatformUtils.isDesktop)
_ModernButton(
key: const ValueKey("add_by_qr_code_button"),
label: t.profile.add.scanQr,
subtitle: "Camera scanner",
icon: FluentIcons.qr_code_24_filled,
gradient: LinearGradient(
colors: [
theme.colorScheme.tertiaryContainer,
theme.colorScheme.primaryContainer.withOpacity(0.7),
],
),
onTap: () async {
final cr = await QRCodeScannerScreen().open(context);
if (cr == null) return;
if (addProfileState.isLoading) return;
ref.read(addProfileProvider.notifier).add(cr);
},
)
else
_ModernButton(
key: const ValueKey("add_manually_button"),
label: t.profile.add.manually,
subtitle: "Create new config",
icon: FluentIcons.edit_24_filled,
gradient: LinearGradient(
colors: [
theme.colorScheme.tertiaryContainer,
theme.colorScheme.primaryContainer.withOpacity(0.7),
],
),
onTap: () async {
context.pop();
await const NewProfileRoute().push(context);
},
),
],
),
),
const Gap(16),
// Дополнительные опции
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_CompactButton(
key: const ValueKey("add_warp_button"),
label: t.profile.add.addWarp,
icon: FluentIcons.cloud_add_24_regular,
color: theme.colorScheme.primary,
onTap: () async {
await addProfileModal(context, ref);
},
),
if (!PlatformUtils.isDesktop) const Gap(12),
if (!PlatformUtils.isDesktop)
_CompactButton(
key: const ValueKey("add_manually_button"),
label: t.profile.add.manually,
icon: FluentIcons.edit_24_regular,
color: theme.colorScheme.secondary,
onTap: () async {
context.pop();
await const NewProfileRoute().push(context);
},
),
],
),
),
const Gap(24),
],
),
crossFadeState: addProfileState.isLoading ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: const Duration(milliseconds: 250),
);
},
),
),
);
}
Future<void> addProfileModal(BuildContext context, WidgetRef ref) async {
final _prefs = ref.read(sharedPreferencesProvider).requireValue;
final _warp = ref.read(warpOptionNotifierProvider.notifier);
final _profile = ref.read(addProfileProvider.notifier);
final consent = (_prefs.getBool(warpConsentGiven) ?? false);
final region = ref.read(ConfigOptions.region.notifier).raw();
context.pop();
final t = ref.read(translationsProvider);
final notification = ref.read(inAppNotificationControllerProvider);
if (!consent) {
final agreed = await showDialog<bool>(
context: context,
builder: (context) => const WarpLicenseAgreementModal(),
);
if (agreed != true) return;
}
await _prefs.setBool(warpConsentGiven, true);
var toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100));
toast?.pause();
await _warp.generateWarpConfig();
toast?.start();
// final accountId = _prefs.getString("warp2-account-id");
// final accessToken = _prefs.getString("warp2-access-token");
// final hasWarp2Config = accountId != null && accessToken != null;
// if (!hasWarp2Config || true) {
toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100));
toast?.pause();
await _warp.generateWarp2Config();
toast?.start();
// }
if (region == "cn") {
await _profile.add("#profile-title: Hiddify WARP\nwarp://p1@auto#National&&detour=warp://p2@auto#WoW"); //
} else {
await _profile.add("https://raw.githubusercontent.com/hiddify/hiddify-next/main/test.configs/warp"); //
}
}
}
// Современная большая кнопка с градиентом
class _ModernButton extends StatelessWidget {
const _ModernButton({
super.key,
required this.label,
required this.subtitle,
required this.icon,
required this.gradient,
required this.onTap,
});
final String label;
final String subtitle;
final IconData icon;
final Gradient gradient;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Semantics(
button: true,
child: Material(
elevation: 4,
shadowColor: theme.colorScheme.primary.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Container(
height: 80,
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: theme.colorScheme.surface.withOpacity(0.9),
borderRadius: BorderRadius.circular(16),
),
child: Icon(
icon,
size: 28,
color: theme.colorScheme.primary,
),
),
const Gap(16),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onPrimaryContainer,
),
),
const Gap(2),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onPrimaryContainer.withOpacity(0.7),
),
),
],
),
),
Icon(
FluentIcons.chevron_right_24_regular,
color: theme.colorScheme.onPrimaryContainer.withOpacity(0.5),
),
],
),
),
),
),
),
);
}
}
// Компактная кнопка для дополнительных опций
class _CompactButton extends StatelessWidget {
const _CompactButton({
super.key,
required this.label,
required this.icon,
required this.color,
required this.onTap,
});
final String label;
final IconData icon;
final Color color;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Semantics(
button: true,
child: Material(
elevation: 2,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 20,
color: color,
),
),
const Gap(12),
Expanded(
child: Text(
label,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: color,
),
),
),
Icon(
FluentIcons.add_24_regular,
size: 20,
color: color.withOpacity(0.5),
),
],
),
),
),
),
);
}
}
class _Button extends StatelessWidget {
const _Button({
super.key,
required this.label,
required this.icon,
required this.size,
required this.onTap,
});
final String label;
final IconData icon;
final double size;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final color = theme.colorScheme.primary;
return Semantics(
button: true,
child: SizedBox(
width: size,
height: size,
child: Material(
elevation: 8,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: size / 3,
color: color,
),
const Gap(16),
Flexible(
child: Text(
label,
style: theme.textTheme.labelLarge?.copyWith(color: color),
textAlign: TextAlign.center,
),
),
],
),
),
),
),
);
}
}