mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Refactor the apps routing structure to enable app-wide navigation through a Router to improve how deep link URLs are handled
This commit is contained in:
parent
e0640143df
commit
3a746af27e
25 changed files with 1056 additions and 575 deletions
|
|
@ -18853,6 +18853,7 @@
|
|||
}
|
||||
},
|
||||
"select.menu.item" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
25AECD4F2C2F723200862C8E /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 25AECD4E2C2F723200862C8E /* Localizable.xcstrings */; };
|
||||
25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0C2C285F00007E03CA /* Logger.swift */; };
|
||||
25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB152C28B1E4007E03CA /* AppData.swift */; };
|
||||
25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BD2C3F6D87008036E3 /* NavigationState.swift */; };
|
||||
25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BF2C3F6DA6008036E3 /* Router.swift */; };
|
||||
25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; };
|
||||
25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; };
|
||||
6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D825E612C34786C008DBEE4 /* CommonRegex.swift */; };
|
||||
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
|
||||
|
|
@ -205,6 +209,13 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
25F5D5CB2C4375A8008036E3 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = DDC2E14C26CE248E0042C5E4 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = DDC2E15326CE248E0042C5E4;
|
||||
remoteInfo = Meshtastic;
|
||||
};
|
||||
DDDE5A0129AF163E00490C6C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = DDC2E14C26CE248E0042C5E4 /* Project object */;
|
||||
|
|
@ -236,6 +247,11 @@
|
|||
2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientHistoryButton.swift; sourceTree = "<group>"; };
|
||||
251926912C3CB52300249DF5 /* DeleteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNodeButton.swift; sourceTree = "<group>"; };
|
||||
25AECD4E2C2F723200862C8E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
25F5D5BD2C3F6D87008036E3 /* NavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationState.swift; sourceTree = "<group>"; };
|
||||
25F5D5BF2C3F6DA6008036E3 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
|
||||
25F5D5C12C3F6E4B008036E3 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||
25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = "<group>"; };
|
||||
6D825E612C34786C008DBEE4 /* CommonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRegex.swift; sourceTree = "<group>"; };
|
||||
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
|
||||
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -464,6 +480,13 @@
|
|||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
25F5D5C42C4375A8008036E3 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DDC2E15126CE248E0042C5E4 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -500,6 +523,23 @@
|
|||
path = Actions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
25F5D5BC2C3F6D7B008036E3 /* Router */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
25F5D5BD2C3F6D87008036E3 /* NavigationState.swift */,
|
||||
25F5D5BF2C3F6DA6008036E3 /* Router.swift */,
|
||||
);
|
||||
path = Router;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
25F5D5C82C4375A8008036E3 /* MeshtasticTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
25F5D5D02C4375DF008036E3 /* RouterTests.swift */,
|
||||
);
|
||||
path = MeshtasticTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C9483F6B2773016700998F6B /* MapKitMap */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -739,6 +779,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */,
|
||||
DDC2E15626CE248E0042C5E4 /* Meshtastic */,
|
||||
DDDE59F729AF163D00490C6C /* Widgets */,
|
||||
25F5D5C82C4375A8008036E3 /* MeshtasticTests */,
|
||||
DDC2E15526CE248E0042C5E4 /* Products */,
|
||||
DD8EDE9226F97A2B00A5A10B /* Frameworks */,
|
||||
);
|
||||
|
|
@ -750,6 +791,7 @@
|
|||
children = (
|
||||
DDC2E15426CE248E0042C5E4 /* Meshtastic.app */,
|
||||
DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */,
|
||||
25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -757,6 +799,7 @@
|
|||
DDC2E15626CE248E0042C5E4 /* Meshtastic */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
25F5D5BC2C3F6D7B008036E3 /* Router */,
|
||||
DD7709392AA1ABA1007A8BF0 /* Tips */,
|
||||
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */,
|
||||
DD8ED9C6289CE4A100B3B0AB /* Enums */,
|
||||
|
|
@ -768,6 +811,7 @@
|
|||
DDC2E18926CE24F70042C5E4 /* Resources */,
|
||||
DDC2E18726CE24E40042C5E4 /* Views */,
|
||||
DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */,
|
||||
25F5D5C12C3F6E4B008036E3 /* AppState.swift */,
|
||||
DDC2E16526CE248F0042C5E4 /* Info.plist */,
|
||||
DDC2E15D26CE248F0042C5E4 /* Preview Content */,
|
||||
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */,
|
||||
|
|
@ -962,6 +1006,24 @@
|
|||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
25F5D5C62C4375A8008036E3 /* MeshtasticTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 25F5D5CF2C4375A8008036E3 /* Build configuration list for PBXNativeTarget "MeshtasticTests" */;
|
||||
buildPhases = (
|
||||
25F5D5C32C4375A8008036E3 /* Sources */,
|
||||
25F5D5C42C4375A8008036E3 /* Frameworks */,
|
||||
25F5D5C52C4375A8008036E3 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
25F5D5CC2C4375A8008036E3 /* PBXTargetDependency */,
|
||||
);
|
||||
name = MeshtasticTests;
|
||||
productName = MeshtasticTests;
|
||||
productReference = 25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
DDC2E15326CE248E0042C5E4 /* Meshtastic */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = DDC2E17E26CE248F0042C5E4 /* Build configuration list for PBXNativeTarget "Meshtastic" */;
|
||||
|
|
@ -1014,9 +1076,13 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1420;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
TargetAttributes = {
|
||||
25F5D5C62C4375A8008036E3 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
TestTargetID = DDC2E15326CE248E0042C5E4;
|
||||
};
|
||||
DDC2E15326CE248E0042C5E4 = {
|
||||
CreatedOnToolsVersion = 12.5.1;
|
||||
LastSwiftMigration = 1340;
|
||||
|
|
@ -1055,11 +1121,19 @@
|
|||
targets = (
|
||||
DDC2E15326CE248E0042C5E4 /* Meshtastic */,
|
||||
DDDE59F329AF163D00490C6C /* WidgetsExtension */,
|
||||
25F5D5C62C4375A8008036E3 /* MeshtasticTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
25F5D5C52C4375A8008036E3 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DDC2E15226CE248E0042C5E4 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -1106,6 +1180,14 @@
|
|||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
25F5D5C32C4375A8008036E3 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DDC2E15026CE248E0042C5E4 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -1160,6 +1242,7 @@
|
|||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
|
||||
25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */,
|
||||
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */,
|
||||
DDDB445429F8AD1600EE2349 /* Data.swift in Sources */,
|
||||
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
|
||||
|
|
@ -1186,6 +1269,7 @@
|
|||
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
|
||||
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */,
|
||||
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */,
|
||||
25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */,
|
||||
DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */,
|
||||
DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */,
|
||||
DDF45C372BC46A5A005ED5F2 /* TimeZone.swift in Sources */,
|
||||
|
|
@ -1264,6 +1348,7 @@
|
|||
DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */,
|
||||
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */,
|
||||
DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */,
|
||||
25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */,
|
||||
DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */,
|
||||
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */,
|
||||
DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */,
|
||||
|
|
@ -1303,6 +1388,11 @@
|
|||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
25F5D5CC2C4375A8008036E3 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = DDC2E15326CE248E0042C5E4 /* Meshtastic */;
|
||||
targetProxy = 25F5D5CB2C4375A8008036E3 /* PBXContainerItemProxy */;
|
||||
};
|
||||
DDDE5A0229AF163E00490C6C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
platformFilter = ios;
|
||||
|
|
@ -1312,6 +1402,52 @@
|
|||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
25F5D5CD2C4375A8008036E3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Meshtastic.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Meshtastic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
25F5D5CE2C4375A8008036E3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Meshtastic.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Meshtastic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
DDC2E17C26CE248F0042C5E4 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
|
@ -1574,6 +1710,15 @@
|
|||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
25F5D5CF2C4375A8008036E3 /* Build configuration list for PBXNativeTarget "MeshtasticTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
25F5D5CD2C4375A8008036E3 /* Debug */,
|
||||
25F5D5CE2C4375A8008036E3 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
DDC2E14F26CE248E0042C5E4 /* Build configuration list for PBXProject "Meshtastic" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DDC2E16926CE248F0042C5E4"
|
||||
BlueprintIdentifier = "25F5D5C62C4375A8008036E3"
|
||||
BuildableName = "MeshtasticTests.xctest"
|
||||
BlueprintName = "MeshtasticTests"
|
||||
ReferencedContainer = "container:Meshtastic.xcodeproj">
|
||||
|
|
|
|||
33
Meshtastic/AppState.swift
Normal file
33
Meshtastic/AppState.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
class AppState: ObservableObject {
|
||||
@Published
|
||||
var router: Router
|
||||
|
||||
@Published
|
||||
var unreadChannelMessages: Int
|
||||
|
||||
@Published
|
||||
var unreadDirectMessages: Int
|
||||
|
||||
var totalUnreadMessages: Int {
|
||||
unreadChannelMessages + unreadDirectMessages
|
||||
}
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
init(router: Router) {
|
||||
self.router = router
|
||||
self.unreadChannelMessages = 0
|
||||
self.unreadDirectMessages = 0
|
||||
|
||||
// Keep app icon badge count in sync with messages read status
|
||||
$unreadChannelMessages.combineLatest($unreadDirectMessages)
|
||||
.sink(receiveValue: { badgeCounts in
|
||||
UNUserNotificationCenter.current()
|
||||
.setBadgeCount(badgeCounts.0 + badgeCounts.1)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,12 @@ import OSLog
|
|||
// ---------------------------------------------------------------------------------------
|
||||
class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate, ObservableObject {
|
||||
|
||||
var appState: AppState
|
||||
|
||||
var context: NSManagedObjectContext?
|
||||
|
||||
static let shared = BLEManager()
|
||||
private var centralManager: CBCentralManager!
|
||||
|
||||
@Published var peripherals: [Peripheral] = []
|
||||
@Published var connectedPeripheral: Peripheral!
|
||||
@Published var lastConnectionError: String
|
||||
|
|
@ -24,7 +26,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
@Published var automaticallyReconnect: Bool = true
|
||||
@Published var mqttProxyConnected: Bool = false
|
||||
@Published var mqttError: String = ""
|
||||
@StateObject var appState = AppState.shared
|
||||
public var minimumVersion = "2.0.0"
|
||||
public var connectedVersion: String
|
||||
public var isConnecting: Bool = false
|
||||
|
|
@ -52,8 +53,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let LEGACY_LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2")
|
||||
let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547")
|
||||
|
||||
// MARK: init BLEManager
|
||||
override init() {
|
||||
// MARK: init
|
||||
|
||||
init(
|
||||
appState: AppState
|
||||
) {
|
||||
self.appState = appState
|
||||
self.lastConnectionError = ""
|
||||
self.connectedVersion = "0.0.0"
|
||||
super.init()
|
||||
|
|
@ -238,7 +243,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
subtitle: "\(peripheral.name ?? "unknown".localized)",
|
||||
content: e.localizedDescription,
|
||||
target: "bluetooth",
|
||||
path: "meshtastic://bluetooth"
|
||||
path: "meshtastic:///bluetooth"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
|
@ -258,7 +263,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
subtitle: "\(peripheral.name ?? "unknown".localized)",
|
||||
content: e.localizedDescription,
|
||||
target: "bluetooth",
|
||||
path: "meshtastic://bluetooth"
|
||||
path: "meshtastic:///bluetooth"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
|
@ -726,7 +731,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let version = decodedInfo.metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: decodedInfo.metadata.firmwareVersion))]
|
||||
nowKnown = true
|
||||
connectedVersion = String(version.dropLast())
|
||||
appState.firmwareVersion = connectedVersion
|
||||
UserDefaults.firmwareVersion = connectedVersion
|
||||
}
|
||||
let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame
|
||||
|
|
@ -739,7 +743,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
// Log any other unknownApp calls
|
||||
if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") }
|
||||
case .textMessageApp, .detectionSensorApp:
|
||||
textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
textMessageAppPacket(
|
||||
packet: decodedInfo.packet,
|
||||
wantRangeTestPackets: wantRangeTestPackets,
|
||||
connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0),
|
||||
context: context!,
|
||||
appState: appState
|
||||
)
|
||||
case .remoteHardwareApp:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
case .positionApp:
|
||||
|
|
@ -754,7 +764,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
adminAppPacket(packet: decodedInfo.packet, context: context!)
|
||||
case .replyApp:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Reply App handling as a text message")
|
||||
textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
textMessageAppPacket(
|
||||
packet: decodedInfo.packet,
|
||||
wantRangeTestPackets: wantRangeTestPackets,
|
||||
connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0),
|
||||
context: context!,
|
||||
appState: appState
|
||||
)
|
||||
case .ipTunnelApp:
|
||||
// MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED")
|
||||
|
|
@ -769,7 +785,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
case .rangeTestApp:
|
||||
if wantRangeTestPackets {
|
||||
textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: true, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
textMessageAppPacket(
|
||||
packet: decodedInfo.packet,
|
||||
wantRangeTestPackets: true,
|
||||
connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0),
|
||||
context: context!,
|
||||
appState: appState
|
||||
)
|
||||
} else {
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Range Test App Range testing is disabled.")
|
||||
}
|
||||
|
|
@ -893,12 +915,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
}
|
||||
// Set initial unread message badge states
|
||||
let appState = AppState.shared
|
||||
appState.unreadChannelMessages = fetchedNodeInfo[0].myInfo?.unreadMessages ?? 0
|
||||
appState.unreadDirectMessages = fetchedNodeInfo[0].user?.unreadMessages ?? 0
|
||||
// appState.connectedNode = fetchedNodeInfo[0]
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
|
||||
}
|
||||
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true {
|
||||
wantRangeTestPackets = true
|
||||
|
|
@ -1117,6 +1135,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return success
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func getPositionFromPhoneGPS(destNum: Int64) -> Position? {
|
||||
var positionPacket = Position()
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
|
|
@ -1162,6 +1181,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return positionPacket
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool {
|
||||
var adminPacket = AdminMessage()
|
||||
guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num) else {
|
||||
|
|
@ -1216,6 +1236,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum) else {
|
||||
|
|
@ -1256,6 +1277,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc func positionTimerFired(timer: Timer) {
|
||||
// Check for connected node
|
||||
if connectedPeripheral != nil {
|
||||
|
|
@ -3060,10 +3083,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")
|
||||
case .routerTextDirect:
|
||||
MeshLogger.log("💬 Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")
|
||||
textMessageAppPacket(packet: packet, wantRangeTestPackets: false, connectedNode: connectedNodeNum, storeForward: true, context: context)
|
||||
textMessageAppPacket(
|
||||
packet: packet,
|
||||
wantRangeTestPackets: false,
|
||||
connectedNode: connectedNodeNum,
|
||||
storeForward: true,
|
||||
context: context,
|
||||
appState: appState
|
||||
)
|
||||
case .routerTextBroadcast:
|
||||
MeshLogger.log("✉️ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")
|
||||
textMessageAppPacket(packet: packet, wantRangeTestPackets: false, connectedNode: connectedNodeNum, storeForward: true, context: context)
|
||||
textMessageAppPacket(
|
||||
packet: packet,
|
||||
wantRangeTestPackets: false,
|
||||
connectedNode: connectedNodeNum,
|
||||
storeForward: true,
|
||||
context: context,
|
||||
appState: appState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,18 +40,19 @@ class LocalNotificationManager {
|
|||
content.interruptionLevel = .timeSensitive
|
||||
|
||||
if notification.target != nil {
|
||||
content.userInfo["target"] = notification.target
|
||||
content.userInfo["target"] = notification.target
|
||||
}
|
||||
if notification.path != nil {
|
||||
content.userInfo["path"] = notification.path
|
||||
content.userInfo["path"] = notification.path
|
||||
}
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
|
||||
guard error == nil else { return }
|
||||
if let error {
|
||||
Logger.services.error("Error Scheduling Notification: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -710,7 +710,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
|
||||
content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.",
|
||||
target: "nodes",
|
||||
path: "meshtastic://nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
|
||||
path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
|
@ -744,7 +744,14 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
}
|
||||
}
|
||||
|
||||
func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connectedNode: Int64, storeForward: Bool = false, context: NSManagedObjectContext) {
|
||||
func textMessageAppPacket(
|
||||
packet: MeshPacket,
|
||||
wantRangeTestPackets: Bool,
|
||||
connectedNode: Int64,
|
||||
storeForward: Bool = false,
|
||||
context: NSManagedObjectContext,
|
||||
appState: AppState
|
||||
) {
|
||||
|
||||
var messageText = String(bytes: packet.decoded.payload, encoding: .utf8)
|
||||
let rangeRef = Reference(Int.self)
|
||||
|
|
@ -828,12 +835,10 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
|
||||
return
|
||||
}
|
||||
let appState = AppState.shared
|
||||
if newMessage.fromUser != nil && newMessage.toUser != nil {
|
||||
// Set Unread Message Indicators
|
||||
if packet.to == connectedNode {
|
||||
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
}
|
||||
if !(newMessage.fromUser?.mute ?? false) {
|
||||
// Create an iOS Notification for the received DM message and schedule it immediately
|
||||
|
|
@ -845,7 +850,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic://messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)"
|
||||
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
|
@ -860,7 +865,6 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
|
||||
if !fetchedMyInfo.isEmpty {
|
||||
appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
|
||||
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
|
||||
if channel.index == newMessage.channel {
|
||||
|
|
@ -876,7 +880,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic://messages?channel=\(newMessage.channel)&messageId=\(newMessage.messageId)")
|
||||
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)")
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
|
||||
|
|
@ -941,10 +945,10 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")",
|
||||
content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")",
|
||||
target: "map",
|
||||
path: "meshtastic://map?waypontid=\(waypoint.id)"
|
||||
path: "meshtastic:///map?waypointid=\(waypoint.id)"
|
||||
)
|
||||
]
|
||||
Logger.data.debug("meshtastic://map?waypontid=\(waypoint.id)")
|
||||
Logger.data.debug("meshtastic:///map?waypointid=\(waypoint.id)")
|
||||
manager.schedule()
|
||||
} catch {
|
||||
context.rollback()
|
||||
|
|
|
|||
|
|
@ -11,22 +11,44 @@ import TipKit
|
|||
@main
|
||||
struct MeshtasticAppleApp: App {
|
||||
|
||||
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate
|
||||
let persistenceController = PersistenceController.shared
|
||||
@ObservedObject private var bleManager: BLEManager = BLEManager.shared
|
||||
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self)
|
||||
private var appDelegate
|
||||
|
||||
@ObservedObject
|
||||
var appState: AppState
|
||||
|
||||
@ObservedObject
|
||||
private var bleManager: BLEManager
|
||||
|
||||
private let persistenceController: PersistenceController
|
||||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
|
||||
@State var saveChannels = false
|
||||
@State var incomingUrl: URL?
|
||||
@State var channelSettings: String?
|
||||
@State var addChannels = false
|
||||
@StateObject var appState = AppState.shared
|
||||
|
||||
init() {
|
||||
let persistenceController = PersistenceController.shared
|
||||
let appState = AppState(
|
||||
router: Router()
|
||||
)
|
||||
self._appState = ObservedObject(wrappedValue: appState)
|
||||
|
||||
self.bleManager = BLEManager(appState: appState)
|
||||
self.persistenceController = persistenceController
|
||||
|
||||
// Wire up router
|
||||
self.appDelegate.router = appState.router
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
ContentView(
|
||||
appState: appState
|
||||
)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(bleManager)
|
||||
.sheet(isPresented: $saveChannels) {
|
||||
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager)
|
||||
|
|
@ -34,14 +56,13 @@ struct MeshtasticAppleApp: App {
|
|||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
|
||||
|
||||
Logger.mesh.debug("URL received \(userActivity)")
|
||||
self.incomingUrl = userActivity.webpageURL
|
||||
|
||||
if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil {
|
||||
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
|
||||
self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false
|
||||
if ((self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil) {
|
||||
if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil {
|
||||
guard let cs = components.last!.components(separatedBy: "?").first else {
|
||||
return
|
||||
}
|
||||
|
|
@ -83,15 +104,8 @@ struct MeshtasticAppleApp: App {
|
|||
}
|
||||
self.saveChannels = true
|
||||
Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
|
||||
} else if url.absoluteString.lowercased().contains("meshtastic://") {
|
||||
appState.navigationPath = url.absoluteString
|
||||
let path = appState.navigationPath ?? ""
|
||||
if path.starts(with: "meshtastic://map") {
|
||||
AppState.shared.tabSelection = Tab.map
|
||||
} else if path.starts(with: "meshtastic://nodes") {
|
||||
AppState.shared.tabSelection = Tab.nodes
|
||||
}
|
||||
|
||||
} else if url.absoluteString.lowercased().contains("meshtastic:///") {
|
||||
appState.router.route(url: url)
|
||||
} else {
|
||||
saveChannels = false
|
||||
Logger.mesh.debug("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")")
|
||||
|
|
@ -179,13 +193,3 @@ struct MeshtasticAppleApp: App {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppState: ObservableObject {
|
||||
static let shared = AppState()
|
||||
|
||||
@Published var tabSelection: Tab = .ble
|
||||
@Published var unreadDirectMessages: Int = 0
|
||||
@Published var unreadChannelMessages: Int = 0
|
||||
@Published var firmwareVersion: String = "0.0.0"
|
||||
@Published var navigationPath: String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
|
||||
var router: Router?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
Logger.services.info("🚀 [App] Meshtstic Apple App launched!")
|
||||
// Default User Default Values
|
||||
|
|
@ -28,23 +31,30 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
return true
|
||||
}
|
||||
// Lets us show the notification in the app in the foreground
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
completionHandler([.list, .banner, .sound])
|
||||
}
|
||||
// This method is called when a user clicks on the notification
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
let targetValue = userInfo["target"] as? String
|
||||
let deepLink = userInfo["path"] as? String
|
||||
|
||||
AppState.shared.navigationPath = deepLink
|
||||
if targetValue == "map" {
|
||||
AppState.shared.tabSelection = Tab.map
|
||||
} else if targetValue == "messages" {
|
||||
AppState.shared.tabSelection = Tab.messages
|
||||
} else if targetValue == "nodes" {
|
||||
AppState.shared.tabSelection = Tab.nodes
|
||||
// This method is called when a user clicks on the notification
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
if let targetValue = userInfo["target"] as? String,
|
||||
let deepLink = userInfo["path"] as? String,
|
||||
let url = URL(string: deepLink) {
|
||||
Logger.services.info("userNotificationCenter didReceiveResponse \(targetValue) \(deepLink)")
|
||||
router?.route(url: url)
|
||||
} else {
|
||||
Logger.services.error("Failed to handle notification response: \(userInfo)")
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
subtitle: "\(newUser.longName ?? "unknown".localized)",
|
||||
content: "New Node has been discovered",
|
||||
target: "nodes",
|
||||
path: "meshtastic://nodes?nodenum=\(newUser.num)"
|
||||
path: "meshtastic:///nodes?nodenum=\(newUser.num)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
|
|
|||
106
Meshtastic/Router/NavigationState.swift
Normal file
106
Meshtastic/Router/NavigationState.swift
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: Messages
|
||||
|
||||
enum MessagesNavigationState: Hashable {
|
||||
case channels(
|
||||
channelId: Int32? = nil,
|
||||
messageId: Int64? = nil
|
||||
)
|
||||
case directMessages(
|
||||
userNum: Int64? = nil,
|
||||
messageId: Int64? = nil
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Map
|
||||
|
||||
enum MapNavigationState: Hashable {
|
||||
case selectedNode(Int64)
|
||||
case waypoint(Int64)
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
enum SettingsNavigationState: String {
|
||||
case about
|
||||
case appSettings
|
||||
case routes
|
||||
case routeRecorder
|
||||
case lora
|
||||
case channels
|
||||
case shareQRCode
|
||||
case user
|
||||
case bluetooth
|
||||
case device
|
||||
case display
|
||||
case network
|
||||
case position
|
||||
case power
|
||||
case ambientLighting
|
||||
case cannedMessages
|
||||
case detectionSensor
|
||||
case externalNotification
|
||||
case mqtt
|
||||
case rangeTest
|
||||
case paxCounter
|
||||
case ringtone
|
||||
case serial
|
||||
case storeAndForward
|
||||
case telemetry
|
||||
case meshLog
|
||||
case debugLogs
|
||||
case appFiles
|
||||
case firmwareUpdates
|
||||
}
|
||||
|
||||
enum NavigationState: Hashable {
|
||||
case messages(MessagesNavigationState? = nil)
|
||||
case bluetooth
|
||||
case nodes(selectedNodeNum: Int64? = nil)
|
||||
case map(MapNavigationState? = nil)
|
||||
case settings(SettingsNavigationState? = nil)
|
||||
}
|
||||
|
||||
// MARK: Tab Bar
|
||||
|
||||
extension NavigationState {
|
||||
enum Tab: String, Hashable {
|
||||
case messages
|
||||
case bluetooth
|
||||
case nodes
|
||||
case map
|
||||
case settings
|
||||
}
|
||||
|
||||
var tab: Tab {
|
||||
get {
|
||||
switch self {
|
||||
case .messages:
|
||||
.messages
|
||||
case .bluetooth:
|
||||
.bluetooth
|
||||
case .nodes:
|
||||
.nodes
|
||||
case .map:
|
||||
.map
|
||||
case .settings:
|
||||
.settings
|
||||
}
|
||||
}
|
||||
set {
|
||||
self = switch newValue {
|
||||
case .messages:
|
||||
.messages()
|
||||
case .bluetooth:
|
||||
.bluetooth
|
||||
case .nodes:
|
||||
.nodes()
|
||||
case .map:
|
||||
.map()
|
||||
case .settings:
|
||||
.settings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
Meshtastic/Router/Router.swift
Normal file
109
Meshtastic/Router/Router.swift
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import CoreData
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
class Router: ObservableObject {
|
||||
|
||||
@Published
|
||||
var navigationState: NavigationState
|
||||
|
||||
init(
|
||||
navigationState: NavigationState = .bluetooth
|
||||
) {
|
||||
self.navigationState = navigationState
|
||||
}
|
||||
|
||||
func route(to destination: NavigationState) {
|
||||
navigationState = destination
|
||||
}
|
||||
|
||||
func route(url: URL) {
|
||||
guard url.scheme == "meshtastic" else {
|
||||
Logger.services.error("Received routing URL \(url) with invalid scheme. Ignoring route.")
|
||||
return
|
||||
}
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
Logger.services.error("Received routing URL \(url) with invalid host path. Ignoring route.")
|
||||
return
|
||||
}
|
||||
|
||||
if components.path == "/messages" {
|
||||
routeMessages(components)
|
||||
} else if components.path == "/bluetooth" {
|
||||
route(to: .bluetooth)
|
||||
} else if components.path == "/nodes" {
|
||||
routeNodes(components)
|
||||
} else if components.path == "/map" {
|
||||
routeMap(components)
|
||||
} else if components.path.hasPrefix("/settings") {
|
||||
routeSettings(components)
|
||||
} else {
|
||||
Logger.services.warning("Failed to route url: \(url)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Routing Helpers
|
||||
|
||||
private func routeMessages(
|
||||
_ components: URLComponents
|
||||
) {
|
||||
let channelId = components.queryItems?
|
||||
.first(where: { $0.name == "channelId" })?
|
||||
.value
|
||||
.flatMap(Int32.init)
|
||||
let userNum = components.queryItems?
|
||||
.first(where: { $0.name == "userNum" })?
|
||||
.value
|
||||
.flatMap(Int64.init)
|
||||
let messageId = components.queryItems?
|
||||
.first(where: { $0.name == "messageId" })?
|
||||
.value
|
||||
.flatMap(Int64.init)
|
||||
|
||||
let state: MessagesNavigationState? = if let channelId {
|
||||
.channels(channelId: channelId, messageId: messageId)
|
||||
} else if let userNum {
|
||||
.directMessages(userNum: userNum, messageId: messageId)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
route(to: .messages(state))
|
||||
}
|
||||
|
||||
private func routeNodes(_ components: URLComponents) {
|
||||
let nodeId = components.queryItems?
|
||||
.first(where: { $0.name == "nodenum" })?
|
||||
.value
|
||||
.flatMap(Int64.init)
|
||||
route(to: .nodes(selectedNodeNum: nodeId))
|
||||
}
|
||||
|
||||
private func routeMap(_ components: URLComponents) {
|
||||
let nodeId = components.queryItems?
|
||||
.first(where: { $0.name == "nodenum" })?
|
||||
.value
|
||||
.flatMap(Int64.init)
|
||||
let waypointId = components.queryItems?
|
||||
.first(where: { $0.name == "waypointId" })?
|
||||
.value
|
||||
.flatMap(Int64.init)
|
||||
if let nodeId {
|
||||
route(to: .map(.selectedNode(nodeId)))
|
||||
} else if let waypointId {
|
||||
route(to: .map(.waypoint(waypointId)))
|
||||
} else {
|
||||
route(to: .map())
|
||||
}
|
||||
}
|
||||
|
||||
private func routeSettings(_ components: URLComponents) {
|
||||
let settingFromPath = components.path
|
||||
.split(separator: "/")
|
||||
.dropFirst()
|
||||
.first
|
||||
.flatMap(String.init)
|
||||
.flatMap(SettingsNavigationState.init(rawValue:))
|
||||
|
||||
route(to: .settings(settingFromPath))
|
||||
}
|
||||
}
|
||||
|
|
@ -6,75 +6,69 @@ import SwiftUI
|
|||
|
||||
@available(iOS 17.0, *)
|
||||
struct ContentView: View {
|
||||
@ObservedObject
|
||||
var appState: AppState
|
||||
|
||||
private var tabBinding: Binding<NavigationState.Tab> {
|
||||
Binding(
|
||||
get: {
|
||||
appState.router.navigationState.tab
|
||||
},
|
||||
set: { newValue in
|
||||
appState.router.navigationState.tab = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
var body: some View {
|
||||
TabView(selection: $appState.tabSelection) {
|
||||
Messages()
|
||||
.tabItem {
|
||||
Label("messages", systemImage: "message")
|
||||
}
|
||||
.tag(Tab.contacts)
|
||||
.badge(appState.unreadDirectMessages + appState.unreadChannelMessages)
|
||||
TabView(selection: tabBinding) {
|
||||
Messages(
|
||||
router: appState.router,
|
||||
unreadChannelMessages: $appState.unreadChannelMessages,
|
||||
unreadDirectMessages: $appState.unreadDirectMessages
|
||||
)
|
||||
.tabItem {
|
||||
Label("messages", systemImage: "message")
|
||||
}
|
||||
.tag(NavigationState.Tab.messages)
|
||||
.badge(appState.totalUnreadMessages)
|
||||
|
||||
Connect()
|
||||
.tabItem {
|
||||
Label("bluetooth", systemImage: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
.tag(Tab.ble)
|
||||
NodeList()
|
||||
.tabItem {
|
||||
Label("nodes", systemImage: "flipphone")
|
||||
}
|
||||
.tag(Tab.nodes)
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
if UserDefaults.mapUseLegacy {
|
||||
NodeMap()
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
}
|
||||
.tag(Tab.map)
|
||||
} else {
|
||||
MeshMap()
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
}
|
||||
.tag(Tab.map)
|
||||
}
|
||||
} else {
|
||||
NodeMap()
|
||||
.tag(NavigationState.Tab.bluetooth)
|
||||
|
||||
NodeList(
|
||||
router: appState.router
|
||||
)
|
||||
.tabItem {
|
||||
Label("nodes", systemImage: "flipphone")
|
||||
}
|
||||
.tag(NavigationState.Tab.nodes)
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *), !UserDefaults.mapUseLegacy {
|
||||
MeshMap(router: appState.router)
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
}
|
||||
.tag(Tab.map)
|
||||
.tag(NavigationState.Tab.map)
|
||||
} else {
|
||||
NodeMap(router: appState.router)
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
}
|
||||
.tag(NavigationState.Tab.map)
|
||||
}
|
||||
Settings()
|
||||
.tabItem {
|
||||
Label("settings", systemImage: "gear")
|
||||
.font(.title)
|
||||
}
|
||||
.tag(Tab.settings)
|
||||
|
||||
Settings(
|
||||
router: appState.router
|
||||
)
|
||||
.tabItem {
|
||||
Label("settings", systemImage: "gear")
|
||||
.font(.title)
|
||||
}
|
||||
.tag(NavigationState.Tab.settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
// #Preview {
|
||||
// if #available(iOS 17.0, *) {
|
||||
// // ContentView(deepLinkManager: .init())
|
||||
// } else {
|
||||
// // Fallback on earlier versions
|
||||
// }
|
||||
// }
|
||||
|
||||
// struct ContentView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ContentView()
|
||||
// }
|
||||
// }
|
||||
|
||||
enum Tab: Hashable {
|
||||
case contacts
|
||||
case messages
|
||||
case map
|
||||
case ble
|
||||
case nodes
|
||||
case settings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import OSLog
|
|||
|
||||
struct ChannelList: View {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
|
|
@ -142,7 +141,6 @@ struct ChannelList: View {
|
|||
Button(role: .destructive) {
|
||||
deleteChannelMessages(channel: channelSelection!, context: context)
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
channelSelection = nil
|
||||
} label: {
|
||||
Text("delete")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import OSLog
|
|||
import SwiftUI
|
||||
|
||||
struct ChannelMessageList: View {
|
||||
@StateObject var appState = AppState.shared
|
||||
@EnvironmentObject var appState: AppState
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
|
|
@ -80,7 +80,6 @@ struct ChannelMessageList: View {
|
|||
|
||||
TapbackResponses(message: message) {
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +117,6 @@ struct ChannelMessageList: View {
|
|||
try context.save()
|
||||
Logger.data.info("📖 [App] Read message \(message.messageId) ")
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
} catch {
|
||||
Logger.data.error("Failed to read message \(message.messageId): \(error.localizedDescription)")
|
||||
|
|
|
|||
|
|
@ -14,52 +14,72 @@ import TipKit
|
|||
|
||||
struct Messages: View {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@ObservedObject
|
||||
var router: Router
|
||||
|
||||
@Binding
|
||||
var unreadChannelMessages: Int
|
||||
|
||||
@Binding
|
||||
var unreadDirectMessages: Int
|
||||
|
||||
// Aliases the navigation state for the NavigationSplitView sidebar selection
|
||||
private var messagesSelection: Binding<MessagesNavigationState?> {
|
||||
Binding(
|
||||
get: {
|
||||
guard case .messages(let state) = router.navigationState else {
|
||||
return nil
|
||||
}
|
||||
return state
|
||||
},
|
||||
set: { newValue in
|
||||
router.navigationState = .messages(newValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@State var node: NodeInfoEntity?
|
||||
@State private var userSelection: UserEntity? // Nothing selected by default.
|
||||
@State private var channelSelection: ChannelEntity? // Nothing selected by default.
|
||||
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
|
||||
enum MessagesSidebar {
|
||||
case groupMessages
|
||||
case directMessages
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
// Sidebar
|
||||
List {
|
||||
NavigationLink {
|
||||
ChannelList(node: node)
|
||||
} label: {
|
||||
Image(systemName: "person.3")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.brightness(0.2)
|
||||
.font(.title)
|
||||
Text("channels")
|
||||
.font(.title2)
|
||||
.badge(appState.unreadChannelMessages)
|
||||
.padding(.vertical)
|
||||
List(selection: messagesSelection) {
|
||||
NavigationLink(value: MessagesNavigationState.channels()) {
|
||||
Label {
|
||||
Text("channels")
|
||||
.badge(unreadChannelMessages)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
} icon: {
|
||||
Image(systemName: "person.3")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
}
|
||||
|
||||
}
|
||||
NavigationLink {
|
||||
UserList(node: node)
|
||||
} label: {
|
||||
Image(systemName: "person.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.brightness(0.2)
|
||||
.font(.largeTitle)
|
||||
Text("direct.messages")
|
||||
.font(.title2)
|
||||
.badge(appState.unreadDirectMessages)
|
||||
.padding(.vertical)
|
||||
NavigationLink(value: MessagesNavigationState.directMessages()) {
|
||||
Label {
|
||||
Text("direct.messages")
|
||||
.badge(unreadDirectMessages)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
} icon: {
|
||||
Image(systemName: "person.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(MessagesTip(), arrowEdge: .top)
|
||||
}
|
||||
|
|
@ -67,48 +87,33 @@ struct Messages: View {
|
|||
.navigationTitle("messages")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
if (newPath?.hasPrefix("meshtastic://messages")) != nil {
|
||||
|
||||
if let urlComponent = URLComponents(string: newPath ?? "") {
|
||||
let queryItems = urlComponent.queryItems
|
||||
let messageId = queryItems?.first(where: { $0.name == "messageId" })?.value
|
||||
let channel = queryItems?.first(where: { $0.name == "channel" })?.value
|
||||
|
||||
if let channel {
|
||||
Logger.services.info("Deep Link Channel \(channel)")
|
||||
// selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
|
||||
// AppState.shared.navigationPath = nil
|
||||
} else {
|
||||
Logger.services.info("Channel Deep Link not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
if UserDefaults.preferredPeripheralId.count > 0 {
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(UserDefaults.preferredPeripheralNum))
|
||||
do {
|
||||
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
|
||||
// Found a node, check it for a region
|
||||
if !fetchedNode.isEmpty {
|
||||
node = fetchedNode[0]
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
let nodeId = Int64(UserDefaults.preferredPeripheralNum)
|
||||
if nodeId > 0 {
|
||||
node = getNodeInfo(id: nodeId, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
} content: {
|
||||
|
||||
if case .messages(let state) = router.navigationState {
|
||||
switch state {
|
||||
case .channels:
|
||||
// TODO: support linking to the channel
|
||||
ChannelList(node: node)
|
||||
case .directMessages(userNum: let userNum, messageId: _):
|
||||
UserList(
|
||||
node: node,
|
||||
selectedUserNum: userNum
|
||||
)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
} detail: {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import TipKit
|
|||
|
||||
struct UserList: View {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@State private var searchText = ""
|
||||
|
|
@ -161,7 +160,6 @@ struct UserList: View {
|
|||
Button(role: .destructive) {
|
||||
deleteUserMessages(user: userSelection!, context: context)
|
||||
context.refresh(node!.user!, mergeChanges: true)
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
} label: {
|
||||
Text("delete")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import CoreData
|
|||
import OSLog
|
||||
|
||||
struct UserMessageList: View {
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
// Keyboard State
|
||||
@FocusState var messageFieldFocused: Bool
|
||||
// View State Items
|
||||
|
|
@ -64,7 +65,6 @@ struct UserMessageList: View {
|
|||
|
||||
TapbackResponses(message: message) {
|
||||
appState.unreadDirectMessages = user.unreadMessages
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
}
|
||||
|
||||
HStack {
|
||||
|
|
@ -102,7 +102,6 @@ struct UserMessageList: View {
|
|||
try context.save()
|
||||
Logger.data.info("📖 [App] Read message \(message.messageId) ")
|
||||
appState.unreadDirectMessages = user.unreadMessages
|
||||
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
|
||||
|
||||
} catch {
|
||||
Logger.data.error("Failed to read message \(message.messageId): \(error.localizedDescription)")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import MapKit
|
|||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMapContent: MapContent {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
/// Parameters
|
||||
@Binding var showUserLocation: Bool
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ struct MeshMap: View {
|
|||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@StateObject var appState = AppState.shared
|
||||
|
||||
@ObservedObject
|
||||
var router: Router
|
||||
|
||||
/// Parameters
|
||||
@State var showUserLocation: Bool = true
|
||||
/// Map State User Defaults
|
||||
|
|
@ -106,33 +109,10 @@ struct MeshMap: View {
|
|||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
}
|
||||
// .onChange(of: (appState.navigationPath)) { newPath in
|
||||
//
|
||||
// if ((newPath?.hasPrefix("meshtastic://open-waypoint")) != nil) {
|
||||
// guard let url = URL(string: appState.navigationPath ?? "NONE") else {
|
||||
// logger.error("Invalid URL")
|
||||
// return
|
||||
// }
|
||||
// guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
||||
// logger.error("Invalid URL Components")
|
||||
// return
|
||||
// }
|
||||
// guard let action = components.host, action == "open-waypoint" else {
|
||||
// logger.error("Unknown waypoint URL action")
|
||||
// return
|
||||
// }
|
||||
// guard let waypointId = components.queryItems?.first(where: { $0.name == "id" })?.value else {
|
||||
// logger.error("Waypoint id not found")
|
||||
// return
|
||||
// }
|
||||
// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else {
|
||||
// logger.error("Waypoint not found")
|
||||
// return
|
||||
// }
|
||||
// //showWaypoints = true
|
||||
// //position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60))
|
||||
// }
|
||||
// }
|
||||
.onChange(of: router.navigationState) {
|
||||
guard case .map(let selectedNodeNum) = router.navigationState else { return }
|
||||
//TODO: handle deep link for waypoints
|
||||
}
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
|
|
|
|||
|
|
@ -9,8 +9,15 @@ import CoreLocation
|
|||
import OSLog
|
||||
|
||||
struct NodeList: View {
|
||||
@Environment(\.managedObjectContext)
|
||||
var context
|
||||
|
||||
@EnvironmentObject
|
||||
var bleManager: BLEManager
|
||||
|
||||
@ObservedObject
|
||||
var router: Router
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
@State private var searchText = ""
|
||||
|
|
@ -37,14 +44,11 @@ struct NodeList: View {
|
|||
|
||||
@SceneStorage("selectedDetailView") var selectedDetailView: String?
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)
|
||||
],
|
||||
animation: .spring
|
||||
)
|
||||
|
|
@ -198,7 +202,6 @@ struct NodeList: View {
|
|||
} else {
|
||||
Text("Select something to view")
|
||||
}
|
||||
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.onChange(of: searchText) { _ in
|
||||
|
|
@ -242,32 +245,23 @@ struct NodeList: View {
|
|||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
guard let deepLink = newPath else {
|
||||
return
|
||||
}
|
||||
if deepLink.hasPrefix("meshtastic://nodes") {
|
||||
|
||||
if let urlComponent = URLComponents(string: deepLink) {
|
||||
let queryItems = urlComponent.queryItems
|
||||
let nodeNum = queryItems?.first(where: { $0.name == "nodenum" })?.value
|
||||
if nodeNum == nil {
|
||||
Logger.data.debug("nodeNum not found")
|
||||
} else {
|
||||
selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
|
||||
AppState.shared.navigationPath = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: distanceFilter) { _ in
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
// Handle deep link routing
|
||||
if case .nodes(let selected) = router.navigationState, let selectedNodeNum = selected {
|
||||
self.selectedNode = getNodeInfo(id: selectedNodeNum, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ import CoreData
|
|||
struct NodeMap: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@StateObject var appState = AppState.shared
|
||||
|
||||
@ObservedObject
|
||||
var router: Router
|
||||
|
||||
@State var selectedMapLayer: MapLayer = UserDefaults.mapLayer
|
||||
@State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering
|
||||
@State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines
|
||||
|
|
|
|||
|
|
@ -14,44 +14,22 @@ import TipKit
|
|||
struct Settings: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default)
|
||||
@FetchRequest(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(key: "favorite", ascending: false),
|
||||
NSSortDescriptor(key: "user.longName", ascending: true)
|
||||
],
|
||||
animation: .default
|
||||
)
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
@State private var selectedNode: Int = 0
|
||||
@State private var preferredNodeNum: Int = 0
|
||||
@State private var selection: SettingsSidebar = .about
|
||||
|
||||
enum SettingsSidebar {
|
||||
case appSettings
|
||||
case routes
|
||||
case routeRecorder
|
||||
case shareChannels
|
||||
case userConfig
|
||||
case loraConfig
|
||||
case channelConfig
|
||||
case bluetoothConfig
|
||||
case deviceConfig
|
||||
case displayConfig
|
||||
case networkConfig
|
||||
case paxCounterConfig
|
||||
case positionConfig
|
||||
case powerConfig
|
||||
case ambientLightingConfig
|
||||
case cannedMessagesConfig
|
||||
case detectionSensorConfig
|
||||
case externalNotificationConfig
|
||||
case mqttConfig
|
||||
case rangeTestConfig
|
||||
case ringtoneConfig
|
||||
case serialConfig
|
||||
case storeAndForwardConfig
|
||||
case telemetryConfig
|
||||
case meshLog
|
||||
case adminMessageLog
|
||||
case about
|
||||
case appLog
|
||||
case appData
|
||||
}
|
||||
@ObservedObject
|
||||
var router: Router
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var radioConfigurationSection: some View {
|
||||
Section("radio.configuration") {
|
||||
|
|
@ -77,9 +55,7 @@ struct Settings: View {
|
|||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
NavigationLink(value: SettingsNavigationState.lora) {
|
||||
Label {
|
||||
Text("lora")
|
||||
} icon: {
|
||||
|
|
@ -87,72 +63,272 @@ struct Settings: View {
|
|||
.rotationEffect(.degrees(-90))
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.loraConfig)
|
||||
|
||||
NavigationLink {
|
||||
Channels(node: node)
|
||||
} label: {
|
||||
NavigationLink(value: SettingsNavigationState.channels) {
|
||||
Label {
|
||||
Text("channels")
|
||||
} icon: {
|
||||
Image(systemName: "fibrechannel")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.channelConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
|
||||
NavigationLink {
|
||||
ShareChannels(node: node)
|
||||
} label: {
|
||||
NavigationLink(value: SettingsNavigationState.shareQRCode) {
|
||||
Label {
|
||||
Text("share.channels")
|
||||
} icon: {
|
||||
Image(systemName: "qrcode")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.shareChannels)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
}
|
||||
}
|
||||
|
||||
var deviceConfigurationSection: some View {
|
||||
Section("device.configuration") {
|
||||
NavigationLink(value: SettingsNavigationState.user) {
|
||||
Label {
|
||||
Text("user")
|
||||
} icon: {
|
||||
Image(systemName: "person.crop.rectangle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.bluetooth) {
|
||||
Label {
|
||||
Text("bluetooth")
|
||||
} icon: {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.device) {
|
||||
Label {
|
||||
Text("device")
|
||||
} icon: {
|
||||
Image(systemName: "flipphone")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.display) {
|
||||
Label {
|
||||
Text("display")
|
||||
} icon: {
|
||||
Image(systemName: "display")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.network) {
|
||||
Label {
|
||||
Text("network")
|
||||
} icon: {
|
||||
Image(systemName: "network")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.position) {
|
||||
Label {
|
||||
Text("position")
|
||||
} icon: {
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.power) {
|
||||
Label {
|
||||
Text("config.power.settings")
|
||||
} icon: {
|
||||
Image(systemName: "bolt.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var moduleConfigurationSection: some View {
|
||||
Section("module.configuration") {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink(value: SettingsNavigationState.ambientLighting) {
|
||||
Label {
|
||||
Text("ambient.lighting")
|
||||
} icon: {
|
||||
Image(systemName: "light.max")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.cannedMessages) {
|
||||
Label {
|
||||
Text("canned.messages")
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.rectangle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.detectionSensor) {
|
||||
Label {
|
||||
Text("detection.sensor")
|
||||
} icon: {
|
||||
Image(systemName: "sensor")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.externalNotification) {
|
||||
Label {
|
||||
Text("external.notification")
|
||||
} icon: {
|
||||
Image(systemName: "megaphone")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.mqtt) {
|
||||
Label {
|
||||
Text("mqtt")
|
||||
} icon: {
|
||||
Image(systemName: "dot.radiowaves.up.forward")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.rangeTest) {
|
||||
Label {
|
||||
Text("range.test")
|
||||
} icon: {
|
||||
Image(systemName: "point.3.connected.trianglepath.dotted")
|
||||
}
|
||||
}
|
||||
|
||||
if let node = nodes.first(where: { $0.num == preferredNodeNum }),
|
||||
node.metadata?.hasWifi ?? false {
|
||||
NavigationLink(value: SettingsNavigationState.paxCounter) {
|
||||
Label {
|
||||
Text("config.module.paxcounter.settings")
|
||||
} icon: {
|
||||
Image(systemName: "figure.walk.motion")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.ringtone) {
|
||||
Label {
|
||||
Text("ringtone")
|
||||
} icon: {
|
||||
Image(systemName: "music.note.list")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.serial) {
|
||||
Label {
|
||||
Text("serial")
|
||||
} icon: {
|
||||
Image(systemName: "terminal")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.storeAndForward) {
|
||||
Label {
|
||||
Text("storeforward")
|
||||
} icon: {
|
||||
Image(systemName: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.telemetry) {
|
||||
Label {
|
||||
Text("telemetry")
|
||||
} icon: {
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var loggingSection: some View {
|
||||
Section(header: Text("logging")) {
|
||||
NavigationLink(value: SettingsNavigationState.meshLog) {
|
||||
Label {
|
||||
Text("mesh.log")
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.rectangle")
|
||||
}
|
||||
}
|
||||
|
||||
if #available (iOS 17.4, *) {
|
||||
NavigationLink(value: SettingsNavigationState.debugLogs) {
|
||||
Label {
|
||||
Text("Debug Logs")
|
||||
} icon: {
|
||||
Image(systemName: "stethoscope")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var developersSection: some View {
|
||||
Section(header: Text("Developers")) {
|
||||
NavigationLink(value: SettingsNavigationState.appFiles) {
|
||||
Label {
|
||||
Text("App Files")
|
||||
} icon: {
|
||||
Image(systemName: "folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var firmwareSection: some View {
|
||||
Section(header: Text("Firmware")) {
|
||||
NavigationLink(value: SettingsNavigationState.firmwareUpdates) {
|
||||
Label {
|
||||
Text("Firmware Updates")
|
||||
} icon: {
|
||||
Image(systemName: "arrow.up.arrow.down.square")
|
||||
}
|
||||
}
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
NavigationStack(
|
||||
path: Binding<[SettingsNavigationState]>(
|
||||
get: {
|
||||
guard case .settings(let route) = router.navigationState, let setting = route else {
|
||||
return []
|
||||
}
|
||||
return [setting]
|
||||
},
|
||||
set: { newPath in
|
||||
router.navigationState = .settings(newPath.first)
|
||||
}
|
||||
)
|
||||
) {
|
||||
let node = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
List {
|
||||
NavigationLink {
|
||||
AboutMeshtastic()
|
||||
} label: {
|
||||
NavigationLink(value: SettingsNavigationState.about) {
|
||||
Label {
|
||||
Text("about.meshtastic")
|
||||
} icon: {
|
||||
Image(systemName: "questionmark.app")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.about)
|
||||
NavigationLink {
|
||||
AppSettings()
|
||||
} label: {
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.appSettings) {
|
||||
Label {
|
||||
Text("appsettings")
|
||||
} icon: {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.appSettings)
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink {
|
||||
Routes()
|
||||
} label: {
|
||||
NavigationLink(value: SettingsNavigationState.routes) {
|
||||
Label {
|
||||
Text("routes")
|
||||
} icon: {
|
||||
Image(systemName: "road.lanes.curved.right")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.routes)
|
||||
NavigationLink {
|
||||
RouteRecorder()
|
||||
} label: {
|
||||
|
||||
NavigationLink(value: SettingsNavigationState.routeRecorder) {
|
||||
Label {
|
||||
Text("route.recorder")
|
||||
} icon: {
|
||||
|
|
@ -160,10 +336,9 @@ struct Settings: View {
|
|||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.routeRecorder)
|
||||
}
|
||||
|
||||
let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 ? true : false
|
||||
let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0
|
||||
|
||||
if !(node?.deviceConfig?.isManaged ?? false) {
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
|
|
@ -225,246 +400,84 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
radioConfigurationSection
|
||||
Section("device.configuration") {
|
||||
NavigationLink {
|
||||
UserConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("user")
|
||||
} icon: {
|
||||
Image(systemName: "person.crop.rectangle.fill")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.userConfig)
|
||||
NavigationLink {
|
||||
BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("bluetooth")
|
||||
} icon: {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.bluetoothConfig)
|
||||
NavigationLink {
|
||||
DeviceConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("device")
|
||||
} icon: {
|
||||
Image(systemName: "flipphone")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.deviceConfig)
|
||||
NavigationLink {
|
||||
DisplayConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("display")
|
||||
} icon: {
|
||||
Image(systemName: "display")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.displayConfig)
|
||||
NavigationLink {
|
||||
NetworkConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("network")
|
||||
} icon: {
|
||||
Image(systemName: "network")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.networkConfig)
|
||||
NavigationLink {
|
||||
PositionConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("position")
|
||||
} icon: {
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.positionConfig)
|
||||
|
||||
NavigationLink {
|
||||
PowerConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("config.power.settings")
|
||||
} icon: {
|
||||
Image(systemName: "bolt.fill")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.powerConfig)
|
||||
}
|
||||
Section("module.configuration") {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink {
|
||||
AmbientLightingConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("ambient.lighting")
|
||||
} icon: {
|
||||
Image(systemName: "light.max")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.ambientLightingConfig)
|
||||
}
|
||||
NavigationLink {
|
||||
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("canned.messages")
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.rectangle.fill")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.cannedMessagesConfig)
|
||||
NavigationLink {
|
||||
DetectionSensorConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("detection.sensor")
|
||||
} icon: {
|
||||
Image(systemName: "sensor")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.detectionSensorConfig)
|
||||
NavigationLink {
|
||||
ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("external.notification")
|
||||
} icon: {
|
||||
Image(systemName: "megaphone")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.externalNotificationConfig)
|
||||
NavigationLink {
|
||||
MQTTConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("mqtt")
|
||||
} icon: {
|
||||
Image(systemName: "dot.radiowaves.up.forward")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.mqttConfig)
|
||||
NavigationLink {
|
||||
RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("range.test")
|
||||
} icon: {
|
||||
Image(systemName: "point.3.connected.trianglepath.dotted")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.rangeTestConfig)
|
||||
if node?.metadata?.hasWifi ?? false {
|
||||
NavigationLink {
|
||||
PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("config.module.paxcounter.settings")
|
||||
} icon: {
|
||||
Image(systemName: "figure.walk.motion")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.paxCounterConfig)
|
||||
}
|
||||
NavigationLink {
|
||||
RtttlConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("ringtone")
|
||||
} icon: {
|
||||
Image(systemName: "music.note.list")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.ringtoneConfig)
|
||||
NavigationLink {
|
||||
SerialConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("serial")
|
||||
} icon: {
|
||||
Image(systemName: "terminal")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.serialConfig)
|
||||
NavigationLink {
|
||||
StoreForwardConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("storeforward")
|
||||
} icon: {
|
||||
Image(systemName: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.storeAndForwardConfig)
|
||||
NavigationLink {
|
||||
TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("telemetry")
|
||||
} icon: {
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.telemetryConfig)
|
||||
}
|
||||
Section(header: Text("logging")) {
|
||||
NavigationLink {
|
||||
MeshLog()
|
||||
} label: {
|
||||
Label {
|
||||
Text("mesh.log")
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.rectangle")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.meshLog)
|
||||
if #available (iOS 17.4, *) {
|
||||
NavigationLink {
|
||||
AppLog()
|
||||
} label: {
|
||||
Label {
|
||||
Text("Debug Logs")
|
||||
} icon: {
|
||||
Image(systemName: "stethoscope")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.appLog)
|
||||
}
|
||||
}
|
||||
deviceConfigurationSection
|
||||
moduleConfigurationSection
|
||||
loggingSection
|
||||
#if DEBUG
|
||||
Section(header: Text("Developers")) {
|
||||
NavigationLink {
|
||||
AppData()
|
||||
} label: {
|
||||
Label {
|
||||
Text("App Files")
|
||||
} icon: {
|
||||
Image(systemName: "folder")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.appData)
|
||||
}
|
||||
developersSection
|
||||
#endif
|
||||
Section(header: Text("Firmware")) {
|
||||
NavigationLink {
|
||||
Firmware(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
} label: {
|
||||
Label {
|
||||
Text("Firmware Updates")
|
||||
} icon: {
|
||||
Image(systemName: "arrow.up.arrow.down.square")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.about)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
firmwareSection
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: SettingsNavigationState.self) { destination in
|
||||
let node = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
switch destination {
|
||||
case .about:
|
||||
AboutMeshtastic()
|
||||
case .appSettings:
|
||||
AppSettings()
|
||||
case .routes:
|
||||
if #available(iOS 17.0, *) {
|
||||
Routes()
|
||||
}
|
||||
case .routeRecorder:
|
||||
if #available(iOS 17.0, *) {
|
||||
RouteRecorder()
|
||||
}
|
||||
case .lora:
|
||||
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
case .channels:
|
||||
Channels(node: node)
|
||||
case .shareQRCode:
|
||||
ShareChannels(node: node)
|
||||
case .user:
|
||||
UserConfig(node: node)
|
||||
case .bluetooth:
|
||||
BluetoothConfig(node: node)
|
||||
case .device:
|
||||
DeviceConfig(node: node)
|
||||
case .display:
|
||||
DisplayConfig(node: node)
|
||||
case .network:
|
||||
NetworkConfig(node: node)
|
||||
case .position:
|
||||
PositionConfig(node: node)
|
||||
case .power:
|
||||
PowerConfig(node: node)
|
||||
case .ambientLighting:
|
||||
if #available(iOS 17.0, *) {
|
||||
AmbientLightingConfig(node: node)
|
||||
}
|
||||
case .cannedMessages:
|
||||
CannedMessagesConfig(node: node)
|
||||
case .detectionSensor:
|
||||
DetectionSensorConfig(node: node)
|
||||
case .externalNotification:
|
||||
ExternalNotificationConfig(node: node)
|
||||
case .mqtt:
|
||||
MQTTConfig(node: node)
|
||||
case .rangeTest:
|
||||
RangeTestConfig(node: node)
|
||||
case .paxCounter:
|
||||
PaxCounterConfig(node: node)
|
||||
case .ringtone:
|
||||
RtttlConfig(node: node)
|
||||
case .serial:
|
||||
SerialConfig(node: node)
|
||||
case .storeAndForward:
|
||||
StoreForwardConfig(node: node)
|
||||
case .telemetry:
|
||||
TelemetryConfig(node: node)
|
||||
case .meshLog:
|
||||
MeshLog()
|
||||
case .debugLogs:
|
||||
if #available(iOS 17.4, *) {
|
||||
AppLog()
|
||||
}
|
||||
case .appFiles:
|
||||
AppData()
|
||||
case .firmwareUpdates:
|
||||
Firmware(node: node)
|
||||
}
|
||||
}
|
||||
.onChange(of: UserDefaults.preferredPeripheralNum ) { newConnectedNode in
|
||||
|
|
@ -489,18 +502,11 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("settings")
|
||||
.navigationBarItems(leading:
|
||||
MeshtasticLogo()
|
||||
.navigationBarItems(
|
||||
leading: MeshtasticLogo()
|
||||
)
|
||||
}
|
||||
detail: {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("select.menu.item", systemImage: "gear")
|
||||
} else {
|
||||
Text("select.menu.item")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
MeshtasticTests/RouterTests.swift
Normal file
57
MeshtasticTests/RouterTests.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Meshtastic
|
||||
|
||||
final class RouterTests: XCTestCase {
|
||||
|
||||
func testInitialState() throws {
|
||||
XCTAssertEqual(Router().navigationState, .bluetooth)
|
||||
}
|
||||
|
||||
func testRouteTo() throws {
|
||||
let router = Router(navigationState: .bluetooth)
|
||||
router.route(to: .settings(.about))
|
||||
XCTAssertEqual(router.navigationState, .settings(.about))
|
||||
}
|
||||
|
||||
func testRouteURL() throws {
|
||||
// Messages
|
||||
try assertRoute("meshtastic:///messages", .messages())
|
||||
try assertRoute(
|
||||
"meshtastic:///messages?channelId=0&messageId=1122334455",
|
||||
.messages(.channels(channelId: 0, messageId: 1122334455))
|
||||
)
|
||||
try assertRoute(
|
||||
"meshtastic:///messages?userNum=123456789&messageId=9876543210",
|
||||
.messages(.directMessages(userNum: 123456789, messageId: 9876543210))
|
||||
)
|
||||
|
||||
// Bluetooth
|
||||
try assertRoute("meshtastic:///bluetooth", .bluetooth)
|
||||
|
||||
// Nodes
|
||||
try assertRoute("meshtastic:///nodes", .nodes())
|
||||
try assertRoute("meshtastic:///nodes?nodenum=1234567890", .nodes(selectedNodeNum: 1234567890))
|
||||
|
||||
// Map
|
||||
try assertRoute("meshtastic:///map", .map())
|
||||
try assertRoute("meshtastic:///map?waypointId=123456", .map(.waypoint(123456)))
|
||||
try assertRoute("meshtastic:///map?nodenum=1234567890", .map(.selectedNode(1234567890)))
|
||||
|
||||
// Settings
|
||||
try assertRoute("meshtastic:///settings", .settings())
|
||||
try assertRoute("meshtastic:///settings/about", .settings(.about))
|
||||
try assertRoute("meshtastic:///settings/invalidSetting", .settings())
|
||||
}
|
||||
|
||||
private func assertRoute(
|
||||
router: Router = Router(),
|
||||
_ urlString: String,
|
||||
_ destination: NavigationState
|
||||
) throws {
|
||||
let url = try XCTUnwrap(URL(string: urlString))
|
||||
router.route(url: url)
|
||||
XCTAssertEqual(router.navigationState, destination)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ struct WidgetsLiveActivity: Widget {
|
|||
|
||||
ActivityConfiguration(for: MeshActivityAttributes.self) { context in
|
||||
LiveActivityView(nodeName: context.attributes.name, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel, nodes: 17, nodesOnline: 7, timerRange: context.state.timerRange)
|
||||
.widgetURL(URL(string: "meshtastic://node/\(context.attributes.name)"))
|
||||
.widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)"))
|
||||
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
|
|
@ -95,7 +95,7 @@ struct WidgetsLiveActivity: Widget {
|
|||
.contentMargins(.trailing, 32, for: .expanded)
|
||||
.contentMargins([.leading, .top, .bottom], 6, for: .compactLeading)
|
||||
.contentMargins(.all, 6, for: .minimal)
|
||||
.widgetURL(URL(string: "meshtastic://node/\(context.attributes.name)"))
|
||||
.widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue