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

@@ -1,7 +1,7 @@
import 'package:fpdart/fpdart.dart';
import 'package:grpc/grpc.dart';
import 'package:hiddify/singbox/generated/core.pbgrpc.dart';
import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:umbrix/singbox/generated/core.pbgrpc.dart';
import 'package:umbrix/singbox/service/singbox_service.dart';
abstract class CoreSingboxService extends CoreServiceClient
implements SingboxService {

View File

@@ -6,15 +6,15 @@ import 'dart:isolate';
import 'package:combine/combine.dart';
import 'package:ffi/ffi.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/model/directories.dart';
import 'package:hiddify/gen/singbox_generated_bindings.dart';
import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_outbound.dart';
import 'package:hiddify/singbox/model/singbox_stats.dart';
import 'package:hiddify/singbox/model/singbox_status.dart';
import 'package:hiddify/singbox/model/warp_account.dart';
import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:umbrix/core/model/directories.dart';
import 'package:umbrix/gen/singbox_generated_bindings.dart';
import 'package:umbrix/singbox/model/singbox_config_option.dart';
import 'package:umbrix/singbox/model/singbox_outbound.dart';
import 'package:umbrix/singbox/model/singbox_stats.dart';
import 'package:umbrix/singbox/model/singbox_status.dart';
import 'package:umbrix/singbox/model/warp_account.dart';
import 'package:umbrix/singbox/service/singbox_service.dart';
import 'package:umbrix/utils/utils.dart';
import 'package:loggy/loggy.dart';
import 'package:path/path.dart' as p;
import 'package:rxdart/rxdart.dart';

View File

@@ -3,18 +3,47 @@ import 'dart:io';
import 'package:flutter/services.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/model/directories.dart';
import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_outbound.dart';
import 'package:hiddify/singbox/model/singbox_stats.dart';
import 'package:hiddify/singbox/model/singbox_status.dart';
import 'package:hiddify/singbox/model/warp_account.dart';
import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:umbrix/core/model/directories.dart';
import 'package:umbrix/singbox/model/singbox_config_option.dart';
import 'package:umbrix/singbox/model/singbox_outbound.dart';
import 'package:umbrix/singbox/model/singbox_stats.dart';
import 'package:umbrix/singbox/model/singbox_status.dart';
import 'package:umbrix/singbox/model/warp_account.dart';
import 'package:umbrix/singbox/service/singbox_service.dart';
import 'package:umbrix/utils/custom_loggers.dart';
import 'package:rxdart/rxdart.dart';
Map<String, dynamic> _normalizeOutboundGroupJson(Object? raw) {
if (raw is Map<String, dynamic>) {
if (raw.containsKey('tag')) return raw;
if (raw.containsKey('a') && raw.containsKey('b')) {
return <String, dynamic>{
'tag': raw['a'],
'type': raw['b'],
'selected': raw['c'] ?? '',
'items': (raw['d'] as List<dynamic>? ?? const <dynamic>[]).map(_normalizeOutboundGroupItemJson).toList(),
};
}
}
throw FormatException('Invalid outbound group payload: ${raw.runtimeType}');
}
Map<String, dynamic> _normalizeOutboundGroupItemJson(Object? raw) {
if (raw is Map<String, dynamic>) {
if (raw.containsKey('tag')) return raw;
if (raw.containsKey('a') && raw.containsKey('b')) {
return <String, dynamic>{
'tag': raw['a'],
'type': raw['b'],
'url-test-delay': raw['c'] ?? 999999,
};
}
}
throw FormatException('Invalid outbound group item payload: ${raw.runtimeType}');
}
class PlatformSingboxService with InfraLogger implements SingboxService {
static const channelPrefix = "com.hiddify.app";
static const channelPrefix = "com.umbrix.app";
static const methodChannel = MethodChannel("$channelPrefix/method");
static const statusChannel = EventChannel("$channelPrefix/service.status", JSONMethodCodec());
@@ -74,7 +103,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
() async {
loggy.debug("changing options");
await methodChannel.invokeMethod(
"change_hiddify_options",
"change_options",
jsonEncode(options.toJson()),
);
return right(unit);
@@ -170,9 +199,8 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
return groupsChannel.receiveBroadcastStream().map(
(event) {
if (event case String _) {
return (jsonDecode(event) as List).map((e) {
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
}).toList();
final decoded = jsonDecode(event) as List;
return decoded.map((e) => SingboxOutboundGroup.fromJson(_normalizeOutboundGroupJson(e))).toList();
}
loggy.error("[group client] unexpected type, msg: $event");
throw "invalid type";
@@ -187,11 +215,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
(event) {
if (event case String _) {
final decoded = jsonDecode(event) as List;
loggy.info("🔍 DECODED JSON: ${decoded.length} groups");
return decoded.map((e) {
loggy.info("🔍 GROUP DATA: $e");
return SingboxOutboundGroup.fromJson(e as Map<String, dynamic>);
}).toList();
return decoded.map((e) => SingboxOutboundGroup.fromJson(_normalizeOutboundGroupJson(e))).toList();
}
loggy.error("[active group client] unexpected type, msg: $event");
throw "invalid type";
@@ -205,7 +229,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
@override
Stream<SingboxStats> watchStats() {
loggy.debug("watching stats");
return statsChannel.receiveBroadcastStream().map(
return statsChannel.receiveBroadcastStream().throttleTime(const Duration(milliseconds: 500)).map(
(event) {
if (event case Map<String, dynamic> _) {
return SingboxStats.fromJson(event);

View File

@@ -1,14 +1,14 @@
import 'dart:io';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/model/directories.dart';
import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_outbound.dart';
import 'package:hiddify/singbox/model/singbox_stats.dart';
import 'package:hiddify/singbox/model/singbox_status.dart';
import 'package:hiddify/singbox/model/warp_account.dart';
import 'package:hiddify/singbox/service/ffi_singbox_service.dart';
import 'package:hiddify/singbox/service/platform_singbox_service.dart';
import 'package:umbrix/core/model/directories.dart';
import 'package:umbrix/singbox/model/singbox_config_option.dart';
import 'package:umbrix/singbox/model/singbox_outbound.dart';
import 'package:umbrix/singbox/model/singbox_stats.dart';
import 'package:umbrix/singbox/model/singbox_status.dart';
import 'package:umbrix/singbox/model/warp_account.dart';
import 'package:umbrix/singbox/service/ffi_singbox_service.dart';
import 'package:umbrix/singbox/service/platform_singbox_service.dart';
abstract interface class SingboxService {
factory SingboxService() {

View File

@@ -1,4 +1,4 @@
import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:umbrix/singbox/service/singbox_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'singbox_service_provider.g.dart';