завтра доделаю

This commit is contained in:
Jganenok
2026-05-25 00:23:45 +07:00
parent ee9eda568d
commit 11fafc8fff
8 changed files with 454 additions and 4 deletions
@@ -9,6 +9,7 @@ import '../../../core/storage/token_storage.dart';
import '../../../core/utils/haptics.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/info_action_sheet.dart';
import '../auth/login_screen.dart';
import '../auth/proxy_settings_sheet.dart';
import 'customization_screen.dart';
@@ -98,6 +99,38 @@ class _SettingsTabState extends State<SettingsTab> {
if (mounted) setState(() => _hapticsEnabled = value);
}
Future<void> _openCloudStorage(BuildContext context) async {
final cs = Theme.of(context).colorScheme;
await showInfoActionSheet(
context,
headerIcon: Symbols.cloud,
title: 'Облачное хранилище',
subtitle: 'Через МАХ',
items: [
const InfoActionSheetItem(
icon: Symbols.cloud_done,
title: 'Работает при белых списках',
body: 'Вы сможете передать файл даже при ограниченном интернете.',
),
const InfoActionSheetItem(
icon: Symbols.inventory_2,
title: 'Файлы до 4ГБ, безлимитное количество.',
body: 'Можете хранить массивный обьем информации.',
),
InfoActionSheetItem(
icon: Symbols.gpp_maybe,
title: 'Не обеспечивается конфединциальность файлов',
body:
'Облачное хранилище работает через ваш аккаунт на сервере МАХ, '
'нужные люди всё равно могут его посмотреть.',
titleColor: cs.error,
),
],
confirmLabel: 'ОК',
confirmDelay: const Duration(seconds: 3),
);
}
Future<void> _confirmLogout() async {
final cs = Theme.of(context).colorScheme;
final confirmed = await showModalBottomSheet<bool>(
@@ -277,6 +310,11 @@ child: _buildSection(
toggleValue: _hapticsEnabled,
onToggle: _setHaptics,
),
_SettingsItem(
icon: Symbols.cloud,
label: 'Облачное хранилище [BETA]',
onTap: () => _openCloudStorage(context),
),
_SettingsItem(
icon: Symbols.vpn_lock,
label: 'Прокси',
+85 -4
View File
@@ -8,10 +8,12 @@ import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/config/device_presets.dart';
import '../../../core/storage/device_identity.dart';
import '../../../core/storage/spoofing_service.dart';
import '../../../core/storage/token_storage.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart';
import '../../widgets/info_action_sheet.dart';
import '../auth/login_screen.dart';
enum SpoofingMethod { partial, full }
@@ -36,6 +38,10 @@ class _SpoofScreenState extends State<SpoofScreen> {
final _deviceIdController = TextEditingController();
final _appVersionController = TextEditingController();
final _buildNumberController = TextEditingController();
final _instanceIdController = TextEditingController();
final _clientSessionIdController = TextEditingController();
final _deviceLocaleController = TextEditingController();
final _pushDeviceTypeController = TextEditingController(text: 'GCM');
String _selectedDeviceType = 'ANDROID';
String _selectedArch = 'arm64-v8a';
@@ -45,9 +51,34 @@ class _SpoofScreenState extends State<SpoofScreen> {
@override
void initState() {
super.initState();
_localeController.addListener(_syncDeviceLocale);
_loadInitialData();
}
void _syncDeviceLocale() {
if (_selectedMethod == SpoofingMethod.full) return;
final derived = _localeController.text.split(RegExp(r'[-_]')).first;
if (_deviceLocaleController.text != derived) {
_deviceLocaleController.text = derived;
}
}
Future<bool> _confirmFullSpoofing() {
return showInfoActionSheet(
context,
headerIcon: Icons.warning_amber_rounded,
title: 'Могут быть последствия.',
subtitle: 'Меняй, только если знаешь что делаешь.',
confirmLabel: 'ОК',
confirmDelay: const Duration(seconds: 3),
);
}
Future<void> _loadSessionIdentifiers() async {
_instanceIdController.text = await DeviceIdentity.instanceId();
_clientSessionIdController.text = '${DeviceIdentity.clientSessionId}';
}
String _generateDeviceId() {
final bytes = List<int>.generate(8, (_) => _random.nextInt(256));
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
@@ -55,6 +86,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
Future<void> _loadInitialData() async {
setState(() => _isLoading = true);
await _loadSessionIdentifiers();
final prefs = await SharedPreferences.getInstance();
final isSpoofingEnabled = prefs.getBool('spoofing_enabled') ?? false;
@@ -336,6 +368,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
@override
void dispose() {
_localeController.removeListener(_syncDeviceLocale);
_deviceNameController.dispose();
_osVersionController.dispose();
_screenController.dispose();
@@ -344,6 +377,10 @@ class _SpoofScreenState extends State<SpoofScreen> {
_deviceIdController.dispose();
_appVersionController.dispose();
_buildNumberController.dispose();
_instanceIdController.dispose();
_clientSessionIdController.dispose();
_deviceLocaleController.dispose();
_pushDeviceTypeController.dispose();
super.dispose();
}
@@ -453,8 +490,16 @@ class _SpoofScreenState extends State<SpoofScreen> {
),
],
selected: {_selectedMethod},
onSelectionChanged: (s) =>
setState(() => _selectedMethod = s.first),
onSelectionChanged: (s) async {
final next = s.first;
if (next == _selectedMethod) return;
if (next == SpoofingMethod.full) {
final confirmed = await _confirmFullSpoofing();
if (!confirmed || !mounted) return;
}
setState(() => _selectedMethod = next);
_syncDeviceLocale();
},
),
const SizedBox(height: 12),
descriptionWidget,
@@ -604,6 +649,15 @@ class _SpoofScreenState extends State<SpoofScreen> {
Icons.language_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _deviceLocaleController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldDeviceLocale,
Icons.translate_outlined,
),
),
],
),
),
@@ -625,6 +679,24 @@ class _SpoofScreenState extends State<SpoofScreen> {
text: l10n.spoofIdentifiersDescription,
),
const SizedBox(height: 12),
TextField(
controller: _instanceIdController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldInstanceId,
Icons.fingerprint_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _clientSessionIdController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldClientSessionId,
Icons.vpn_key_outlined,
),
),
const SizedBox(height: 16),
TextField(
controller: _deviceIdController,
decoration:
@@ -642,7 +714,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
const SizedBox(height: 16),
TextField(
controller: _appVersionController,
enabled: false,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldAppVersion,
Icons.info_outline_rounded,
@@ -651,7 +723,7 @@ class _SpoofScreenState extends State<SpoofScreen> {
const SizedBox(height: 16),
TextField(
controller: _buildNumberController,
enabled: false,
enabled: _selectedMethod == SpoofingMethod.full,
keyboardType: TextInputType.number,
decoration: _inputDecoration(
l10n.spoofFieldBuildNumber,
@@ -659,6 +731,15 @@ class _SpoofScreenState extends State<SpoofScreen> {
),
),
const SizedBox(height: 16),
TextField(
controller: _pushDeviceTypeController,
enabled: _selectedMethod == SpoofingMethod.full,
decoration: _inputDecoration(
l10n.spoofFieldPushDeviceType,
Icons.notifications_outlined,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
+275
View File
@@ -0,0 +1,275 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
class InfoActionSheetItem {
final IconData icon;
final String title;
final String body;
final Color? titleColor;
final Color? iconColor;
const InfoActionSheetItem({
required this.icon,
required this.title,
required this.body,
this.titleColor,
this.iconColor,
});
}
Future<bool> showInfoActionSheet(
BuildContext context, {
String? headerEmoji,
IconData? headerIcon,
required String title,
String? subtitle,
List<InfoActionSheetItem> items = const [],
String confirmLabel = 'ОК',
Duration confirmDelay = Duration.zero,
String? seenKey,
}) async {
assert(
headerEmoji == null || headerIcon == null,
'Use either headerEmoji or headerIcon, not both.',
);
if (seenKey != null) {
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool(seenKey) ?? false) return true;
}
if (!context.mounted) return false;
final cs = Theme.of(context).colorScheme;
final confirmed = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: cs.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) => _InfoActionSheet(
headerEmoji: headerEmoji,
headerIcon: headerIcon,
title: title,
subtitle: subtitle,
items: items,
confirmLabel: confirmLabel,
confirmDelay: confirmDelay,
),
);
final ok = confirmed == true;
if (ok && seenKey != null) {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(seenKey, true);
}
return ok;
}
class _InfoActionSheet extends StatefulWidget {
final String? headerEmoji;
final IconData? headerIcon;
final String title;
final String? subtitle;
final List<InfoActionSheetItem> items;
final String confirmLabel;
final Duration confirmDelay;
const _InfoActionSheet({
this.headerEmoji,
this.headerIcon,
required this.title,
this.subtitle,
this.items = const [],
required this.confirmLabel,
required this.confirmDelay,
});
@override
State<_InfoActionSheet> createState() => _InfoActionSheetState();
}
class _InfoActionSheetState extends State<_InfoActionSheet> {
Timer? _ticker;
int _remaining = 0;
@override
void initState() {
super.initState();
_remaining = widget.confirmDelay.inSeconds;
if (_remaining > 0) {
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) return;
setState(() {
_remaining--;
if (_remaining == 0) {
_ticker?.cancel();
_ticker = null;
}
});
});
}
}
@override
void dispose() {
_ticker?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final enabled = _remaining == 0;
final buttonText = enabled
? widget.confirmLabel
: '${widget.confirmLabel}($_remaining)';
final hasHeader = widget.headerEmoji != null || widget.headerIcon != null;
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (hasHeader) ...[
_buildHeader(cs),
const SizedBox(height: 18),
],
Text(
widget.title,
textAlign: TextAlign.center,
style: GoogleFonts.inter(
color: cs.onSurface,
fontSize: 22,
fontWeight: FontWeight.w700,
),
),
if (widget.subtitle != null) ...[
const SizedBox(height: 6),
Text(
widget.subtitle!,
textAlign: TextAlign.center,
style: GoogleFonts.inter(
color: cs.onSurfaceVariant,
fontSize: 13,
fontWeight: FontWeight.w400,
),
),
],
if (widget.items.isNotEmpty) ...[
const SizedBox(height: 22),
for (int i = 0; i < widget.items.length; i++) ...[
_buildItem(cs, widget.items[i]),
if (i < widget.items.length - 1)
const SizedBox(height: 18),
],
],
],
),
),
),
const SizedBox(height: 28),
FilledButton(
onPressed: enabled ? () => Navigator.pop(context, true) : null,
style: FilledButton.styleFrom(
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
disabledBackgroundColor: cs.primary.withValues(alpha: 0.45),
disabledForegroundColor: cs.onPrimary.withValues(alpha: 0.85),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
),
child: Text(
buttonText,
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
);
}
Widget _buildHeader(ColorScheme cs) {
if (widget.headerEmoji != null) {
return Center(
child: Text(
widget.headerEmoji!,
style: const TextStyle(fontSize: 72, height: 1.0),
),
);
}
return Center(
child: Icon(
widget.headerIcon,
size: 72,
color: cs.primary,
weight: 400,
),
);
}
Widget _buildItem(ColorScheme cs, InfoActionSheetItem item) {
final titleColor = item.titleColor ?? cs.onSurface;
final iconColor = item.iconColor ?? cs.onSurfaceVariant;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 2),
child: Icon(
item.icon,
size: 26,
color: iconColor,
weight: 400,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: GoogleFonts.inter(
color: titleColor,
fontSize: 16,
fontWeight: FontWeight.w700,
height: 1.25,
),
),
const SizedBox(height: 4),
Text(
item.body,
style: GoogleFonts.inter(
color: cs.onSurfaceVariant,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.35,
),
),
],
),
),
],
);
}
}
+4
View File
@@ -73,8 +73,12 @@
"spoofFieldScreen": "Screen resolution",
"spoofFieldTimezone": "Timezone",
"spoofFieldLocale": "Locale",
"spoofFieldDeviceLocale": "Device locale (derived)",
"spoofIdentifiersSectionTitle": "Identifiers",
"spoofIdentifiersDescription": "mt_instanceid and clientSessionId are generated automatically on every app launch. Only the Device ID can be changed.",
"spoofFieldInstanceId": "mt_instanceid",
"spoofFieldClientSessionId": "clientSessionId",
"spoofFieldPushDeviceType": "Push device type",
"spoofFieldDeviceId": "Device ID",
"spoofRegenerateIdTooltip": "Generate a new ID",
"spoofFieldAppVersion": "App version",
+24
View File
@@ -482,6 +482,12 @@ abstract class AppLocalizations {
/// **'Locale'**
String get spoofFieldLocale;
/// No description provided for @spoofFieldDeviceLocale.
///
/// In en, this message translates to:
/// **'Device locale (derived)'**
String get spoofFieldDeviceLocale;
/// No description provided for @spoofIdentifiersSectionTitle.
///
/// In en, this message translates to:
@@ -494,6 +500,24 @@ abstract class AppLocalizations {
/// **'mt_instanceid and clientSessionId are generated automatically on every app launch. Only the Device ID can be changed.'**
String get spoofIdentifiersDescription;
/// No description provided for @spoofFieldInstanceId.
///
/// In en, this message translates to:
/// **'mt_instanceid'**
String get spoofFieldInstanceId;
/// No description provided for @spoofFieldClientSessionId.
///
/// In en, this message translates to:
/// **'clientSessionId'**
String get spoofFieldClientSessionId;
/// No description provided for @spoofFieldPushDeviceType.
///
/// In en, this message translates to:
/// **'Push device type'**
String get spoofFieldPushDeviceType;
/// No description provided for @spoofFieldDeviceId.
///
/// In en, this message translates to:
+12
View File
@@ -209,6 +209,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get spoofFieldLocale => 'Locale';
@override
String get spoofFieldDeviceLocale => 'Device locale (derived)';
@override
String get spoofIdentifiersSectionTitle => 'Identifiers';
@@ -216,6 +219,15 @@ class AppLocalizationsEn extends AppLocalizations {
String get spoofIdentifiersDescription =>
'mt_instanceid and clientSessionId are generated automatically on every app launch. Only the Device ID can be changed.';
@override
String get spoofFieldInstanceId => 'mt_instanceid';
@override
String get spoofFieldClientSessionId => 'clientSessionId';
@override
String get spoofFieldPushDeviceType => 'Push device type';
@override
String get spoofFieldDeviceId => 'Device ID';
+12
View File
@@ -211,6 +211,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get spoofFieldLocale => 'Локаль';
@override
String get spoofFieldDeviceLocale => 'Системная локаль (производная)';
@override
String get spoofIdentifiersSectionTitle => 'Идентификаторы';
@@ -218,6 +221,15 @@ class AppLocalizationsRu extends AppLocalizations {
String get spoofIdentifiersDescription =>
'mt_instanceid и clientSessionId генерируются автоматически при каждом запуске приложения. Изменить можно только Device ID.';
@override
String get spoofFieldInstanceId => 'mt_instanceid';
@override
String get spoofFieldClientSessionId => 'clientSessionId';
@override
String get spoofFieldPushDeviceType => 'Тип push-уведомлений';
@override
String get spoofFieldDeviceId => 'ID Устройства';
+4
View File
@@ -73,8 +73,12 @@
"spoofFieldScreen": "Разрешение экрана",
"spoofFieldTimezone": "Часовой пояс",
"spoofFieldLocale": "Локаль",
"spoofFieldDeviceLocale": "Системная локаль (производная)",
"spoofIdentifiersSectionTitle": "Идентификаторы",
"spoofIdentifiersDescription": "mt_instanceid и clientSessionId генерируются автоматически при каждом запуске приложения. Изменить можно только Device ID.",
"spoofFieldInstanceId": "mt_instanceid",
"spoofFieldClientSessionId": "clientSessionId",
"spoofFieldPushDeviceType": "Тип push-уведомлений",
"spoofFieldDeviceId": "ID Устройства",
"spoofRegenerateIdTooltip": "Сгенерировать новый ID",
"spoofFieldAppVersion": "Версия приложения",