ironOS native ios app
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: ipad layout

+147 -49
+4 -4
ios/PinecilTime.xcodeproj/project.pbxproj
··· 256 256 DEVELOPMENT_TEAM = M67B42LX8D; 257 257 ENABLE_PREVIEWS = YES; 258 258 GENERATE_INFOPLIST_FILE = YES; 259 - INFOPLIST_KEY_CFBundleDisplayName = "Pinecil Time"; 259 + INFOPLIST_KEY_CFBundleDisplayName = Tinksil; 260 260 INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 261 261 INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 262 262 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; ··· 270 270 "@executable_path/Frameworks", 271 271 ); 272 272 MARKETING_VERSION = 1.0; 273 - PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.Pinecil-Time"; 273 + PRODUCT_BUNDLE_IDENTIFIER = sh.dunkirk.tinksil; 274 274 PRODUCT_NAME = "$(TARGET_NAME)"; 275 275 STRING_CATALOG_GENERATE_SYMBOLS = YES; 276 276 SWIFT_APPROACHABLE_CONCURRENCY = YES; ··· 292 292 DEVELOPMENT_TEAM = M67B42LX8D; 293 293 ENABLE_PREVIEWS = YES; 294 294 GENERATE_INFOPLIST_FILE = YES; 295 - INFOPLIST_KEY_CFBundleDisplayName = "Pinecil Time"; 295 + INFOPLIST_KEY_CFBundleDisplayName = Tinksil; 296 296 INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 297 297 INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "PinecilTime needs Bluetooth to connect to your Pinecil soldering iron."; 298 298 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; ··· 306 306 "@executable_path/Frameworks", 307 307 ); 308 308 MARKETING_VERSION = 1.0; 309 - PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.Pinecil-Time"; 309 + PRODUCT_BUNDLE_IDENTIFIER = sh.dunkirk.tinksil; 310 310 PRODUCT_NAME = "$(TARGET_NAME)"; 311 311 STRING_CATALOG_GENERATE_SYMBOLS = YES; 312 312 SWIFT_APPROACHABLE_CONCURRENCY = YES;
+143 -45
ios/PinecilTime/ContentView.swift
··· 14 14 @State private var isTopBarExpanded = false 15 15 @State private var showingSettings = false 16 16 @State private var showingError = false 17 + @Environment(\.horizontalSizeClass) var horizontalSizeClass 17 18 18 19 private var isHeating: Bool { 19 20 bleManager.liveData.mode?.isActive == true 20 21 } 21 22 23 + private var isIPad: Bool { 24 + horizontalSizeClass == .regular 25 + } 26 + 22 27 var body: some View { 23 28 ZStack { 24 - // Background graph 25 - if bleManager.temperatureHistory.count > 0 { 29 + // Background graph (only show on iPhone) 30 + if !isIPad && bleManager.temperatureHistory.count > 0 { 26 31 TemperatureGraph( 27 32 history: bleManager.temperatureHistoryArray, 28 33 currentSetpoint: Int(targetTemp) ··· 33 38 34 39 // Main content 35 40 if bleManager.connectionState.isConnected { 36 - connectedView 41 + if isIPad { 42 + connectedViewIPad 43 + } else { 44 + connectedView 45 + } 37 46 } else { 38 47 scanningView 39 48 } ··· 92 101 } 93 102 } 94 103 104 + // MARK: - Connected View iPad 105 + 106 + private var connectedViewIPad: some View { 107 + GeometryReader { geometry in 108 + HStack(spacing: 0) { 109 + // Left side: Temperature Graph 110 + if bleManager.temperatureHistory.count > 0 { 111 + TemperatureGraph( 112 + history: bleManager.temperatureHistoryArray, 113 + currentSetpoint: Int(targetTemp) 114 + ) 115 + .frame(width: geometry.size.width * 0.55) 116 + .padding(32) 117 + } else { 118 + // Placeholder when no data 119 + VStack { 120 + Image(systemName: "chart.line.uptrend.xyaxis") 121 + .font(.system(size: 60)) 122 + .foregroundStyle(.tertiary) 123 + Text("No temperature history yet") 124 + .font(.headline) 125 + .foregroundStyle(.secondary) 126 + } 127 + .frame(width: geometry.size.width * 0.55) 128 + } 129 + 130 + // Right side: Controls and Stats 131 + VStack(spacing: 0) { 132 + // Top bar with stats 133 + topBar 134 + .padding(.top, 32) 135 + 136 + Spacer() 137 + 138 + // Big temperature number 139 + VStack(spacing: 12) { 140 + temperatureDisplay 141 + 142 + // Target indicator 143 + if isHeating { 144 + HStack(spacing: 4) { 145 + Image(systemName: "arrow.right") 146 + .font(.body) 147 + Text("\(bleManager.liveData.setpoint)°") 148 + .font(.title2.monospacedDigit()) 149 + } 150 + .foregroundStyle(.secondary) 151 + } 152 + } 153 + 154 + Spacer() 155 + 156 + // Slider panel 157 + sliderPanel 158 + .padding(.horizontal, 32) 159 + .padding(.bottom, 32) 160 + } 161 + .frame(width: geometry.size.width * 0.45) 162 + } 163 + } 164 + } 165 + 95 166 // MARK: - Top Bar 96 167 97 168 private var topBar: some View { ··· 200 271 private var temperatureDisplay: some View { 201 272 let currentTemp = Double(bleManager.liveData.liveTemp) 202 273 let maxTemp = Double(bleManager.liveData.maxTemp) 274 + let fontSize: CGFloat = isIPad ? 160 : 120 275 + let degreeSize: CGFloat = isIPad ? 80 : 60 203 276 204 277 return HStack(alignment: .firstTextBaseline, spacing: 0) { 205 278 Text("\(bleManager.liveData.liveTemp)") 206 - .font(.system(size: 120, weight: .thin, design: .rounded)) 279 + .font(.system(size: fontSize, weight: .thin, design: .rounded)) 207 280 .contentTransition(.numericText()) 208 281 Text("°") 209 - .font(.system(size: 60, weight: .ultraLight)) 282 + .font(.system(size: degreeSize, weight: .ultraLight)) 210 283 .foregroundStyle(.secondary) 211 284 } 212 285 .foregroundStyle(colorForTemp(currentTemp, maxTemp: maxTemp)) ··· 215 288 // MARK: - Slider Panel 216 289 217 290 private var sliderPanel: some View { 218 - HStack(spacing: 16) { 219 - // Target temperature display 220 - HStack(alignment: .firstTextBaseline, spacing: 1) { 221 - Text("\(Int(targetTemp))") 222 - .font(.system(size: 24, weight: .semibold, design: .rounded)) 223 - .contentTransition(.numericText()) 224 - Text("°") 225 - .font(.system(size: 14, weight: .regular)) 226 - .foregroundStyle(.secondary) 291 + VStack(spacing: isIPad ? 20 : 0) { 292 + // iPad: Show label above slider 293 + if isIPad { 294 + HStack { 295 + Text("Target Temperature") 296 + .font(.headline) 297 + .foregroundStyle(.secondary) 298 + Spacer() 299 + // Target temperature display 300 + HStack(alignment: .firstTextBaseline, spacing: 1) { 301 + Text("\(Int(targetTemp))") 302 + .font(.system(size: 32, weight: .semibold, design: .rounded)) 303 + .contentTransition(.numericText()) 304 + Text("°") 305 + .font(.system(size: 18, weight: .regular)) 306 + .foregroundStyle(.secondary) 307 + } 308 + .foregroundStyle(colorForTemp(targetTemp, maxTemp: 450)) 309 + } 227 310 } 228 - .foregroundStyle(colorForTemp(targetTemp, maxTemp: 450)) 229 - .frame(width: 60) 230 311 231 - // Slider 232 - Slider( 233 - value: $targetTemp, 234 - in: 10...450, 235 - step: 5, 236 - onEditingChanged: { editing in 237 - isEditingSlider = editing 238 - if editing { 239 - bleManager.setSlowPolling() 240 - } else { 241 - // Only send if value changed 242 - if abs(targetTemp - lastSentTemp) >= 5 { 243 - bleManager.setTemperature(UInt32(targetTemp)) 244 - lastSentTemp = targetTemp 245 - } 246 - bleManager.setFastPolling() 312 + HStack(spacing: 16) { 313 + // iPhone: Target temperature display on the side 314 + if !isIPad { 315 + HStack(alignment: .firstTextBaseline, spacing: 1) { 316 + Text("\(Int(targetTemp))") 317 + .font(.system(size: 24, weight: .semibold, design: .rounded)) 318 + .contentTransition(.numericText()) 319 + Text("°") 320 + .font(.system(size: 14, weight: .regular)) 321 + .foregroundStyle(.secondary) 247 322 } 323 + .foregroundStyle(colorForTemp(targetTemp, maxTemp: 450)) 324 + .frame(width: 60) 248 325 } 249 - ) 250 - .tint(colorForTemp(targetTemp, maxTemp: 450)) 251 - .accessibilityLabel("Target temperature") 252 - .accessibilityValue("\(Int(targetTemp)) degrees") 326 + 327 + // Slider 328 + Slider( 329 + value: $targetTemp, 330 + in: 10...450, 331 + step: 5, 332 + onEditingChanged: { editing in 333 + isEditingSlider = editing 334 + if editing { 335 + bleManager.setSlowPolling() 336 + } else { 337 + // Only send if value changed 338 + if abs(targetTemp - lastSentTemp) >= 5 { 339 + bleManager.setTemperature(UInt32(targetTemp)) 340 + lastSentTemp = targetTemp 341 + } 342 + bleManager.setFastPolling() 343 + } 344 + } 345 + ) 346 + .tint(colorForTemp(targetTemp, maxTemp: 450)) 347 + .accessibilityLabel("Target temperature") 348 + .accessibilityValue("\(Int(targetTemp)) degrees") 349 + } 253 350 } 254 351 .padding(.horizontal, 20) 255 - .padding(.vertical, 14) 352 + .padding(.vertical, isIPad ? 20 : 14) 256 353 .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) 257 354 } 258 355 ··· 314 411 Color.black.opacity(0.4) 315 412 .ignoresSafeArea() 316 413 317 - VStack(spacing: 20) { 414 + VStack(spacing: isIPad ? 28 : 20) { 318 415 if bleManager.isScanning || bleManager.connectionState.isConnecting { 319 416 ProgressView() 320 - .scaleEffect(1.2) 417 + .scaleEffect(isIPad ? 2.0 : 1.2) 321 418 .padding(.bottom, 4) 322 419 323 420 Text(bleManager.connectionState.isConnecting ? "Connecting..." : "Scanning...") 324 - .font(.headline) 421 + .font(isIPad ? .largeTitle : .headline) 325 422 326 423 Text("Looking for your Pinecil") 327 - .font(.subheadline) 424 + .font(isIPad ? .title3 : .subheadline) 328 425 .foregroundStyle(.secondary) 329 426 } else { 330 427 Image(systemName: "antenna.radiowaves.left.and.right.slash") 331 - .font(.system(size: 36)) 428 + .font(.system(size: isIPad ? 60 : 36)) 332 429 .foregroundStyle(.secondary) 333 430 .padding(.bottom, 4) 334 431 335 432 Text("No Device Found") 336 - .font(.headline) 433 + .font(isIPad ? .largeTitle : .headline) 337 434 338 435 Button("Scan Again") { 339 436 bleManager.startScanning() 340 437 } 341 438 .buttonStyle(.borderedProminent) 439 + .controlSize(isIPad ? .large : .regular) 342 440 } 343 441 } 344 - .padding(32) 345 - .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24)) 442 + .padding(isIPad ? 48 : 32) 443 + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: isIPad ? 32 : 24)) 346 444 .shadow(color: .black.opacity(0.2), radius: 20) 347 445 } 348 446 }