- 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)
346 lines
14 KiB
Dart
346 lines
14 KiB
Dart
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,
|
||
),
|
||
);
|
||
}
|
||
}
|