Files
umbrix/lib/features/intro/widget/intro_page.dart
Umbrix Developer 76a374950f feat: mobile-like window size and always-visible stats
- Changed window size to mobile phone format (400x800)
- Removed width condition for ActiveProxyFooter - now always visible
- Added run-umbrix.sh launch script with icon copying
- Stats cards now display on all screen sizes
2026-01-17 13:09:20 +03:00

354 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 'dart:ui' show PlatformDispatcher;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:umbrix/core/analytics/analytics_controller.dart';
import 'package:umbrix/core/localization/locale_preferences.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/model/region.dart';
import 'package:umbrix/core/preferences/general_preferences.dart';
import 'package:umbrix/features/common/general_pref_tiles.dart';
import 'package:umbrix/features/config_option/data/config_option_repository.dart';
import 'package:umbrix/features/settings/about/terms_and_conditions_screen.dart';
import 'package:umbrix/gen/assets.gen.dart';
import 'package:umbrix/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 {
const IntroPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final theme = Theme.of(context);
final isStarting = useState(false);
// Автовыбор региона теперь через useEffect (хуки)
useEffect(() {
autoSelectRegion(ref).then((value) => loggy.debug("Auto Region selection finished!"));
return null;
}, const [],);
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(
t.intro.welcomeTitle,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
t.intro.subtitle,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
),
// Настройки в виде карточек
SliverCrossAxisConstrained(
maxCrossAxisExtent: 400,
child: MultiSliver(
children: [
// Язык
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: _SettingCard(
child: LocalePrefTile(),
),
),
),
// Регион
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: _SettingCard(
child: RegionPrefTile(),
),
),
),
// Аналитика
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: _SettingCard(
child: EnableAnalyticsPrefTile(),
),
),
),
const SliverGap(16),
// Условия использования
SliverToBoxAdapter(
child: 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 = () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const TermsAndConditionsScreen(),
),
);
},
),
),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
),
// Кнопка начать
SliverToBoxAdapter(
child: 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,
);
}
// UMBRIX: Используем timezone вместо IP для приватности
try {
final timezone = DateTime.now().timeZoneName;
final systemLocale = PlatformDispatcher.instance.locale;
loggy.debug('Using timezone: $timezone, system locale: ${systemLocale.languageCode}');
// Определяем регион по системной локали (без интернет-запросов!)
final countryCode = systemLocale.countryCode ?? '';
if (countryCode.isNotEmpty) {
final regionLocale = _getRegionLocale(countryCode);
await ref.read(ConfigOptions.region.notifier).update(regionLocale.region);
await ref.read(localePreferencesProvider.notifier).changeLocale(regionLocale.locale);
loggy.debug('Region set from system locale: ${regionLocale.region}');
} else {
loggy.debug('Could not determine region from system locale, using default');
}
} catch (e) {
loggy.warning('Could not auto-select region: $e');
}
}
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,
),
);
}
}