291 lines
8.5 KiB
Swift
291 lines
8.5 KiB
Swift
import SwiftUI
|
|
import SFSafeSymbols
|
|
|
|
struct ContentView: View {
|
|
|
|
private let deviceInfoUpdateInterval = 3.0
|
|
|
|
private let minTempColor = Color(hue: 0.624, saturation: 0.5, brightness: 1.0)
|
|
private let minTemperature = -20.0
|
|
|
|
private let maxTempColor = Color(hue: 1.0, saturation: 0.5, brightness: 1.0)
|
|
private let maxTemperature = 40.0
|
|
|
|
private let disconnectedColor = Color(white: 0.8)
|
|
|
|
@ObservedObject
|
|
var scanner: BluetoothScanner
|
|
|
|
@EnvironmentObject
|
|
var storage: PersistentStorage
|
|
|
|
@EnvironmentObject
|
|
var transfer: TransferHandler
|
|
|
|
@State
|
|
var showDeviceInfo = false
|
|
|
|
@State
|
|
var showHistory = false
|
|
|
|
@State
|
|
var showLog = false
|
|
|
|
@State
|
|
var showDataTransferView = false
|
|
|
|
@State
|
|
var deviceInfoUpdateTimer: Timer?
|
|
|
|
var averageTemperature: Double? {
|
|
guard scanner.configuredDevice != nil else {
|
|
return nil
|
|
}
|
|
guard let info = scanner.lastDeviceInfo else {
|
|
return nil
|
|
}
|
|
let t1 = info.sensor1?.optionalValue
|
|
guard let t0 = info.sensor0?.optionalValue else {
|
|
return t1
|
|
}
|
|
guard let t1 else {
|
|
return t0
|
|
}
|
|
return (t0 + t1) / 2
|
|
}
|
|
|
|
var hasTemperature: Bool {
|
|
averageTemperature != nil
|
|
}
|
|
var temperatureString: String {
|
|
guard let temp = averageTemperature else {
|
|
return "?"
|
|
}
|
|
return String(format: "%.0f°", locale: .current, temp)
|
|
}
|
|
|
|
var temperatureIcon: SFSymbol {
|
|
guard let temp = averageTemperature else {
|
|
return .thermometerMediumSlash
|
|
}
|
|
guard temp > 0 else {
|
|
return .thermometerSnowflake
|
|
}
|
|
guard temp > 15 else {
|
|
return .thermometerLow
|
|
}
|
|
guard temp > 25 else {
|
|
return .thermometerMedium
|
|
}
|
|
return .thermometerHigh
|
|
}
|
|
|
|
var backgroundColor: Color {
|
|
guard let temp = averageTemperature else {
|
|
return disconnectedColor
|
|
}
|
|
guard temp > minTemperature else {
|
|
return minTempColor
|
|
}
|
|
guard temp < maxTemperature else {
|
|
return maxTempColor
|
|
}
|
|
let ratio = (temp - minTemperature) / (maxTemperature - minTemperature)
|
|
return minTempColor.blend(to: maxTempColor, intensity: ratio)
|
|
}
|
|
|
|
var backgroundGradient: Gradient {
|
|
let color = backgroundColor
|
|
let lighter = color.opacity(0.5)
|
|
return .init(colors: [lighter, color])
|
|
}
|
|
|
|
var connectionSymbol: SFSymbol {
|
|
if scanner.configuredDevice != nil {
|
|
return .iphoneCircle
|
|
}
|
|
if !scanner.bluetoothIsAvailable {
|
|
return .antennaRadiowavesLeftAndRightSlash
|
|
}
|
|
switch scanner.connectionState {
|
|
case .noDeviceFound:
|
|
if scanner.isScanningForDevices {
|
|
return .iphoneRadiowavesLeftAndRightCircle
|
|
}
|
|
return .iphoneSlashCircle
|
|
case .connecting:
|
|
return .arrowRightToLineCircle
|
|
case .discoveringService:
|
|
return .linkCircle
|
|
case .discoveringCharacteristic:
|
|
return .magnifyingglassCircle
|
|
}
|
|
|
|
}
|
|
|
|
var hasNoDeviceInfo: Bool {
|
|
scanner.lastDeviceInfo == nil
|
|
}
|
|
|
|
var isDisconnected: Bool {
|
|
scanner.configuredDevice == nil
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
Spacer()
|
|
if hasTemperature {
|
|
Text(temperatureString)
|
|
.font(.system(size: 150, weight: .light))
|
|
.foregroundColor(.white)
|
|
} else {
|
|
Image(systemSymbol: temperatureIcon)
|
|
.font(.system(size: 100, weight: .thin))
|
|
.foregroundColor(.gray)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button {
|
|
self.showHistory = true
|
|
} label: {
|
|
ZStack {
|
|
TemperatureHistoryChart(points: $storage.recentMeasurements)
|
|
.frame(height: 300)
|
|
.background(Color.white.opacity(0.1))
|
|
.cornerRadius(8)
|
|
if storage.recentMeasurements.isEmpty {
|
|
Text("No recent measurements")
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
}
|
|
HStack(alignment: .center) {
|
|
Button {
|
|
self.showLog = true
|
|
} label: {
|
|
Image(systemSymbol: .paperclipCircle)
|
|
.foregroundColor(.white)
|
|
}
|
|
Spacer()
|
|
Button {
|
|
if scanner.isScanningForDevices {
|
|
scanner.isScanningForDevices = false
|
|
} else if scanner.isConnectingOrConnected {
|
|
scanner.disconnect()
|
|
} else {
|
|
scanner.isScanningForDevices = true
|
|
}
|
|
} label: {
|
|
Image(systemSymbol: connectionSymbol)
|
|
.foregroundColor(.white)
|
|
}
|
|
.foregroundColor(.white)
|
|
Spacer()
|
|
if scanner.lastDeviceInfo != nil {
|
|
Button {
|
|
self.showDeviceInfo = true
|
|
} label: {
|
|
Image(systemSymbol: .infoCircle)
|
|
.foregroundColor(.white)
|
|
}
|
|
Spacer()
|
|
Button {
|
|
showDataTransferView = true
|
|
} label: {
|
|
Image(systemSymbol: .arrowUpArrowDownCircle)
|
|
.foregroundColor(.white)
|
|
}
|
|
} else {
|
|
Image(systemSymbol: .infoCircle)
|
|
.foregroundColor(.gray)
|
|
Spacer()
|
|
Image(systemSymbol: .arrowUpArrowDownCircle)
|
|
.foregroundColor(.gray)
|
|
}
|
|
}
|
|
.padding()
|
|
.font(.system(size: 30, weight: .light))
|
|
|
|
}
|
|
.padding()
|
|
.sheet(isPresented: $showDeviceInfo) {
|
|
if let info = scanner.lastDeviceInfo {
|
|
DeviceInfoView(info: info, isPresented: $showDeviceInfo)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
.sheet(isPresented: $showHistory) {
|
|
HistoryList()
|
|
.environmentObject(storage)
|
|
}
|
|
.sheet(isPresented: $showLog) {
|
|
LogView()
|
|
.environmentObject(log)
|
|
.environmentObject(storage)
|
|
}
|
|
.sheet(isPresented: $showDataTransferView) {
|
|
if let client = scanner.configuredDevice {
|
|
TransferView(
|
|
bluetoothClient: client, info: $scanner.lastDeviceInfo)
|
|
.environmentObject(storage)
|
|
.environmentObject(transfer)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
.background(backgroundGradient)
|
|
.onAppear(perform: startDeviceInfoUpdates)
|
|
.onDisappear(perform: endDeviceInfoUpdates)
|
|
}
|
|
|
|
// MARK: Device info updates
|
|
|
|
private func startDeviceInfoUpdates() {
|
|
deviceInfoUpdateTimer?.invalidate()
|
|
|
|
log.info("Starting device info updates")
|
|
deviceInfoUpdateTimer = Timer.scheduledTimer(withTimeInterval: deviceInfoUpdateInterval, repeats: true) { timer in
|
|
self.updateDeviceInfo()
|
|
}
|
|
|
|
deviceInfoUpdateTimer?.fire()
|
|
}
|
|
|
|
private func updateDeviceInfo() {
|
|
guard let bluetoothDevice = scanner.configuredDevice else {
|
|
return
|
|
}
|
|
Task {
|
|
await bluetoothDevice.updateInfo()
|
|
}
|
|
}
|
|
|
|
private func endDeviceInfoUpdates() {
|
|
deviceInfoUpdateTimer?.invalidate()
|
|
deviceInfoUpdateTimer = nil
|
|
}
|
|
}
|
|
|
|
struct ContentView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData)
|
|
ContentView(scanner: .init())
|
|
.environmentObject(storage)
|
|
}
|
|
}
|
|
|
|
extension HorizontalAlignment {
|
|
|
|
private struct InfoTextAlignment: AlignmentID {
|
|
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
|
context[HorizontalAlignment.leading]
|
|
}
|
|
}
|
|
|
|
static let infoTextAlignmentGuide = HorizontalAlignment(
|
|
InfoTextAlignment.self
|
|
)
|
|
}
|