Showing 13 changed files with 187 additions and 1121 deletions
+15 -0
Documentation/Charger View Organization Policy.md
@@ -0,0 +1,15 @@
1
+# Politica de organizare a view-urilor Charger
2
+
3
+- View-urile care reprezintă flows sau ecrane specifice încărcătoarelor trebuie plasate în folderul `Views/Chargers/`.
4
+- Acest folder este destinat doar charger-only views, nu pentru view-uri generale ale device-urilor sau pentru bara laterală.
5
+- Dacă un view se adresează doar funcționalității de charger (editare charger, wizard standby power, configurații charger), atunci el trebuie să fie în `Views/Chargers/`.
6
+- Nu se folosesc foldere `Chargers` sub `Views/Meter/`, `Views/ChargedDevices/` sau `Views/Sidebar/`.
7
+
8
+## Exemplu
9
+
10
+- `ChargerEditorSheetView.swift` → `USB Meter/Views/Chargers/ChargerEditorSheetView.swift`
11
+- `ChargerStandbyPowerWizardView.swift` → `USB Meter/Views/Chargers/ChargerStandbyPowerWizardView.swift`
12
+
13
+## Motiv
14
+
15
+Separarea charger-only views într-un folder dedicat reduce ambiguitatea și face structura `Views/` mai predictibilă pentru toate flow-urile aplicației.
+13 -4
Documentation/Project Structure and Naming.md
@@ -102,7 +102,6 @@ Views/ChargedDevices/
102 102
     ChargedDeviceIdentityViews.swift
103 103
     ChargedDeviceLibraryRowView.swift
104 104
     ChargedDeviceQRCodeView.swift
105
-    ChargedDeviceSidebarCardView.swift
106 105
   Details/
107 106
     ChargedDeviceDetailView.swift
108 107
   Sessions/
@@ -118,11 +117,21 @@ Views/ChargedDevices/
118 117
       ChargerEditorSheetView.swift
119 118
     Library/
120 119
       ChargedDeviceLibrarySheetView.swift
121
-  Sidebar/
122
-    SidebarChargedDeviceLibraryView.swift
123
-    SidebarChargedDevicesSectionView.swift
124 120
 ```
125 121
 
122
+Views/Sidebar/
123
+  ChargedDeviceSidebarCardView.swift
124
+  SidebarChargedDeviceLibraryView.swift
125
+  SidebarChargedDevicesSectionView.swift
126
+
127
+Views/Chargers/
128
+  ChargerEditorSheetView.swift
129
+  ChargerStandbyPowerWizardView.swift
130
+
131
+Note: sidebar-specific views for the app live in `USB Meter/Views/Sidebar/`, not under feature-specific subfolders.
132
+
133
+Note: charger-specific views live in `USB Meter/Views/Chargers/` when they represent charger-only screens or flows. There is no `USB Meter/Views/Sidebar/Chargers`; sidebar charger views are still part of the shared `Views/Sidebar/` section because the sidebar is a global navigation area.
134
+
126 135
 ## Refactor Examples
127 136
 
128 137
 - `Connection/` -> `Home/`
+21 -0
Documentation/Sidebar View Organization Policy.md
@@ -0,0 +1,21 @@
1
+# Politica de organizare a view-urilor Sidebar
2
+
3
+- View-urile care fac parte din sidebar trebuie plasate în folderul `Views/Sidebar/`.
4
+- Organizarea trebuie să urmeze structura de navigație, nu modelul de date.
5
+- Când căut un element al sidebar-ului în folderul `Sidebar`, este de așteptat să-l găsesc acolo.
6
+- Nu se folosesc subfoldere `Sidebar` în interiorul altor feature folders, cum ar fi `Views/ChargedDevices/Sidebar/`.
7
+- Folderul `Components/` este rezervat pentru componente reutilizabile care nu sunt legate direct de structura sidebar-ului.
8
+
9
+## Exemplu
10
+
11
+`ChargedDeviceSidebarCardView.swift`, `SidebarChargedDeviceLibraryView.swift`, și `SidebarChargedDevicesSectionView.swift` sunt view-uri utilizate direct în sidebar și, prin urmare, se mută din:
12
+
13
+- `USB Meter/Views/ChargedDevices/Components/` / `USB Meter/Views/ChargedDevices/Sidebar/`
14
+
15
+în:
16
+
17
+- `USB Meter/Views/Sidebar/`
18
+
19
+## Motiv
20
+
21
+Această decizie accelerează navigarea dezvoltatorilor și reduce căutările inutile. Când un view aparține unei secțiuni de navigație specifice, el trebuie să fie imediat vizibil în ierarhia de foldere care reprezintă acea navigație.
+11 -9
USB Meter.xcodeproj/project.pbxproj
@@ -228,6 +228,7 @@
228 228
 		E23F11146F5D4C95B6487C94 /* USB_Meter 14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 14.xcdatamodel"; sourceTree = "<group>"; };
229 229
 		F10000016F5D4C95B6487F15 /* USB_Meter 15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 15.xcdatamodel"; sourceTree = "<group>"; };
230 230
 		F10000026F5D4C95B6487F17 /* USB_Meter 16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 16.xcdatamodel"; sourceTree = "<group>"; };
231
+		F10000036F5D4C95B6487F18 /* USB_Meter 17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 17.xcdatamodel"; sourceTree = "<group>"; };
231 232
 		F20000016F5D4C95B6487F16 /* ChargingWindowDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargingWindowDetector.swift; sourceTree = "<group>"; };
232 233
 /* End PBXFileReference section */
233 234
 
@@ -486,6 +487,7 @@
486 487
 				C10000203C8E4A7A00A10020 /* ChargedDevices */,
487 488
 				56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */,
488 489
 				437D47CF2415F8CF00B7768E /* Meter */,
490
+				F1F1F1F1F1F1F1F1F1F1F1F1 /* Chargers */,
489 491
 				D28F10023C8E4A7A00A10002 /* Components */,
490 492
 				4311E639241384960080EA59 /* DeviceHelpView.swift */,
491 493
 			);
@@ -513,6 +515,9 @@
513 515
 			children = (
514 516
 				AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */,
515 517
 				43BE08E12F78F49500250EEC /* SidebarList */,
518
+				CD0001033FA0000000000003 /* ChargedDeviceSidebarCardView.swift */,
519
+				CD0001053FA0000000000005 /* SidebarChargedDeviceLibraryView.swift */,
520
+				C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */,
516 521
 			);
517 522
 			path = Sidebar;
518 523
 			sourceTree = "<group>";
@@ -524,7 +529,6 @@
524 529
 				CD0000113FA0000000000011 /* Details */,
525 530
 				CD0000123FA0000000000012 /* Sessions */,
526 531
 				CD0000133FA0000000000013 /* Sheets */,
527
-				CD0000173FA0000000000017 /* Sidebar */,
528 532
 			);
529 533
 			path = ChargedDevices;
530 534
 			sourceTree = "<group>";
@@ -537,7 +541,6 @@
537 541
 				CD0001013FA0000000000001 /* ChargedDeviceIdentityViews.swift */,
538 542
 				CD0001023FA0000000000002 /* ChargedDeviceLibraryRowView.swift */,
539 543
 				C10000133C8E4A7A00A10013 /* ChargedDeviceQRCodeView.swift */,
540
-				CD0001033FA0000000000003 /* ChargedDeviceSidebarCardView.swift */,
541 544
 			);
542 545
 			path = Components;
543 546
 			sourceTree = "<group>";
@@ -573,7 +576,6 @@
573 576
 			isa = PBXGroup;
574 577
 			children = (
575 578
 				C10000143C8E4A7A00A10014 /* ChargedDeviceEditorSheetView.swift */,
576
-				CE00CE023C8E4A7A00CE00CE /* ChargerEditorSheetView.swift */,
577 579
 			);
578 580
 			path = Editors;
579 581
 			sourceTree = "<group>";
@@ -595,13 +597,13 @@
595 597
 			path = ChargeSession;
596 598
 			sourceTree = "<group>";
597 599
 		};
598
-		CD0000173FA0000000000017 /* Sidebar */ = {
600
+		F1F1F1F1F1F1F1F1F1F1F1F1 /* Chargers */ = {
599 601
 			isa = PBXGroup;
600 602
 			children = (
601
-				CD0001053FA0000000000005 /* SidebarChargedDeviceLibraryView.swift */,
602
-				C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */,
603
+				CE00CE023C8E4A7A00CE00CE /* ChargerEditorSheetView.swift */,
604
+				B0A000023C8F000100A10002 /* ChargerStandbyPowerWizardView.swift */,
603 605
 			);
604
-			path = Sidebar;
606
+			path = Chargers;
605 607
 			sourceTree = "<group>";
606 608
 		};
607 609
 		D28F10013C8E4A7A00A10001 /* Sheets */ = {
@@ -660,7 +662,6 @@
660 662
 			isa = PBXGroup;
661 663
 			children = (
662 664
 				D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */,
663
-				B0A000023C8F000100A10002 /* ChargerStandbyPowerWizardView.swift */,
664 665
 				D28F11253C8E4A7A00A10035 /* Subviews */,
665 666
 			);
666 667
 			path = Live;
@@ -1194,8 +1195,9 @@
1194 1195
 				E23F11146F5D4C95B6487C94 /* USB_Meter 14.xcdatamodel */,
1195 1196
 				F10000016F5D4C95B6487F15 /* USB_Meter 15.xcdatamodel */,
1196 1197
 				F10000026F5D4C95B6487F17 /* USB_Meter 16.xcdatamodel */,
1198
+				F10000036F5D4C95B6487F18 /* USB_Meter 17.xcdatamodel */,
1197 1199
 			);
1198
-			currentVersion = F10000026F5D4C95B6487F17 /* USB_Meter 16.xcdatamodel */;
1200
+			currentVersion = F10000036F5D4C95B6487F18 /* USB_Meter 17.xcdatamodel */;
1199 1201
 			path = CKModel.xcdatamodeld;
1200 1202
 			sourceTree = "<group>";
1201 1203
 			versionGroupType = wrapper.xcdatamodel;
+1 -1
USB Meter/Model/CKModel.xcdatamodeld/.xccurrentversion
@@ -3,6 +3,6 @@
3 3
 <plist version="1.0">
4 4
 <dict>
5 5
 	<key>_XCCurrentVersionName</key>
6
-	<string>USB_Meter 16.xcdatamodel</string>
6
+	<string>USB_Meter 17.xcdatamodel</string>
7 7
 </dict>
8 8
 </plist>
+126 -0
USB Meter/Model/CKModel.xcdatamodeld/USB_Meter 17.xcdatamodel/contents
@@ -0,0 +1,126 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23657" systemVersion="24G81" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES">
3
+    <entity name="ChargedDevice" representedClassName="ChargedDevice" syncable="YES" codeGenerationType="class">
4
+        <attribute name="id" optional="YES" attributeType="String"/>
5
+        <attribute name="name" optional="YES" attributeType="String"/>
6
+        <attribute name="deviceClassRawValue" optional="YES" attributeType="String"/>
7
+        <attribute name="deviceTemplateID" optional="YES" attributeType="String"/>
8
+        <attribute name="supportsChargingWhileOff" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
9
+        <attribute name="chargingStateAvailabilityRawValue" optional="YES" attributeType="String"/>
10
+        <attribute name="supportsWiredCharging" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
11
+        <attribute name="supportsWirelessCharging" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
12
+        <attribute name="preferredChargingTransportRawValue" optional="YES" attributeType="String"/>
13
+        <attribute name="wirelessChargingProfileRawValue" optional="YES" attributeType="String"/>
14
+        <attribute name="configuredCompletionCurrentsRawValue" optional="YES" attributeType="String"/>
15
+        <attribute name="learnedCompletionCurrentsRawValue" optional="YES" attributeType="String"/>
16
+        <attribute name="wiredChargeCompletionCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
17
+        <attribute name="wirelessChargeCompletionCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
18
+        <attribute name="minimumCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
19
+        <attribute name="estimatedBatteryCapacityWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
20
+        <attribute name="wiredMinimumCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
21
+        <attribute name="wirelessMinimumCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
22
+        <attribute name="wiredEstimatedBatteryCapacityWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
23
+        <attribute name="wirelessEstimatedBatteryCapacityWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
24
+        <attribute name="wirelessChargerEfficiencyFactor" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
25
+        <attribute name="chargerObservedVoltageSelectionsRawValue" optional="YES" attributeType="String"/>
26
+        <attribute name="chargerIdleCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
27
+        <attribute name="chargerEfficiencyFactor" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
28
+        <attribute name="chargerMaximumPowerWatts" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
29
+        <attribute name="qrIdentifier" optional="YES" attributeType="String"/>
30
+        <attribute name="notes" optional="YES" attributeType="String"/>
31
+        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
32
+        <attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
33
+    </entity>
34
+    <entity name="ChargeSession" representedClassName="ChargeSession" syncable="YES" codeGenerationType="class">
35
+        <attribute name="id" optional="YES" attributeType="String"/>
36
+        <attribute name="chargedDeviceID" optional="YES" attributeType="String"/>
37
+        <attribute name="chargerID" optional="YES" attributeType="String"/>
38
+        <attribute name="meterMACAddress" optional="YES" attributeType="String"/>
39
+        <attribute name="meterName" optional="YES" attributeType="String"/>
40
+        <attribute name="meterModel" optional="YES" attributeType="String"/>
41
+        <attribute name="startedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
42
+        <attribute name="endedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
43
+        <attribute name="lastObservedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
44
+        <attribute name="pausedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
45
+        <attribute name="statusRawValue" optional="YES" attributeType="String"/>
46
+        <attribute name="sourceModeRawValue" optional="YES" attributeType="String"/>
47
+        <attribute name="chargingTransportRawValue" optional="YES" attributeType="String"/>
48
+        <attribute name="chargingStateRawValue" optional="YES" attributeType="String"/>
49
+        <attribute name="autoStopEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
50
+        <attribute name="selectedDataGroup" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
51
+        <attribute name="meterEnergyBaselineWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
52
+        <attribute name="meterChargeBaselineAh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
53
+        <attribute name="meterDurationBaselineSeconds" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
54
+        <attribute name="meterLastEnergyWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
55
+        <attribute name="meterLastChargeAh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
56
+        <attribute name="meterLastDurationSeconds" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
57
+        <attribute name="measuredEnergyWh" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
58
+        <attribute name="effectiveBatteryEnergyWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
59
+        <attribute name="measuredChargeAh" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
60
+        <attribute name="minimumObservedCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
61
+        <attribute name="maximumObservedCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
62
+        <attribute name="maximumObservedPowerWatts" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
63
+        <attribute name="maximumObservedVoltageVolts" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
64
+        <attribute name="selectedSourceVoltageVolts" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
65
+        <attribute name="completionCurrentAmps" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
66
+        <attribute name="stopThresholdAmps" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
67
+        <attribute name="startBatteryPercent" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
68
+        <attribute name="endBatteryPercent" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
69
+        <attribute name="capacityEstimateWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
70
+        <attribute name="wirelessEfficiencyFactor" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
71
+        <attribute name="usesEstimatedWirelessEfficiency" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
72
+        <attribute name="shouldWarnAboutLowWirelessEfficiency" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
73
+        <attribute name="supportsChargingWhileOff" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
74
+        <attribute name="usedOfflineMeterCounters" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
75
+        <attribute name="belowThresholdSince" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
76
+        <attribute name="lastObservedCurrentAmps" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
77
+        <attribute name="lastObservedPowerWatts" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
78
+        <attribute name="lastObservedVoltageVolts" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
79
+        <attribute name="hasObservedChargeFlow" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
80
+        <attribute name="targetBatteryPercent" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
81
+        <attribute name="targetBatteryAlertTriggeredAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
82
+        <attribute name="requiresCompletionConfirmation" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
83
+        <attribute name="completionConfirmationRequestedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
84
+        <attribute name="completionContradictionPercent" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
85
+        <attribute name="completionConfirmationCooldownUntil" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
86
+        <attribute name="trimStart" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
87
+        <attribute name="trimEnd" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
88
+        <attribute name="wasConflictHealed" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
89
+        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
90
+        <attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
91
+    </entity>
92
+    <entity name="ChargeCheckpoint" representedClassName="ChargeCheckpoint" syncable="YES" codeGenerationType="class">
93
+        <attribute name="id" optional="YES" attributeType="String"/>
94
+        <attribute name="sessionID" optional="YES" attributeType="String"/>
95
+        <attribute name="chargedDeviceID" optional="YES" attributeType="String"/>
96
+        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
97
+        <attribute name="batteryPercent" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
98
+        <attribute name="measuredEnergyWh" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
99
+        <attribute name="measuredChargeAh" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
100
+        <attribute name="currentAmps" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
101
+        <attribute name="voltageVolts" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
102
+        <attribute name="label" optional="YES" attributeType="String"/>
103
+        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
104
+    </entity>
105
+    <entity name="ChargeSessionSample" representedClassName="ChargeSessionSample" syncable="NO" codeGenerationType="class">
106
+        <attribute name="id" optional="YES" attributeType="String"/>
107
+        <attribute name="sessionID" optional="YES" attributeType="String"/>
108
+        <attribute name="chargedDeviceID" optional="YES" attributeType="String"/>
109
+        <attribute name="bucketIndex" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
110
+        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
111
+        <attribute name="averageCurrentAmps" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
112
+        <attribute name="averageVoltageVolts" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
113
+        <attribute name="averagePowerWatts" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
114
+        <attribute name="measuredEnergyWh" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
115
+        <attribute name="measuredChargeAh" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
116
+        <attribute name="sampleCount" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
117
+        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
118
+        <attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
119
+    </entity>
120
+    <elements>
121
+        <element name="ChargedDevice" positionX="-72" positionY="-36" width="128" height="433"/>
122
+        <element name="ChargeSession" positionX="136" positionY="-36" width="128" height="883"/>
123
+        <element name="ChargeCheckpoint" positionX="344" positionY="-36" width="128" height="208"/>
124
+        <element name="ChargeSessionSample" positionX="552" positionY="-36" width="128" height="253"/>
125
+    </elements>
126
+</model>
+0 -2
USB Meter/Model/ChargeInsightsModel.swift
@@ -1269,7 +1269,6 @@ struct ChargedDeviceSummary: Identifiable, Hashable {
1269 1269
     let wirelessMinimumCurrentAmps: Double?
1270 1270
     let wiredEstimatedBatteryCapacityWh: Double?
1271 1271
     let wirelessEstimatedBatteryCapacityWh: Double?
1272
-    let lastAssociatedMeterMAC: String?
1273 1272
     let createdAt: Date
1274 1273
     let updatedAt: Date
1275 1274
     let sessions: [ChargeSessionSummary]
@@ -1562,7 +1561,6 @@ struct ChargedDeviceSummary: Identifiable, Hashable {
1562 1561
             wirelessMinimumCurrentAmps: wirelessMinimumCurrentAmps,
1563 1562
             wiredEstimatedBatteryCapacityWh: wiredEstimatedBatteryCapacityWh,
1564 1563
             wirelessEstimatedBatteryCapacityWh: wirelessEstimatedBatteryCapacityWh,
1565
-            lastAssociatedMeterMAC: lastAssociatedMeterMAC,
1566 1564
             createdAt: createdAt,
1567 1565
             updatedAt: updatedAt,
1568 1566
             sessions: sessions,
+0 -1105
USB Meter/Views/ChargedDevices/Details/ChargedDeviceDetailView.swift
@@ -1,1105 +0,0 @@
1
-//
2
-//  ChargedDeviceDetailView.swift
3
-//  USB Meter
4
-//
5
-//  Created by Codex on 10/04/2026.
6
-//
7
-
8
-import SwiftUI
9
-
10
-struct ChargedDeviceDetailView: View {
11
-    private enum DetailTab: Hashable {
12
-        case overview
13
-        case standby
14
-        case sessions
15
-        case trends
16
-        case settings
17
-    }
18
-
19
-    @EnvironmentObject private var appData: AppData
20
-    @Environment(\.dismiss) private var dismiss
21
-
22
-    @State private var editorVisibility = false
23
-    @State private var deleteConfirmationVisibility = false
24
-    @State private var selectedTab: DetailTab = .overview
25
-    @State private var sessionSelectMode = false
26
-    @State private var selectedSessionIDs: Set<UUID> = []
27
-    @State private var pendingBatchDeletion = false
28
-
29
-    let chargedDeviceID: UUID
30
-
31
-    var body: some View {
32
-        Group {
33
-            if let chargedDevice = appData.chargedDeviceSummary(id: chargedDeviceID) {
34
-                tabbedDetailView(chargedDevice)
35
-                .navigationTitle(chargedDevice.name)
36
-                .navigationBarTitleDisplayMode(.inline)
37
-            } else {
38
-                Text("This device is no longer available.")
39
-                    .foregroundColor(.secondary)
40
-                    .navigationTitle("Device")
41
-                    .navigationBarTitleDisplayMode(.inline)
42
-            }
43
-        }
44
-        .sidebarToggleToolbarItem()
45
-        .sheet(isPresented: $editorVisibility) {
46
-            if let chargedDevice = appData.chargedDeviceSummary(id: chargedDeviceID) {
47
-                if chargedDevice.isCharger {
48
-                    ChargerEditorSheetView(chargedDevice: chargedDevice)
49
-                        .environmentObject(appData)
50
-                } else {
51
-                    ChargedDeviceEditorSheetView(
52
-                        meterMACAddress: nil,
53
-                        chargedDevice: chargedDevice
54
-                    )
55
-                    .environmentObject(appData)
56
-                }
57
-            }
58
-        }
59
-        .confirmationDialog("Delete \(deletionTitle)?", isPresented: $deleteConfirmationVisibility, titleVisibility: .visible) {
60
-            Button("Delete", role: .destructive) {
61
-                if appData.deleteChargedDevice(id: chargedDeviceID) {
62
-                    dismiss()
63
-                }
64
-            }
65
-            Button("Cancel", role: .cancel) {}
66
-        } message: {
67
-            Text(deletionMessage)
68
-        }
69
-        .confirmationDialog(
70
-            "Delete \(selectedSessionIDs.count) Session\(selectedSessionIDs.count == 1 ? "" : "s")?",
71
-            isPresented: $pendingBatchDeletion,
72
-            titleVisibility: .visible
73
-        ) {
74
-            Button("Delete", role: .destructive, action: deleteSelectedSessions)
75
-            Button("Cancel", role: .cancel) {}
76
-        } message: {
77
-            Text("Deleting these sessions also recalculates capacity and every derived metric that used them.")
78
-        }
79
-    }
80
-
81
-    private func tabbedDetailView(_ chargedDevice: ChargedDeviceSummary) -> some View {
82
-        GeometryReader { proxy in
83
-            let tabs = availableTabs(for: chargedDevice)
84
-            let displayedTab = displayedTab(from: tabs)
85
-            let tabBarPresentation = AdaptiveTabBarPresentation.standard(for: proxy.size)
86
-
87
-            VStack(spacing: 0) {
88
-                ChargedDeviceDetailTabBarView(
89
-                    tabs: tabs,
90
-                    selection: $selectedTab,
91
-                    tint: tint(for: chargedDevice),
92
-                    presentation: tabBarPresentation,
93
-                    title: title(for:),
94
-                    systemImage: systemImage(for:)
95
-                )
96
-
97
-                Group {
98
-                    if displayedTab == .sessions {
99
-                        sessionsTabLayout(chargedDevice)
100
-                    } else {
101
-                        ScrollView {
102
-                            tabContent(displayedTab, chargedDevice: chargedDevice)
103
-                                .padding()
104
-                        }
105
-                    }
106
-                }
107
-                .id(displayedTab)
108
-                .transition(.opacity.combined(with: .move(edge: .trailing)))
109
-                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
110
-            }
111
-            .animation(.easeInOut(duration: 0.22), value: displayedTab)
112
-            .animation(.easeInOut(duration: 0.22), value: tabs)
113
-            .onChange(of: selectedTab) { _ in
114
-                sessionSelectMode = false
115
-                selectedSessionIDs.removeAll()
116
-            }
117
-        }
118
-        .background(detailBackground(for: chargedDevice))
119
-        .onAppear {
120
-            ensureSelectedTabExists(for: chargedDevice)
121
-        }
122
-        .onChange(of: chargedDevice.isCharger) { _ in
123
-            ensureSelectedTabExists(for: chargedDevice)
124
-        }
125
-    }
126
-
127
-    @ViewBuilder
128
-    private func tabContent(_ tab: DetailTab, chargedDevice: ChargedDeviceSummary) -> some View {
129
-        VStack(spacing: 18) {
130
-            switch tab {
131
-            case .overview:
132
-                overviewTab(chargedDevice)
133
-            case .standby:
134
-                standbyTab(chargedDevice)
135
-            case .sessions:
136
-                sessionsTab(chargedDevice)
137
-            case .trends:
138
-                trendsTab(chargedDevice)
139
-            case .settings:
140
-                settingsTab(chargedDevice)
141
-            }
142
-        }
143
-    }
144
-
145
-    @ViewBuilder
146
-    private func overviewTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
147
-        headerCard(chargedDevice)
148
-        insightsCard(chargedDevice)
149
-
150
-        if let activeSession = chargedDevice.activeSession {
151
-            activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice)
152
-        }
153
-    }
154
-
155
-    @ViewBuilder
156
-    private func standbyTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
157
-        standbyPowerCard(chargedDevice)
158
-    }
159
-
160
-    @ViewBuilder
161
-    private func sessionsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
162
-        if let activeSession = chargedDevice.activeSession {
163
-            activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice)
164
-        }
165
-
166
-        let sessions = closedSessions(for: chargedDevice)
167
-        if !sessions.isEmpty {
168
-            sessionListCard(sessions, chargedDevice: chargedDevice)
169
-        } else if chargedDevice.activeSession == nil {
170
-            emptyStateCard(
171
-                title: "No Sessions",
172
-                message: "Charging sessions will appear here after this device is used in a recording.",
173
-                tint: .teal
174
-            )
175
-        }
176
-    }
177
-
178
-    @ViewBuilder
179
-    private func trendsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
180
-        if !chargedDevice.capacityHistory.isEmpty {
181
-            capacityEvolutionCard(chargedDevice)
182
-        }
183
-
184
-        if !chargedDevice.typicalCurve.isEmpty {
185
-            typicalCurveCard(chargedDevice)
186
-        }
187
-
188
-        if chargedDevice.capacityHistory.isEmpty && chargedDevice.typicalCurve.isEmpty {
189
-            emptyStateCard(
190
-                title: "Learning Trends",
191
-                message: "Capacity history and charge curves will appear after enough completed sessions are available.",
192
-                tint: .blue
193
-            )
194
-        }
195
-    }
196
-
197
-    @ViewBuilder
198
-    private func settingsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
199
-        settingsCard(chargedDevice)
200
-    }
201
-
202
-    @ViewBuilder
203
-    private func sessionsTabLayout(_ chargedDevice: ChargedDeviceSummary) -> some View {
204
-        let allSessions = chargedDevice.sessions.sorted { lhs, rhs in
205
-            let lOpen = lhs.status.isOpen, rOpen = rhs.status.isOpen
206
-            if lOpen != rOpen { return lOpen }
207
-            return lhs.startedAt > rhs.startedAt
208
-        }
209
-        let totalEnergyWh = allSessions.reduce(0.0) { $0 + $1.effectiveOrMeasuredEnergyWh }
210
-        let totalDuration  = allSessions.reduce(0.0) { $0 + max($1.effectiveDuration, 0) }
211
-
212
-        VStack(spacing: 0) {
213
-            // Fixed non-scrolling header
214
-            VStack(spacing: 10) {
215
-                sessionsSummaryStrip(
216
-                    count: allSessions.count,
217
-                    totalEnergyWh: totalEnergyWh,
218
-                    totalDuration: totalDuration,
219
-                    hasActive: chargedDevice.activeSession != nil
220
-                )
221
-
222
-                if !allSessions.isEmpty {
223
-                    HStack(spacing: 12) {
224
-                        if sessionSelectMode && !selectedSessionIDs.isEmpty {
225
-                            Text("\(selectedSessionIDs.count) selected")
226
-                                .font(.subheadline)
227
-                                .foregroundColor(.secondary)
228
-                                .transition(.opacity.combined(with: .move(edge: .leading)))
229
-                        }
230
-                        Spacer()
231
-                        if sessionSelectMode && !selectedSessionIDs.isEmpty {
232
-                            Button {
233
-                                pendingBatchDeletion = true
234
-                            } label: {
235
-                                Image(systemName: "trash").foregroundColor(.red)
236
-                            }
237
-                            .transition(.opacity.combined(with: .scale))
238
-                        }
239
-                        Button(sessionSelectMode ? "Cancel" : "Select") {
240
-                            withAnimation(.easeInOut(duration: 0.2)) {
241
-                                sessionSelectMode.toggle()
242
-                                if !sessionSelectMode { selectedSessionIDs.removeAll() }
243
-                            }
244
-                        }
245
-                    }
246
-                    .animation(.easeInOut(duration: 0.2), value: sessionSelectMode)
247
-                    .animation(.easeInOut(duration: 0.2), value: selectedSessionIDs.isEmpty)
248
-                }
249
-            }
250
-            .padding()
251
-
252
-            // Scrollable session list
253
-            if allSessions.isEmpty {
254
-                emptyStateCard(
255
-                    title: "No Sessions",
256
-                    message: "Charging sessions will appear here after this device is used in a recording.",
257
-                    tint: .teal
258
-                )
259
-                .padding([.horizontal, .bottom])
260
-            } else {
261
-                ScrollView {
262
-                    VStack(spacing: 10) {
263
-                        ForEach(allSessions, id: \.id) { session in
264
-                            sessionListItem(session, chargedDevice: chargedDevice)
265
-                        }
266
-                    }
267
-                    .padding([.horizontal, .bottom])
268
-                }
269
-            }
270
-        }
271
-    }
272
-
273
-    private func sessionsSummaryStrip(
274
-        count: Int,
275
-        totalEnergyWh: Double,
276
-        totalDuration: TimeInterval,
277
-        hasActive: Bool
278
-    ) -> some View {
279
-        HStack(spacing: 0) {
280
-            summaryCell(value: "\(count)", label: count == 1 ? "session" : "sessions")
281
-            Divider().frame(height: 30)
282
-            summaryCell(value: "\(totalEnergyWh.format(decimalDigits: 2)) Wh", label: "energy")
283
-            Divider().frame(height: 30)
284
-            summaryCell(value: formatAccumulatedDuration(totalDuration), label: "duration")
285
-            if hasActive {
286
-                Divider().frame(height: 30)
287
-                HStack(spacing: 4) {
288
-                    Circle().fill(Color.green).frame(width: 6, height: 6)
289
-                    Text("Live")
290
-                        .font(.caption2.weight(.semibold))
291
-                        .foregroundColor(.green)
292
-                }
293
-                .frame(maxWidth: .infinity)
294
-            }
295
-        }
296
-        .padding(.vertical, 8)
297
-        .padding(.horizontal, 12)
298
-        .meterCard(tint: .teal, fillOpacity: 0.08, strokeOpacity: 0.14, cornerRadius: 14)
299
-    }
300
-
301
-    private func summaryCell(value: String, label: String) -> some View {
302
-        VStack(spacing: 2) {
303
-            Text(value)
304
-                .font(.subheadline.weight(.bold))
305
-                .foregroundColor(.primary)
306
-                .monospacedDigit()
307
-                .lineLimit(1)
308
-                .minimumScaleFactor(0.7)
309
-            Text(label)
310
-                .font(.caption2)
311
-                .foregroundColor(.secondary)
312
-        }
313
-        .frame(maxWidth: .infinity)
314
-    }
315
-
316
-    private func formatAccumulatedDuration(_ duration: TimeInterval) -> String {
317
-        let formatter = DateComponentsFormatter()
318
-        formatter.allowedUnits = duration >= 3600 ? [.hour, .minute] : [.minute, .second]
319
-        formatter.unitsStyle = .abbreviated
320
-        formatter.zeroFormattingBehavior = .dropAll
321
-        return formatter.string(from: duration) ?? "0m"
322
-    }
323
-
324
-    private func headerCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
325
-        HStack(alignment: .top, spacing: 18) {
326
-            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 118)
327
-
328
-            VStack(alignment: .leading, spacing: 10) {
329
-                ChargedDeviceIdentityLabelView(
330
-                    chargedDevice: chargedDevice,
331
-                    iconPointSize: 22
332
-                )
333
-                .font(.title3.weight(.bold))
334
-
335
-                Text(chargedDevice.identityTitle)
336
-                    .font(.subheadline.weight(.semibold))
337
-                    .foregroundColor(.secondary)
338
-
339
-                if let meterMAC = chargedDevice.lastAssociatedMeterMAC {
340
-                    Text("Default meter: \(meterMAC)")
341
-                        .font(.caption)
342
-                        .foregroundColor(.secondary)
343
-                }
344
-
345
-                Text(chargedDevice.qrIdentifier)
346
-                    .font(.caption2.monospaced())
347
-                    .foregroundColor(.secondary)
348
-                    .textSelection(.enabled)
349
-            }
350
-
351
-            Spacer(minLength: 0)
352
-        }
353
-        .frame(maxWidth: .infinity, alignment: .leading)
354
-        .padding(18)
355
-        .meterCard(tint: tint(for: chargedDevice), fillOpacity: 0.20, strokeOpacity: 0.26, cornerRadius: 20)
356
-    }
357
-
358
-    private func settingsCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
359
-        MeterInfoCardView(title: "Settings", tint: tint(for: chargedDevice)) {
360
-            MeterInfoRowView(
361
-                label: "Kind",
362
-                value: chargedDevice.isCharger ? "Charger" : chargedDevice.deviceClass.title
363
-            )
364
-            MeterInfoRowView(label: "Template", value: chargedDevice.identityTitle)
365
-            MeterInfoRowView(label: "QR ID", value: chargedDevice.qrIdentifier)
366
-
367
-            if let meterMAC = chargedDevice.lastAssociatedMeterMAC {
368
-                MeterInfoRowView(label: "Default Meter", value: meterMAC)
369
-            }
370
-
371
-            MeterInfoRowView(label: "Created", value: chargedDevice.createdAt.format())
372
-            MeterInfoRowView(label: "Updated", value: chargedDevice.updatedAt.format())
373
-
374
-            Divider()
375
-
376
-            Button(action: showEditor) {
377
-                Label("Edit \(chargedDevice.isCharger ? "Charger" : "Device")", systemImage: "pencil")
378
-                    .font(.subheadline.weight(.semibold))
379
-                    .frame(maxWidth: .infinity)
380
-                    .padding(.vertical, 10)
381
-                    .meterCard(tint: tint(for: chargedDevice), fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
382
-            }
383
-            .buttonStyle(.plain)
384
-
385
-            Button(role: .destructive, action: showDeleteConfirmation) {
386
-                Label("Delete \(chargedDevice.isCharger ? "Charger" : "Device")", systemImage: "trash")
387
-                    .font(.subheadline.weight(.semibold))
388
-                    .frame(maxWidth: .infinity)
389
-                    .padding(.vertical, 10)
390
-                    .meterCard(tint: .red, fillOpacity: 0.10, strokeOpacity: 0.18, cornerRadius: 14)
391
-            }
392
-            .buttonStyle(.plain)
393
-        }
394
-    }
395
-
396
-    private func emptyStateCard(title: String, message: String, tint: Color) -> some View {
397
-        VStack(alignment: .leading, spacing: 8) {
398
-            Text(title)
399
-                .font(.headline)
400
-            Text(message)
401
-                .font(.footnote)
402
-                .foregroundColor(.secondary)
403
-                .fixedSize(horizontal: false, vertical: true)
404
-        }
405
-        .frame(maxWidth: .infinity, alignment: .leading)
406
-        .padding(18)
407
-        .meterCard(tint: tint, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
408
-    }
409
-
410
-    private func insightsCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
411
-        MeterInfoCardView(title: "Insights", tint: tint(for: chargedDevice)) {
412
-            if chargedDevice.isCharger {
413
-                chargerInsights(chargedDevice)
414
-            } else {
415
-                deviceInsights(chargedDevice)
416
-            }
417
-
418
-            if let notes = chargedDevice.notes, !notes.isEmpty {
419
-                Divider()
420
-                Text(notes)
421
-                    .font(.footnote)
422
-                    .foregroundColor(.secondary)
423
-                    .frame(maxWidth: .infinity, alignment: .leading)
424
-            }
425
-        }
426
-    }
427
-
428
-    @ViewBuilder
429
-    private func deviceInsights(_ chargedDevice: ChargedDeviceSummary) -> some View {
430
-        if chargedDevice.hasMultipleChargingStateModes {
431
-            MeterInfoRowView(
432
-                label: "Charge Modes",
433
-                value: chargedDevice.chargingStateAvailability.title
434
-            )
435
-        }
436
-        if chargedDevice.hasMultipleChargingTransports {
437
-            MeterInfoRowView(
438
-                label: "Charging Support",
439
-                value: chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + ")
440
-            )
441
-        }
442
-        if chargedDevice.showsWirelessProfileDetails {
443
-            MeterInfoRowView(
444
-                label: "Wireless Profile",
445
-                value: chargedDevice.wirelessChargingProfile.title
446
-            )
447
-        }
448
-
449
-        ForEach(completionSessionKinds(for: chargedDevice), id: \.rawValue) { sessionKind in
450
-            MeterInfoRowView(
451
-                label: completionCurrentLabel(for: chargedDevice, sessionKind: sessionKind),
452
-                value: completionCurrentDescription(for: chargedDevice, sessionKind: sessionKind)
453
-            )
454
-        }
455
-        MeterInfoRowView(
456
-            label: "Estimated Capacity",
457
-            value: chargedDevice.estimatedBatteryCapacityWh.map { "\($0.format(decimalDigits: 2)) Wh" } ?? "Not enough data"
458
-        )
459
-        if let wiredCapacity = chargedDevice.wiredEstimatedBatteryCapacityWh {
460
-            if chargedDevice.hasMultipleChargingTransports {
461
-                MeterInfoRowView(
462
-                    label: "Wired Capacity",
463
-                    value: "\(wiredCapacity.format(decimalDigits: 2)) Wh"
464
-                )
465
-            }
466
-        }
467
-        if let wirelessCapacity = chargedDevice.wirelessEstimatedBatteryCapacityWh {
468
-            if chargedDevice.hasMultipleChargingTransports {
469
-                MeterInfoRowView(
470
-                    label: "Wireless Capacity",
471
-                    value: "\(wirelessCapacity.format(decimalDigits: 2)) Wh"
472
-                )
473
-            }
474
-        }
475
-        if let wirelessEfficiencyFactor = chargedDevice.wirelessChargerEfficiencyFactor,
476
-           chargedDevice.showsWirelessProfileDetails {
477
-            MeterInfoRowView(
478
-                label: "Wireless Efficiency",
479
-                value: "\(Int((wirelessEfficiencyFactor * 100).rounded()))%"
480
-            )
481
-        }
482
-        MeterInfoRowView(
483
-            label: "Charge Sessions",
484
-            value: "\(chargedDevice.sessionCount)"
485
-        )
486
-    }
487
-
488
-    @ViewBuilder
489
-    private func chargerInsights(_ chargedDevice: ChargedDeviceSummary) -> some View {
490
-        if let chargerType = chargedDevice.chargerType {
491
-            MeterInfoRowView(
492
-                label: "Type",
493
-                value: chargerType.title
494
-            )
495
-        }
496
-        if !chargedDevice.chargerObservedVoltageSelections.isEmpty {
497
-            MeterInfoRowView(
498
-                label: "Observed Voltages",
499
-                value: chargedDevice.chargerObservedVoltageSelections
500
-                    .map { "\($0.format(decimalDigits: 1)) V" }
501
-                    .joined(separator: ", ")
502
-            )
503
-        }
504
-        if let chargerIdleCurrentAmps = chargedDevice.chargerIdleCurrentAmps {
505
-            MeterInfoRowView(
506
-                label: "Idle Current",
507
-                value: "\(chargerIdleCurrentAmps.format(decimalDigits: 2)) A"
508
-            )
509
-        }
510
-        if let chargerEfficiencyFactor = chargedDevice.chargerEfficiencyFactor {
511
-            MeterInfoRowView(
512
-                label: "Efficiency",
513
-                value: "\(Int((chargerEfficiencyFactor * 100).rounded()))%"
514
-            )
515
-        }
516
-        if let chargerMaximumPowerWatts = chargedDevice.chargerMaximumPowerWatts {
517
-            MeterInfoRowView(
518
-                label: "Max Power",
519
-                value: "\(chargerMaximumPowerWatts.format(decimalDigits: 2)) W"
520
-            )
521
-        }
522
-        if let latestStandbyPowerMeasurement = chargedDevice.latestStandbyPowerMeasurement {
523
-            MeterInfoRowView(
524
-                label: "Standby Power",
525
-                value: "\(latestStandbyPowerMeasurement.averagePowerWatts.format(decimalDigits: 3)) W"
526
-            )
527
-            MeterInfoRowView(
528
-                label: "Standby Projection",
529
-                value: standbyEnergyLabel(latestStandbyPowerMeasurement.projectedYearlyEnergyWh) + " / year"
530
-            )
531
-        }
532
-        MeterInfoRowView(
533
-            label: "Wireless Sessions",
534
-            value: "\(chargedDevice.sessionCount)"
535
-        )
536
-
537
-    }
538
-
539
-    private func standbyPowerCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
540
-        let latestMeasurement = chargedDevice.latestStandbyPowerMeasurement
541
-
542
-        return MeterInfoCardView(
543
-            title: "Standby Power",
544
-            tint: .orange
545
-        ) {
546
-            if standbyMeasurementMeters.isEmpty {
547
-                Text("Connect a meter first. Standby measurement is launched from a live meter feed, so only currently available meters can be selected here.")
548
-                    .font(.footnote)
549
-                    .foregroundColor(.secondary)
550
-                    .frame(maxWidth: .infinity, alignment: .leading)
551
-            } else {
552
-                NavigationLink(
553
-                    destination: ChargerStandbyPowerWizardView(
554
-                        preferredChargerID: chargedDevice.id,
555
-                        locksChargerSelection: true
556
-                    )
557
-                ) {
558
-                    Label("New Measurement", systemImage: "plus.circle.fill")
559
-                        .font(.subheadline.weight(.semibold))
560
-                        .foregroundColor(.orange)
561
-                }
562
-                .buttonStyle(.plain)
563
-            }
564
-
565
-            if let latestMeasurement {
566
-                Divider()
567
-
568
-                NavigationLink(
569
-                    destination: ChargerStandbyPowerMeasurementDetailView(
570
-                        chargerID: chargedDevice.id,
571
-                        measurementID: latestMeasurement.id
572
-                    )
573
-                ) {
574
-                    VStack(alignment: .leading, spacing: 8) {
575
-                        HStack {
576
-                            Text("Latest Measurement")
577
-                                .font(.subheadline.weight(.semibold))
578
-                                .foregroundColor(.primary)
579
-                            Spacer()
580
-                            Text("\(latestMeasurement.averagePowerWatts.format(decimalDigits: 3)) W")
581
-                                .font(.subheadline.weight(.bold))
582
-                                .foregroundColor(.primary)
583
-                                .monospacedDigit()
584
-                        }
585
-
586
-                        Text(
587
-                            "\(latestMeasurement.endedAt.format()) • \(latestMeasurement.sampleCount) samples • \(standbyEnergyLabel(latestMeasurement.projectedYearlyEnergyWh)) / year"
588
-                        )
589
-                        .font(.caption)
590
-                        .foregroundColor(.secondary)
591
-                    }
592
-                }
593
-                .buttonStyle(.plain)
594
-            }
595
-
596
-            if chargedDevice.standbyPowerMeasurements.isEmpty == false {
597
-                Divider()
598
-
599
-                NavigationLink(
600
-                    destination: ChargerStandbyPowerMeasurementsView(chargerID: chargedDevice.id)
601
-                ) {
602
-                    Label("View Saved Measurements", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
603
-                        .font(.subheadline.weight(.semibold))
604
-                        .foregroundColor(.blue)
605
-                }
606
-                .buttonStyle(.plain)
607
-            }
608
-        }
609
-    }
610
-
611
-    private func activeSessionSummaryCard(
612
-        _ activeSession: ChargeSessionSummary,
613
-        chargedDevice: ChargedDeviceSummary
614
-    ) -> some View {
615
-        NavigationLink(
616
-            destination: ChargeSessionDetailView(
617
-                chargedDeviceID: chargedDevice.id,
618
-                sessionID: activeSession.id
619
-            )
620
-        ) {
621
-            VStack(alignment: .leading, spacing: 14) {
622
-                HStack(alignment: .firstTextBaseline) {
623
-                    VStack(alignment: .leading, spacing: 4) {
624
-                        Text("Current Session")
625
-                            .font(.headline)
626
-                            .foregroundColor(.primary)
627
-                        Text(activeSession.status.title)
628
-                            .font(.caption.weight(.semibold))
629
-                            .foregroundColor(statusTint(for: activeSession))
630
-                    }
631
-
632
-                    Spacer()
633
-
634
-                    Image(systemName: "chevron.right")
635
-                        .font(.caption.weight(.semibold))
636
-                        .foregroundColor(.secondary)
637
-                }
638
-
639
-                LazyVGrid(columns: activeSessionSummaryColumns, spacing: 8) {
640
-                    activeSessionMetricCell(
641
-                        label: "Energy",
642
-                        value: "\(activeSession.effectiveOrMeasuredEnergyWh.format(decimalDigits: 2)) Wh",
643
-                        tint: .teal
644
-                    )
645
-                    activeSessionMetricCell(
646
-                        label: "Duration",
647
-                        value: sessionDurationText(activeSession),
648
-                        tint: .orange
649
-                    )
650
-                    if let maximumObservedPowerWatts = activeSession.maximumObservedPowerWatts {
651
-                        activeSessionMetricCell(
652
-                            label: "Max Power",
653
-                            value: "\(maximumObservedPowerWatts.format(decimalDigits: 2)) W",
654
-                            tint: .blue
655
-                        )
656
-                    }
657
-                    if let batteryPrediction = chargedDevice.batteryLevelPrediction(for: activeSession) {
658
-                        activeSessionMetricCell(
659
-                            label: "Battery",
660
-                            value: "\(batteryPrediction.predictedPercent.format(decimalDigits: 0))%",
661
-                            tint: .green
662
-                        )
663
-                    } else if let targetBatteryPercent = activeSession.targetBatteryPercent {
664
-                        activeSessionMetricCell(
665
-                            label: "Target",
666
-                            value: "\(targetBatteryPercent.format(decimalDigits: 0))%",
667
-                            tint: .indigo
668
-                        )
669
-                    }
670
-                }
671
-
672
-                Text("Started \(activeSession.startedAt.format())")
673
-                    .font(.caption)
674
-                    .foregroundColor(.secondary)
675
-            }
676
-        }
677
-        .buttonStyle(.plain)
678
-        .padding(18)
679
-        .meterCard(tint: statusTint(for: activeSession), fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
680
-    }
681
-
682
-    private var activeSessionSummaryColumns: [GridItem] {
683
-        [
684
-            GridItem(.flexible(minimum: 92), spacing: 8),
685
-            GridItem(.flexible(minimum: 92), spacing: 8)
686
-        ]
687
-    }
688
-
689
-    private func activeSessionMetricCell(label: String, value: String, tint: Color) -> some View {
690
-        VStack(alignment: .leading, spacing: 4) {
691
-            Text(label)
692
-                .font(.caption2)
693
-                .foregroundColor(.secondary)
694
-            Text(value)
695
-                .font(.footnote.weight(.semibold))
696
-                .foregroundColor(.primary)
697
-                .monospacedDigit()
698
-                .lineLimit(1)
699
-                .minimumScaleFactor(0.8)
700
-        }
701
-        .frame(maxWidth: .infinity, alignment: .leading)
702
-        .padding(10)
703
-        .meterCard(tint: tint, fillOpacity: 0.08, strokeOpacity: 0.12, cornerRadius: 12)
704
-    }
705
-
706
-    private func capacityEvolutionCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
707
-        VStack(alignment: .leading, spacing: 12) {
708
-            Text("Capacity Evolution")
709
-                .font(.headline)
710
-
711
-            ForEach(chargedDevice.capacityHistory.suffix(6)) { point in
712
-                HStack {
713
-                    Text(point.timestamp.format())
714
-                        .font(.caption)
715
-                        .foregroundColor(.secondary)
716
-                    Spacer()
717
-                    if chargedDevice.shouldShowChargingTransport(point.chargingTransportMode) {
718
-                        Text(point.chargingTransportMode.title)
719
-                            .font(.caption2)
720
-                            .foregroundColor(.secondary)
721
-                        Text("•")
722
-                            .foregroundColor(.secondary)
723
-                    }
724
-                    Text("\(point.capacityWh.format(decimalDigits: 2)) Wh")
725
-                        .font(.footnote.weight(.semibold))
726
-                }
727
-            }
728
-        }
729
-        .frame(maxWidth: .infinity, alignment: .leading)
730
-        .padding(18)
731
-        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 18)
732
-    }
733
-
734
-    private func typicalCurveCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
735
-        VStack(alignment: .leading, spacing: 12) {
736
-            Text("Typical Charge Curve")
737
-                .font(.headline)
738
-
739
-            ForEach(chargedDevice.typicalCurve) { point in
740
-                HStack {
741
-                    Text("\(point.percentBin)%")
742
-                        .font(.footnote.weight(.semibold))
743
-                    Spacer()
744
-                    Text("\(point.averageEnergyWh.format(decimalDigits: 2)) Wh")
745
-                        .font(.caption.weight(.semibold))
746
-                    Text("•")
747
-                        .foregroundColor(.secondary)
748
-                    Text("\(point.sampleCount) sample\(point.sampleCount == 1 ? "" : "s")")
749
-                        .font(.caption2)
750
-                        .foregroundColor(.secondary)
751
-                }
752
-            }
753
-        }
754
-        .frame(maxWidth: .infinity, alignment: .leading)
755
-        .padding(18)
756
-        .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 18)
757
-    }
758
-
759
-    private func sessionListCard(
760
-        _ sessions: [ChargeSessionSummary],
761
-        chargedDevice: ChargedDeviceSummary
762
-    ) -> some View {
763
-        let totalEnergyWh = sessions.reduce(0) { $0 + $1.effectiveOrMeasuredEnergyWh }
764
-        let completedCount = sessions.filter { $0.status == .completed }.count
765
-        let sortedSessions = sessions.sorted { $0.startedAt > $1.startedAt }
766
-
767
-        return VStack(alignment: .leading, spacing: 14) {
768
-            MeterInfoCardView(title: "Closed Sessions", tint: .teal) {
769
-                MeterInfoRowView(label: "Sessions", value: "\(sessions.count)")
770
-                MeterInfoRowView(label: "Completed", value: "\(completedCount)")
771
-                MeterInfoRowView(label: "Total Energy", value: "\(totalEnergyWh.format(decimalDigits: 2)) Wh")
772
-            }
773
-
774
-            HStack(spacing: 12) {
775
-                if sessionSelectMode && !selectedSessionIDs.isEmpty {
776
-                    Text("\(selectedSessionIDs.count) selected")
777
-                        .font(.subheadline)
778
-                        .foregroundColor(.secondary)
779
-                        .transition(.opacity.combined(with: .move(edge: .leading)))
780
-                }
781
-                Spacer()
782
-                if sessionSelectMode && !selectedSessionIDs.isEmpty {
783
-                    Button {
784
-                        pendingBatchDeletion = true
785
-                    } label: {
786
-                        Image(systemName: "trash")
787
-                            .foregroundColor(.red)
788
-                    }
789
-                    .transition(.opacity.combined(with: .scale))
790
-                }
791
-                Button(sessionSelectMode ? "Cancel" : "Select") {
792
-                    withAnimation(.easeInOut(duration: 0.2)) {
793
-                        sessionSelectMode.toggle()
794
-                        if !sessionSelectMode { selectedSessionIDs.removeAll() }
795
-                    }
796
-                }
797
-            }
798
-            .animation(.easeInOut(duration: 0.2), value: sessionSelectMode)
799
-            .animation(.easeInOut(duration: 0.2), value: selectedSessionIDs.isEmpty)
800
-
801
-            VStack(spacing: 10) {
802
-                ForEach(sortedSessions, id: \.id) { session in
803
-                    sessionListItem(session, chargedDevice: chargedDevice)
804
-                }
805
-            }
806
-        }
807
-    }
808
-
809
-    private func sessionListItem(
810
-        _ session: ChargeSessionSummary,
811
-        chargedDevice: ChargedDeviceSummary
812
-    ) -> some View {
813
-        let sessionTint = statusTint(for: session)
814
-        let isOpen = session.status.isOpen
815
-        let isSelected = selectedSessionIDs.contains(session.id)
816
-
817
-        return Group {
818
-            if sessionSelectMode && !isOpen {
819
-                Button {
820
-                    withAnimation(.easeInOut(duration: 0.15)) {
821
-                        if isSelected { selectedSessionIDs.remove(session.id) }
822
-                        else          { selectedSessionIDs.insert(session.id) }
823
-                    }
824
-                } label: {
825
-                    sessionRowContent(session, sessionTint: sessionTint, isOpen: isOpen, isSelected: isSelected)
826
-                }
827
-                .buttonStyle(.plain)
828
-            } else {
829
-                NavigationLink(
830
-                    destination: ChargeSessionDetailView(
831
-                        chargedDeviceID: chargedDevice.id,
832
-                        sessionID: session.id
833
-                    )
834
-                ) {
835
-                    sessionRowContent(session, sessionTint: sessionTint, isOpen: isOpen, isSelected: false)
836
-                }
837
-                .buttonStyle(.plain)
838
-            }
839
-        }
840
-    }
841
-
842
-    private func sessionRowContent(
843
-        _ session: ChargeSessionSummary,
844
-        sessionTint: Color,
845
-        isOpen: Bool,
846
-        isSelected: Bool
847
-    ) -> some View {
848
-        VStack(alignment: .leading, spacing: 10) {
849
-            HStack(alignment: .firstTextBaseline, spacing: 10) {
850
-                if sessionSelectMode {
851
-                    Group {
852
-                        if isOpen {
853
-                            Image(systemName: "minus.circle")
854
-                                .foregroundColor(.secondary.opacity(0.35))
855
-                        } else {
856
-                            Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
857
-                                .foregroundColor(isSelected ? .teal : .secondary)
858
-                        }
859
-                    }
860
-                    .font(.body)
861
-                    .transition(.opacity)
862
-                }
863
-
864
-                VStack(alignment: .leading, spacing: 2) {
865
-                    Text(session.startedAt.format())
866
-                        .font(.subheadline.weight(.semibold))
867
-                    Text(session.status.title)
868
-                        .font(.caption2)
869
-                        .foregroundColor(sessionTint)
870
-                }
871
-
872
-                Spacer()
873
-
874
-                VStack(alignment: .trailing, spacing: 2) {
875
-                    Text("\(session.effectiveOrMeasuredEnergyWh.format(decimalDigits: 2)) Wh")
876
-                        .font(.subheadline.weight(.semibold))
877
-                        .foregroundColor(.primary)
878
-                    Text(sessionDurationText(session))
879
-                        .font(.caption)
880
-                        .foregroundColor(.secondary)
881
-                }
882
-            }
883
-
884
-            Divider()
885
-
886
-            HStack(spacing: 8) {
887
-                if let batteryDelta = session.batteryDeltaPercent {
888
-                    Label("\(batteryDelta >= 0 ? "+" : "")\(Int(batteryDelta.rounded()))% charged", systemImage: "battery.100percent")
889
-                        .font(.caption2)
890
-                        .foregroundColor(.secondary)
891
-                }
892
-
893
-                if let capacityWh = session.capacityEstimateWh {
894
-                    Text("est. \(capacityWh.format(decimalDigits: 1)) Wh")
895
-                        .font(.caption2)
896
-                        .foregroundColor(.secondary)
897
-                }
898
-
899
-                Spacer()
900
-
901
-                if !session.displayedAggregatedSamples.isEmpty {
902
-                    Label("\(session.displayedAggregatedSamples.count) points", systemImage: "chart.xyaxis.line")
903
-                        .font(.caption2)
904
-                        .foregroundColor(.secondary)
905
-                }
906
-            }
907
-        }
908
-        .padding(12)
909
-        .meterCard(
910
-            tint: sessionTint,
911
-            fillOpacity: isSelected ? 0.16 : (isOpen ? 0.14 : 0.08),
912
-            strokeOpacity: isSelected ? 0.22 : (isOpen ? 0.30 : 0.14),
913
-            cornerRadius: 14
914
-        )
915
-    }
916
-
917
-    private func closedSessions(for chargedDevice: ChargedDeviceSummary) -> [ChargeSessionSummary] {
918
-        chargedDevice.sessions.filter { !$0.status.isOpen }
919
-    }
920
-
921
-    private func availableTabs(for chargedDevice: ChargedDeviceSummary) -> [DetailTab] {
922
-        if chargedDevice.isCharger {
923
-            return [.overview, .standby, .settings]
924
-        }
925
-        return [.overview, .sessions, .trends, .settings]
926
-    }
927
-
928
-    private func displayedTab(from tabs: [DetailTab]) -> DetailTab {
929
-        if tabs.contains(selectedTab) {
930
-            return selectedTab
931
-        }
932
-        return tabs.first ?? .overview
933
-    }
934
-
935
-    private func ensureSelectedTabExists(for chargedDevice: ChargedDeviceSummary) {
936
-        let tabs = availableTabs(for: chargedDevice)
937
-        if !tabs.contains(selectedTab) {
938
-            selectedTab = tabs.first ?? .overview
939
-        }
940
-    }
941
-
942
-    private func title(for tab: DetailTab) -> String {
943
-        switch tab {
944
-        case .overview:
945
-            return "Overview"
946
-        case .standby:
947
-            return "Standby"
948
-        case .sessions:
949
-            return "Sessions"
950
-        case .trends:
951
-            return "Trends"
952
-        case .settings:
953
-            return "Settings"
954
-        }
955
-    }
956
-
957
-    private func systemImage(for tab: DetailTab) -> String {
958
-        switch tab {
959
-        case .overview:
960
-            return "house.fill"
961
-        case .standby:
962
-            return "bolt.badge.clock"
963
-        case .sessions:
964
-            return "clock.arrow.trianglehead.counterclockwise.rotate.90"
965
-        case .trends:
966
-            return "chart.xyaxis.line"
967
-        case .settings:
968
-            return "gearshape.fill"
969
-        }
970
-    }
971
-
972
-    private func detailBackground(for chargedDevice: ChargedDeviceSummary) -> some View {
973
-        LinearGradient(
974
-            colors: [tint(for: chargedDevice).opacity(0.18), Color.clear],
975
-            startPoint: .topLeading,
976
-            endPoint: .bottomTrailing
977
-        )
978
-        .ignoresSafeArea()
979
-    }
980
-
981
-    private func tint(for chargedDevice: ChargedDeviceSummary) -> Color {
982
-        switch chargedDevice.deviceClass {
983
-        case .iphone:
984
-            return .blue
985
-        case .watch:
986
-            return .green
987
-        case .powerbank:
988
-            return .orange
989
-        case .charger:
990
-            return .pink
991
-        case .other:
992
-            return .secondary
993
-        }
994
-    }
995
-
996
-    private func statusTint(for session: ChargeSessionSummary) -> Color {
997
-        switch session.status {
998
-        case .active:
999
-            return .green
1000
-        case .paused:
1001
-            return .orange
1002
-        case .completed:
1003
-            return .teal
1004
-        case .abandoned:
1005
-            return .secondary
1006
-        }
1007
-    }
1008
-
1009
-    private func sessionDurationText(_ session: ChargeSessionSummary) -> String {
1010
-        let formatter = DateComponentsFormatter()
1011
-        let effectiveDuration = max(session.effectiveDuration, 0)
1012
-        formatter.allowedUnits = effectiveDuration >= 3600 ? [.hour, .minute] : [.minute, .second]
1013
-        formatter.unitsStyle = .abbreviated
1014
-        formatter.zeroFormattingBehavior = .dropAll
1015
-        return formatter.string(from: effectiveDuration) ?? "0m"
1016
-    }
1017
-
1018
-    private func standbyEnergyLabel(_ wattHours: Double) -> String {
1019
-        if wattHours >= 1000 {
1020
-            return "\((wattHours / 1000).format(decimalDigits: 3)) kWh"
1021
-        }
1022
-        return "\(wattHours.format(decimalDigits: 2)) Wh"
1023
-    }
1024
-
1025
-    private var standbyMeasurementMeters: [AppData.MeterSummary] {
1026
-        appData.meterSummaries.filter { $0.meter != nil }
1027
-    }
1028
-
1029
-    private func completionCurrentDescription(
1030
-        for chargedDevice: ChargedDeviceSummary,
1031
-        sessionKind: ChargeSessionKind
1032
-    ) -> String {
1033
-        if let configuredCurrent = chargedDevice.configuredCompletionCurrentAmps(for: sessionKind) {
1034
-            if let learnedCurrent = chargedDevice.learnedCompletionCurrentAmps(for: sessionKind),
1035
-               abs(configuredCurrent - learnedCurrent) >= 0.01 {
1036
-                return "\(configuredCurrent.format(decimalDigits: 2)) A configured • \(learnedCurrent.format(decimalDigits: 2)) A learned"
1037
-            }
1038
-            return "\(configuredCurrent.format(decimalDigits: 2)) A configured"
1039
-        }
1040
-
1041
-        if let learnedCurrent = chargedDevice.learnedCompletionCurrentAmps(for: sessionKind) {
1042
-            return "\(learnedCurrent.format(decimalDigits: 2)) A learned"
1043
-        }
1044
-
1045
-        return "Learning"
1046
-    }
1047
-
1048
-    private func completionCurrentLabel(
1049
-        for chargedDevice: ChargedDeviceSummary,
1050
-        sessionKind: ChargeSessionKind
1051
-    ) -> String {
1052
-        let showsTransport = chargedDevice.shouldShowChargingTransport(sessionKind.chargingTransportMode)
1053
-        let showsState = chargedDevice.shouldShowChargingStateMode(sessionKind.chargingStateMode)
1054
-
1055
-        switch (showsTransport, showsState) {
1056
-        case (true, true):
1057
-            return "\(sessionKind.shortTitle) Stop Current"
1058
-        case (true, false):
1059
-            return "\(sessionKind.chargingTransportMode.title) Stop Current"
1060
-        case (false, true):
1061
-            return "\(sessionKind.chargingStateMode.title) Stop Current"
1062
-        case (false, false):
1063
-            return "Stop Current"
1064
-        }
1065
-    }
1066
-
1067
-    private func completionSessionKinds(for chargedDevice: ChargedDeviceSummary) -> [ChargeSessionKind] {
1068
-        chargedDevice.supportedChargingModes.flatMap { chargingTransportMode in
1069
-            chargedDevice.supportedChargingStateModes.map { chargingStateMode in
1070
-                ChargeSessionKind(
1071
-                    chargingTransportMode: chargingTransportMode,
1072
-                    chargingStateMode: chargingStateMode
1073
-                )
1074
-            }
1075
-        }
1076
-    }
1077
-
1078
-    private func deleteSelectedSessions() {
1079
-        for id in selectedSessionIDs {
1080
-            _ = appData.deleteChargeSession(sessionID: id)
1081
-        }
1082
-        selectedSessionIDs.removeAll()
1083
-        sessionSelectMode = false
1084
-    }
1085
-
1086
-    private func showEditor() {
1087
-        editorVisibility = true
1088
-    }
1089
-
1090
-    private func showDeleteConfirmation() {
1091
-        deleteConfirmationVisibility = true
1092
-    }
1093
-
1094
-    private var deletionTitle: String {
1095
-        appData.chargedDeviceSummary(id: chargedDeviceID)?.isCharger == true ? "charger" : "device"
1096
-    }
1097
-
1098
-    private var deletionMessage: String {
1099
-        if appData.chargedDeviceSummary(id: chargedDeviceID)?.isCharger == true {
1100
-            return "This removes the charger from the library and unlinks it from wireless sessions that used it."
1101
-        }
1102
-        return "This removes the device and its stored charging history from the library."
1103
-    }
1104
-
1105
-}
+0 -0
USB Meter/Views/ChargedDevices/Sheets/Editors/ChargerEditorSheetView.swift → USB Meter/Views/Chargers/ChargerEditorSheetView.swift
File renamed without changes.
+0 -0
USB Meter/Views/Meter/Tabs/Live/ChargerStandbyPowerWizardView.swift → USB Meter/Views/Chargers/ChargerStandbyPowerWizardView.swift
File renamed without changes.
+0 -0
USB Meter/Views/ChargedDevices/Components/ChargedDeviceSidebarCardView.swift → USB Meter/Views/Sidebar/ChargedDeviceSidebarCardView.swift
File renamed without changes.
+0 -0
USB Meter/Views/ChargedDevices/Sidebar/SidebarChargedDeviceLibraryView.swift → USB Meter/Views/Sidebar/SidebarChargedDeviceLibraryView.swift
File renamed without changes.
+0 -0
USB Meter/Views/ChargedDevices/Sidebar/SidebarChargedDevicesSectionView.swift → USB Meter/Views/Sidebar/SidebarChargedDevicesSectionView.swift
File renamed without changes.