Files
umbrix/lib/features/intro/widget/intro_page.dart
Hiddify User 5411a30519 feat: Umbrix branding - logo and name changes
- Changed app title from Hiddify to Umbrix in home page
- Added custom Umbrix logo (192x192 PNG)
- Updated intro page with Umbrix branding and gradient design
- Updated drawer menu with Umbrix logo
- Modified add profile modal with modern gradient cards
- Updated Russian translations (appTitle: Umbrix)
2025-12-26 18:41:25 +03:00

346 lines
14 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:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/analytics/analytics_controller.dart';
import 'package:hiddify/core/http_client/dio_http_client.dart';
import 'package:hiddify/core/localization/locale_preferences.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/constants.dart';
import 'package:hiddify/core/model/region.dart';
import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/features/common/general_pref_tiles.dart';
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:timezone_to_country/timezone_to_country.dart';
class IntroPage extends HookConsumerWidget with PresLogger {
IntroPage({super.key});
bool locationInfoLoaded = false;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final theme = Theme.of(context);
final isStarting = useState(false);
if (!locationInfoLoaded) {
autoSelectRegion(ref).then((value) => loggy.debug("Auto Region selection finished!"));
locationInfoLoaded = true;
}
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
theme.colorScheme.surface,
theme.colorScheme.surfaceContainerHighest,
],
),
),
child: SafeArea(
child: CustomScrollView(
shrinkWrap: true,
slivers: [
// Логотип и заголовок
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
child: Column(
children: [
// Логотип с анимацией
Hero(
tag: 'app_logo',
child: Container(
width: 120,
height: 120,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Assets.images.umbrixLogo.image(
fit: BoxFit.contain,
),
),
),
const Gap(24),
// Заголовок
Text(
'Welcome to Umbrix',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
'Fast and Secure',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
),
// Настройки в виде карточек
SliverCrossAxisConstrained(
maxCrossAxisExtent: 400,
child: MultiSliver(
children: [
// Язык
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: _SettingCard(
child: const LocalePrefTile(),
),
),
// Регион
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: _SettingCard(
child: const RegionPrefTile(),
),
),
// Аналитика
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: _SettingCard(
child: const EnableAnalyticsPrefTile(),
),
),
const SliverGap(16),
// Условия использования
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text.rich(
t.intro.termsAndPolicyCaution(
tap: (text) => TextSpan(
text: text,
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
await UriUtils.tryLaunch(
Uri.parse(Constants.termsAndConditionsUrl),
);
},
),
),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
// Кнопка начать
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.primary.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.4),
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isStarting.value
? null
: () async {
isStarting.value = true;
if (!ref.read(analyticsControllerProvider).requireValue) {
loggy.info("disabling analytics per user request");
try {
await ref.read(analyticsControllerProvider.notifier).disableAnalytics();
} catch (error, stackTrace) {
loggy.error(
"could not disable analytics",
error,
stackTrace,
);
}
}
await ref.read(Preferences.introCompleted.notifier).update(true);
},
borderRadius: BorderRadius.circular(16),
child: Container(
height: 56,
alignment: Alignment.center,
child: isStarting.value
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.onPrimary,
),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
t.intro.start,
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
const Gap(8),
Icon(
Icons.arrow_forward_rounded,
color: theme.colorScheme.onPrimary,
),
],
),
),
),
),
),
),
],
),
),
],
),
),
),
);
}
Future<void> autoSelectRegion(WidgetRef ref) async {
try {
final countryCode = await TimeZoneToCountry.getLocalCountryCode();
final regionLocale = _getRegionLocale(countryCode);
loggy.debug(
'Timezone Region: ${regionLocale.region} Locale: ${regionLocale.locale}',
);
await ref.read(ConfigOptions.region.notifier).update(regionLocale.region);
await ref.watch(ConfigOptions.directDnsAddress.notifier).reset();
await ref.read(localePreferencesProvider.notifier).changeLocale(regionLocale.locale);
return;
} catch (e) {
loggy.warning(
'Could not get the local country code based on timezone',
e,
);
}
try {
final DioHttpClient client = DioHttpClient(
timeout: const Duration(seconds: 2),
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
debug: true,
);
final response = await client.get<Map<String, dynamic>>('https://api.ip.sb/geoip/');
if (response.statusCode == 200) {
final jsonData = response.data!;
final regionLocale = _getRegionLocale(jsonData['country_code']?.toString() ?? "");
loggy.debug(
'Region: ${regionLocale.region} Locale: ${regionLocale.locale}',
);
await ref.read(ConfigOptions.region.notifier).update(regionLocale.region);
await ref.read(localePreferencesProvider.notifier).changeLocale(regionLocale.locale);
} else {
loggy.warning('Request failed with status: ${response.statusCode}');
}
} catch (e) {
loggy.warning('Could not get the local country code from ip');
}
}
RegionLocale _getRegionLocale(String country) {
switch (country.toUpperCase()) {
case "IR":
return RegionLocale(Region.ir, AppLocale.fa);
case "CN":
return RegionLocale(Region.cn, AppLocale.zhCn);
case "RU":
return RegionLocale(Region.ru, AppLocale.ru);
case "AF":
return RegionLocale(Region.af, AppLocale.fa);
case "BR":
return RegionLocale(Region.other, AppLocale.ptBr);
case "TR":
return RegionLocale(Region.other, AppLocale.tr);
default:
return RegionLocale(Region.other, AppLocale.en);
}
}
}
class RegionLocale {
final Region region;
final AppLocale locale;
RegionLocale(this.region, this.locale);
}
// Карточка для настроек
class _SettingCard extends StatelessWidget {
const _SettingCard({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.1),
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: child,
),
);
}
}