From 279bb87e6a1613559b253d7ae47b0e216737585f Mon Sep 17 00:00:00 2001 From: klockky Date: Sun, 5 Apr 2026 21:14:05 +0300 Subject: [PATCH] feat(settings): implement debug menu toggle in settings tab with version label tap detection --- .../screens/profile/debug_menu_screen.dart | 56 +++++++++ .../screens/profile/settings_tab.dart | 113 ++++++++++++++++-- 2 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 lib/frontend/screens/profile/debug_menu_screen.dart diff --git a/lib/frontend/screens/profile/debug_menu_screen.dart b/lib/frontend/screens/profile/debug_menu_screen.dart new file mode 100644 index 0000000..dc83d3f --- /dev/null +++ b/lib/frontend/screens/profile/debug_menu_screen.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class DebugMenuScreen extends StatelessWidget { + const DebugMenuScreen({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: cs.surface, + body: SafeArea( + bottom: false, + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + child: Row( + children: [ + IconButton( + icon: Icon( + Symbols.arrow_back, + color: cs.onSurface, + size: 24, + weight: 400, + ), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 4), + Expanded( + child: Text( + 'Для разработчиков', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Outfit', + ), + ), + ), + ], + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 120)), + ], + ), + ), + ); + } +} diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 4892345..4a37280 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../../../core/storage/app_database.dart'; +import 'debug_menu_screen.dart'; import 'devices_screen.dart'; import 'security_screen.dart'; import 'spoof_screen.dart'; @@ -17,6 +20,9 @@ class _SettingsTabState extends State { ProfileData? _profile; bool _isPhoneVisible = false; String? _appVersionLabel; + bool _debugMenuVisible = false; + int _versionSecretTapCount = 0; + Timer? _versionSecretTapResetTimer; @override void initState() { @@ -25,6 +31,31 @@ class _SettingsTabState extends State { _loadAppVersion(); } + @override + void dispose() { + _versionSecretTapResetTimer?.cancel(); + super.dispose(); + } + + void _scheduleVersionSecretTapReset() { + _versionSecretTapResetTimer?.cancel(); + _versionSecretTapResetTimer = Timer(const Duration(seconds: 2), () { + if (mounted) setState(() => _versionSecretTapCount = 0); + }); + } + + void _onVersionLabelTap() { + _scheduleVersionSecretTapReset(); + setState(() { + _versionSecretTapCount++; + if (_versionSecretTapCount >= 7) { + _versionSecretTapCount = 0; + _versionSecretTapResetTimer?.cancel(); + _debugMenuVisible = !_debugMenuVisible; + } + }); + } + Future _loadProfile() async { final p = await AppDatabase.loadActiveProfile(); if (mounted) setState(() => _profile = p); @@ -127,18 +158,86 @@ class _SettingsTabState extends State { ), ), ), + SliverToBoxAdapter( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 340), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + transitionBuilder: (child, animation) { + return ClipRect( + child: Align( + alignment: Alignment.topCenter, + heightFactor: animation.value.clamp(0.0, 1.0), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + ); + }, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.topCenter, + clipBehavior: Clip.none, + children: [ + ...previousChildren, + ?currentChild, + ], + ); + }, + child: _debugMenuVisible + ? KeyedSubtree( + key: const ValueKey('developers_settings_row'), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: _buildSection( + context, + cs, + items: [ + _SettingsItem( + icon: Symbols.construction, + label: 'Для разработчиков', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const DebugMenuScreen(), + ), + ); + }, + ), + ], + ), + ), + ) + : const SizedBox.shrink( + key: ValueKey('developers_settings_hidden'), + ), + ), + ), if (_appVersionLabel != null) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 28, 16, 12), child: Center( - child: Text( - _appVersionLabel!, - textAlign: TextAlign.center, - style: TextStyle( - color: cs.onSurfaceVariant.withValues(alpha: 0.75), - fontSize: 13, - fontWeight: FontWeight.w400, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _onVersionLabelTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + child: Text( + _appVersionLabel!, + textAlign: TextAlign.center, + style: TextStyle( + color: cs.onSurfaceVariant.withValues(alpha: 0.75), + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), ), ), ),