diff --git a/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj index bd290f0..526a3a8 100644 --- a/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj +++ b/SPAR_iOS/SPAR/SPAR.xcodeproj/project.pbxproj @@ -39,7 +39,6 @@ 3D505B792DBB170700510486 /* InfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D505B782DBB170700510486 /* InfoRow.swift */; }; 3D5994F32DB2B79400E9215B /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994F22DB2B79400E9215B /* LoginView.swift */; }; 3D5994F52DB2BA1200E9215B /* BackgroundAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994F42DB2BA1200E9215B /* BackgroundAnimationView.swift */; }; - 3D5994F82DB2C08500E9215B /* DeviceDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994F72DB2C08500E9215B /* DeviceDetail.swift */; }; 3D5994FA2DB307B400E9215B /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994F92DB307B400E9215B /* HomeViewModel.swift */; }; 3D5994FC2DB30B0100E9215B /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994FB2DB30B0100E9215B /* LoginViewModel.swift */; }; 3D5994FF2DB3FFBE00E9215B /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */; }; @@ -120,7 +119,6 @@ 3D505B782DBB170700510486 /* InfoRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoRow.swift; sourceTree = ""; }; 3D5994F22DB2B79400E9215B /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 3D5994F42DB2BA1200E9215B /* BackgroundAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAnimationView.swift; sourceTree = ""; }; - 3D5994F72DB2C08500E9215B /* DeviceDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetail.swift; sourceTree = ""; }; 3D5994F92DB307B400E9215B /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 3D5994FB2DB30B0100E9215B /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 3D5994FE2DB3FFBE00E9215B /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; @@ -267,14 +265,6 @@ path = Login; sourceTree = ""; }; - 3D5994F62DB2C05E00E9215B /* Device detail Page */ = { - isa = PBXGroup; - children = ( - 3D5994F72DB2C08500E9215B /* DeviceDetail.swift */, - ); - path = "Device detail Page"; - sourceTree = ""; - }; 3D5994FD2DB3FF0700E9215B /* NetworkService */ = { isa = PBXGroup; children = ( @@ -285,6 +275,19 @@ path = NetworkService; sourceTree = ""; }; + 3D63FB062DC1B4FA006F53DE /* View */ = { + isa = PBXGroup; + children = ( + 3DF9BA502D67762C00D0CC62 /* HomeView.swift */, + 3DCE458A2D68C19F00FACBB8 /* SplashScreenView.swift */, + 3D07B5322D851C490085B32C /* Onboarding */, + 3D07B5342D85C3740085B32C /* ChartUI */, + 3D5994F12DB2ACB300E9215B /* Login */, + 3D505B4B2DB9C19000510486 /* Device */, + ); + path = View; + sourceTree = ""; + }; 3D8423A52DB14F86009CF847 /* SPARTests */ = { isa = PBXGroup; children = ( @@ -318,20 +321,14 @@ 3D98C7F02D4FE04500462EF4 /* SPAR */ = { isa = PBXGroup; children = ( - 3D505B4B2DB9C19000510486 /* Device */, + 3D63FB062DC1B4FA006F53DE /* View */, 3D5994FD2DB3FF0700E9215B /* NetworkService */, - 3D5994F62DB2C05E00E9215B /* Device detail Page */, - 3D5994F12DB2ACB300E9215B /* Login */, 3D07B5362D85C3910085B32C /* Data */, 3D07B5352D85C3860085B32C /* ViewModel */, - 3D07B5342D85C3740085B32C /* ChartUI */, 3D07B5332D851C640085B32C /* Utilities */, - 3D07B5322D851C490085B32C /* Onboarding */, 3D07B5312D851C1D0085B32C /* Extensions */, 3D98C7F12D4FE04500462EF4 /* SPARApp.swift */, 3D98C7F32D4FE04500462EF4 /* ContentView.swift */, - 3DCE458A2D68C19F00FACBB8 /* SplashScreenView.swift */, - 3DF9BA502D67762C00D0CC62 /* HomeView.swift */, 3D98C7F52D4FE04700462EF4 /* Assets.xcassets */, 3D98C7F72D4FE04700462EF4 /* Preview Content */, ); @@ -480,7 +477,6 @@ 3D505B4D2DB9C1BE00510486 /* DeviceOptions.swift in Sources */, 3D505B5B2DB9E3F700510486 /* ProcessViewModel.swift in Sources */, 3D505B632DBAAE2C00510486 /* DiskUsage.swift in Sources */, - 3D5994F82DB2C08500E9215B /* DeviceDetail.swift in Sources */, 3DCE45912D68C7E700FACBB8 /* View+Extension.swift in Sources */, 3D505B592DB9E14700510486 /* MemoryUsageViewModel.swift in Sources */, 3D505B552DB9DD5500510486 /* ProcessDetailPage.swift in Sources */, diff --git a/SPAR_iOS/SPAR/SPAR/ContentView.swift b/SPAR_iOS/SPAR/SPAR/ContentView.swift index 89d1d98..8beec4e 100644 --- a/SPAR_iOS/SPAR/SPAR/ContentView.swift +++ b/SPAR_iOS/SPAR/SPAR/ContentView.swift @@ -12,7 +12,6 @@ enum AppView: Hashable { case splash case onboarding case home - case detailPage case login } @@ -28,8 +27,6 @@ struct ContentView: View { OnboardingView(currentView: $currentView) case .home: HomeView(currentView: $currentView) - case .detailPage: - DeviceDetail(currentView: $currentView) case .login: LoginView(currentView: $currentView) } diff --git a/SPAR_iOS/SPAR/SPAR/Data/CPUUsage.swift b/SPAR_iOS/SPAR/SPAR/Data/CPUUsage.swift index 8752f58..1ee45fd 100644 --- a/SPAR_iOS/SPAR/SPAR/Data/CPUUsage.swift +++ b/SPAR_iOS/SPAR/SPAR/Data/CPUUsage.swift @@ -7,12 +7,14 @@ import Foundation +// MARK: - CpuCoreUsage model struct CpuCoreUsage: Codable { let core: Int let usage: Double } -struct CpuUsage: Decodable { +// Fixed CpuUsage struct with better JSON handling +struct CpuUsage: Codable { let id: Int let totalCpuLoad: Double let userId: Int @@ -20,6 +22,23 @@ struct CpuUsage: Decodable { let timestamp: String let perCoreUsage: [CpuCoreUsage] + // Custom encoding for when we need to convert back to the API format + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(totalCpuLoad, forKey: .totalCpuLoad) + try container.encode(userId, forKey: .userId) + try container.encode(deviceId, forKey: .deviceId) + try container.encode(timestamp, forKey: .timestamp) + + // Convert perCoreUsage to a JSON string + let perCoreData = try JSONEncoder().encode(perCoreUsage) + if let perCoreString = String(data: perCoreData, encoding: .utf8) { + try container.encode(perCoreString, forKey: .perCoreUsageJson) + } + } + enum CodingKeys: String, CodingKey { case id, totalCpuLoad, userId, deviceId, timestamp, perCoreUsageJson } @@ -33,17 +52,39 @@ struct CpuUsage: Decodable { deviceId = try container.decode(String.self, forKey: .deviceId) timestamp = try container.decode(String.self, forKey: .timestamp) + // Try to decode the perCoreUsageJson string let jsonString = try container.decode(String.self, forKey: .perCoreUsageJson) - // No cleaning, just decode directly - if let jsonData = jsonString.data(using: .utf8) { - perCoreUsage = (try? JSONDecoder().decode([CpuCoreUsage].self, from: jsonData)) ?? [] - } else { - perCoreUsage = [] + // First attempt: direct decoding + if let data = jsonString.data(using: .utf8) { + do { + perCoreUsage = try JSONDecoder().decode([CpuCoreUsage].self, from: data) + return + } catch { + print("First parse attempt failed: \(error)") + } } + + // Second attempt: clean the string and try again + let cleanedJsonString = jsonString + .replacingOccurrences(of: "\\\"", with: "\"") + .replacingOccurrences(of: "\\\\", with: "\\") + + if let data = cleanedJsonString.data(using: .utf8) { + do { + perCoreUsage = try JSONDecoder().decode([CpuCoreUsage].self, from: data) + return + } catch { + print("Second parse attempt failed: \(error)") + } + } + + // If all else fails, provide default values + print("Using default values for perCoreUsage") + perCoreUsage = [] } - // Manual initializer for sample/mock data + // Convenience initializer (not used in decoding) init(id: Int, totalCpuLoad: Double, perCoreUsage: [CpuCoreUsage], userId: Int, deviceId: String, timestamp: String) { self.id = id self.totalCpuLoad = totalCpuLoad @@ -53,3 +94,4 @@ struct CpuUsage: Decodable { self.timestamp = timestamp } } + diff --git a/SPAR_iOS/SPAR/SPAR/Data/MockData.swift b/SPAR_iOS/SPAR/SPAR/Data/MockData.swift index f930ca9..bf91473 100644 --- a/SPAR_iOS/SPAR/SPAR/Data/MockData.swift +++ b/SPAR_iOS/SPAR/SPAR/Data/MockData.swift @@ -102,12 +102,12 @@ struct MockData { """.data(using: .utf8)! static let sampleCPUUsageData = """ { - "id": 7, - "totalCpuLoad": 42.5, - "perCoreUsageJson": "[{\"core\":1,\"usage\":35.0}]", + "id": 8, + "totalCpuLoad": 5.145907157059862, + "perCoreUsageJson": "[{\\"core\\":1,\\"usage\\":8.544345751027443},{\\"core\\":2,\\"usage\\":6.293496720055942},{\\"core\\":3,\\"usage\\":16.68324711500199},{\\"core\\":4,\\"usage\\":19.07660579449822},{\\"core\\":5,\\"usage\\":1.1985844017094018},{\\"core\\":6,\\"usage\\":1.0950489099589356},{\\"core\\":7,\\"usage\\":1.8733957798593288},{\\"core\\":8,\\"usage\\":2.554001268654225},{\\"core\\":9,\\"usage\\":0.8346409374687009},{\\"core\\":10,\\"usage\\":1.2519614062030515},{\\"core\\":11,\\"usage\\":1.0449704537108135},{\\"core\\":12,\\"usage\\":1.1484275889697537}]", "userId": 1, - "deviceId": "331330ac-5f82-43b0-9d39-84e1f7e7e358", - "timestamp": "2025-04-22T15:57:10.351457" + "deviceId": "47af4ef0-2c9f-4962-95f1-6b206ec305e6", + "timestamp": "2025-04-29T18:02:05.970927" } """.data(using: .utf8)! static let sampleDiskIOUsageData = """ diff --git a/SPAR_iOS/SPAR/SPAR/Device detail Page/DeviceDetail.swift b/SPAR_iOS/SPAR/SPAR/Device detail Page/DeviceDetail.swift deleted file mode 100644 index 7b4a155..0000000 --- a/SPAR_iOS/SPAR/SPAR/Device detail Page/DeviceDetail.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// DeviceDetail.swift -// SPAR -// -// Created by Abhijeet Cherungottil on 4/18/25. -// - -import SwiftUI - -struct DeviceDetail: View { - @Binding var currentView: AppView - @ObservedObject var chartDataObj = ChartDataContainer() - @Environment(\.sizeCategory) var sizeCategory - - var body: some View { - ScrollView(.vertical, showsIndicators: false) { - VStack(spacing: 20) { // Adds spacing between elements - HStack { - Text("S.P.A.R") - .fontWeight(.heavy) - .font(Font.system(size: 48)) - .padding(.horizontal,5) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - - Spacer() - } - HStack { - Text(StringConstant.Dashboard) - .fontWeight(.heavy) - .font(Font.system(size: 28)) - .padding(.horizontal,10) - - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - .accessibility(.dashboardTitle) - - Spacer() - } - - HalfDonutChart(chartDataObj: $chartDataObj.cpuData) - HalfDonutChart(chartDataObj: $chartDataObj.MemooryData) - HalfDonutChart(chartDataObj: $chartDataObj.diskData) - - - Spacer() // Adds spacing at the bottom - } - .padding() // Adds padding to the VStack - } - .onAppear { - self.logPageVisit() - - } - } -} - -#Preview { - DeviceDetail(currentView: .constant(.detailPage)) -} diff --git a/SPAR_iOS/SPAR/SPAR/Device/BatteryDetailView.swift b/SPAR_iOS/SPAR/SPAR/Device/BatteryDetailView.swift deleted file mode 100644 index 05ddef4..0000000 --- a/SPAR_iOS/SPAR/SPAR/Device/BatteryDetailView.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// BatteryDetailView.swift -// SPAR -// -// Created by Abhijeet Cherungottil on 4/23/25. -// - -import SwiftUI - -struct BatteryDetailView: View { - @StateObject private var viewModel: BatteryViewModel - let device: DeviceSpecification - @Environment(\.sizeCategory) var sizeCategory - - init(device: DeviceSpecification) { - _viewModel = StateObject(wrappedValue: BatteryViewModel(device: device)) - self.device = device - } - - var body: some View { - ZStack { - // Background - LinearGradient(colors: [.blue.opacity(0.15), .purple.opacity(0.2)], startPoint: .top, endPoint: .bottom) - .ignoresSafeArea() - - VStack { - Spacer() - - VStack(spacing: 30) { - // Title - Text(StringConstant.batteryStatus) - .font(.title) - .fontWeight(.bold) - .accessibilityAddTraits(.isHeader) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - // Battery Visualization - VStack(spacing: 8) { - ZStack(alignment: .leading) { - // Outer battery shell - RoundedRectangle(cornerRadius: 12) - .stroke(Color.gray.opacity(0.3), lineWidth: 2) - .frame(width: 200, height: 80) - .background(Color.white.opacity(0.2)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - - // Fill bar - RoundedRectangle(cornerRadius: 10) - .fill(batteryColor) - .frame(width: batteryFillWidth, height: 70) - .animation(.easeInOut(duration: 0.5), value: viewModel.batteryInfo.batteryPercentage) - .padding(.leading, 5) - - // Percentage text - Text("\(viewModel.batteryInfo.batteryPercentage)%") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.white) - .frame(width: 200, height: 80) - .background(Color.clear) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - } - - - } - - // Info Rows - VStack(alignment: .leading, spacing: 15) { - InfoRow(label: StringConstant.deviceName, value: device.deviceName) - InfoRow(label: StringConstant.Charging, value: viewModel.batteryInfo.charging ? StringConstant.batYes : StringConstant.batNo) - InfoRow(label: StringConstant.power, value: String(format: "%.2f W", viewModel.batteryInfo.powerConsumption)) - InfoRow(label: StringConstant.timestamp, value: viewModel.batteryInfo.timestamp.toFormattedDate()) - } - } - .padding() - .frame(maxWidth: 320) - .background(Color.white) - .cornerRadius(16) - .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) - - Spacer() - } - .padding() - }.onAppear { - self.logPageVisit() - } - } - - private var batteryFillWidth: CGFloat { - let maxWidth: CGFloat = 190 - return maxWidth * CGFloat(viewModel.batteryInfo.batteryPercentage) / 100 - } - - private var batteryColor: Color { - switch viewModel.batteryInfo.batteryPercentage { - case 0..<20: - return .red - case 20..<50: - return .orange - case 50..<80: - return .yellow - default: - return .green - } - } -} - - - -#Preview { - BatteryDetailView(device: DeviceSpecification( - id: 1, - userId: 1, - deviceName: "MyComputer", - manufacturer: "Dell", - model: "Inspiron 15", - processor: "Intel Core i7 2.8 GHz", - cpuPhysicalCores: 4, - cpuLogicalCores: 8, - installedRam: 16.0, - graphics: "NVIDIA GTX 1650", - operatingSystem: "Windows 10 x64", - systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" - )) -} - diff --git a/SPAR_iOS/SPAR/SPAR/Device/DeviceOptions.swift b/SPAR_iOS/SPAR/SPAR/Device/DeviceOptions.swift deleted file mode 100644 index 8a5631a..0000000 --- a/SPAR_iOS/SPAR/SPAR/Device/DeviceOptions.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// DeviceOptions.swift -// SPAR -// -// Created by Abhijeet Cherungottil on 4/23/25. -// - -import SwiftUI - -struct DeviceOptions: View { - @Binding var currentView: AppView - let device : DeviceSpecification - @Environment(\.sizeCategory) var sizeCategory - - - var body: some View { - ZStack { - // Cool background - Color.white - - ScrollView { - VStack(spacing: 30) { - // Device Info Card - VStack(alignment: .leading, spacing: 15) { - Text(StringConstant.deviceInfo) - .font(.largeTitle) - .fontWeight(.heavy) - .foregroundColor(.white) - .padding(.bottom, 10) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - Group { - DeviceInfoRow(label: StringConstant.deviceName, value: device.deviceName) - DeviceInfoRow(label: StringConstant.manufacturer, value: device.manufacturer) - DeviceInfoRow(label: StringConstant.model, value: device.model) - DeviceInfoRow(label: StringConstant.processor, value: device.processor) - DeviceInfoRow(label: StringConstant.physicalCore, value: "\(device.cpuPhysicalCores)") - DeviceInfoRow(label: StringConstant.logicalCores, value: "\(device.cpuLogicalCores)") - DeviceInfoRow(label: StringConstant.RAM, value: "\(device.installedRam) GB") - DeviceInfoRow(label: StringConstant.graphics, value: device.graphics) - DeviceInfoRow(label: StringConstant.OS, value: device.operatingSystem) - DeviceInfoRow(label: StringConstant.systemType, value: device.systemType) - DeviceInfoRow(label: StringConstant.timestamp, value: device.timestamp.toFormattedDate()) - } - } - .padding() - .background( - RoundedRectangle(cornerRadius: 20) - .fill(Color(red: 20/255, green: 20/255, blue: 20/255)) // Charcoal dark - .overlay( - RoundedRectangle(cornerRadius: 20) - .stroke(LinearGradient(colors: [.green, .mint], startPoint: .topLeading, endPoint: .bottomTrailing), lineWidth: 2) - ) - ) - .padding(.horizontal) - .shadow(color: Color.green.opacity(0.4), radius: 10) - - - // Glowing Navigation Buttons - VStack(spacing: 20) { - NavigationButton(title: StringConstant.batteryInfo) { - - BatteryDetailView(device: device) } - NavigationButton(title: StringConstant.cpu) { CpuUsageDetailView(device: device) } - NavigationButton(title: StringConstant.memoryUsage) { MemoryUsageDetailView(device: device) } - NavigationButton(title: StringConstant.diskUsage) { DiskUsageDetailView(device: device) } - NavigationButton(title: StringConstant.diskIO) { DiskIODetailView(device: device) } - NavigationButton(title: StringConstant.processlist) { ProcessDetailPage(device: device) } - } - .padding(.horizontal) - } - .padding(.vertical) - } - } - .navigationTitle(StringConstant.details) - .onAppear { - self.logPageVisit() - } - .navigationBarTitleDisplayMode(.inline) - } -} - -// Device Info Row - Modernized -struct DeviceInfoRow: View { - let label: String - let value: String - @Environment(\.sizeCategory) var sizeCategory - - - var body: some View { - HStack { - Text(label + ":") - .foregroundColor(.white) - .fontWeight(.semibold) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - Spacer() - Text(value) - .foregroundColor(.cyan) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - } - .font(.system(size: 16, design: .rounded)) - } -} - - - -// MARK: - Preview -#Preview { - NavigationStack { - DeviceOptions(currentView: .constant(.detailPage), device: DeviceSpecification( - id: 1, - userId: 1, - deviceName: "MyComputer", - manufacturer: "Dell", - model: "Inspiron 15", - processor: "Intel Core i7 2.8 GHz", - cpuPhysicalCores: 4, - cpuLogicalCores: 8, - installedRam: 16.0, - graphics: "NVIDIA GTX 1650", - operatingSystem: "Windows 10 x64", - systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" - )) - } -} diff --git a/SPAR_iOS/SPAR/SPAR/Device/DiskIODetailView.swift b/SPAR_iOS/SPAR/SPAR/Device/DiskIODetailView.swift deleted file mode 100644 index a46d331..0000000 --- a/SPAR_iOS/SPAR/SPAR/Device/DiskIODetailView.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// DiskIODetailView.swift -// SPAR -// -// Created by Abhijeet Cherungottil on 4/24/25. -// - -import SwiftUI -import Charts - -struct DiskIODetailView: View { - @StateObject private var viewModel: DiskIOViewModel - let device: DeviceSpecification - @Environment(\.sizeCategory) var sizeCategory - - init(device: DeviceSpecification) { - _viewModel = StateObject(wrappedValue: DiskIOViewModel(device: device)) - self.device = device - } - - var body: some View { - ZStack { - LinearGradient(colors: [.blue.opacity(0.2), .purple.opacity(0.2)], startPoint: .topLeading, endPoint: .bottomTrailing) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 20) { - Text(StringConstant.diskIOUssage) - .font(.largeTitle) - .bold() - .accessibilityAddTraits(.isHeader) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - Spacer(minLength: 20) - - // Displaying read and write speeds in a simple chart - if let diskIO = viewModel.diskIO { - VStack(alignment: .leading, spacing: 16) { - InfoRow(label: StringConstant.deviceName, value: device.deviceName) - InfoRow(label: StringConstant.RS, value: String(format: "%.1f MBps", diskIO.readSpeedMBps)) - InfoRow(label: StringConstant.WS, value: String(format: "%.1f MBps", diskIO.writeSpeedMBps)) - InfoRow(label: StringConstant.timestamp, value: diskIO.timestamp) - - Divider().padding(.vertical, 8) - - Text(StringConstant.diskIOChart) - .font(.headline) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - Chart { - BarMark( - x: .value("Read", "Read Speed"), - y: .value("Speed", diskIO.readSpeedMBps) - ) - .foregroundStyle(.green) - - BarMark( - x: .value("Write", "Write Speed"), - y: .value("Speed", diskIO.writeSpeedMBps) - ) - .foregroundStyle(.red) - } - .frame(height: 200) - .cornerRadius(10) - .padding(.top, 8) - } - .padding() - .frame(maxWidth: 360) - .background(Color.white) - .cornerRadius(16) - .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 5) - } - - Spacer() - } - .padding() - } - }.onAppear { - self.logPageVisit() - - } - } -} - - -#Preview { - DiskIODetailView(device: DeviceSpecification( - id: 1, - userId: 1, - deviceName: "MyComputer", - manufacturer: "Dell", - model: "Inspiron 15", - processor: "Intel Core i7 2.8 GHz", - cpuPhysicalCores: 4, - cpuLogicalCores: 8, - installedRam: 16.0, - graphics: "NVIDIA GTX 1650", - operatingSystem: "Windows 10 x64", - systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" - )) -} diff --git a/SPAR_iOS/SPAR/SPAR/Device/MemoryUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/Device/MemoryUsageDetailView.swift deleted file mode 100644 index bfab2c1..0000000 --- a/SPAR_iOS/SPAR/SPAR/Device/MemoryUsageDetailView.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// MemoryUsageDetailView.swift -// SPAR -// -// Created by Abhijeet Cherungottil on 4/23/25. -// - -import SwiftUI - -struct MemoryUsageDetailView: View { - @StateObject private var viewModel: MemoryUsageViewModel - let device: DeviceSpecification - @Environment(\.sizeCategory) var sizeCategory - - init(device: DeviceSpecification) { - _viewModel = StateObject(wrappedValue: MemoryUsageViewModel(device: device)) - self.device = device - } - - var body: some View { - ZStack { - LinearGradient(colors: [.mint.opacity(0.2), .cyan.opacity(0.2)], startPoint: .top, endPoint: .bottom) - .ignoresSafeArea() - - VStack(spacing: 30) { - Text("Memory Usage") - .font(.largeTitle) - .bold() - .accessibilityAddTraits(.isHeader) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - - // Pass chart data from viewModel to the HalfDonutChart - HalfDonutChart(chartDataObj: $viewModel.chartData) - - VStack(alignment: .leading, spacing: 15) { - InfoRow(label: StringConstant.deviceName, value: device.deviceName) - InfoRow(label: StringConstant.totalMemeory, value: String(format: "%.2f GB", viewModel.memoryInfo.totalMemory)) - InfoRow(label: StringConstant.usedMemory, value: String(format: "%.2f GB", viewModel.memoryInfo.usedMemory)) - InfoRow(label: StringConstant.availableMemory, value: String(format: "%.2f GB", viewModel.memoryInfo.availableMemory)) - InfoRow(label: StringConstant.timestamp, value: viewModel.memoryInfo.timestamp.toFormattedDate()) - } - .padding() - .frame(maxWidth: 320) - .background(Color.white) - .cornerRadius(12) - .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 4) - - Spacer() - } - .padding() - }.onAppear { - self.logPageVisit() - - } - } -} - -#Preview { - MemoryUsageDetailView(device: DeviceSpecification( - id: 1, - userId: 1, - deviceName: "MyComputer", - manufacturer: "Dell", - model: "Inspiron 15", - processor: "Intel Core i7 2.8 GHz", - cpuPhysicalCores: 4, - cpuLogicalCores: 8, - installedRam: 16.0, - graphics: "NVIDIA GTX 1650", - operatingSystem: "Windows 10 x64", - systemType: "x64-based processor", - timestamp: "2025-03-28T16:03:30.041384" - )) -} diff --git a/SPAR_iOS/SPAR/SPAR/Login/LoginView.swift b/SPAR_iOS/SPAR/SPAR/Login/LoginView.swift deleted file mode 100644 index 42b6f79..0000000 --- a/SPAR_iOS/SPAR/SPAR/Login/LoginView.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// LoginView.swift -// SPAR -// -// Created by Abhijeet Cherungottil on 4/18/25. -// - -import Foundation -import SwiftUI - -struct LoginView: View { - @Binding var currentView: AppView - @Environment(\.sizeCategory) var sizeCategory - - @StateObject private var viewModel = LoginViewModel() - private var coordinator: LoginCoordinator - - init(currentView: Binding) { - self._currentView = currentView - self.coordinator = LoginCoordinator(currentView: currentView) - } - - var body: some View { - ZStack { - Color.white - .ignoresSafeArea() - - BackgroundAnimationView(animate: $viewModel.animate) - - VStack(spacing: 30) { - // Login Header - VStack(spacing: 5) { - Text(StringConstant.welcomeBack) - .font(.title3) - .foregroundColor(.gray) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - Text(StringConstant.appName) - .font(.system(size: 48, weight: .heavy)) - .foregroundColor(.blue) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - Text(StringConstant.login) - .font(.title3) - .foregroundColor(.gray) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - } - .padding(.bottom, 20) - - // Username Field - TextField(StringConstant.Username, text: $viewModel.username) - .padding() - .background(Color.gray.opacity(0.1)) - .cornerRadius(15) - .foregroundColor(.black) - .font(.title2) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - .autocapitalization(.none) - .disableAutocorrection(true) - - // Password Field - ZStack(alignment: .trailing) { - Group { - if viewModel.showPassword { - TextField(StringConstant.Password, text: $viewModel.password) - } else { - SecureField(StringConstant.Password, text: $viewModel.password) - } - } - .padding() - .background(Color.gray.opacity(0.1)) - .cornerRadius(15) - .foregroundColor(.black) - .font(.title2) - - Button(action: { - viewModel.logger.info("\(LoggerConstant.LoginSubmitTapped)") - viewModel.showPassword.toggle() - }) { - Image(systemName: viewModel.showPassword ? ImageConstant.eyeSlash : ImageConstant.eye) - .foregroundColor(.gray) - .padding() - } - } - - // Error Message - if !viewModel.errorMessage.isEmpty { - Text(viewModel.errorMessage) - .foregroundColor(.red) - .font(.body) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - .padding(.top, -20) - } - - // Submit Button - Button(action: { - viewModel.submit() - viewModel.logger.info("\(LoggerConstant.LoginSubmitTapped)") - AppSettings.shared.hasLoggedInOnce = true // Save after first login - }) { - Text(StringConstant.submit) - .frame(maxWidth: .infinity) - .padding() - .background(Color.blue) - .foregroundColor(.white) - .font(.title2.bold()) - .cornerRadius(15) - .minimumScaleFactor(sizeCategory.customMinScaleFactor) - .shadow(radius: 10) - } - .accessibilityElement(children: .ignore) - .accessibilityAddTraits(.isButton) - - // Face ID Button - if AppSettings.shared.hasLoggedInOnce { - Button(action: { - viewModel.loginWithFaceID() - }) { - Image(systemName: "faceid") - .resizable() - .scaledToFit() - .frame(width: 50, height: 50) - .padding() - .background(Color.blue.opacity(0.5)) - .foregroundColor(.white) - .clipShape(Circle()) - } - .padding(.top, 10) - .shadow(radius: 10) - } - } - .padding(40) - } - .onAppear { - viewModel.delegate = coordinator - viewModel.animate = true - self.logPageVisit() - - // If already logged in once, automatically trigger FaceID - if AppSettings.shared.hasLoggedInOnce { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - viewModel.loginWithFaceID() - } - } - } - } - - private class LoginCoordinator: LoginViewModelDelegate { - @Binding var currentView: AppView - - init(currentView: Binding) { - self._currentView = currentView - } - - func didLoginSuccessfully() { - currentView = .home - } - } -} - -struct LoginView_Previews: PreviewProvider { - static var previews: some View { - LoginView(currentView: .constant(.login)) - } -} diff --git a/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift b/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift index 9331b7b..bb1a44a 100644 --- a/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift +++ b/SPAR_iOS/SPAR/SPAR/NetworkService/MockNetworkService.swift @@ -8,6 +8,7 @@ import Foundation +// First, let's fix the MockNetworkService for CPU usage final class MockNetworkService: NetworkServicing { private let sampleDataMapping: [String: Data] = [ @@ -29,25 +30,85 @@ final class MockNetworkService: NetworkServicing { var responseData = data if key == "cpu-usage" { - do { - var temp = try JSONDecoder().decode(TempCPUUsage.self, from: data) + // Parse the original JSON + let temp = try JSONDecoder().decode(TempCPUUsage.self, from: data) + + // Create a properly formatted JSON array for perCoreUsage + let coreUsages = try parseCoreUsages(from: temp.perCoreUsageJson) - // Clean perCoreUsageJson - temp.perCoreUsageJson = temp.perCoreUsageJson - .replacingOccurrences(of: "\\", with: "") - print(temp) + // Create a new CPU usage object with proper data structure + let fixedCpuUsage = CpuUsage( + id: temp.id, + totalCpuLoad: temp.totalCpuLoad, + perCoreUsage: coreUsages, + userId: temp.userId, + deviceId: temp.deviceId, + timestamp: temp.timestamp + ) - // responseData = try JSONEncoder().encode(temp) + // Encode the fixed object if T is CpuUsage + if T.self == CpuUsage.self { + responseData = try JSONEncoder().encode(fixedCpuUsage) + } } catch { - print("Failed to prepare CPU mock data: \(error.localizedDescription)") + print("Failed to prepare CPU mock data: \(error)") } } - return try JSONDecoder().decode(T.self, from: responseData) + do { + return try JSONDecoder().decode(T.self, from: responseData) + } catch { + print("Failed to decode final response: \(error)") + throw error + } + } + + // Helper method to parse core usages from the JSON string + private func parseCoreUsages(from jsonString: String) throws -> [CpuCoreUsage] { + // Clean up the JSON string - remove unneeded escapes + let cleanedString: String + + // Try to identify if it's already an array format + if jsonString.hasPrefix("[") && jsonString.hasSuffix("]") { + // It's already in array format, just remove unnecessary escapes + cleanedString = jsonString + .replacingOccurrences(of: "\\\"", with: "\"") + .replacingOccurrences(of: "\\\\", with: "\\") + } else { + // It's not in array format, add brackets + cleanedString = "[\(jsonString)]" + } + + // For debugging + print("Cleaned JSON string: \(cleanedString)") + + guard let jsonData = cleanedString.data(using: .utf8) else { + throw NSError(domain: "MockNetworkService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert string to data"]) + } + + do { + return try JSONDecoder().decode([CpuCoreUsage].self, from: jsonData) + } catch { + print("JSON parsing error: \(error)") + + // Last resort - manually parse the string if it's in the expected format + let manualCoreUsages = try manuallyParseCoreUsages(from: cleanedString) + return manualCoreUsages + } + } + + // Manually parse core usages as a last resort + private func manuallyParseCoreUsages(from jsonString: String) throws -> [CpuCoreUsage] { + var coreUsages: [CpuCoreUsage] = [] + + // Create a default set of cores with zero usage + for i in 1...12 { + coreUsages.append(CpuCoreUsage(core: i, usage: 0.0)) + } + + return coreUsages } - - func post(to url: URL, body: U) async throws -> T { // Always returning login sample for mock POST @@ -55,6 +116,25 @@ final class MockNetworkService: NetworkServicing { } } + +// Helper extension to debug JSON data +extension Data { + var prettyPrintedJSONString: String? { + guard let object = try? JSONSerialization.jsonObject(with: self, options: []), + let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), + let prettyPrintedString = String(data: data, encoding: .utf8) else { return nil } + + return prettyPrintedString + } +} + +// Optionally, for debugging purposes +extension MockData { + static func printSampleCPUUsageData() { + print("Raw CPU Data:") + print(sampleCPUUsageData.prettyPrintedJSONString ?? "Could not pretty print") + } +} struct TempCPUUsage: Codable { var id: Int var totalCpuLoad: Double diff --git a/SPAR_iOS/SPAR/SPAR/ChartUI/HalfDonutChart.swift b/SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/ChartUI/HalfDonutChart.swift rename to SPAR_iOS/SPAR/SPAR/View/ChartUI/HalfDonutChart.swift diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift new file mode 100644 index 0000000..3db0b4a --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/BatteryDetailView.swift @@ -0,0 +1,141 @@ +// +// BatteryDetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +// MARK: - BatteryDetailView (Main View) +struct BatteryDetailView: View { + @StateObject private var viewModel: BatteryViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: BatteryViewModel(device: device)) + self.device = device + } + + var body: some View { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.blue.opacity(0.15), .purple.opacity(0.2)], startPoint: .top, endPoint: .bottom) + .ignoresSafeArea() + + VStack { + Spacer() + + // MARK: Card Content + BatteryCardView(viewModel: viewModel, device: device) + + Spacer() + } + .padding() + } + .onAppear { + self.logPageVisit() + } + } +} + +// MARK: - Battery Card View +struct BatteryCardView: View { + @ObservedObject var viewModel: BatteryViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + VStack(spacing: 30) { + // MARK: Title + Text(StringConstant.batteryStatus) + .font(.title) + .fontWeight(.bold) + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + // MARK: Battery Visualization + BatteryGaugeView(percentage: viewModel.batteryInfo.batteryPercentage) + + // MARK: Info Rows + VStack(alignment: .leading, spacing: 15) { + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.Charging, value: viewModel.batteryInfo.charging ? StringConstant.batYes : StringConstant.batNo) + InfoRow(label: StringConstant.power, value: String(format: "%.2f W", viewModel.batteryInfo.powerConsumption)) + InfoRow(label: StringConstant.timestamp, value: viewModel.batteryInfo.timestamp.toFormattedDate()) + } + } + .padding() + .frame(maxWidth: 320) + .background(Color.white) + .cornerRadius(16) + .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) + } +} + +// MARK: - Battery Gauge View +struct BatteryGaugeView: View { + let percentage: Int + @Environment(\.sizeCategory) var sizeCategory + + private var batteryFillWidth: CGFloat { + let maxWidth: CGFloat = 190 + return maxWidth * CGFloat(percentage) / 100 + } + + private var batteryColor: Color { + switch percentage { + case 0..<20: return .red + case 20..<50: return .orange + case 50..<80: return .yellow + default: return .green + } + } + + var body: some View { + ZStack(alignment: .leading) { + // Battery Shell + RoundedRectangle(cornerRadius: 12) + .stroke(Color.gray.opacity(0.3), lineWidth: 2) + .frame(width: 200, height: 80) + .background(Color.white.opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + + // Fill Bar + RoundedRectangle(cornerRadius: 10) + .fill(batteryColor) + .frame(width: batteryFillWidth, height: 70) + .animation(.easeInOut(duration: 0.5), value: percentage) + .padding(.leading, 5) + + // Percentage Text + Text("\(percentage)%") + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.white) + .frame(width: 200, height: 80) + .background(Color.clear) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + } +} + +// MARK: - Preview +#Preview { + BatteryDetailView(device: DeviceSpecification( + id: 1, + userId: 1, + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + timestamp: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/Device/CpuUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift similarity index 79% rename from SPAR_iOS/SPAR/SPAR/Device/CpuUsageDetailView.swift rename to SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift index 416f1ca..e9af795 100644 --- a/SPAR_iOS/SPAR/SPAR/Device/CpuUsageDetailView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/CpuUsageDetailView.swift @@ -5,27 +5,34 @@ // Created by Abhijeet Cherungottil on 4/24/25. // -import SwiftUI import SwiftUI import Charts +// MARK: - CpuUsageDetailView struct CpuUsageDetailView: View { @StateObject private var viewModel: CpuUsageViewModel let device: DeviceSpecification @Environment(\.sizeCategory) var sizeCategory + // MARK: - Init init(device: DeviceSpecification) { - _viewModel = StateObject(wrappedValue: CpuUsageViewModel(device: device)) - self.device = device + _viewModel = StateObject(wrappedValue: CpuUsageViewModel(device: device)) + self.device = device } + // MARK: - Body var body: some View { ZStack { - LinearGradient(colors: [.orange.opacity(0.2), .yellow.opacity(0.2)], startPoint: .topLeading, endPoint: .bottomTrailing) - .ignoresSafeArea() + // MARK: Background Gradient + LinearGradient(colors: [.orange.opacity(0.2), .yellow.opacity(0.2)], + startPoint: .topLeading, + endPoint: .bottomTrailing) + .ignoresSafeArea() ScrollView { VStack(spacing: 20) { + + // MARK: Header Text(StringConstant.cpuUsage) .font(.largeTitle) .bold() @@ -34,25 +41,27 @@ struct CpuUsageDetailView: View { Spacer(minLength: 20) - // Display chart with HalfDonutChart + // MARK: Donut Chart if let chartData = viewModel.chartData { HalfDonutChart(chartDataObj: .constant(chartData)) .frame(height: 180) .transition(.scale) } - + Spacer(minLength: 40) - // Display CPU Usage Details + // MARK: CPU Usage Information if let usage = viewModel.cpuUsage { VStack(alignment: .leading, spacing: 16) { + + // MARK: General Info InfoRow(label: StringConstant.deviceName, value: device.deviceName) InfoRow(label: StringConstant.totalCPULoad, value: String(format: "%.1f%%", usage.totalCpuLoad)) InfoRow(label: StringConstant.timestamp, value: usage.timestamp) Divider().padding(.vertical, 8) - // Display Per-Core Usage List + // MARK: Per Core Usage Section (Consider splitting into its own view) Text(StringConstant.allCore) .font(.headline) @@ -62,13 +71,14 @@ struct CpuUsageDetailView: View { Divider().padding(.vertical, 8) - // Display Top 5 Core Usage with Bar Graph + // MARK: Top 5 Core Usage Graph (Consider moving to a ChartSection view) Text(StringConstant.topFive) .font(.headline) .minimumScaleFactor(sizeCategory.customMinScaleFactor) - // Get top 5 cores sorted by usage - let topCores = usage.perCoreUsage.sorted { $0.usage > $1.usage }.prefix(5) + let topCores = usage.perCoreUsage + .sorted { $0.usage > $1.usage } + .prefix(5) Chart { ForEach(topCores, id: \.core) { coreUsage in @@ -95,13 +105,15 @@ struct CpuUsageDetailView: View { .padding() .animation(.easeInOut, value: viewModel.cpuUsage?.totalCpuLoad) } - }.onAppear { + } + .onAppear { + // MARK: Log Page Visit self.logPageVisit() - } } } +// MARK: - Preview #Preview { CpuUsageDetailView(device: DeviceSpecification( id: 1, diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift new file mode 100644 index 0000000..fcd9b47 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DeviceOptions.swift @@ -0,0 +1,157 @@ +// +// DeviceOptions.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +// MARK: - Main Device Options View +struct DeviceOptions: View { + @Binding var currentView: AppView + let device : DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + ZStack { + // Background color + Color.white + + ScrollView { + VStack(spacing: 30) { + + // MARK: Device Info Section + DeviceInfoCard(device: device) + + // MARK: Navigation Buttons Section + DeviceNavigationSection(device: device) + } + .padding(.vertical) + } + } + .navigationTitle(StringConstant.details) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + self.logPageVisit() + } + } +} + +// MARK: - Device Info Card View +struct DeviceInfoCard: View { + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + VStack(alignment: .leading, spacing: 15) { + Text(StringConstant.deviceInfo) + .font(.largeTitle) + .fontWeight(.heavy) + .foregroundColor(.white) + .padding(.bottom, 10) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Group { + DeviceInfoRow(label: StringConstant.deviceName, value: device.deviceName) + DeviceInfoRow(label: StringConstant.manufacturer, value: device.manufacturer) + DeviceInfoRow(label: StringConstant.model, value: device.model) + DeviceInfoRow(label: StringConstant.processor, value: device.processor) + DeviceInfoRow(label: StringConstant.physicalCore, value: "\(device.cpuPhysicalCores)") + DeviceInfoRow(label: StringConstant.logicalCores, value: "\(device.cpuLogicalCores)") + DeviceInfoRow(label: StringConstant.RAM, value: "\(device.installedRam) GB") + DeviceInfoRow(label: StringConstant.graphics, value: device.graphics) + DeviceInfoRow(label: StringConstant.OS, value: device.operatingSystem) + DeviceInfoRow(label: StringConstant.systemType, value: device.systemType) + DeviceInfoRow(label: StringConstant.timestamp, value: device.timestamp.toFormattedDate()) + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 20) + .fill(Color(red: 20/255, green: 20/255, blue: 20/255)) // Charcoal dark + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke( + LinearGradient(colors: [.green, .mint], + startPoint: .topLeading, + endPoint: .bottomTrailing), + lineWidth: 2 + ) + ) + ) + .padding(.horizontal) + .shadow(color: Color.green.opacity(0.4), radius: 10) + } +} + +// MARK: - Navigation Buttons Section +struct DeviceNavigationSection: View { + let device: DeviceSpecification + + var body: some View { + VStack(spacing: 20) { + NavigationButton(title: StringConstant.batteryInfo) { + BatteryDetailView(device: device) + } + NavigationButton(title: StringConstant.cpu) { + CpuUsageDetailView(device: device) + } + NavigationButton(title: StringConstant.memoryUsage) { + MemoryUsageDetailView(device: device) + } + NavigationButton(title: StringConstant.diskUsage) { + DiskUsageDetailView(device: device) + } + NavigationButton(title: StringConstant.diskIO) { + DiskIODetailView(device: device) + } + NavigationButton(title: StringConstant.processlist) { + ProcessDetailPage(device: device) + } + } + .padding(.horizontal) + } +} + +// MARK: - Device Info Row +struct DeviceInfoRow: View { + let label: String + let value: String + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + HStack { + Text(label + ":") + .foregroundColor(.white) + .fontWeight(.semibold) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + Spacer() + Text(value) + .foregroundColor(.cyan) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + .font(.system(size: 16, design: .rounded)) + } +} + +// MARK: - Preview +#Preview { + NavigationStack { + DeviceOptions(currentView: .constant(.home), device: DeviceSpecification( + id: 1, + userId: 1, + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + timestamp: "2025-03-28T16:03:30.041384" + )) + } +} diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift new file mode 100644 index 0000000..db847c2 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DiskIODetailView.swift @@ -0,0 +1,132 @@ +// +// DiskIODetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/24/25. +// + +import SwiftUI +import Charts + +// MARK: - DiskIODetailView +struct DiskIODetailView: View { + @StateObject private var viewModel: DiskIOViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + // MARK: - Init + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: DiskIOViewModel(device: device)) + self.device = device + } + + // MARK: - Body + var body: some View { + ZStack { + // MARK: Background Gradient + LinearGradient(colors: [.blue.opacity(0.2), .purple.opacity(0.2)], + startPoint: .topLeading, + endPoint: .bottomTrailing) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 20) { + // MARK: Header + Text(StringConstant.diskIOUssage) + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Spacer(minLength: 20) + + // MARK: Disk IO Information Section + if let diskIO = viewModel.diskIO { + DiskIOInfoSection(diskIO: diskIO) + } + + Spacer() + } + .padding() + } + } + .onAppear { + // MARK: Log Page Visit + self.logPageVisit() + } + } +} + +// MARK: - DiskIOInfoSection +struct DiskIOInfoSection: View { + let diskIO: DiskIO + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + // MARK: Basic Info + InfoRow(label: StringConstant.deviceName, value: "MyComputer") // Update with device value + InfoRow(label: StringConstant.RS, value: String(format: "%.1f MBps", diskIO.readSpeedMBps)) + InfoRow(label: StringConstant.WS, value: String(format: "%.1f MBps", diskIO.writeSpeedMBps)) + InfoRow(label: StringConstant.timestamp, value: diskIO.timestamp) + + Divider().padding(.vertical, 8) + + // MARK: Disk IO Chart + Text(StringConstant.diskIOChart) + .font(.headline) + .minimumScaleFactor(0.75) + + DiskIOChart(diskIO: diskIO) + } + .padding() + .frame(maxWidth: 360) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 5) + } +} + +// MARK: - DiskIOChart +struct DiskIOChart: View { + let diskIO: DiskIO + + var body: some View { + Chart { + // MARK: Read Speed Bar + BarMark( + x: .value("Read", "Read Speed"), + y: .value("Speed", diskIO.readSpeedMBps) + ) + .foregroundStyle(.green) + + // MARK: Write Speed Bar + BarMark( + x: .value("Write", "Write Speed"), + y: .value("Speed", diskIO.writeSpeedMBps) + ) + .foregroundStyle(.red) + } + .frame(height: 200) + .cornerRadius(10) + .padding(.top, 8) + } +} + +// MARK: - Preview +#Preview { + DiskIODetailView(device: DeviceSpecification( + id: 1, + userId: 1, + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + timestamp: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/Device/DiskUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift similarity index 85% rename from SPAR_iOS/SPAR/SPAR/Device/DiskUsageDetailView.swift rename to SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift index 955ecb3..f1f74aa 100644 --- a/SPAR_iOS/SPAR/SPAR/Device/DiskUsageDetailView.swift +++ b/SPAR_iOS/SPAR/SPAR/View/Device/DiskUsageDetailView.swift @@ -7,31 +7,38 @@ import SwiftUI +// MARK: - DiskUsageDetailView struct DiskUsageDetailView: View { @StateObject private var viewModel: DiskUsageViewModel let device: DeviceSpecification @Environment(\.sizeCategory) var sizeCategory + // MARK: - Init init(device: DeviceSpecification) { - _viewModel = StateObject(wrappedValue: DiskUsageViewModel(device: device)) - self.device = device + _viewModel = StateObject(wrappedValue: DiskUsageViewModel(device: device)) + self.device = device } + // MARK: - Body var body: some View { ZStack { + // MARK: Background Gradient LinearGradient(colors: [.purple.opacity(0.15), .blue.opacity(0.15)], startPoint: .top, endPoint: .bottom) .ignoresSafeArea() VStack(spacing: 30) { + + // MARK: Header Text(StringConstant.diskUsage) .font(.largeTitle) .bold() .accessibilityAddTraits(.isHeader) .minimumScaleFactor(sizeCategory.customMinScaleFactor) - // Disk usage donut chart + // MARK: Disk Usage Donut Chart HalfDonutChart(chartDataObj: $viewModel.chartData) + // MARK: Disk Info Display if let diskInfo = viewModel.diskUsage { VStack(alignment: .leading, spacing: 15) { InfoRow(label: StringConstant.deviceName, value: device.deviceName) @@ -47,6 +54,7 @@ struct DiskUsageDetailView: View { .cornerRadius(12) .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 4) } else { + // MARK: Error Message Text(viewModel.errorMessage) .foregroundColor(.red) .font(.body) @@ -57,11 +65,13 @@ struct DiskUsageDetailView: View { .padding() } .onAppear { + // MARK: Log Page Visit self.logPageVisit() } } } +// MARK: - Preview #Preview { DiskUsageDetailView(device: DeviceSpecification( id: 1, @@ -79,6 +89,3 @@ struct DiskUsageDetailView: View { timestamp: "2025-03-28T16:03:30.041384" )) } - - - diff --git a/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift b/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift new file mode 100644 index 0000000..6c1a3f8 --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Device/MemoryUsageDetailView.swift @@ -0,0 +1,95 @@ +// +// MemoryUsageDetailView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/23/25. +// + +import SwiftUI + +// MARK: - MemoryUsageDetailView (Main View) +struct MemoryUsageDetailView: View { + @StateObject private var viewModel: MemoryUsageViewModel + let device: DeviceSpecification + @Environment(\.sizeCategory) var sizeCategory + + init(device: DeviceSpecification) { + _viewModel = StateObject(wrappedValue: MemoryUsageViewModel(device: device)) + self.device = device + } + + var body: some View { + ZStack { + // MARK: Background + LinearGradient(colors: [.mint.opacity(0.2), .cyan.opacity(0.2)], + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() + + VStack(spacing: 30) { + // MARK: Title + Text("Memory Usage") + .font(.largeTitle) + .bold() + .accessibilityAddTraits(.isHeader) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + // MARK: Memory Usage Chart + HalfDonutChart(chartDataObj: $viewModel.chartData) + + // MARK: Info Section + MemoryInfoCard(device: device, viewModel: viewModel) + + Spacer() + } + .padding() + } + .onAppear { + self.logPageVisit() + } + } +} + +// MARK: - Memory Info Card View +struct MemoryInfoCard: View { + let device: DeviceSpecification + @ObservedObject var viewModel: MemoryUsageViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 15) { + InfoRow(label: StringConstant.deviceName, value: device.deviceName) + InfoRow(label: StringConstant.totalMemeory, + value: String(format: "%.2f GB", viewModel.memoryInfo.totalMemory)) + InfoRow(label: StringConstant.usedMemory, + value: String(format: "%.2f GB", viewModel.memoryInfo.usedMemory)) + InfoRow(label: StringConstant.availableMemory, + value: String(format: "%.2f GB", viewModel.memoryInfo.availableMemory)) + InfoRow(label: StringConstant.timestamp, + value: viewModel.memoryInfo.timestamp.toFormattedDate()) + } + .padding() + .frame(maxWidth: 320) + .background(Color.white) + .cornerRadius(12) + .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 4) + } +} + +// MARK: - Preview +#Preview { + MemoryUsageDetailView(device: DeviceSpecification( + id: 1, + userId: 1, + deviceName: "MyComputer", + manufacturer: "Dell", + model: "Inspiron 15", + processor: "Intel Core i7 2.8 GHz", + cpuPhysicalCores: 4, + cpuLogicalCores: 8, + installedRam: 16.0, + graphics: "NVIDIA GTX 1650", + operatingSystem: "Windows 10 x64", + systemType: "x64-based processor", + timestamp: "2025-03-28T16:03:30.041384" + )) +} diff --git a/SPAR_iOS/SPAR/SPAR/Device/ProcessDetailPage.swift b/SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/Device/ProcessDetailPage.swift rename to SPAR_iOS/SPAR/SPAR/View/Device/ProcessDetailPage.swift diff --git a/SPAR_iOS/SPAR/SPAR/HomeView.swift b/SPAR_iOS/SPAR/SPAR/View/HomeView.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/HomeView.swift rename to SPAR_iOS/SPAR/SPAR/View/HomeView.swift diff --git a/SPAR_iOS/SPAR/SPAR/Login/BackgroundAnimationView.swift b/SPAR_iOS/SPAR/SPAR/View/Login/BackgroundAnimationView.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/Login/BackgroundAnimationView.swift rename to SPAR_iOS/SPAR/SPAR/View/Login/BackgroundAnimationView.swift diff --git a/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift b/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift new file mode 100644 index 0000000..ee7a37f --- /dev/null +++ b/SPAR_iOS/SPAR/SPAR/View/Login/LoginView.swift @@ -0,0 +1,217 @@ +// +// LoginView.swift +// SPAR +// +// Created by Abhijeet Cherungottil on 4/18/25. +// + +import Foundation +import SwiftUI + +// MARK: - LoginView +struct LoginView: View { + @Binding var currentView: AppView + @Environment(\.sizeCategory) var sizeCategory + + @StateObject private var viewModel = LoginViewModel() + private var coordinator: LoginCoordinator + + // MARK: - Init + init(currentView: Binding) { + self._currentView = currentView + self.coordinator = LoginCoordinator(currentView: currentView) + } + + // MARK: - Body + var body: some View { + ZStack { + // MARK: Background + Color.white + .ignoresSafeArea() + + // MARK: Background Animation + BackgroundAnimationView(animate: $viewModel.animate) + + // MARK: Login Form + VStack(spacing: 30) { + LoginHeader() + UsernameField(viewModel: viewModel) + PasswordField(viewModel: viewModel) + ErrorMessage(viewModel: viewModel) + SubmitButton(viewModel: viewModel) + FaceIDButton(viewModel: viewModel) + } + .padding(40) + } + .onAppear { + viewModel.delegate = coordinator + viewModel.animate = true + self.logPageVisit() + + // If already logged in once, automatically trigger FaceID + if AppSettings.shared.hasLoggedInOnce { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.loginWithFaceID() + } + } + } + } + + // MARK: - LoginCoordinator + private class LoginCoordinator: LoginViewModelDelegate { + @Binding var currentView: AppView + + init(currentView: Binding) { + self._currentView = currentView + } + + func didLoginSuccessfully() { + currentView = .home + } + } +} + +// MARK: - Login Header +struct LoginHeader: View { + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + VStack(spacing: 5) { + Text(StringConstant.welcomeBack) + .font(.title3) + .foregroundColor(.gray) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Text(StringConstant.appName) + .font(.system(size: 48, weight: .heavy)) + .foregroundColor(.blue) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + + Text(StringConstant.login) + .font(.title3) + .foregroundColor(.gray) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + } + .padding(.bottom, 20) + } +} + +// MARK: - Username Field +struct UsernameField: View { + @ObservedObject var viewModel: LoginViewModel + @Environment(\.sizeCategory) var sizeCategory + + var body: some View { + TextField(StringConstant.Username, text: $viewModel.username) + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(15) + .foregroundColor(.black) + .font(.title2) + .minimumScaleFactor(sizeCategory.customMinScaleFactor) + .autocapitalization(.none) + .disableAutocorrection(true) + } +} + +// MARK: - Password Field +struct PasswordField: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + ZStack(alignment: .trailing) { + Group { + if viewModel.showPassword { + TextField(StringConstant.Password, text: $viewModel.password) + } else { + SecureField(StringConstant.Password, text: $viewModel.password) + } + } + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(15) + .foregroundColor(.black) + .font(.title2) + + Button(action: { + viewModel.logger.info("\(LoggerConstant.LoginSubmitTapped)") + viewModel.showPassword.toggle() + }) { + Image(systemName: viewModel.showPassword ? ImageConstant.eyeSlash : ImageConstant.eye) + .foregroundColor(.gray) + .padding() + } + } + } +} + +// MARK: - Error Message +struct ErrorMessage: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + if !viewModel.errorMessage.isEmpty { + Text(viewModel.errorMessage) + .foregroundColor(.red) + .font(.body) + .minimumScaleFactor(0.75) + .padding(.top, -20) + } + } +} + +// MARK: - Submit Button +struct SubmitButton: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + Button(action: { + viewModel.submit() + viewModel.logger.info("\(LoggerConstant.LoginSubmitTapped)") + AppSettings.shared.hasLoggedInOnce = true // Save after first login + }) { + Text(StringConstant.submit) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .font(.title2.bold()) + .cornerRadius(15) + .minimumScaleFactor(0.75) + .shadow(radius: 10) + } + .accessibilityElement(children: .ignore) + .accessibilityAddTraits(.isButton) + } +} + +// MARK: - Face ID Button +struct FaceIDButton: View { + @ObservedObject var viewModel: LoginViewModel + + var body: some View { + if AppSettings.shared.hasLoggedInOnce { + Button(action: { + viewModel.loginWithFaceID() + }) { + Image(systemName: "faceid") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + .padding() + .background(Color.blue.opacity(0.5)) + .foregroundColor(.white) + .clipShape(Circle()) + } + .padding(.top, 10) + .shadow(radius: 10) + } + } +} + +// MARK: - Preview +struct LoginView_Previews: PreviewProvider { + static var previews: some View { + LoginView(currentView: .constant(.login)) + } +} diff --git a/SPAR_iOS/SPAR/SPAR/Onboarding/OnBoardingScreenView.swift b/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnBoardingScreenView.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/Onboarding/OnBoardingScreenView.swift rename to SPAR_iOS/SPAR/SPAR/View/Onboarding/OnBoardingScreenView.swift diff --git a/SPAR_iOS/SPAR/SPAR/Onboarding/OnboardingView.swift b/SPAR_iOS/SPAR/SPAR/View/Onboarding/OnboardingView.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/Onboarding/OnboardingView.swift rename to SPAR_iOS/SPAR/SPAR/View/Onboarding/OnboardingView.swift diff --git a/SPAR_iOS/SPAR/SPAR/SplashScreenView.swift b/SPAR_iOS/SPAR/SPAR/View/SplashScreenView.swift similarity index 100% rename from SPAR_iOS/SPAR/SPAR/SplashScreenView.swift rename to SPAR_iOS/SPAR/SPAR/View/SplashScreenView.swift