mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
319 lines
12 KiB
Swift
319 lines
12 KiB
Swift
//
|
|
// ShareChannel.swift
|
|
// MeshtasticApple
|
|
//
|
|
// Copyright(c) Garth Vander Houwen 4/8/22.
|
|
//
|
|
import SwiftUI
|
|
import CoreData
|
|
import CoreImage.CIFilterBuiltins
|
|
import MeshtasticProtobufs
|
|
import TipKit
|
|
|
|
struct QrCodeImage {
|
|
let context = CIContext()
|
|
|
|
func generateQRCode(from text: String) -> UIImage {
|
|
var qrImage = UIImage(systemName: "xmark.circle") ?? UIImage()
|
|
let data = Data(text.utf8)
|
|
let filter = CIFilter.qrCodeGenerator()
|
|
filter.setValue(data, forKey: "inputMessage")
|
|
|
|
let transform = CGAffineTransform(scaleX: 20, y: 20)
|
|
if let outputImage = filter.outputImage?.transformed(by: transform) {
|
|
if let image = context.createCGImage(
|
|
outputImage,
|
|
from: outputImage.extent) {
|
|
qrImage = UIImage(cgImage: image)
|
|
}
|
|
}
|
|
return qrImage
|
|
}
|
|
}
|
|
|
|
struct ShareChannels: View {
|
|
|
|
@Environment(\.managedObjectContext) var context
|
|
@EnvironmentObject var bleManager: BLEManager
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State var channelSet: ChannelSet = ChannelSet()
|
|
@State var includeChannel0 = true
|
|
@State var includeChannel1 = true
|
|
@State var includeChannel2 = true
|
|
@State var includeChannel3 = true
|
|
@State var includeChannel4 = true
|
|
@State var includeChannel5 = true
|
|
@State var includeChannel6 = true
|
|
@State var includeChannel7 = true
|
|
@State var replaceChannels = true
|
|
var node: NodeInfoEntity?
|
|
@State private var channelsUrl = "https://www.meshtastic.org/e/#"
|
|
var qrCodeImage = QrCodeImage()
|
|
|
|
var body: some View {
|
|
|
|
VStack {
|
|
TipView(ShareChannelsTip(), arrowEdge: .bottom)
|
|
}
|
|
GeometryReader { bounds in
|
|
let smallest = min(bounds.size.width, bounds.size.height)
|
|
ScrollView {
|
|
if node != nil && node?.myInfo != nil {
|
|
Grid {
|
|
GridRow {
|
|
Spacer()
|
|
Text("Include")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.padding(.trailing)
|
|
Text("Channel")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.padding(.trailing)
|
|
Text("Encrypted")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
}
|
|
ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in
|
|
GridRow {
|
|
Spacer()
|
|
if channel.index == 0 {
|
|
Toggle("Channel 0 Included", isOn: $includeChannel0)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords())
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if channel.index == 1 && channel.role > 0 {
|
|
Toggle("Channel 1 Included", isOn: $includeChannel1)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.disabled(channel.role == 1)
|
|
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if channel.index == 2 && channel.role > 0 {
|
|
Toggle("Channel 2 Included", isOn: $includeChannel2)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.disabled(channel.role == 1)
|
|
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if channel.index == 3 && channel.role > 0 {
|
|
Toggle("Channel 3 Included", isOn: $includeChannel3)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.disabled(channel.role == 1)
|
|
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if channel.index == 4 && channel.role > 0 {
|
|
Toggle("Channel 4 Included", isOn: $includeChannel4)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.disabled(channel.role == 1)
|
|
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if channel.index == 5 && channel.role > 0 {
|
|
Toggle("Channel 5 Included", isOn: $includeChannel5)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.disabled(channel.role == 1)
|
|
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if channel.index == 6 && channel.role > 0 {
|
|
Toggle("Channel 6 Included", isOn: $includeChannel6)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.disabled(channel.role == 1)
|
|
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
} else if channel.index == 7 && channel.role > 0 {
|
|
Toggle("Channel 7 Included", isOn: $includeChannel7)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.disabled(channel.role == 1)
|
|
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
|
|
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
|
Image(systemName: "lock.slash")
|
|
.foregroundColor(.red)
|
|
} else {
|
|
Image(systemName: "lock.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
|
|
let qrImage = qrCodeImage.generateQRCode(from: channelsUrl)
|
|
VStack {
|
|
if node != nil {
|
|
Toggle(isOn: $replaceChannels) {
|
|
Label(replaceChannels ? "Replace Channels" : "Add Channels", systemImage: replaceChannels ? "arrow.triangle.2.circlepath.circle" : "plus.app")
|
|
}
|
|
.tint(.accentColor)
|
|
.toggleStyle(.button)
|
|
.buttonStyle(.bordered)
|
|
.buttonBorderShape(.capsule)
|
|
.controlSize(.large)
|
|
.padding(.top)
|
|
.padding(.bottom)
|
|
|
|
ShareLink("Share QR Code & Link",
|
|
item: Image(uiImage: qrImage),
|
|
subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"),
|
|
message: Text(channelsUrl),
|
|
preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you",
|
|
image: Image(uiImage: qrImage))
|
|
)
|
|
.buttonStyle(.bordered)
|
|
.buttonBorderShape(.capsule)
|
|
.controlSize(.large)
|
|
.padding(.bottom)
|
|
|
|
Image(uiImage: qrImage)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(
|
|
minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
|
maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
|
minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
|
maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
|
alignment: .top
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("generate.qr.code")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationBarItems(trailing:
|
|
ZStack {
|
|
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
|
})
|
|
.onAppear {
|
|
generateChannelSet()
|
|
}
|
|
.onChange(of: includeChannel0) { generateChannelSet() }
|
|
.onChange(of: includeChannel1) { generateChannelSet() }
|
|
.onChange(of: includeChannel2) { generateChannelSet() }
|
|
.onChange(of: includeChannel3) { generateChannelSet() }
|
|
.onChange(of: includeChannel4) { generateChannelSet() }
|
|
.onChange(of: includeChannel5) { generateChannelSet() }
|
|
.onChange(of: includeChannel6) { generateChannelSet() }
|
|
.onChange(of: includeChannel7) { generateChannelSet() }
|
|
.onChange(of: replaceChannels) { generateChannelSet() }
|
|
}
|
|
}
|
|
func generateChannelSet() {
|
|
channelSet = ChannelSet()
|
|
var loRaConfig = Config.LoRaConfig()
|
|
loRaConfig.region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))!.protoEnumValue()
|
|
loRaConfig.modemPreset = ModemPresets(rawValue: Int(node?.loRaConfig?.modemPreset ?? 0))!.protoEnumValue()
|
|
loRaConfig.bandwidth = UInt32(node?.loRaConfig?.bandwidth ?? 0)
|
|
loRaConfig.spreadFactor = UInt32(node?.loRaConfig?.spreadFactor ?? 0)
|
|
loRaConfig.codingRate = UInt32(node?.loRaConfig?.codingRate ?? 0)
|
|
loRaConfig.frequencyOffset = node?.loRaConfig?.frequencyOffset ?? 0
|
|
loRaConfig.hopLimit = UInt32(node?.loRaConfig?.hopLimit ?? 3)
|
|
loRaConfig.txEnabled = node?.loRaConfig?.txEnabled ?? false
|
|
loRaConfig.txPower = node?.loRaConfig?.txPower ?? 0
|
|
loRaConfig.usePreset = node?.loRaConfig?.usePreset ?? true
|
|
loRaConfig.channelNum = UInt32(node?.loRaConfig?.channelNum ?? 0)
|
|
loRaConfig.sx126XRxBoostedGain = node?.loRaConfig?.sx126xRxBoostedGain ?? false
|
|
loRaConfig.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false
|
|
channelSet.loraConfig = loRaConfig
|
|
if node?.myInfo?.channels != nil && node?.myInfo?.channels?.count ?? 0 > 0 {
|
|
for ch in node?.myInfo?.channels?.array as? [ChannelEntity] ?? [] where ch.role > 0 {
|
|
var includeChannel = false
|
|
switch ch.index {
|
|
case 0:
|
|
if includeChannel0 {
|
|
includeChannel = true
|
|
}
|
|
case 1:
|
|
if includeChannel1 {
|
|
includeChannel = true
|
|
}
|
|
case 2:
|
|
if includeChannel2 {
|
|
includeChannel = true
|
|
}
|
|
case 3:
|
|
if includeChannel3 {
|
|
includeChannel = true
|
|
}
|
|
case 4:
|
|
if includeChannel4 {
|
|
includeChannel = true
|
|
}
|
|
case 5:
|
|
if includeChannel5 {
|
|
includeChannel = true
|
|
}
|
|
case 6:
|
|
if includeChannel6 {
|
|
includeChannel = true
|
|
}
|
|
case 7:
|
|
if includeChannel7 {
|
|
includeChannel = true
|
|
}
|
|
default:
|
|
includeChannel = false
|
|
}
|
|
if includeChannel {
|
|
var channelSettings = ChannelSettings()
|
|
channelSettings.name = ch.name!
|
|
channelSettings.psk = ch.psk!
|
|
channelSettings.id = UInt32(ch.id)
|
|
channelSet.settings.append(channelSettings)
|
|
}
|
|
}
|
|
guard let settingsString = try? channelSet.serializedData().base64EncodedString() else {
|
|
return
|
|
}
|
|
channelsUrl = ("https://meshtastic.org/e/\(replaceChannels ? "" : "?add=true")#" + settingsString.base64ToBase64url())
|
|
}
|
|
}
|
|
}
|