From 915bdcd2777b67f96fa55d870911c5dce0eab703 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Fri, 2 Oct 2026 21:57:53 +0200 Subject: [PATCH] =?UTF-8?q?Die=20Downloadzeile=20stand=20nicht=20still=20?= =?UTF-8?q?=E2=80=94=20und=20sagte=20nicht,=20wie=20weit=20sie=20ist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aus deiner Aufnahme: „181,2 MB … 3,4 MB/s" wird „190,1 MB … 5,8 MB/s", und dabei wandert alles dahinter mit. In einer Proportionalschrift ist die 1 schmal und die 8 breit; zweimal je Sekunde ergibt das ein Zappeln. Drei Dinge halten die Zeile jetzt still: **Ziffern in fester Breite** (`monospacedDigit`) — damit kostet jede Ziffer gleich viel Platz, egal welche. **Eine feste Nachkommastelle.** `.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. Die Zahlen kommen deshalb über `Measurement` mit fester Stelle. **Die Prozente am rechten Rand.** Dort wachsen sie nach innen und schieben nichts; die Zeile links wächst nach rechts und schiebt ebenfalls nichts. Gemessen an acht aufeinanderfolgenden Sekunden: „343.1 / 347.0 / 350.9 / 355.0 / 359.1 / 363.5 / 368.1 / 373.0 MB" — jede Spalte beginnt auf demselben Punkt. **Ein Umweg, der nicht blieb.** Beide Zahlen in das Mass des Ziels zu zwingen hätte auch den Wechsel von MB auf GB beseitigt — machte den Anfang aber unlesbar: bei 2 GB Ziel stand dort „0,03 GB / 2 GB". Der Wechsel kommt einmal je Download; das ist der kleinere Preis. **Und die Prozente**, nach denen man eigentlich sieht. „181,2 MB von 890,2 MB" sagt dasselbe, aber man muss es ausrechnen. Co-Authored-By: Claude Opus 5 --- Flixy/Views/Downloads/DownloadsView.swift | 82 +++++++++++++++++++---- 1 file changed, 70 insertions(+), 12 deletions(-) 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: