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
This commit is contained in:
Umbrix Developer
2026-01-17 13:09:20 +03:00
parent ec5ebbd54b
commit 76a374950f
245 changed files with 7931 additions and 1315 deletions

View File

@@ -2,10 +2,10 @@
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/widget/animated_visibility.dart';
import 'package:hiddify/core/widget/shimmer_skeleton.dart';
import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/widget/animated_visibility.dart';
import 'package:umbrix/core/widget/shimmer_skeleton.dart';
import 'package:umbrix/features/proxy/active/active_proxy_notifier.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ActiveProxyDelayIndicator extends HookConsumerWidget {

View File

@@ -2,15 +2,15 @@ import 'package:dartx/dartx.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/widget/animated_visibility.dart';
import 'package:hiddify/core/widget/shimmer_skeleton.dart';
import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
import 'package:hiddify/features/proxy/active/ip_widget.dart';
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
import 'package:hiddify/features/stats/notifier/stats_notifier.dart';
import 'package:hiddify/gen/fonts.gen.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/widget/animated_visibility.dart';
import 'package:umbrix/core/widget/shimmer_skeleton.dart';
import 'package:umbrix/features/proxy/active/active_proxy_notifier.dart';
import 'package:umbrix/features/proxy/active/ip_widget.dart';
import 'package:umbrix/features/proxy/model/proxy_failure.dart';
import 'package:umbrix/features/stats/notifier/stats_notifier.dart';
import 'package:umbrix/gen/fonts.gen.dart';
import 'package:umbrix/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ActiveProxyFooter extends HookConsumerWidget {

View File

@@ -1,16 +1,16 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:hiddify/core/haptic/haptic_service.dart';
import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/core/utils/throttler.dart';
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
import 'package:hiddify/features/proxy/data/proxy_data_providers.dart';
import 'package:hiddify/features/proxy/model/ip_info_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:umbrix/core/haptic/haptic_service.dart';
import 'package:umbrix/core/preferences/general_preferences.dart';
import 'package:umbrix/core/utils/throttler.dart';
import 'package:umbrix/features/connection/notifier/connection_notifier.dart';
import 'package:umbrix/features/proxy/data/proxy_data_providers.dart';
import 'package:umbrix/features/proxy/model/ip_info_entity.dart';
import 'package:umbrix/features/proxy/model/proxy_entity.dart';
import 'package:umbrix/features/proxy/model/proxy_failure.dart';
import 'package:umbrix/utils/riverpod_utils.dart';
import 'package:umbrix/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'active_proxy_notifier.g.dart';
@@ -19,7 +19,7 @@ part 'active_proxy_notifier.g.dart';
class IpInfoNotifier extends _$IpInfoNotifier with AppLogger {
@override
Future<IpInfo> build() async {
ref.disposeDelay(const Duration(seconds: 20));
ref.disposeDelay(const Duration(seconds: 120));
final cancelToken = CancelToken();
Timer? timer;
ref.onDispose(() {
@@ -48,13 +48,12 @@ class IpInfoNotifier extends _$IpInfoNotifier with AppLogger {
final info = await ref.watch(proxyRepositoryProvider).getCurrentIpInfo(cancelToken).getOrElse(
(err) {
loggy.warning("error getting proxy ip info", err, StackTrace.current);
// throw err; //hiddify: remove exception to be logged
throw const UnknownIp();
},
).run();
timer = Timer(
const Duration(seconds: 10),
const Duration(seconds: 60),
() {
loggy.debug("entering idle mode");
_idle = true;

View File

@@ -1,10 +1,10 @@
import 'package:circle_flags/circle_flags.dart';
import 'package:flutter/material.dart';
import 'package:hiddify/core/haptic/haptic_service.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/utils/ip_utils.dart';
import 'package:hiddify/gen/fonts.gen.dart';
import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:umbrix/core/haptic/haptic_service.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/utils/ip_utils.dart';
import 'package:umbrix/gen/fonts.gen.dart';
import 'package:umbrix/utils/riverpod_utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final _showIp = StateProvider.autoDispose((ref) {

View File

@@ -1,6 +1,6 @@
import 'package:hiddify/core/http_client/http_client_provider.dart';
import 'package:hiddify/features/proxy/data/proxy_repository.dart';
import 'package:hiddify/singbox/service/singbox_service_provider.dart';
import 'package:umbrix/core/http_client/http_client_provider.dart';
import 'package:umbrix/features/proxy/data/proxy_repository.dart';
import 'package:umbrix/singbox/service/singbox_service_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'proxy_data_providers.g.dart';

View File

@@ -1,12 +1,12 @@
import 'package:dio/dio.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/http_client/dio_http_client.dart';
import 'package:hiddify/core/utils/exception_handler.dart';
import 'package:hiddify/features/proxy/model/ip_info_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:umbrix/core/http_client/dio_http_client.dart';
import 'package:umbrix/core/utils/exception_handler.dart';
import 'package:umbrix/features/proxy/model/ip_info_entity.dart';
import 'package:umbrix/features/proxy/model/proxy_entity.dart';
import 'package:umbrix/features/proxy/model/proxy_failure.dart';
import 'package:umbrix/singbox/service/singbox_service.dart';
import 'package:umbrix/utils/custom_loggers.dart';
abstract interface class ProxyRepository {
Stream<Either<ProxyFailure, List<ProxyGroupEntity>>> watchProxies();

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/singbox/model/singbox_proxy_type.dart';
import 'package:umbrix/singbox/model/singbox_proxy_type.dart';
part 'proxy_entity.freezed.dart';

View File

@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/model/failures.dart';
part 'proxy_failure.freezed.dart';

View File

@@ -2,16 +2,16 @@ import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:hiddify/core/haptic/haptic_service.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/core/utils/preferences_utils.dart';
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
import 'package:hiddify/features/proxy/data/proxy_data_providers.dart';
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:umbrix/core/haptic/haptic_service.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/preferences/preferences_provider.dart';
import 'package:umbrix/core/utils/preferences_utils.dart';
import 'package:umbrix/features/connection/notifier/connection_notifier.dart';
import 'package:umbrix/features/proxy/data/proxy_data_providers.dart';
import 'package:umbrix/features/proxy/model/proxy_entity.dart';
import 'package:umbrix/features/proxy/model/proxy_failure.dart';
import 'package:umbrix/utils/riverpod_utils.dart';
import 'package:umbrix/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';

View File

@@ -1,12 +1,14 @@
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
import 'package:hiddify/features/common/nested_app_bar.dart';
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
import 'package:hiddify/features/proxy/overview/proxies_overview_notifier.dart';
import 'package:hiddify/features/proxy/widget/proxy_tile.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/model/failures.dart';
import 'package:umbrix/core/router/routes.dart';
import 'package:umbrix/features/common/nested_app_bar.dart';
import 'package:umbrix/features/proxy/model/proxy_entity.dart';
import 'package:umbrix/features/proxy/overview/proxies_overview_notifier.dart';
import 'package:umbrix/features/proxy/widget/proxy_tile.dart';
import 'package:umbrix/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
@@ -16,6 +18,19 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final interceptBackToHome = !PlatformUtils.isDesktop;
Widget withBackToHome(Widget child) {
return PopScope<void>(
canPop: !interceptBackToHome,
onPopInvokedWithResult: (didPop, result) {
if (!didPop && interceptBackToHome) {
const HomeRoute().go(context);
}
},
child: child,
);
}
final asyncProxies = ref.watch(proxiesOverviewNotifierProvider);
final notifier = ref.watch(proxiesOverviewNotifierProvider.notifier);
final sortBy = ref.watch(proxiesSortNotifierProvider);
@@ -49,19 +64,21 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
switch (asyncProxies) {
case AsyncData(value: final groups):
if (groups.isEmpty) {
return Scaffold(
body: CustomScrollView(
slivers: [
appBar,
SliverFillRemaining(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(t.proxies.emptyProxiesMsg),
],
return withBackToHome(
Scaffold(
body: CustomScrollView(
slivers: [
appBar,
SliverFillRemaining(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(t.proxies.emptyProxiesMsg),
],
),
),
),
],
],
),
),
);
}
@@ -97,104 +114,110 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
return avgA.compareTo(avgB);
});
return Scaffold(
body: CustomScrollView(
slivers: [
appBar,
SliverPadding(
padding: const EdgeInsets.only(bottom: 86, left: 12, right: 12, top: 8),
sliver: SliverList.builder(
// +1 для глобальной карточки "Авто по всем"
itemCount: 1 + sortedCountries.length,
itemBuilder: (context, index) {
// Первая карточка - "Авто по всем локациям"
if (index == 0) {
return _GlobalAutoCard(
currentSelection: currentSelection,
avgDelay: autoGroup.items.firstOrNull?.urlTestDelay.toDouble() ?? 999999.0,
return withBackToHome(
Scaffold(
body: CustomScrollView(
slivers: [
appBar,
SliverPadding(
padding: const EdgeInsets.only(bottom: 86, left: 12, right: 12, top: 8),
sliver: SliverList.builder(
// +1 для глобальной карточки "Авто по всем"
itemCount: 1 + sortedCountries.length,
itemBuilder: (context, index) {
// Первая карточка - "Авто по всем локациям"
if (index == 0) {
return _GlobalAutoCard(
currentSelection: currentSelection,
avgDelay: autoGroup.items.firstOrNull?.urlTestDelay.toDouble() ?? 999999.0,
selectGroup: selectGroup,
selectActiveProxyMutation: selectActiveProxyMutation,
notifier: notifier,
t: t,
);
}
// Остальные карточки - страны
final countryIndex = index - 1;
final countryCode = sortedCountries[countryIndex];
final proxies = proxiesByCountry[countryCode]!;
final avgDelay = _averageDelay(proxies);
return _CountryGroupCard(
countryCode: countryCode,
proxies: proxies,
avgDelay: avgDelay,
selectGroup: selectGroup,
currentSelection: currentSelection,
selectActiveProxyMutation: selectActiveProxyMutation,
notifier: notifier,
t: t,
countryFlag: _countryFlag(countryCode),
delayColor: _delayColor(context, avgDelay),
);
}
// Остальные карточки - страны
final countryIndex = index - 1;
final countryCode = sortedCountries[countryIndex];
final proxies = proxiesByCountry[countryCode]!;
final avgDelay = _averageDelay(proxies);
return _CountryGroupCard(
countryCode: countryCode,
proxies: proxies,
avgDelay: avgDelay,
selectGroup: selectGroup,
currentSelection: currentSelection,
selectActiveProxyMutation: selectActiveProxyMutation,
notifier: notifier,
countryFlag: _countryFlag(countryCode),
delayColor: _delayColor(context, avgDelay),
);
},
),
),
],
),
floatingActionButton: Builder(
builder: (context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.primary.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
},
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.4),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
floatingActionButton: Builder(
builder: (context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.primary.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
],
),
child: FloatingActionButton(
onPressed: () async => notifier.urlTest(selectGroup.tag),
tooltip: t.proxies.delayTestTooltip,
backgroundColor: Colors.transparent,
elevation: 0,
child: const Icon(FluentIcons.arrow_clockwise_24_filled),
),
);
},
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.4),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: FloatingActionButton(
onPressed: () async => notifier.urlTest(selectGroup.tag),
tooltip: t.proxies.delayTestTooltip,
backgroundColor: Colors.transparent,
elevation: 0,
child: const Icon(FluentIcons.arrow_clockwise_24_filled),
),
);
},
),
),
);
case AsyncError(:final error):
return Scaffold(
body: CustomScrollView(
slivers: [
appBar,
SliverErrorBodyPlaceholder(
t.presentShortError(error),
icon: null,
),
],
return withBackToHome(
Scaffold(
body: CustomScrollView(
slivers: [
appBar,
SliverErrorBodyPlaceholder(
t.presentShortError(error),
icon: null,
),
],
),
),
);
case AsyncLoading():
return Scaffold(
body: CustomScrollView(
slivers: [
appBar,
const SliverLoadingBodyPlaceholder(),
],
return withBackToHome(
Scaffold(
body: CustomScrollView(
slivers: [
appBar,
const SliverLoadingBodyPlaceholder(),
],
),
),
);

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
import 'package:hiddify/gen/fonts.gen.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:umbrix/features/proxy/model/proxy_entity.dart';
import 'package:umbrix/gen/fonts.gen.dart';
import 'package:umbrix/utils/custom_loggers.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ProxyTile extends HookConsumerWidget with PresLogger {