backup: before proxies page modernization
This commit is contained in:
@@ -113,3 +113,20 @@ class PerAppProxyList extends _$PerAppProxyList {
|
||||
return _exclude.write(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ExcludedDomainsList extends _$ExcludedDomainsList {
|
||||
late final _pref = PreferencesEntry(
|
||||
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||
key: "excluded_domains_list",
|
||||
defaultValue: <String>[],
|
||||
);
|
||||
|
||||
@override
|
||||
List<String> build() => _pref.read();
|
||||
|
||||
Future<void> update(List<String> value) {
|
||||
state = value;
|
||||
return _pref.write(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ part 'app_router.g.dart';
|
||||
|
||||
bool _debugMobileRouter = false;
|
||||
|
||||
final useMobileRouter =
|
||||
!PlatformUtils.isDesktop || (kDebugMode && _debugMobileRouter);
|
||||
final useMobileRouter = !PlatformUtils.isDesktop || (kDebugMode && _debugMobileRouter);
|
||||
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// TODO: test and improve handling of deep link
|
||||
@@ -53,6 +52,7 @@ GoRouter router(RouterRef ref) {
|
||||
final tabLocations = [
|
||||
const HomeRoute().location,
|
||||
const ProxiesRoute().location,
|
||||
const PerAppProxyRoute().location,
|
||||
const ConfigOptionsRoute().location,
|
||||
const SettingsRoute().location,
|
||||
const LogsOverviewRoute().location,
|
||||
@@ -77,9 +77,7 @@ void switchTab(int index, BuildContext context) {
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class RouterListenable extends _$RouterListenable
|
||||
with AppLogger
|
||||
implements Listenable {
|
||||
class RouterListenable extends _$RouterListenable with AppLogger implements Listenable {
|
||||
VoidCallback? _routerListener;
|
||||
bool _introCompleted = false;
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ class AppTheme {
|
||||
final String fontFamily;
|
||||
|
||||
ThemeData lightTheme(ColorScheme? lightColorScheme) {
|
||||
final ColorScheme scheme = lightColorScheme ??
|
||||
ColorScheme.fromSeed(seedColor: const Color(0xFF293CA0));
|
||||
final ColorScheme scheme = lightColorScheme ?? ColorScheme.fromSeed(seedColor: const Color(0xFF293CA0));
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
@@ -29,8 +28,7 @@ class AppTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor:
|
||||
mode.trueBlack ? Colors.black : scheme.background,
|
||||
scaffoldBackgroundColor: mode.trueBlack ? Colors.black : scheme.background,
|
||||
fontFamily: fontFamily,
|
||||
extensions: const <ThemeExtension<dynamic>>{
|
||||
ConnectionButtonTheme.light,
|
||||
|
||||
@@ -8,19 +8,13 @@ part 'theme_preferences.g.dart';
|
||||
class ThemePreferences extends _$ThemePreferences {
|
||||
@override
|
||||
AppThemeMode build() {
|
||||
final persisted = ref
|
||||
.watch(sharedPreferencesProvider)
|
||||
.requireValue
|
||||
.getString("theme_mode");
|
||||
final persisted = ref.watch(sharedPreferencesProvider).requireValue.getString("theme_mode");
|
||||
if (persisted == null) return AppThemeMode.system;
|
||||
return AppThemeMode.values.byName(persisted);
|
||||
}
|
||||
|
||||
Future<void> changeThemeMode(AppThemeMode value) async {
|
||||
state = value;
|
||||
await ref
|
||||
.read(sharedPreferencesProvider)
|
||||
.requireValue
|
||||
.setString("theme_mode", value.name);
|
||||
await ref.read(sharedPreferencesProvider).requireValue.setString("theme_mode", value.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
abstract interface class RootScaffold {
|
||||
static final stateKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
static bool canShowDrawer(BuildContext context) =>
|
||||
Breakpoints.small.isActive(context);
|
||||
static bool canShowDrawer(BuildContext context) => Breakpoints.small.isActive(context);
|
||||
}
|
||||
|
||||
class AdaptiveRootScaffold extends HookConsumerWidget {
|
||||
@@ -26,13 +25,20 @@ class AdaptiveRootScaffold extends HookConsumerWidget {
|
||||
|
||||
final destinations = [
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.power_20_filled),
|
||||
icon: const Icon(FluentIcons.home_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.home_20_filled),
|
||||
label: t.home.pageTitle,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.filter_20_filled),
|
||||
icon: const Icon(FluentIcons.list_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.list_20_filled),
|
||||
label: t.proxies.pageTitle,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.more_vertical_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.more_vertical_20_filled),
|
||||
label: t.settings.network.excludedDomains.pageTitle,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.box_edit_20_filled),
|
||||
label: t.config.pageTitle,
|
||||
@@ -58,8 +64,8 @@ class AdaptiveRootScaffold extends HookConsumerWidget {
|
||||
switchTab(index, context);
|
||||
},
|
||||
destinations: destinations,
|
||||
drawerDestinationRange: useMobileRouter ? (2, null) : (0, null),
|
||||
bottomDestinationRange: (0, 2),
|
||||
drawerDestinationRange: useMobileRouter ? (3, null) : (0, null),
|
||||
bottomDestinationRange: (0, 3),
|
||||
useBottomSheet: useMobileRouter,
|
||||
sidebarTrailing: const Expanded(
|
||||
child: Align(
|
||||
@@ -93,18 +99,14 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
final Widget? sidebarTrailing;
|
||||
final Widget body;
|
||||
|
||||
List<NavigationDestination> destinationsSlice((int, int?) range) =>
|
||||
destinations.sublist(range.$1, range.$2);
|
||||
List<NavigationDestination> destinationsSlice((int, int?) range) => destinations.sublist(range.$1, range.$2);
|
||||
|
||||
int? selectedWithOffset((int, int?) range) {
|
||||
final index = selectedIndex - range.$1;
|
||||
return index < 0 || (range.$2 != null && index > (range.$2! - 1))
|
||||
? null
|
||||
: index;
|
||||
return index < 0 || (range.$2 != null && index > (range.$2! - 1)) ? null : index;
|
||||
}
|
||||
|
||||
void selectWithOffset(int index, (int, int?) range) =>
|
||||
onSelectedIndexChange(index + range.$1);
|
||||
void selectWithOffset(int index, (int, int?) range) => onSelectedIndexChange(index + range.$1);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -113,14 +115,67 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
drawer: Breakpoints.small.isActive(context)
|
||||
? Drawer(
|
||||
width: (MediaQuery.sizeOf(context).width * 0.88).clamp(1, 304),
|
||||
child: NavigationRail(
|
||||
extended: true,
|
||||
selectedIndex: selectedWithOffset(drawerDestinationRange),
|
||||
destinations: destinationsSlice(drawerDestinationRange)
|
||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
||||
.toList(),
|
||||
onDestinationSelected: (index) =>
|
||||
selectWithOffset(index, drawerDestinationRange),
|
||||
child: Column(
|
||||
children: [
|
||||
// Логотип и название приложения
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.shield_outlined,
|
||||
size: 56,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Umbrix',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Список пунктов меню
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: [
|
||||
// Главная
|
||||
_DrawerMenuItem(
|
||||
icon: FluentIcons.home_20_regular,
|
||||
selectedIcon: FluentIcons.home_20_filled,
|
||||
label: destinationsSlice(drawerDestinationRange)[0].label,
|
||||
isSelected: selectedWithOffset(drawerDestinationRange) == 0,
|
||||
onTap: () => selectWithOffset(0, drawerDestinationRange),
|
||||
),
|
||||
// Остальные пункты
|
||||
...List.generate(
|
||||
destinationsSlice(drawerDestinationRange).length - 1,
|
||||
(index) {
|
||||
final dest = destinationsSlice(drawerDestinationRange)[index + 1];
|
||||
return _DrawerMenuItem(
|
||||
icon: (dest.icon as Icon).icon!,
|
||||
selectedIcon: dest.selectedIcon != null ? (dest.selectedIcon as Icon).icon! : (dest.icon as Icon).icon!,
|
||||
label: dest.label,
|
||||
isSelected: selectedWithOffset(drawerDestinationRange) == index + 1,
|
||||
onTap: () => selectWithOffset(index + 1, drawerDestinationRange),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -131,9 +186,7 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
key: const Key('primaryNavigation'),
|
||||
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: destinations
|
||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
||||
.toList(),
|
||||
destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(),
|
||||
onDestinationSelected: onSelectedIndexChange,
|
||||
),
|
||||
),
|
||||
@@ -142,9 +195,7 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
||||
extended: true,
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: destinations
|
||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
||||
.toList(),
|
||||
destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(),
|
||||
onDestinationSelected: onSelectedIndexChange,
|
||||
trailing: sidebarTrailing,
|
||||
),
|
||||
@@ -167,10 +218,52 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
? NavigationBar(
|
||||
selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0,
|
||||
destinations: destinationsSlice(bottomDestinationRange),
|
||||
onDestinationSelected: (index) =>
|
||||
selectWithOffset(index, bottomDestinationRange),
|
||||
onDestinationSelected: (index) => selectWithOffset(index, bottomDestinationRange),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DrawerMenuItem extends StatelessWidget {
|
||||
const _DrawerMenuItem({
|
||||
required this.icon,
|
||||
required this.selectedIcon,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
isSelected ? selectedIcon : icon,
|
||||
size: 24,
|
||||
),
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
selectedTileColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onTap: onTap,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ bool showDrawerButton(BuildContext context) {
|
||||
final String location = GoRouterState.of(context).uri.path;
|
||||
if (location == const HomeRoute().location || location == const ProfilesOverviewRoute().location) return true;
|
||||
if (location.startsWith(const ProxiesRoute().location)) return true;
|
||||
if (location.startsWith(const PerAppProxyRoute().location)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,11 +32,13 @@ class NestedAppBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
RootScaffold.canShowDrawer(context);
|
||||
final hasDrawer = RootScaffold.stateKey.currentState?.hasDrawer ?? false;
|
||||
final shouldShowDrawer = showDrawerButton(context);
|
||||
|
||||
return SliverAppBar(
|
||||
leading: (RootScaffold.stateKey.currentState?.hasDrawer ?? false) && showDrawerButton(context)
|
||||
? DrawerButton(
|
||||
leading: hasDrawer && shouldShowDrawer
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
RootScaffold.stateKey.currentState?.openDrawer();
|
||||
},
|
||||
|
||||
@@ -4,15 +4,15 @@ import 'dart:developer';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easy_permission/easy_permissions.dart';
|
||||
// import 'package:flutter_easy_permission/easy_permissions.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
// import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
const permissions = [Permissions.CAMERA];
|
||||
const permissionGroup = [PermissionGroup.Camera];
|
||||
// const permissions = [Permissions.CAMERA];
|
||||
// const permissionGroup = [PermissionGroup.Camera];
|
||||
|
||||
class QRCodeScannerScreen extends StatefulHookConsumerWidget {
|
||||
const QRCodeScannerScreen({super.key});
|
||||
@@ -62,6 +62,11 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
}
|
||||
|
||||
Future<bool> _requestCameraPermission() async {
|
||||
// Simplified: assuming permission is granted
|
||||
// Original code used flutter_easy_permission which is obsolete
|
||||
return true;
|
||||
|
||||
/* Original code:
|
||||
final hasPermission = await FlutterEasyPermission.has(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
@@ -95,6 +100,7 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
*/
|
||||
}
|
||||
|
||||
Future<void> _initializeScanner() async {
|
||||
@@ -110,7 +116,7 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
// _easyPermission.dispose();
|
||||
FlutterEasyPermission().dispose();
|
||||
// FlutterEasyPermission().dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
@@ -124,10 +130,14 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
}
|
||||
|
||||
Future<void> _checkPermissionAndStartScanner() async {
|
||||
// Simplified: assuming permission is granted
|
||||
final hasPermission = true;
|
||||
/* Original:
|
||||
final hasPermission = await FlutterEasyPermission.has(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
);
|
||||
*/
|
||||
if (hasPermission) {
|
||||
_startScanner();
|
||||
} else {
|
||||
@@ -148,10 +158,14 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
}
|
||||
|
||||
Future<void> startQrScannerIfPermissionIsGranted() async {
|
||||
// Simplified: assuming permission is granted
|
||||
final hasPermission = true;
|
||||
/* Original:
|
||||
final hasPermission = await FlutterEasyPermission.has(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
);
|
||||
*/
|
||||
if (hasPermission) {
|
||||
_startScanner();
|
||||
// } else {
|
||||
@@ -176,23 +190,31 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
// }
|
||||
|
||||
void _showPermissionDialog() {
|
||||
// Simplified: no dialog for now
|
||||
/* Original:
|
||||
FlutterEasyPermission.showAppSettingsDialog(
|
||||
title: "Camera Access Required",
|
||||
rationale: "Permission to camera to scan QR Code",
|
||||
positiveButtonText: "Settings",
|
||||
negativeButtonText: "Cancel",
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Translations t = ref.watch(translationsProvider);
|
||||
|
||||
// Simplified: assuming permission is granted
|
||||
final hasPermission = true;
|
||||
return FutureBuilder(
|
||||
future: Future.value(hasPermission),
|
||||
/* Original:
|
||||
future: FlutterEasyPermission.has(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
),
|
||||
*/
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
||||
@@ -95,6 +95,10 @@ class ConnectionButton extends HookConsumerWidget {
|
||||
_ => Assets.images.disconnectNorouz,
|
||||
},
|
||||
useImage: today.day >= 19 && today.day <= 23 && today.month == 3,
|
||||
isConnected: switch (connectionStatus) {
|
||||
AsyncData(value: Connected()) => true,
|
||||
_ => false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -107,6 +111,7 @@ class _ConnectionButton extends StatelessWidget {
|
||||
required this.buttonColor,
|
||||
required this.image,
|
||||
required this.useImage,
|
||||
required this.isConnected,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
@@ -115,6 +120,7 @@ class _ConnectionButton extends StatelessWidget {
|
||||
final Color buttonColor;
|
||||
final AssetGenImage image;
|
||||
final bool useImage;
|
||||
final bool isConnected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -136,8 +142,8 @@ class _ConnectionButton extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
width: 148,
|
||||
height: 148,
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Material(
|
||||
key: const ValueKey("home_connection_button"),
|
||||
shape: const CircleBorder(),
|
||||
@@ -145,7 +151,7 @@ class _ConnectionButton extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(36),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: TweenAnimationBuilder(
|
||||
tween: ColorTween(end: buttonColor),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
@@ -153,11 +159,11 @@ class _ConnectionButton extends StatelessWidget {
|
||||
if (useImage) {
|
||||
return image.image(filterQuality: FilterQuality.medium);
|
||||
} else {
|
||||
return Assets.images.logo.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
value!,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
// Определяем какую иконку показывать: play для отключенного, stop для подключенного
|
||||
return Icon(
|
||||
isConnected ? Icons.stop_rounded : Icons.play_arrow_rounded,
|
||||
color: value,
|
||||
size: 60,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -68,13 +68,16 @@ class HomePage extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConnectionButton(),
|
||||
ActiveProxyDelayIndicator(),
|
||||
],
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 160),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConnectionButton(),
|
||||
ActiveProxyDelayIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (MediaQuery.sizeOf(context).width < 840) const ActiveProxyFooter(),
|
||||
|
||||
@@ -5,13 +5,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hiddify/core/widget/adaptive_icon.dart';
|
||||
import 'package:hiddify/core/router/routes.dart';
|
||||
import 'package:hiddify/features/common/nested_app_bar.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
const PerAppProxyPage({super.key});
|
||||
@@ -28,6 +28,9 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
final showSystemApps = useState(true);
|
||||
final isSearching = useState(false);
|
||||
final searchQuery = useState("");
|
||||
final currentTab = useState(0);
|
||||
final domainInputController = useTextEditingController();
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
|
||||
final filteredPackages = useMemoized(
|
||||
() {
|
||||
@@ -42,9 +45,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
}
|
||||
if (!searchQuery.value.isBlank) {
|
||||
result = result.filter(
|
||||
(e) => e.name
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase()),
|
||||
(e) => e.name.toLowerCase().contains(searchQuery.value.toLowerCase()),
|
||||
);
|
||||
}
|
||||
return result.toList();
|
||||
@@ -54,152 +55,458 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
[asyncPackages, showSystemApps.value, searchQuery.value],
|
||||
);
|
||||
|
||||
final appBar = NestedAppBar(
|
||||
title: Text(t.settings.network.excludedDomains.pageTitle),
|
||||
actions: [
|
||||
if (currentTab.value == 1 && !isSearching.value)
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.search_24_regular),
|
||||
onPressed: () => isSearching.value = true,
|
||||
tooltip: localizations.searchFieldLabel,
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: tabController,
|
||||
onTap: (index) => currentTab.value = index,
|
||||
tabs: [
|
||||
Tab(text: t.settings.network.excludedDomains.domainsTab),
|
||||
Tab(text: t.settings.network.excludedDomains.appsTab),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final searchAppBar = SliverAppBar(
|
||||
title: TextFormField(
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "${localizations.searchFieldLabel}...",
|
||||
isDense: true,
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
searchQuery.value = "";
|
||||
isSearching.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: localizations.cancelButtonLabel,
|
||||
),
|
||||
bottom: TabBar(
|
||||
controller: tabController,
|
||||
onTap: (index) => currentTab.value = index,
|
||||
tabs: [
|
||||
Tab(text: t.settings.network.excludedDomains.domainsTab),
|
||||
Tab(text: t.settings.network.excludedDomains.appsTab),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: isSearching.value
|
||||
? AppBar(
|
||||
title: TextFormField(
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "${localizations.searchFieldLabel}...",
|
||||
isDense: true,
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
searchQuery.value = "";
|
||||
isSearching.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: localizations.cancelButtonLabel,
|
||||
),
|
||||
)
|
||||
: AppBar(
|
||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.search_24_regular),
|
||||
onPressed: () => isSearching.value = true,
|
||||
tooltip: localizations.searchFieldLabel,
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: Icon(AdaptiveIcon(context).more),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(
|
||||
showSystemApps.value
|
||||
? t.settings.network.hideSystemApps
|
||||
: t.settings.network.showSystemApps,
|
||||
),
|
||||
onTap: () =>
|
||||
showSystemApps.value = !showSystemApps.value,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.settings.network.clearSelection),
|
||||
onTap: () => ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update([]),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
isSearching.value ? searchAppBar : appBar,
|
||||
SliverFillRemaining(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
_buildDomainsTab(context, t, ref, domainInputController),
|
||||
_buildAppsTab(
|
||||
context,
|
||||
ref,
|
||||
t,
|
||||
perAppProxyMode,
|
||||
filteredPackages,
|
||||
perAppProxyList,
|
||||
showSystemApps,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPinnedHeader(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Column(
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: currentTab.value == 0
|
||||
? FloatingActionButton.extended(
|
||||
onPressed: () => _showAddDomainModal(context, ref, domainInputController),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(t.settings.network.excludedDomains.fabButton),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (currentTab.value == 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
...PerAppProxyMode.values.map(
|
||||
(e) => RadioListTile<PerAppProxyMode>(
|
||||
title: Text(e.present(t).message),
|
||||
dense: true,
|
||||
value: e,
|
||||
groupValue: perAppProxyMode,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(Preferences.perAppProxyMode.notifier)
|
||||
.update(e);
|
||||
if (e == PerAppProxyMode.off && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: perAppProxyMode == PerAppProxyMode.include
|
||||
? null
|
||||
: () async {
|
||||
await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.include);
|
||||
},
|
||||
child: Text(t.settings.network.perAppProxyModes.include),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: perAppProxyMode == PerAppProxyMode.exclude
|
||||
? null
|
||||
: () async {
|
||||
await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.exclude);
|
||||
},
|
||||
child: Text(t.settings.network.perAppProxyModes.exclude),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${t.settings.network.perAppProxyModes.include}:",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(t.settings.network.perAppProxyModes.includeMsg),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"${t.settings.network.perAppProxyModes.exclude}:",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(t.settings.network.perAppProxyModes.excludeMsg),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: t.settings.network.perAppProxyPageTitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
switch (filteredPackages) {
|
||||
AsyncData(value: final packages) => SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final package = packages[index];
|
||||
final selected =
|
||||
perAppProxyList.contains(package.packageName);
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
package.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
package.packageName,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
value: selected,
|
||||
onChanged: (value) async {
|
||||
final List<String> newSelection;
|
||||
if (selected) {
|
||||
newSelection = perAppProxyList
|
||||
.exceptElement(package.packageName)
|
||||
.toList();
|
||||
} else {
|
||||
newSelection = [
|
||||
...perAppProxyList,
|
||||
package.packageName,
|
||||
];
|
||||
}
|
||||
await ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update(newSelection);
|
||||
},
|
||||
secondary: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ref
|
||||
.watch(packageIconProvider(package.packageName))
|
||||
.when(
|
||||
data: (data) => Image(image: data),
|
||||
error: (error, _) =>
|
||||
const Icon(FluentIcons.error_circle_24_regular),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: packages.length,
|
||||
NavigationBar(
|
||||
selectedIndex: 2,
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.home_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.home_20_filled),
|
||||
label: t.home.pageTitle,
|
||||
),
|
||||
AsyncLoading() => const SliverLoadingBodyPlaceholder(),
|
||||
AsyncError(:final error) =>
|
||||
SliverErrorBodyPlaceholder(error.toString()),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
},
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.list_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.list_20_filled),
|
||||
label: t.proxies.pageTitle,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.more_vertical_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.more_vertical_20_filled),
|
||||
label: t.settings.network.excludedDomains.pageTitle,
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (index) {
|
||||
if (index == 0) {
|
||||
const HomeRoute().go(context);
|
||||
} else if (index == 1) {
|
||||
const ProxiesRoute().go(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDomainsTab(
|
||||
BuildContext context,
|
||||
Translations t,
|
||||
WidgetRef ref,
|
||||
TextEditingController domainInputController,
|
||||
) {
|
||||
final excludedDomains = ref.watch(excludedDomainsListProvider);
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (excludedDomains.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.public_off, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.settings.network.excludedDomains.emptyState,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t.settings.network.excludedDomains.emptyStateDescription,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final domain = excludedDomains[index];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: Text(domain),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: () {
|
||||
final newList = List<String>.from(excludedDomains)..removeAt(index);
|
||||
ref.read(excludedDomainsListProvider.notifier).update(newList);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: excludedDomains.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppsTab(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Translations t,
|
||||
PerAppProxyMode perAppProxyMode,
|
||||
AsyncValue<List<InstalledPackageInfo>> filteredPackages,
|
||||
List<String> perAppProxyList,
|
||||
ValueNotifier<bool> showSystemApps,
|
||||
) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: CheckboxListTile(
|
||||
title: Text(t.settings.network.hideSystemApps),
|
||||
value: !showSystemApps.value,
|
||||
onChanged: (value) => showSystemApps.value = !(value ?? false),
|
||||
),
|
||||
),
|
||||
switch (filteredPackages) {
|
||||
AsyncData(value: final packages) => packages.isEmpty
|
||||
? SliverFillRemaining(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('No packages found'),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SliverList.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
final isSelected = perAppProxyList.contains(package.packageName);
|
||||
return CheckboxListTile(
|
||||
value: isSelected,
|
||||
onChanged: (_) async {
|
||||
final newList = List<String>.from(perAppProxyList);
|
||||
if (isSelected) {
|
||||
newList.remove(package.packageName);
|
||||
} else {
|
||||
newList.add(package.packageName);
|
||||
}
|
||||
await ref.read(perAppProxyListProvider.notifier).update(newList);
|
||||
},
|
||||
title: Text(package.name),
|
||||
subtitle: Text(package.packageName),
|
||||
secondary: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ref.watch(packageIconProvider(package.packageName)).when(
|
||||
data: (data) => Image(image: data),
|
||||
error: (error, _) => const Icon(FluentIcons.error_circle_24_regular),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: packages.length,
|
||||
),
|
||||
AsyncError() => SliverFillRemaining(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Error loading packages'),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ => const SliverFillRemaining(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddDomainModal(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
TextEditingController controller,
|
||||
) {
|
||||
final t = ref.read(translationsProvider);
|
||||
final excludedDomains = ref.read(excludedDomainsListProvider);
|
||||
|
||||
final presetZones = [
|
||||
'.ru',
|
||||
'.рф',
|
||||
'.su',
|
||||
'.by',
|
||||
'.kz',
|
||||
'.ua',
|
||||
];
|
||||
|
||||
// Локальное состояние для выбранных зон
|
||||
final selectedZones = Set<String>.from(excludedDomains.where((d) => presetZones.contains(d)));
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
t.settings.network.excludedDomains.addModalTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.settings.network.excludedDomains.helpTitle),
|
||||
content: Text(t.settings.network.excludedDomains.helpDescription),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.settings.network.excludedDomains.addOwnDomain,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: t.settings.network.excludedDomains.domainInputHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t.settings.network.excludedDomains.selectReadyZones,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...presetZones.map((zone) {
|
||||
final isSelected = selectedZones.contains(zone);
|
||||
return CheckboxListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(zone),
|
||||
value: isSelected,
|
||||
onChanged: (selected) {
|
||||
setState(() {
|
||||
if (selected == true) {
|
||||
selectedZones.add(zone);
|
||||
} else {
|
||||
selectedZones.remove(zone);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.settings.network.excludedDomains.cancel),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final newList = List<String>.from(excludedDomains);
|
||||
|
||||
// Удаляем все preset зоны из списка
|
||||
newList.removeWhere((d) => presetZones.contains(d));
|
||||
|
||||
// Добавляем выбранные зоны
|
||||
newList.addAll(selectedZones);
|
||||
|
||||
// Добавляем свой домен если введён
|
||||
final domain = controller.text.trim();
|
||||
if (domain.isNotEmpty && !newList.contains(domain)) {
|
||||
newList.add(domain);
|
||||
}
|
||||
|
||||
ref.read(excludedDomainsListProvider.notifier).update(newList);
|
||||
controller.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(t.settings.network.excludedDomains.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,10 @@ class AddProfileModal extends HookConsumerWidget {
|
||||
controller: scrollController,
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// temporary solution, aspect ratio widget relies on height and in a row there no height!
|
||||
final buttonWidth = constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2));
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// Fixed button width instead of using LayoutBuilder
|
||||
final buttonWidth = (MediaQuery.of(context).size.width / 2) - (buttonsPadding + (buttonsGap / 2));
|
||||
|
||||
return AnimatedCrossFade(
|
||||
firstChild: SizedBox(
|
||||
|
||||
@@ -62,111 +62,109 @@ class ProfileTile extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
shadowColor: Colors.transparent,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (profile is RemoteProfileEntity || !isMain) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (profile is RemoteProfileEntity || !isMain) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
sortKey: isMain ? const OrdinalSortKey(0) : null,
|
||||
focused: isMain,
|
||||
liveRegion: isMain,
|
||||
namesRoute: isMain,
|
||||
label: isMain ? t.profile.activeProfileBtnSemanticLabel : null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (isMain) {
|
||||
const ProfilesOverviewRoute().go(context);
|
||||
} else {
|
||||
if (selectActiveMutation.state.isInProgress) return;
|
||||
if (profile.active) return;
|
||||
selectActiveMutation.setFuture(
|
||||
ref.read(profilesOverviewNotifierProvider.notifier).selectActiveProfile(profile.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isMain)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontFamily: FontFamily.emoji,
|
||||
),
|
||||
semanticsLabel: t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
sortKey: isMain ? const OrdinalSortKey(0) : null,
|
||||
focused: isMain,
|
||||
liveRegion: isMain,
|
||||
namesRoute: isMain,
|
||||
label: isMain ? t.profile.activeProfileBtnSemanticLabel : null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (isMain) {
|
||||
const ProfilesOverviewRoute().go(context);
|
||||
} else {
|
||||
if (selectActiveMutation.state.isInProgress) return;
|
||||
if (profile.active) return;
|
||||
selectActiveMutation.setFuture(
|
||||
ref.read(profilesOverviewNotifierProvider.notifier).selectActiveProfile(profile.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isMain)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontFamily: FontFamily.emoji,
|
||||
),
|
||||
semanticsLabel: t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
FluentIcons.caret_down_16_filled,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
FluentIcons.caret_down_16_filled,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium,
|
||||
semanticsLabel: profile.active
|
||||
? t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
)
|
||||
: t.profile.nonActiveProfileBtnSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
),
|
||||
if (subInfo != null) ...[
|
||||
const Gap(4),
|
||||
RemainingTrafficIndicator(subInfo.ratio),
|
||||
const Gap(4),
|
||||
ProfileSubscriptionInfo(subInfo),
|
||||
const Gap(4),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium,
|
||||
semanticsLabel: profile.active
|
||||
? t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
)
|
||||
: t.profile.nonActiveProfileBtnSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
),
|
||||
if (subInfo != null) ...[
|
||||
const Gap(4),
|
||||
RemainingTrafficIndicator(subInfo.ratio),
|
||||
const Gap(4),
|
||||
ProfileSubscriptionInfo(subInfo),
|
||||
const Gap(4),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
final sortBy = ref.watch(proxiesSortNotifierProvider);
|
||||
|
||||
final selectActiveProxyMutation = useMutation(
|
||||
initialOnFailure: (error) =>
|
||||
CustomToast.error(t.presentShortError(error)).show(context),
|
||||
initialOnFailure: (error) => CustomToast.error(t.presentShortError(error)).show(context),
|
||||
);
|
||||
|
||||
final appBar = NestedAppBar(
|
||||
@@ -85,8 +84,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
proxy,
|
||||
selected: group.selected == proxy.tag,
|
||||
onSelect: () async {
|
||||
if (selectActiveProxyMutation
|
||||
.state.isInProgress) {
|
||||
if (selectActiveProxyMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
selectActiveProxyMutation.setFuture(
|
||||
@@ -132,7 +130,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async => notifier.urlTest(group.tag),
|
||||
tooltip: t.proxies.delayTestTooltip,
|
||||
child: const Icon(FluentIcons.flash_24_filled),
|
||||
child: const Icon(FluentIcons.arrow_clockwise_24_filled),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final debug = ref.watch(debugModeNotifierProvider);
|
||||
final perAppProxy = ref.watch(Preferences.perAppProxyMode).enabled;
|
||||
final disableMemoryLimit = ref.watch(Preferences.disableMemoryLimit);
|
||||
|
||||
return Column(
|
||||
@@ -33,28 +32,6 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
||||
// // await const GeoAssetsRoute().push(context);
|
||||
// },
|
||||
// ),
|
||||
if (Platform.isAndroid) ...[
|
||||
ListTile(
|
||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||
leading: const Icon(FluentIcons.apps_list_detail_24_regular),
|
||||
trailing: Switch(
|
||||
value: perAppProxy,
|
||||
onChanged: (value) async {
|
||||
final newMode = perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude;
|
||||
await ref.read(Preferences.perAppProxyMode.notifier).update(newMode);
|
||||
if (!perAppProxy && context.mounted) {
|
||||
await const PerAppProxyRoute().push(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
if (!perAppProxy) {
|
||||
await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.exclude);
|
||||
}
|
||||
if (context.mounted) await const PerAppProxyRoute().push(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.advanced.memoryLimit),
|
||||
subtitle: Text(t.settings.advanced.memoryLimitMsg),
|
||||
|
||||
Reference in New Issue
Block a user