diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4a8369d0..05973189 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -107,6 +107,7 @@ DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */; }; DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */; }; DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; }; + DDB233CF2B5A140B00DA6FB1 /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB233CE2B5A140B00DA6FB1 /* CarPlaySceneDelegate.swift */; }; DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */; }; DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */; }; DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */; }; @@ -334,6 +335,7 @@ DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = ""; }; DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMap.swift; sourceTree = ""; }; DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; + DDB233CE2B5A140B00DA6FB1 /* CarPlaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlaySceneDelegate.swift; sourceTree = ""; }; DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothConfig.swift; sourceTree = ""; }; DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothModes.swift; sourceTree = ""; }; DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistanceText.swift; sourceTree = ""; }; @@ -698,6 +700,14 @@ path = Protobufs; sourceTree = ""; }; + DDB233CD2B5A13C600DA6FB1 /* CarPlay */ = { + isa = PBXGroup; + children = ( + DDB233CE2B5A140B00DA6FB1 /* CarPlaySceneDelegate.swift */, + ); + path = CarPlay; + sourceTree = ""; + }; DDB75A122A0593CD006ED576 /* Map */ = { isa = PBXGroup; children = ( @@ -736,6 +746,7 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + DDB233CD2B5A13C600DA6FB1 /* CarPlay */, DD7709392AA1ABA1007A8BF0 /* Tips */, DD90860A26F645B700DC5189 /* Meshtastic.entitlements */, DD8ED9C6289CE4A100B3B0AB /* Enums */, @@ -1171,6 +1182,7 @@ DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, + DDB233CF2B5A140B00DA6FB1 /* CarPlaySceneDelegate.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, diff --git a/Meshtastic/Assets.xcassets/RoundIcon.imageset/Contents.json b/Meshtastic/Assets.xcassets/RoundIcon.imageset/Contents.json new file mode 100644 index 00000000..0e5bd761 --- /dev/null +++ b/Meshtastic/Assets.xcassets/RoundIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "RoundIcon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "RoundIcon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/RoundIcon.imageset/RoundIcon@2x.png b/Meshtastic/Assets.xcassets/RoundIcon.imageset/RoundIcon@2x.png new file mode 100644 index 00000000..7982bf51 Binary files /dev/null and b/Meshtastic/Assets.xcassets/RoundIcon.imageset/RoundIcon@2x.png differ diff --git a/Meshtastic/Assets.xcassets/RoundIcon.imageset/RoundIcon@3x.png b/Meshtastic/Assets.xcassets/RoundIcon.imageset/RoundIcon@3x.png new file mode 100644 index 00000000..1930c19e Binary files /dev/null and b/Meshtastic/Assets.xcassets/RoundIcon.imageset/RoundIcon@3x.png differ diff --git a/Meshtastic/CarPlay/CarPlaySceneDelegate.swift b/Meshtastic/CarPlay/CarPlaySceneDelegate.swift new file mode 100644 index 00000000..c9a9a409 --- /dev/null +++ b/Meshtastic/CarPlay/CarPlaySceneDelegate.swift @@ -0,0 +1,182 @@ +// +// CarPlaySceneDelegate.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 1/18/24. +// + +import Foundation +import CarPlay + +@objc class CarPlaySceneDelegate: NSObject, CPTemplateApplicationSceneDelegate { + + private var interfaceController: CPInterfaceController? + private var savedTabBarTemplate: CPTabBarTemplate? + + // https://developer.apple.com/documentation/carplay/displaying_content_in_carplay + // CarPlay calls this function to initialize the scene. + func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) { + // Save the interface controller + self.interfaceController = interfaceController + + let template = tabBarTemplate() + self.savedTabBarTemplate = template + + // Create the root template (screen) and install it at the root of the navigation hierarchy. + interfaceController.setRootTemplate(template, animated: true, completion: nil) + } + + func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) { + self.interfaceController = nil + } + + private func tabBarTemplate() -> CPTabBarTemplate { + return CPTabBarTemplate(templates: [ + channelListTemplate(), + listTemplate(), + // gridTemplate(), + // informationTemplate(layout: .leading) + ]) + } + + private func replaceTabs() { + self.savedTabBarTemplate?.updateTemplates([ + channelListTemplate(), + listTemplate(), + gridTemplate(), + informationTemplate(layout: .leading), + informationTemplate(layout: .leading), + ]) + } + + private func channelListTemplate() -> CPListTemplate { + let template = CPListTemplate( + title: "Channels", + sections: [ + CPListSection(items: [ + listItem(), + listItem(), + ], header: nil, sectionIndexTitle: nil), + ] + ) + template.tabTitle = "Channels" + template.tabImage = UIImage(systemName: "fibrechannel")// UIImage(named: "RoundIcon")! + + return template + } + + private func listTemplate() -> CPListTemplate { + let template = CPListTemplate( + title: "Direct Messages", + sections: [ + CPListSection(items: [ + listItem(), + listItem(), + ], header: nil, sectionIndexTitle: nil), + ] + ) + template.tabTitle = "Nodes" + template.tabImage = UIImage(systemName: "message.fill")// UIImage(named: "RoundIcon")! + + return template + } + + private func listItem() -> CPListTemplateItem { + let item = CPListItem(text: "Text", detailText: "Detail Text", image: UIImage(named: "RoundIcon")!, accessoryImage: nil, accessoryType: .none) + + item.handler = { [weak self] (item, completion) in + guard let self = self else { + completion() + return + } + + self.interfaceController?.pushTemplate( + self.listTemplate(), + animated: true, + completion: { (didPresent, error) in + completion() + } + ) + } + + return item + } + + private func gridTemplate() -> CPGridTemplate { + let template = CPGridTemplate( + title: "Grid Title", + gridButtons: [ + gridButton(), + gridButton(), + gridButton(), + gridButton(), + gridButton(), + gridButton(), + ] + ) + template.tabTitle = "Grid" + template.tabImage = UIImage(named: "RoundIcon")! + + return template + } + + private func gridButton() -> CPGridButton { + return CPGridButton( + titleVariants: [ + "Maybe a bit much too long of a title", + "Medium Title", + "Title" + ], + image: UIImage(named: "RoundIcon")!, + handler: { [weak self] button in + guard let self = self else { return } + self.interfaceController?.pushTemplate( + self.gridTemplate(), + animated: true, + completion: nil + ) + } + ) + } + + private func informationTemplate(layout: CPInformationTemplateLayout) -> CPInformationTemplate { + let template = CPInformationTemplate( + title: "Information Title", + layout: layout, + items: [ + CPInformationItem(title: "Item\nTitle\nThird\nFourth", detail: "Item\nDetail\nThird line\nFourth line"), + CPInformationItem(title: "Item Title", detail: nil), + CPInformationItem(title: "Item Title", detail: "Item Detail"), + CPInformationItem(title: "Item Title", detail: nil), + CPInformationItem(title: "Item Title Item Title Item Title Item Title Item Title", detail: "Item Detail Item Detail Item Detail Item Detail Item Detail "), + CPInformationItem(title: "Item Title", detail: nil), + ], + actions: [ + textButton(style: .confirm), + textButton(style: .normal), +// textButton(style: .cancel), + ] + ) + template.tabTitle = "Information" + template.tabImage = UIImage(named: "RoundIcon")! + + return template + } + + private func textButton(style: CPTextButtonStyle) -> CPTextButton{ + return CPTextButton( + title: "Text Button", + textStyle: style, + handler: { [weak self] button in + guard let self = self else { return } + self.interfaceController?.pushTemplate( + self.informationTemplate(layout: .twoColumn), + animated: true, + completion: nil + ) + +// self.replaceTabs() + } + ) + } +} diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index 48ae0608..ae02c2b4 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -33,6 +33,10 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + INIntentsSupported + + Intent + ITSAppUsesNonExemptEncryption LSApplicationCategoryType @@ -66,7 +70,21 @@ UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - + + UISceneConfigurations + + CPTemplateApplicationSceneSessionRoleApplication + + + UISceneClassName + CPTemplateApplicationScene + UISceneDelegateClassName + $(SWIFT_MODULE_NAME).CarPlaySceneDelegate + UISceneConfigurationName + CarPlay scene + + + UIApplicationSupportsIndirectInputEvents diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index 26f4ce01..241de35a 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -18,5 +18,7 @@ com.apple.security.personal-information.location + com.apple.developer.carplay-communication + diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 9af01055..e1838f96 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -96,7 +96,7 @@ struct RtttlConfig: View { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if connectedNode != nil { - let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save