diff --git a/Flixy/Views/Downloads/DownloadsView.swift b/Flixy/Views/Downloads/DownloadsView.swift index 77defc6..742d314 100644 --- a/Flixy/Views/Downloads/DownloadsView.swift +++ b/Flixy/Views/Downloads/DownloadsView.swift @@ -261,12 +261,42 @@ private struct DownloadRow: View { ProgressView().progressViewStyle(.linear).tint(accent) } - Text(statusLine) - .font(.flixyCaption) - .foregroundStyle(Color.flixyTextMuted) + // **Links die Zeile, rechts die Prozente — und beides steht + // still.** + // + // Vorher war es eine einzige linksbündige Zeile, und sie zappelte + // bei jeder Aktualisierung: in einer Proportionalschrift ist die + // 1 schmal und die 8 breit, also verschob „181,2 → 190,1" alles + // dahinter. Zweimal je Sekunde. + // + // Drei Dinge halten es jetzt ruhig: Ziffern in fester Breite, ein + // gemeinsames Mass für beide Zahlen (siehe `unit`), und die + // Prozente am rechten Rand — die wachsen nach innen und schieben + // nichts. + HStack(alignment: .firstTextBaseline, spacing: FlixySpacing.snug) { + Text(statusLine) + .lineLimit(1) + Spacer(minLength: FlixySpacing.snug) + if let percentage { + Text(percentage) + } + } + .font(.flixyCaption) + .monospacedDigit() + .foregroundStyle(Color.flixyTextMuted) } } + /// Der Fortschritt in Prozent — `nil`, solange die Gesamtgröße fehlt. + /// + /// Die Zahl, nach der man eigentlich sieht. „181,2 MB von 890,2 MB" sagt + /// dasselbe, aber man muss es ausrechnen. + private var percentage: String? { + guard item.state != .completed, let anteil = item.progress else { return nil } + return anteil.formatted(.percent.precision(.fractionLength(0))) + } + + /// Ob ein Antippen dort weitermacht, wo es aufgehört hat. /// /// Ein angehaltener Download hat seinen Wiederaufnahme-Datensatz immer; @@ -276,13 +306,39 @@ private struct DownloadRow: View { item.state == .paused || item.resumeData != nil } + /// Eine Byte-Zahl mit **fester** Nachkommastelle. + /// + /// **Der letzte Rest des Zappelns.** `.byteCount` lässt die Null hinter + /// dem Komma weg: aus „186,0 MB" wird „186 MB", und die Zeile ist zwei + /// Zeichen kürzer. Gemessen an sechs aufeinanderfolgenden Sekunden war das + /// die einzige, die noch sprang. + /// + /// `usage: .asProvided`, damit die Einheit bleibt, die hier gewählt wurde + /// — sonst rechnet die Formatierung eigenmächtig um. + private func size(_ value: Int64) -> String { + let (menge, einheit): (Double, UnitInformationStorage) = switch value { + case 1_000_000_000...: (Double(value) / 1_000_000_000, .gigabytes) + case 1_000_000...: (Double(value) / 1_000_000, .megabytes) + case 1_000...: (Double(value) / 1_000, .kilobytes) + default: (Double(value), .bytes) + } + return Measurement(value: menge, unit: einheit).formatted( + .measurement( + width: .abbreviated, + usage: .asProvided, + numberFormatStyle: .number.precision( + .fractionLength(einheit == .bytes ? 0 : 1) + ) + ) + ) + } + /// Wie schnell es gerade geht — `nil`, solange nichts gemessen ist. private var speed: String? { guard let perSecond = downloads.rate(forRatingKey: item.ratingKey)?.bytesPerSecond, perSecond > 0 else { return nil } - let menge = Int64(perSecond).formatted(.byteCount(style: .file)) - return String(localized: "downloads.perSecond \(menge)") + return String(localized: "downloads.perSecond \(size(Int64(perSecond)))") } /// Wie lange es bei diesem Tempo noch dauert. @@ -301,17 +357,19 @@ private struct DownloadRow: View { private var statusLine: String { switch item.state { case .downloading where item.totalBytes > 0: - [ - "\(item.receivedBytes.formatted(.byteCount(style: .file))) / \(item.totalBytes.formatted(.byteCount(style: .file)))", - speed, - remaining, - ].compactMap(\.self).joined(separator: " · ") + // **Jede Zahl in ihrem eigenen Mass.** Beide auf das des Ziels zu + // zwingen war der naheliegende Weg gegen den Einheitenwechsel — + // und er machte den Anfang unlesbar: bei einem Ziel von 2 GB + // stand dort „0,03 GB / 2 GB". Gegen das Zappeln hilft die feste + // Ziffernbreite; der Wechsel von MB auf GB kommt einmal je + // Download und ist der kleinere Preis. + ["\(size(item.receivedBytes)) / \(size(item.totalBytes))", speed, remaining] + .compactMap(\.self).joined(separator: " · ") case .downloading: // Ohne bekannte Gesamtgröße gibt es keine Restzeit — das Tempo // aber schon, und es ist genau dort die einzige Auskunft darüber, // ob überhaupt etwas durchgeht. - [item.receivedBytes.formatted(.byteCount(style: .file)), speed] - .compactMap(\.self).joined(separator: " · ") + [size(item.receivedBytes), speed].compactMap(\.self).joined(separator: " · ") case .paused: String(localized: "downloads.paused") case .queued, .waiting: