commit 6546e086d828fdf61c7ce4ed8b7e907637572b94 Author: meowfox Date: Tue Feb 10 14:39:44 2026 +0300 initial: push diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ba8c6a3 Binary files /dev/null and b/.DS_Store differ diff --git a/MeowRelay.xcodeproj/project.pbxproj b/MeowRelay.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ced1081 --- /dev/null +++ b/MeowRelay.xcodeproj/project.pbxproj @@ -0,0 +1,604 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + 36E7064A2F0ED8500091D2B2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 36E706322F0ED84F0091D2B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 36E706392F0ED84F0091D2B2; + remoteInfo = MeowRelay; + }; + 36E706542F0ED8500091D2B2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 36E706322F0ED84F0091D2B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 36E706392F0ED84F0091D2B2; + remoteInfo = MeowRelay; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 36E7063A2F0ED84F0091D2B2 /* MeowRelay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MeowRelay.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 36E706492F0ED8500091D2B2 /* MeowRelayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeowRelayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 36E706532F0ED8500091D2B2 /* MeowRelayUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeowRelayUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 36E706672F0ED8E60091D2B2 /* Exceptions for "MeowRelay" folder in "MeowRelay" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 36E706392F0ED84F0091D2B2 /* MeowRelay */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 36E7063C2F0ED84F0091D2B2 /* MeowRelay */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 36E706672F0ED8E60091D2B2 /* Exceptions for "MeowRelay" folder in "MeowRelay" target */, + ); + path = MeowRelay; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 36E706372F0ED84F0091D2B2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 36E706462F0ED8500091D2B2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 36E706502F0ED8500091D2B2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 36E706312F0ED84F0091D2B2 = { + isa = PBXGroup; + children = ( + 36E7063C2F0ED84F0091D2B2 /* MeowRelay */, + 36E7063B2F0ED84F0091D2B2 /* Products */, + ); + sourceTree = ""; + }; + 36E7063B2F0ED84F0091D2B2 /* Products */ = { + isa = PBXGroup; + children = ( + 36E7063A2F0ED84F0091D2B2 /* MeowRelay.app */, + 36E706492F0ED8500091D2B2 /* MeowRelayTests.xctest */, + 36E706532F0ED8500091D2B2 /* MeowRelayUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 36E706392F0ED84F0091D2B2 /* MeowRelay */ = { + isa = PBXNativeTarget; + buildConfigurationList = 36E7065D2F0ED8500091D2B2 /* Build configuration list for PBXNativeTarget "MeowRelay" */; + buildPhases = ( + 36E706362F0ED84F0091D2B2 /* Sources */, + 36E706372F0ED84F0091D2B2 /* Frameworks */, + 36E706382F0ED84F0091D2B2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 36E7063C2F0ED84F0091D2B2 /* MeowRelay */, + ); + name = MeowRelay; + packageProductDependencies = ( + ); + productName = MeowRelay; + productReference = 36E7063A2F0ED84F0091D2B2 /* MeowRelay.app */; + productType = "com.apple.product-type.application"; + }; + 36E706482F0ED8500091D2B2 /* MeowRelayTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 36E706602F0ED8500091D2B2 /* Build configuration list for PBXNativeTarget "MeowRelayTests" */; + buildPhases = ( + 36E706452F0ED8500091D2B2 /* Sources */, + 36E706462F0ED8500091D2B2 /* Frameworks */, + 36E706472F0ED8500091D2B2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 36E7064B2F0ED8500091D2B2 /* PBXTargetDependency */, + ); + name = MeowRelayTests; + packageProductDependencies = ( + ); + productName = MeowRelayTests; + productReference = 36E706492F0ED8500091D2B2 /* MeowRelayTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 36E706522F0ED8500091D2B2 /* MeowRelayUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 36E706632F0ED8500091D2B2 /* Build configuration list for PBXNativeTarget "MeowRelayUITests" */; + buildPhases = ( + 36E7064F2F0ED8500091D2B2 /* Sources */, + 36E706502F0ED8500091D2B2 /* Frameworks */, + 36E706512F0ED8500091D2B2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 36E706552F0ED8500091D2B2 /* PBXTargetDependency */, + ); + name = MeowRelayUITests; + packageProductDependencies = ( + ); + productName = MeowRelayUITests; + productReference = 36E706532F0ED8500091D2B2 /* MeowRelayUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 36E706322F0ED84F0091D2B2 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2620; + LastUpgradeCheck = 2620; + TargetAttributes = { + 36E706392F0ED84F0091D2B2 = { + CreatedOnToolsVersion = 26.2; + }; + 36E706482F0ED8500091D2B2 = { + CreatedOnToolsVersion = 26.2; + TestTargetID = 36E706392F0ED84F0091D2B2; + }; + 36E706522F0ED8500091D2B2 = { + CreatedOnToolsVersion = 26.2; + TestTargetID = 36E706392F0ED84F0091D2B2; + }; + }; + }; + buildConfigurationList = 36E706352F0ED84F0091D2B2 /* Build configuration list for PBXProject "MeowRelay" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 36E706312F0ED84F0091D2B2; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 36E7063B2F0ED84F0091D2B2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 36E706392F0ED84F0091D2B2 /* MeowRelay */, + 36E706482F0ED8500091D2B2 /* MeowRelayTests */, + 36E706522F0ED8500091D2B2 /* MeowRelayUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 36E706382F0ED84F0091D2B2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 36E706472F0ED8500091D2B2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 36E706512F0ED8500091D2B2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 36E706362F0ED84F0091D2B2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 36E706452F0ED8500091D2B2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 36E7064F2F0ED8500091D2B2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 36E7064B2F0ED8500091D2B2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 36E706392F0ED84F0091D2B2 /* MeowRelay */; + targetProxy = 36E7064A2F0ED8500091D2B2 /* PBXContainerItemProxy */; + }; + 36E706552F0ED8500091D2B2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 36E706392F0ED84F0091D2B2 /* MeowRelay */; + targetProxy = 36E706542F0ED8500091D2B2 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 36E7065B2F0ED8500091D2B2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = L46RPVUB7F; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 36E7065C2F0ED8500091D2B2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = L46RPVUB7F; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 36E7065E2F0ED8500091D2B2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L46RPVUB7F; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MeowRelay/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MeowRelay; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.meowfox.MeowRelay; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 36E7065F2F0ED8500091D2B2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L46RPVUB7F; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MeowRelay/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MeowRelay; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.meowfox.MeowRelay; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 36E706612F0ED8500091D2B2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L46RPVUB7F; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.meowfox.MeowRelayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MeowRelay.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MeowRelay"; + }; + name = Debug; + }; + 36E706622F0ED8500091D2B2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L46RPVUB7F; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.meowfox.MeowRelayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MeowRelay.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MeowRelay"; + }; + name = Release; + }; + 36E706642F0ED8500091D2B2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L46RPVUB7F; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.meowfox.MeowRelayUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = MeowRelay; + }; + name = Debug; + }; + 36E706652F0ED8500091D2B2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L46RPVUB7F; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.meowfox.MeowRelayUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = MeowRelay; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 36E706352F0ED84F0091D2B2 /* Build configuration list for PBXProject "MeowRelay" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 36E7065B2F0ED8500091D2B2 /* Debug */, + 36E7065C2F0ED8500091D2B2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 36E7065D2F0ED8500091D2B2 /* Build configuration list for PBXNativeTarget "MeowRelay" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 36E7065E2F0ED8500091D2B2 /* Debug */, + 36E7065F2F0ED8500091D2B2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 36E706602F0ED8500091D2B2 /* Build configuration list for PBXNativeTarget "MeowRelayTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 36E706612F0ED8500091D2B2 /* Debug */, + 36E706622F0ED8500091D2B2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 36E706632F0ED8500091D2B2 /* Build configuration list for PBXNativeTarget "MeowRelayUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 36E706642F0ED8500091D2B2 /* Debug */, + 36E706652F0ED8500091D2B2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 36E706322F0ED84F0091D2B2 /* Project object */; +} diff --git a/MeowRelay.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MeowRelay.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/MeowRelay.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MeowRelay.xcodeproj/project.xcworkspace/xcuserdata/meowfox.xcuserdatad/UserInterfaceState.xcuserstate b/MeowRelay.xcodeproj/project.xcworkspace/xcuserdata/meowfox.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..07f392a Binary files /dev/null and b/MeowRelay.xcodeproj/project.xcworkspace/xcuserdata/meowfox.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MeowRelay.xcodeproj/xcuserdata/meowfox.xcuserdatad/xcschemes/xcschememanagement.plist b/MeowRelay.xcodeproj/xcuserdata/meowfox.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..94463ed --- /dev/null +++ b/MeowRelay.xcodeproj/xcuserdata/meowfox.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + MeowRelay.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/MeowRelay/.DS_Store b/MeowRelay/.DS_Store new file mode 100644 index 0000000..b157474 Binary files /dev/null and b/MeowRelay/.DS_Store differ diff --git a/MeowRelay/Assets.xcassets/.DS_Store b/MeowRelay/Assets.xcassets/.DS_Store new file mode 100644 index 0000000..cc63c05 Binary files /dev/null and b/MeowRelay/Assets.xcassets/.DS_Store differ diff --git a/MeowRelay/Assets.xcassets/AccentColor.colorset/Contents.json b/MeowRelay/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/MeowRelay/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/Contents.json b/MeowRelay/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..64dc11e --- /dev/null +++ b/MeowRelay/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..629173b Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..fa5a2e0 Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..a54b4fb Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..957cd80 Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..fa5a2e0 Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..0d61539 Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..4976f6c Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..5eb80ec Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..0d61539 Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..8b0cb40 Binary files /dev/null and b/MeowRelay/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/MeowRelay/Assets.xcassets/Contents.json b/MeowRelay/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/MeowRelay/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/Contents.json b/MeowRelay/Assets.xcassets/trayIcon.appiconset/Contents.json new file mode 100644 index 0000000..64dc11e --- /dev/null +++ b/MeowRelay/Assets.xcassets/trayIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_128x128.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..629173b Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_128x128.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_128x128@2x.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..fa5a2e0 Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_128x128@2x.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_16x16.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..a54b4fb Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_16x16.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_16x16@2x.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..957cd80 Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_16x16@2x.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_256x256.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..fa5a2e0 Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_256x256.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_256x256@2x.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..0d61539 Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_256x256@2x.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_32x32.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..4976f6c Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_32x32.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_32x32@2x.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..5eb80ec Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_32x32@2x.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_512x512.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..0d61539 Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_512x512.png differ diff --git a/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_512x512@2x.png b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..8b0cb40 Binary files /dev/null and b/MeowRelay/Assets.xcassets/trayIcon.appiconset/icon_512x512@2x.png differ diff --git a/MeowRelay/Info.plist b/MeowRelay/Info.plist new file mode 100644 index 0000000..6a6654d --- /dev/null +++ b/MeowRelay/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/MeowRelay/MeowRelayApp.swift b/MeowRelay/MeowRelayApp.swift new file mode 100644 index 0000000..d6bed26 --- /dev/null +++ b/MeowRelay/MeowRelayApp.swift @@ -0,0 +1,482 @@ +import SwiftUI +import Combine + +struct Route: Identifiable, Hashable, Equatable { + let id: String + let code: String + let provider: String + let name: String + let flag: String + + init(code: String, provider: String, name: String) { + self.code = code + self.provider = provider + self.name = name + self.id = code + provider + name + + let base: UInt32 = 127397 + var s = "" + for v in code.uppercased().unicodeScalars { + if let scal = UnicodeScalar(base + v.value) { + s.unicodeScalars.append(scal) + } + } + self.flag = s + } +} + +struct Feature: Identifiable, Codable, Equatable { + var id: String { name } + let name: String + let desc: String + var value: Bool +} + +struct ServerItem: Identifiable, Equatable { + let id: String + let detail: ServerDetail +} + +struct ServerDetail: Decodable, Equatable { + let connected: Int? + let traffic: [Double]? + let uptime: Double? + let ping: [String]? + let speed: [Double]? + let country: String? +} + +struct UserProfile: Decodable { + let user: String? + let current: String? + let ebalka: Bool? + let userData: UserData? + let features: [Feature]? + let provider: String? + enum CodingKeys: String, CodingKey { + case user, current, ebalka, features, provider + case userData = "user_data" + } +} + +struct UserData: Decodable { + let share: Bool? +} + +struct ServerStats: Decodable { + let uptime: Double? + let servers: [String: ServerDetail]? +} + +class RelayViewModel: ObservableObject { + @Published var activeTab: Int = 0 + @Published var routes: [Route] = [] + @Published var serverList: [ServerItem] = [] + @Published var features: [Feature] = [] + @Published var userLabel: String = "..." + @Published var currentCode: String = "" + @Published var currentProvider: String = "" + @Published var isShared: Bool = false + @Published var isEbalka: Bool = false + @Published var statsLabel: String = "--" + @Published var totalServers: Int = 0 + + private var cancellables = Set() + private let baseURL = "http://10.7.0.1:7777" + + init() { + refreshAll() + Timer.publish(every: 3, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + guard let self = self else { return } + if self.activeTab == 1 { self.fetchStats() } + } + .store(in: &cancellables) + } + + func refreshAll() { + fetchProfile() + fetchRoutes() + fetchStats() + } + + func fetchProfile() { + get(url: "/me/profile", type: UserProfile.self) { [weak self] p in + guard let self = self else { return } + let newUser = p.user ?? "Unknown" + if self.userLabel != newUser { self.userLabel = newUser } + + let parts = p.current?.components(separatedBy: "-") + let newCode = parts?.first?.uppercased() ?? "" + if self.currentCode != newCode { self.currentCode = newCode } + + let newProv = p.provider ?? "" + if self.currentProvider != newProv { self.currentProvider = newProv } + + let newShare = p.userData?.share ?? false + if self.isShared != newShare { self.isShared = newShare } + + let newEbalka = p.ebalka ?? false + if self.isEbalka != newEbalka { self.isEbalka = newEbalka } + + let newFeat = p.features ?? [] + if self.features != newFeat { self.features = newFeat } + } + } + + func fetchRoutes() { + guard let url = URL(string: "\(baseURL)/routes") else { return } + URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in + guard let self = self, let data = data else { return } + var newRoutes: [Route] = [] + if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + for (k, v) in dict { self.parseRoute(k, v, &newRoutes) } + } else if let arr = try? JSONSerialization.jsonObject(with: data) as? [Any] { + for item in arr { self.parseRoute(nil, item, &newRoutes) } + } + let sorted = newRoutes.sorted { $0.name < $1.name } + DispatchQueue.main.async { + if self.routes != sorted { self.routes = sorted } + } + }.resume() + } + + private func parseRoute(_ key: String?, _ val: Any, _ list: inout [Route]) { + let code = key?.uppercased() ?? "" + if let name = val as? String { + let c = code.isEmpty ? name.uppercased() : code + list.append(Route(code: c, provider: "", name: name)) + } else if let obj = val as? [String: Any] { + let c = (obj["code"] as? String ?? code).uppercased() + let name = obj["name"] as? String ?? c + let provider = obj["provider"] as? String ?? c + list.append(Route(code: c, provider: provider, name: name)) + } + } + + func fetchStats() { + get(url: "/stats", type: ServerStats.self) { [weak self] s in + guard let self = self else { return } + let sec = Int(s.uptime ?? 0) + let newStatsLabel = "\(sec / 3600)h \((sec % 3600) / 60)m" + if self.statsLabel != newStatsLabel { self.statsLabel = newStatsLabel } + + let dict = s.servers ?? [:] + if self.totalServers != dict.count { self.totalServers = dict.count } + + let list = dict.map { ServerItem(id: $0.key, detail: $0.value) } + let sorted = list.sorted { ($0.detail.connected ?? 0) > ($1.detail.connected ?? 0) } + + if self.serverList != sorted { self.serverList = sorted } + } + } + + func selectRoute(_ r: Route) { + currentCode = r.code + currentProvider = r.provider + post("/me/country", ["countryCode": r.code, "provider": r.provider]) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.fetchProfile() } + } + + func toggleShare(_ val: Bool) { + isShared = val + post("/me/profile", ["shareTraffic": val]) + } + + func toggleEbalka(_ val: Bool) { + isEbalka = val + post("/me/ebalka", ["ip": NSNull(), "ebalka": val]) + } + + func toggleFeature(_ name: String, _ val: Bool) { + post("/me/feature", ["name": name, "value": val]) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.fetchProfile() } + } + + func srvAction(_ endpoint: String, _ id: String) { + post("/admin/\(endpoint)", ["iface": id]) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.fetchStats() } + } + + private func get(url: String, type: T.Type, completion: @escaping (T) -> Void) { + guard let u = URL(string: baseURL + url) else { return } + URLSession.shared.dataTask(with: u) { d, _, _ in + guard let d = d, let obj = try? JSONDecoder().decode(T.self, from: d) else { return } + DispatchQueue.main.async { completion(obj) } + }.resume() + } + + private func post(_ url: String, _ body: [String: Any]) { + guard let u = URL(string: baseURL + url) else { return } + var req = URLRequest(url: u) + req.httpMethod = "POST" + req.addValue("application/json", forHTTPHeaderField: "Content-Type") + req.httpBody = try? JSONSerialization.data(withJSONObject: body) + URLSession.shared.dataTask(with: req).resume() + } + + func flag(_ c: String?) -> String { + guard let code = c else { return "🏳️" } + let base: UInt32 = 127397 + var s = "" + for v in code.uppercased().unicodeScalars { + if let scal = UnicodeScalar(base + v.value) { + s.unicodeScalars.append(scal) + } + } + return s + } + + func fmtTime(_ sec: Double) -> String { + let s = Int(sec) + let d = s / 86400; let h = (s % 86400) / 3600; let m = (s % 3600) / 60 + if d > 0 { return "\(d)d \(h)h" } + return "\(h)h \(m)m" + } + + func avgPing(_ arr: [String]?) -> Double { + guard let arr = arr, !arr.isEmpty else { return 0 } + let sum = arr.compactMap { Double($0) }.reduce(0, +) + return sum / Double(arr.count) + } + + func fmtBytes(_ v: Double?) -> String { + guard let v = v else { return "0B" } + let units = ["B", "KB", "MB", "GB"] + var s = v; var i = 0 + while s > 1024 && i < 3 { s /= 1024; i += 1 } + return String(format: "%.1f%@", s, units[i]) + } + + func fmtSpeed(_ v: Double?) -> String { + guard let v = v else { return "0B/s" } + return fmtBytes(v) + "/s" + } +} + +@main +struct MeowRelayApp: App { + @StateObject var vm = RelayViewModel() + + var body: some Scene { + MenuBarExtra("MeowRelay", systemImage: "pawprint") { + VStack(spacing: 0) { + HStack { + Text("MeowRelay").font(.headline).foregroundColor(.primary) + Spacer() + Text(vm.userLabel).font(.caption).foregroundColor(.secondary) + } + .padding(12) + + Picker("", selection: $vm.activeTab) { + Text("Routes").tag(0) + Text("Stats").tag(1) + Text("Features").tag(2) + } + .pickerStyle(.segmented) + .padding(.horizontal) + .padding(.bottom, 8) + + Divider() + + Group { + if vm.activeTab == 0 { + RoutesView(vm: vm) + } else if vm.activeTab == 1 { + StatsView(vm: vm) + } else { + FeaturesView(vm: vm) + } + } + .frame(height: 320) + + Divider() + + HStack { + Text("Up: \(vm.statsLabel)").font(.caption).foregroundColor(.secondary) + Spacer() + Button("Quit") { NSApplication.shared.terminate(nil) } + .buttonStyle(.plain) + .font(.caption) + .foregroundColor(.secondary) + } + .padding(10) + } + .frame(width: 320) + .background(.ultraThinMaterial) + } + .menuBarExtraStyle(.window) + } +} + +struct RoutesView: View { + @ObservedObject var vm: RelayViewModel + + var body: some View { + VStack(spacing: 0) { + HStack { + Text(vm.currentCode).bold() + if !vm.currentProvider.isEmpty { + Text("(\(vm.currentProvider))").font(.caption).foregroundColor(.secondary) + } + Spacer() + Button(action: { vm.refreshAll() }) { + Image(systemName: "arrow.clockwise") + }.buttonStyle(.plain) + } + .padding(8) + .background(Color.primary.opacity(0.05)) + + ScrollView { + LazyVStack(spacing: 1) { + ForEach(vm.routes) { r in + let isActive = (vm.currentCode == r.code && vm.currentProvider == r.provider) + + Button(action: { vm.selectRoute(r) }) { + HStack { + Text(r.flag) + Text(r.name).foregroundColor(isActive ? .green : .primary) + if !r.provider.isEmpty { + Text(r.provider).font(.caption).foregroundColor(.secondary) + } + Spacer() + if isActive { + Image(systemName: "checkmark").foregroundColor(.green) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 12) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .background(isActive ? Color.primary.opacity(0.1) : Color.clear) + } + } + } + + HStack(spacing: 12) { + Toggle("Share", isOn: Binding( + get: { vm.isShared }, + set: { vm.toggleShare($0) } + )) + .toggleStyle(.switch) + .controlSize(.small) + + Divider().frame(height: 16) + + Toggle("80% Loss", isOn: Binding( + get: { vm.isEbalka }, + set: { vm.toggleEbalka($0) } + )) + .toggleStyle(.switch) + .controlSize(.small) + } + .padding(8) + .background(Color.primary.opacity(0)) + } + } +} + +struct StatsView: View { + @ObservedObject var vm: RelayViewModel + + var body: some View { + ScrollView { + LazyVStack(spacing: 4) { + if vm.serverList.isEmpty { + Text("No active servers").foregroundColor(.secondary).padding() + } + ForEach(vm.serverList) { item in + let d = item.detail + let ping = vm.avgPing(d.ping) + let pingText = String(format: "%.3f", ping) + let pColor: Color = ping < 50 ? .green : (ping < 150 ? .yellow : .red) + + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(vm.flag(d.country) + " " + item.id).bold().foregroundColor(.primary) + Spacer() + Text("\(d.connected ?? 0) users").font(.caption).foregroundColor(.green) + } + HStack { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 4) { + Text("↓").foregroundColor(.green) + Text("\(vm.fmtBytes(d.traffic?[0]))") + Text("(\(vm.fmtSpeed(d.speed?[0])))").foregroundColor(.secondary) + } + HStack(spacing: 4) { + Text("↑").foregroundColor(.blue) + Text("\(vm.fmtBytes(d.traffic?[1]))") + Text("(\(vm.fmtSpeed(d.speed?[1])))").foregroundColor(.secondary) + } + } + .font(.system(size: 10)) + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text("\(pingText) ms").font(.caption).bold().foregroundColor(pColor) + Text(vm.fmtTime(d.uptime ?? 0)).font(.system(size: 10)).foregroundColor(.secondary) + } + } + HStack(spacing: 8) { + TinyBtn("Connect") { vm.srvAction("connect_by_id", item.id) } + TinyBtn("Kill") { vm.srvAction("kill_by_id", item.id) } + TinyBtn("DPI") { vm.srvAction("dpi_test_by_id", item.id) } + }.padding(.top, 2) + } + .padding(10) + .background(Color.primary.opacity(0.05)) + .cornerRadius(6) + .padding(.horizontal, 8) + } + } + .padding(.top, 4) + } + .onAppear { vm.fetchStats() } + } +} + +struct FeaturesView: View { + @ObservedObject var vm: RelayViewModel + + var body: some View { + ScrollView { + LazyVStack { + ForEach(vm.features) { f in + HStack { + VStack(alignment: .leading) { + Text(f.name).font(.subheadline) + Text(f.desc).font(.caption2).foregroundColor(.secondary) + } + Spacer() + Toggle("", isOn: Binding( + get: { f.value }, + set: { vm.toggleFeature(f.name, $0) } + )).toggleStyle(.switch).labelsHidden() + } + .padding(8) + .background(Color.primary.opacity(0.05)) + .cornerRadius(6) + .padding(.horizontal, 8) + } + } + .padding(.top, 4) + } + } +} + +struct TinyBtn: View { + let t: String + let a: () -> Void + init(_ t: String, _ a: @escaping () -> Void) { self.t = t; self.a = a } + var body: some View { + Button(action: a) { + Text(t).font(.system(size: 10)).padding(.horizontal, 6).padding(.vertical, 3) + .background(Color.primary.opacity(0.1)) + .cornerRadius(4) + }.buttonStyle(.plain) + } +}