mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
95 lines
3.6 KiB
Swift
95 lines
3.6 KiB
Swift
import Foundation
|
|
import HapiProtocol
|
|
|
|
/// Donut geometry for the sqlite storage chart — the Swift twin of
|
|
/// `web/src/components/settings/storageUsageSlices.ts` (same fixed slice
|
|
/// order, zero-byte filtering, 12-o'clock start, one-decimal percents) via
|
|
/// the Android port (`feature/settings/StorageMath.kt`).
|
|
///
|
|
/// The iOS chart itself is a Swift Charts `SectorMark`, which derives its own
|
|
/// proportions from ``Slice/bytes``; the precomputed angles keep the geometry
|
|
/// contract test-locked against the web reference regardless.
|
|
public enum StorageMath {
|
|
|
|
public enum SliceKey: CaseIterable, Sendable {
|
|
case database
|
|
case wal
|
|
case shm
|
|
}
|
|
|
|
public struct Slice: Equatable, Sendable {
|
|
public let key: SliceKey
|
|
public let bytes: Int
|
|
/// Share of the drawn total, one decimal (e.g. `97.3`).
|
|
public let percent: Double
|
|
/// Degrees, `-90` = 12 o'clock, clockwise.
|
|
public let startAngle: Double
|
|
public let endAngle: Double
|
|
|
|
public init(key: SliceKey, bytes: Int, percent: Double, startAngle: Double, endAngle: Double) {
|
|
self.key = key
|
|
self.bytes = bytes
|
|
self.percent = percent
|
|
self.startAngle = startAngle
|
|
self.endAngle = endAngle
|
|
}
|
|
}
|
|
|
|
/// Fixed entity order — colors follow the entity, never its rank.
|
|
private static let sliceOrder: [SliceKey] = [.database, .wal, .shm]
|
|
|
|
private static let fullCircle = 360.0
|
|
|
|
/// Start at 12 o'clock so the first slice reads top-heavy on mobile.
|
|
public static let startAngle = -90.0
|
|
|
|
/// Slices for the donut: zero-byte files are dropped, angles partition
|
|
/// the full circle exactly (the last slice absorbs rounding), empty when
|
|
/// nothing is drawn.
|
|
public static func slices(_ usage: SqliteStorageUsageResponse) -> [Slice] {
|
|
let entries = sliceOrder
|
|
.map { key in (key: key, bytes: max(bytes(of: usage, for: key), 0)) }
|
|
.filter { $0.bytes > 0 }
|
|
let total = entries.reduce(0) { $0 + $1.bytes }
|
|
guard total > 0 else { return [] }
|
|
|
|
var cursor = startAngle
|
|
var slices: [Slice] = []
|
|
for (index, entry) in entries.enumerated() {
|
|
let isLast = index == entries.count - 1
|
|
let endAngle = isLast
|
|
? startAngle + fullCircle
|
|
: cursor + Double(entry.bytes) / Double(total) * fullCircle
|
|
slices.append(Slice(
|
|
key: entry.key,
|
|
bytes: entry.bytes,
|
|
// Half-up like JS Math.round (positive values only here).
|
|
percent: (Double(entry.bytes) * 1000 / Double(total))
|
|
.rounded(.toNearestOrAwayFromZero) / 10,
|
|
startAngle: cursor,
|
|
endAngle: endAngle
|
|
))
|
|
cursor = endAngle
|
|
}
|
|
return slices
|
|
}
|
|
|
|
/// `97.25` → `"97.3%"` (web `formatStoragePercent`, half-up); whole
|
|
/// numbers drop the decimal (`75.0` → `"75%"`).
|
|
public static func formatPercent(_ percent: Double) -> String {
|
|
let rounded = (percent * 10).rounded(.toNearestOrAwayFromZero) / 10
|
|
if rounded.truncatingRemainder(dividingBy: 1) == 0 {
|
|
return "\(Int(rounded))%"
|
|
}
|
|
return String(format: "%.1f%%", locale: Locale(identifier: "en_US_POSIX"), rounded)
|
|
}
|
|
|
|
private static func bytes(of usage: SqliteStorageUsageResponse, for key: SliceKey) -> Int {
|
|
switch key {
|
|
case .database: return usage.databaseBytes
|
|
case .wal: return usage.walBytes
|
|
case .shm: return usage.shmBytes
|
|
}
|
|
}
|
|
}
|