diff --git a/Meshtastic/Tips/ChannelTips.swift b/Meshtastic/Tips/ChannelTips.swift index cdd5d9c7..871af684 100644 --- a/Meshtastic/Tips/ChannelTips.swift +++ b/Meshtastic/Tips/ChannelTips.swift @@ -25,3 +25,20 @@ Image(systemName: "qrcode") } } + +@available(iOS 17.0, macOS 14.0, *) +struct CreateChannelsTip: Tip { + + var id: String { + return "tip.channels.create" + } + var title: Text { + Text("tip.channels.create.title") + } + var message: Text? { + Text("tip.channels.create.message") + } + var image: Image? { + Image(systemName: "fibrechannel") + } +} diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index e6c8f99d..7ed6fb04 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -21,6 +21,8 @@ struct ChannelList: View { @State private var isPresentingTraceRouteSentAlert = false + var restrictedChannels = ["admin", "gpio", "mqtt", "serial"] + var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") @@ -29,7 +31,7 @@ struct ChannelList: View { // Display Contacts for the rest of the non admin channels if node != nil && node!.myInfo != nil && node!.myInfo!.channels != nil { List(node!.myInfo!.channels!.array as! [ChannelEntity], id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in - if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" && channel.name?.lowercased() ?? "" != "serial" { + if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { NavigationLink(destination: ChannelMessageList(myInfo: node!.myInfo!, channel: channel)) { @@ -85,9 +87,6 @@ struct ChannelList: View { .foregroundColor(.secondary) } } -// Image(systemName: "chevron.forward") -// .font(.caption) -// .foregroundColor(.secondary) } if channel.allPrivateMessages.count > 0 { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 579aeda5..885379e5 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -19,7 +19,28 @@ struct AppSettings: View { var body: some View { VStack { Form { - Section(header: Text("options")) { + Section(header: Text("Location Settings")) { + Toggle(isOn: $provideLocation) { + Label("appsettings.provide.location", systemImage: "location.circle.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if provideLocation { + Toggle(isOn: $enableSmartPosition) { + Label("appsettings.smartposition", systemImage: "brain.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + VStack { + Picker("update.interval", selection: $provideLocationInterval) { + ForEach(LocationUpdateInterval.allCases) { lu in + Text(lu.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("phone.gps.interval.description") + .font(.caption2) + .foregroundColor(.gray) + } + } Toggle(isOn: $useLegacyMap) { Label("map.use.legacy", systemImage: "map") } @@ -63,35 +84,6 @@ struct AppSettings: View { } } } - Section(header: Text("Location Settings")) { - Toggle(isOn: $provideLocation) { - Label("appsettings.provide.location", systemImage: "location.circle.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if provideLocation { - Toggle(isOn: $enableSmartPosition) { - Label("appsettings.smartposition", systemImage: "brain.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onChange(of: (enableSmartPosition)) { newEnableSmartPosition in - UserDefaults.enableSmartPosition = newEnableSmartPosition - } - VStack { - Picker("update.interval", selection: $provideLocationInterval) { - ForEach(LocationUpdateInterval.allCases) { lu in - Text(lu.description) - } - } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (provideLocationInterval)) { newProvideLocationInterval in - UserDefaults.provideLocationInterval = newProvideLocationInterval - } - Text("phone.gps.interval.description") - .font(.caption2) - .foregroundColor(.gray) - } - } - } Section(header: Text("App Data")) { Button { isPresentingCoreDataResetConfirm = true @@ -158,6 +150,12 @@ struct AppSettings: View { self.bleManager.sendWantConfig() } } + .onChange(of: enableSmartPosition) { newEnableSmartPosition in + UserDefaults.enableSmartPosition = newEnableSmartPosition + } + .onChange(of: (provideLocationInterval)) { newProvideLocationInterval in + UserDefaults.provideLocationInterval = newProvideLocationInterval + } .onChange(of: useLegacyMap) { newMapUseLegacy in UserDefaults.mapUseLegacy = newMapUseLegacy } diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 4878fa2c..33fca131 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -7,6 +7,9 @@ import SwiftUI import CoreData +#if canImport(TipKit) +import TipKit +#endif func generateChannelKey(size: Int) -> String { var keyData = Data(count: size) @@ -40,7 +43,11 @@ struct Channels: View { var body: some View { VStack { + List { + if #available(iOS 17.0, macOS 14.0, *) { + TipView(CreateChannelsTip(), arrowEdge: .bottom) + } if node != nil && node?.myInfo != nil { ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in Button(action: { @@ -91,7 +98,9 @@ struct Channels: View { if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { - let key = generateChannelKey(size: 32) + channelKeySize = 16 + let key = generateChannelKey(size: channelKeySize) + channelName = "" channelIndex = Int32(node!.myInfo!.channels!.array.count) channelRole = 2 @@ -201,18 +210,21 @@ struct Channels: View { .disabled(channelKeySize <= 0) } HStack { - Text("Role") - Spacer() - Picker("Channel Role", selection: $channelRole) { - if channelRole == 1 { + if channelRole == 1 { + Picker("Channel Role", selection: $channelRole) { Text("Primary").tag(1) - } else { - Text("Disabled").tag(0) - Text("Secondary").tag(2) } + .pickerStyle(.automatic) + .disabled(true) + } else { + Text("Channel Role") + Spacer() + Picker("Channel Role", selection: $channelRole) { + Text("Disabled").tag(0) + Text("Secondary").tag(2) + } + .pickerStyle(.segmented) } - .pickerStyle(.segmented) - .disabled(channelRole == 1) } Toggle("Uplink Enabled", isOn: $uplink) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 980b02a2..2f9cc65c 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -91,7 +91,7 @@ struct StoreForwardConfig: View { } if isRouter { - Section(header: Text("options")) { + Section(header: Text("Router Options")) { Toggle(isOn: $heartbeat) { Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 99dfe757..d2f860b0 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -117,27 +117,10 @@ struct Settings: View { } } } else { - Text("Configuring Node \(node?.user?.longName ?? "unknown".localized)") + Text("Connected Node \(node?.user?.longName ?? "unknown".localized)") } } Section("radio.configuration") { - NavigationLink { - ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum })) - } label: { - Image(systemName: "qrcode") - .symbolRenderingMode(.hierarchical) - Text("share.channels") - } - .tag(SettingsSidebar.shareChannels) - .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) - NavigationLink { - UserConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "person.crop.rectangle.fill") - .symbolRenderingMode(.hierarchical) - Text("user") - } - .tag(SettingsSidebar.userConfig) NavigationLink { LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { @@ -155,6 +138,25 @@ struct Settings: View { } .tag(SettingsSidebar.channelConfig) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) + NavigationLink { + ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum })) + } label: { + Image(systemName: "qrcode") + .symbolRenderingMode(.hierarchical) + Text("share.channels") + } + .tag(SettingsSidebar.shareChannels) + .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) + } + Section("device.configuration") { + NavigationLink { + UserConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "person.crop.rectangle.fill") + .symbolRenderingMode(.hierarchical) + Text("user") + } + .tag(SettingsSidebar.userConfig) NavigationLink { BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 707334a0..250af743 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -81,7 +81,7 @@ "device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; "direct.messages"="Direct Messages"; "dismiss.keyboard"="Dismiss"; -"display"="Display (Device Screen)"; +"display"="Display"; "display.config"="Display Config"; "distance"="Distance"; "disconnect"="Disconnect"; @@ -270,7 +270,7 @@ "serial.mode.txtmsg"="Text Message"; "serial.mode.nmea"="NMEA Positions"; "settings"="Settings"; -"share.channels"="Share Channels QR Code"; +"share.channels"="Share QR Code"; "share.position"="Share Position"; "subscribed"="Subscribed to mesh"; "select.contact"="Select a Contact"; @@ -297,9 +297,11 @@ "timeout"="Timeout"; "timestamp"="Timestamp"; "tip.bluetooth.connect.title"="Connected Radio"; -"tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; -"tip.channels.share.message"="In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key."; +"tip.channels.share.message"="A Meshtastic QR code contains the LoRa config and channel values needed to communicate. Most mesh activity takes place on the required Primary channel. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. Other channels are for private groups, each with its own key."; "tip.messages.title"="Messages"; "tip.messages.message"="You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details."; "twitter"="Twitter";