Die Downloadzeile stand nicht still — und sagte nicht, wie weit sie ist

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-02 21:57:53 +02:00
co-authored by Claude Opus 5
parent ad556c9034
commit 915bdcd277
+70 -12
View File
@@ -261,12 +261,42 @@ private struct DownloadRow: View {
ProgressView().progressViewStyle(.linear).tint(accent) ProgressView().progressViewStyle(.linear).tint(accent)
} }
Text(statusLine) // **Links die Zeile, rechts die Prozente — und beides steht
.font(.flixyCaption) // still.**
.foregroundStyle(Color.flixyTextMuted) //
// 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. /// Ob ein Antippen dort weitermacht, wo es aufgehört hat.
/// ///
/// Ein angehaltener Download hat seinen Wiederaufnahme-Datensatz immer; /// Ein angehaltener Download hat seinen Wiederaufnahme-Datensatz immer;
@@ -276,13 +306,39 @@ private struct DownloadRow: View {
item.state == .paused || item.resumeData != nil 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. /// Wie schnell es gerade geht — `nil`, solange nichts gemessen ist.
private var speed: String? { private var speed: String? {
guard let perSecond = downloads.rate(forRatingKey: item.ratingKey)?.bytesPerSecond, guard let perSecond = downloads.rate(forRatingKey: item.ratingKey)?.bytesPerSecond,
perSecond > 0 perSecond > 0
else { return nil } else { return nil }
let menge = Int64(perSecond).formatted(.byteCount(style: .file)) return String(localized: "downloads.perSecond \(size(Int64(perSecond)))")
return String(localized: "downloads.perSecond \(menge)")
} }
/// Wie lange es bei diesem Tempo noch dauert. /// Wie lange es bei diesem Tempo noch dauert.
@@ -301,17 +357,19 @@ private struct DownloadRow: View {
private var statusLine: String { private var statusLine: String {
switch item.state { switch item.state {
case .downloading where item.totalBytes > 0: case .downloading where item.totalBytes > 0:
[ // **Jede Zahl in ihrem eigenen Mass.** Beide auf das des Ziels zu
"\(item.receivedBytes.formatted(.byteCount(style: .file))) / \(item.totalBytes.formatted(.byteCount(style: .file)))", // zwingen war der naheliegende Weg gegen den Einheitenwechsel —
speed, // und er machte den Anfang unlesbar: bei einem Ziel von 2 GB
remaining, // stand dort „0,03 GB / 2 GB". Gegen das Zappeln hilft die feste
].compactMap(\.self).joined(separator: " · ") // 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: case .downloading:
// Ohne bekannte Gesamtgröße gibt es keine Restzeit — das Tempo // Ohne bekannte Gesamtgröße gibt es keine Restzeit — das Tempo
// aber schon, und es ist genau dort die einzige Auskunft darüber, // aber schon, und es ist genau dort die einzige Auskunft darüber,
// ob überhaupt etwas durchgeht. // ob überhaupt etwas durchgeht.
[item.receivedBytes.formatted(.byteCount(style: .file)), speed] [size(item.receivedBytes), speed].compactMap(\.self).joined(separator: " · ")
.compactMap(\.self).joined(separator: " · ")
case .paused: case .paused:
String(localized: "downloads.paused") String(localized: "downloads.paused")
case .queued, .waiting: case .queued, .waiting: