Phase 5: Nachladen, Startseiten-Layout
Nachladen - DownloadRefiller wählt aus, WELCHE Folgen fehlen; wie viele, rechnet die Regel-Engine. Getrennt, weil das Rechnen ohne Server auskommt und damit prüfbar ist. - In Ausstrahlungsreihenfolge, Specials in Staffel 0 bleiben außen vor. - Läuft nach dem Ansehen und beim Start, nicht auf einem Zeitgeber: eine App, die im Hintergrund unaufgefordert Gigabytes lädt, wäre eine unangenehme Überraschung auf der Mobilfunkrechnung. Startseiten-Layout - Reihen ein-/ausblenden und per Bearbeiten-Modus umsortieren. - Die Anordnungslogik liegt als HubArrangement in PlexKit statt im App-Target — dort ist sie ohne Simulator testbar, und dort sitzen die unangenehmen Fälle: neue Reihen vom Server landen am Ende statt zu verschwinden, verschwundene aus der gespeicherten Liste stören nicht, ein doppelter Eintrag stürzt nicht ab. 11 Tests. 126 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ struct FlixyApp: App {
|
||||
/// Beobachtet WLAN und Mobilfunk. Lebt hier, damit der Beobachter einmal
|
||||
/// läuft statt bei jedem Öffnen des Players neu zu starten.
|
||||
@State private var network = NetworkConditions()
|
||||
@State private var homeLayout = HomeLayoutSettings()
|
||||
@State private var downloads: DownloadCoordinator
|
||||
|
||||
private let modelContainer: ModelContainer
|
||||
@@ -57,6 +58,7 @@ struct FlixyApp: App {
|
||||
.environment(playbackSettings)
|
||||
.environment(network)
|
||||
.environment(downloads)
|
||||
.environment(homeLayout)
|
||||
.environment(theme)
|
||||
.flixyAccent(theme)
|
||||
// Der eigentliche Schalter für „nur dunkel" steht in der
|
||||
@@ -67,6 +69,12 @@ struct FlixyApp: App {
|
||||
.task {
|
||||
appDelegate.downloads = downloads
|
||||
await downloads.restore()
|
||||
// Beim Start einmal prüfen: während die App weg war,
|
||||
// können Folgen gesehen oder gelöscht worden sein.
|
||||
await DownloadRefiller(
|
||||
server: session.server,
|
||||
coordinator: downloads
|
||||
).refill()
|
||||
}
|
||||
}
|
||||
.modelContainer(modelContainer)
|
||||
|
||||
@@ -2962,6 +2962,54 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeLayout.title": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Startseite"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Home screen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeLayout.explanation": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Schalte Reihen aus, die du nicht brauchst, und sortiere sie über „Bearbeiten“ um. Neue Reihen vom Server erscheinen am Ende."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Turn off rows you don't need and reorder them via Edit. New rows from the server appear at the end."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeLayout.empty": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Der Server liefert derzeit keine Reihen."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "The server isn't providing any rows right now."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "1.1"
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import FlixyDownloads
|
||||
import PlexKit
|
||||
|
||||
/// Füllt den Vorrat an ungesehenen Folgen wieder auf.
|
||||
///
|
||||
/// Die Regel-Engine rechnet aus, *wie viele* Folgen je Serie fehlen; hier wird
|
||||
/// entschieden, *welche*. Die Trennung ist Absicht: das Rechnen braucht keinen
|
||||
/// Server und ist deshalb prüfbar, das Auswählen braucht ihn zwangsläufig.
|
||||
@MainActor
|
||||
struct DownloadRefiller {
|
||||
let server: PlexServer?
|
||||
let coordinator: DownloadCoordinator
|
||||
|
||||
/// Lädt nach, was den Regeln nach fehlt.
|
||||
///
|
||||
/// Läuft nach dem Ansehen einer Folge und beim Start — nicht auf einem
|
||||
/// Zeitgeber. Eine App, die im Hintergrund unaufgefordert Gigabytes lädt,
|
||||
/// wäre eine unangenehme Überraschung auf der Mobilfunkrechnung.
|
||||
@discardableResult
|
||||
func refill() async -> Int {
|
||||
guard let server, coordinator.rules.keepUnwatchedEpisodes != nil else { return 0 }
|
||||
|
||||
let needed = coordinator.rules.seriesNeedingRefill(in: coordinator.items)
|
||||
guard !needed.isEmpty else { return 0 }
|
||||
|
||||
var added = 0
|
||||
let service = DownloadService(server: server, coordinator: coordinator)
|
||||
|
||||
for (seriesKey, count) in needed {
|
||||
let candidates = await nextUnwatchedEpisodes(
|
||||
inSeries: seriesKey,
|
||||
count: count,
|
||||
server: server
|
||||
)
|
||||
|
||||
for episode in candidates {
|
||||
// Die Kinderliste bringt keine Dateiangaben mit — ohne sie
|
||||
// ließe sich weder das Speicherlimit prüfen noch ein
|
||||
// Fortschritt anzeigen.
|
||||
let detailed = (try? await server.metadata(ratingKey: episode.ratingKey)) ?? episode
|
||||
guard let plan = service.plan(for: detailed) else { continue }
|
||||
if (try? coordinator.enqueue(plan)) != nil { added += 1 }
|
||||
}
|
||||
}
|
||||
return added
|
||||
}
|
||||
|
||||
/// Die nächsten ungesehenen Folgen einer Serie, die noch nicht offline sind.
|
||||
///
|
||||
/// In Ausstrahlungsreihenfolge, nicht nach Beliebtheit oder Zufall: wer
|
||||
/// drei Folgen vorhalten lässt, will die drei, die als Nächstes dran sind.
|
||||
private func nextUnwatchedEpisodes(
|
||||
inSeries seriesKey: String,
|
||||
count: Int,
|
||||
server: PlexServer
|
||||
) async -> [PlexItem] {
|
||||
guard let seasons = try? await server.children(of: seriesKey) else { return [] }
|
||||
|
||||
let offlineKeys = Set(coordinator.items.map(\.ratingKey))
|
||||
var result: [PlexItem] = []
|
||||
|
||||
for season in seasons.sorted(by: { ($0.index ?? 0) < ($1.index ?? 0) }) {
|
||||
// Specials führt Plex als Staffel 0. Sie gehören nicht in den
|
||||
// Vorrat für „was kommt als Nächstes".
|
||||
guard (season.index ?? 0) > 0 else { continue }
|
||||
guard let episodes = try? await server.children(of: season.ratingKey) else { continue }
|
||||
|
||||
for episode in episodes.sorted(by: { ($0.index ?? 0) < ($1.index ?? 0) }) {
|
||||
guard !episode.isWatched, !offlineKeys.contains(episode.ratingKey) else { continue }
|
||||
result.append(episode)
|
||||
if result.count >= count { return result }
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import PlexKit
|
||||
|
||||
/// Welche Reihen die Startseite zeigt und in welcher Reihenfolge.
|
||||
///
|
||||
/// Hält nur die Einstellung und ihre Speicherung. Das Anordnen selbst macht
|
||||
/// `HubArrangement` in PlexKit — dort ist es reine Rechnung und ohne Simulator
|
||||
/// prüfbar, und genau dort liegen die kniffligen Fälle: neue Reihen vom
|
||||
/// Server, verschwundene aus der gespeicherten Liste.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class HomeLayoutSettings {
|
||||
private enum Key {
|
||||
static let hidden = "flixy.home.hiddenHubs"
|
||||
static let order = "flixy.home.hubOrder"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private(set) var arrangement: HubArrangement
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
self.arrangement = HubArrangement(
|
||||
hidden: Set(defaults.stringArray(forKey: Key.hidden) ?? []),
|
||||
order: defaults.stringArray(forKey: Key.order) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
func isHidden(_ identifier: String) -> Bool {
|
||||
arrangement.hidden.contains(identifier)
|
||||
}
|
||||
|
||||
func setHidden(_ hidden: Bool, for identifier: String) {
|
||||
if hidden {
|
||||
arrangement.hidden.insert(identifier)
|
||||
} else {
|
||||
arrangement.hidden.remove(identifier)
|
||||
}
|
||||
defaults.set(Array(arrangement.hidden), forKey: Key.hidden)
|
||||
}
|
||||
|
||||
func setOrder(_ identifiers: [String]) {
|
||||
arrangement.order = identifiers
|
||||
defaults.set(identifiers, forKey: Key.order)
|
||||
}
|
||||
|
||||
func apply(to hubs: [PlexHub]) -> [PlexHub] {
|
||||
arrangement.apply(to: hubs)
|
||||
}
|
||||
|
||||
/// Die Zeilen für den Einstellungsbildschirm.
|
||||
func rows(from hubs: [PlexHub]) -> [HomeRow] {
|
||||
arrangement.arrangedIdentifiers(from: hubs).compactMap { identifier in
|
||||
guard let hub = hubs.first(where: { $0.hubIdentifier == identifier }) else { return nil }
|
||||
return HomeRow(id: identifier, title: hub.title, isHidden: isHidden(identifier))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Eine Reihe im Einstellungsbildschirm.
|
||||
struct HomeRow: Identifiable, Hashable {
|
||||
let id: String
|
||||
let title: String
|
||||
var isHidden: Bool
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import PlexKit
|
||||
/// Die Startseite: Aufmacher oben, darunter die Reihen vom Server.
|
||||
struct HomeView: View {
|
||||
@Environment(FlixySession.self) private var session
|
||||
@Environment(HomeLayoutSettings.self) private var layout
|
||||
@State private var state: LoadState<[PlexHub]> = .loading
|
||||
|
||||
var body: some View {
|
||||
@@ -22,7 +23,8 @@ struct HomeView: View {
|
||||
Button("action.retry") { Task { await load() } }
|
||||
}
|
||||
|
||||
case .loaded(let hubs):
|
||||
case .loaded(let allHubs):
|
||||
let hubs = layout.apply(to: allHubs)
|
||||
if hubs.isEmpty {
|
||||
ContentUnavailableView {
|
||||
Label("home.empty.title", systemImage: "film.stack")
|
||||
|
||||
@@ -381,8 +381,13 @@ final class PlayerModel {
|
||||
case .reachedEnd:
|
||||
isPlaying = false
|
||||
hasReachedEnd = true
|
||||
// Grundlage fürs automatische Löschen nach dem Ansehen.
|
||||
// Grundlage fürs automatische Löschen nach dem Ansehen —
|
||||
// und der Auslöser fürs Nachladen: der Vorrat ist jetzt
|
||||
// um eine Folge kleiner.
|
||||
downloads.markWatched(ratingKey: request.ratingKey)
|
||||
Task { [server, downloads] in
|
||||
await DownloadRefiller(server: server, coordinator: downloads).refill()
|
||||
}
|
||||
case .tracksResolved(let audio, let subtitle):
|
||||
audioTracks = audio
|
||||
subtitleTracks = subtitle
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import SwiftUI
|
||||
import FlixyDesign
|
||||
import PlexKit
|
||||
|
||||
/// Reihen der Startseite ein- und ausblenden und umsortieren.
|
||||
struct HomeLayoutView: View {
|
||||
@Environment(FlixySession.self) private var session
|
||||
@Environment(HomeLayoutSettings.self) private var layout
|
||||
|
||||
@State private var rows: [HomeRow] = []
|
||||
@State private var isLoading = true
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if rows.isEmpty {
|
||||
ContentUnavailableView {
|
||||
Label("home.empty.title", systemImage: "rectangle.stack")
|
||||
} description: {
|
||||
Text("homeLayout.empty")
|
||||
}
|
||||
} else {
|
||||
list
|
||||
}
|
||||
}
|
||||
.background(Color.flixyCanvas)
|
||||
.navigationTitle(Text("homeLayout.title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { EditButton() }
|
||||
.task { await load() }
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
List {
|
||||
Section {
|
||||
ForEach($rows) { $row in
|
||||
Toggle(isOn: Binding(
|
||||
get: { !row.isHidden },
|
||||
set: {
|
||||
row.isHidden = !$0
|
||||
layout.setHidden(!$0, for: row.id)
|
||||
}
|
||||
)) {
|
||||
Text(row.title)
|
||||
.foregroundStyle(Color.flixyTextPrimary)
|
||||
}
|
||||
.listRowBackground(Color.flixySurface)
|
||||
}
|
||||
.onMove { indices, destination in
|
||||
rows.move(fromOffsets: indices, toOffset: destination)
|
||||
layout.setOrder(rows.map(\.id))
|
||||
}
|
||||
} footer: {
|
||||
Text("homeLayout.explanation")
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
defer { isLoading = false }
|
||||
guard let server = session.server else { return }
|
||||
// Die Reihen kommen vom Server — es gibt keine feste Liste, die sich
|
||||
// vorweg anzeigen ließe. Was hier steht, hängt davon ab, welche
|
||||
// Mediatheken und Sammlungen angelegt sind.
|
||||
let hubs = (try? await server.homeHubs()) ?? []
|
||||
rows = layout.rows(from: hubs)
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,12 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
Label("appearance.title", systemImage: "paintpalette")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
HomeLayoutView()
|
||||
} label: {
|
||||
Label("homeLayout.title", systemImage: "rectangle.stack")
|
||||
}
|
||||
}
|
||||
|
||||
downloadSection
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import Foundation
|
||||
|
||||
/// Wie die Reihen der Startseite angeordnet werden.
|
||||
///
|
||||
/// Reine Rechnung ohne Speicher und ohne Oberfläche — die Stelle, an der die
|
||||
/// zwei unangenehmen Fälle entschieden werden: was mit Reihen geschieht, die
|
||||
/// der Server neu liefert, und was mit gespeicherten, die es nicht mehr gibt.
|
||||
public struct HubArrangement: Sendable, Equatable {
|
||||
/// Ausdrücklich ausgeblendete Reihen.
|
||||
public var hidden: Set<String>
|
||||
/// Die gewünschte Reihenfolge. Reihen, die hier fehlen, landen am Ende.
|
||||
public var order: [String]
|
||||
|
||||
public init(hidden: Set<String> = [], order: [String] = []) {
|
||||
self.hidden = hidden
|
||||
self.order = order
|
||||
}
|
||||
|
||||
/// Wendet Auswahl und Reihenfolge an.
|
||||
///
|
||||
/// Unbekannte Reihen landen am Ende statt zu verschwinden. Sie zu
|
||||
/// verwerfen wäre die einfachere Regel — und der Grund dafür, dass ein
|
||||
/// neu angelegtes Regal auf dem Server in der App nie aufgetaucht wäre.
|
||||
public func apply(to hubs: [PlexHub]) -> [PlexHub] {
|
||||
let visible = hubs.filter { !hidden.contains($0.hubIdentifier) }
|
||||
guard !order.isEmpty else { return visible }
|
||||
|
||||
let positions = Dictionary(
|
||||
order.enumerated().map { ($1, $0) },
|
||||
// Ein doppelter Eintrag in der gespeicherten Reihenfolge — etwa
|
||||
// nach einem fehlgeschlagenen Schreibvorgang — soll nicht
|
||||
// abstürzen. Die erste Position gewinnt.
|
||||
uniquingKeysWith: { first, _ in first }
|
||||
)
|
||||
|
||||
return visible.enumerated().sorted { left, right in
|
||||
let leftPosition = positions[left.element.hubIdentifier] ?? Int.max
|
||||
let rightPosition = positions[right.element.hubIdentifier] ?? Int.max
|
||||
// Bei gleicher Position — beide unbekannt — bleibt die Reihenfolge
|
||||
// des Servers erhalten. Ohne diese Rückfalllinie wäre die
|
||||
// Anordnung neuer Reihen zufällig.
|
||||
if leftPosition == rightPosition { return left.offset < right.offset }
|
||||
return leftPosition < rightPosition
|
||||
}.map(\.element)
|
||||
}
|
||||
|
||||
/// Die Liste für den Einstellungsbildschirm: gespeicherte Reihenfolge
|
||||
/// zuerst, danach alles, was der Server sonst noch liefert.
|
||||
///
|
||||
/// Gespeicherte Reihen, die es nicht mehr gibt, fallen dabei heraus — sie
|
||||
/// anzuzeigen hieße, eine Reihe anzubieten, die nie erscheinen kann.
|
||||
public func arrangedIdentifiers(from hubs: [PlexHub]) -> [String] {
|
||||
var result: [String] = []
|
||||
var seen = Set<String>()
|
||||
|
||||
for identifier in order
|
||||
where !seen.contains(identifier) && hubs.contains(where: { $0.hubIdentifier == identifier }) {
|
||||
result.append(identifier)
|
||||
seen.insert(identifier)
|
||||
}
|
||||
for hub in hubs where !seen.contains(hub.hubIdentifier) {
|
||||
result.append(hub.hubIdentifier)
|
||||
seen.insert(hub.hubIdentifier)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import PlexKit
|
||||
|
||||
private func hub(_ identifier: String) -> PlexHub {
|
||||
PlexHub(hubIdentifier: identifier, title: identifier.capitalized)
|
||||
}
|
||||
|
||||
@Suite("Anordnung der Startseiten-Reihen")
|
||||
struct HubArrangementTests {
|
||||
private let serverHubs = [hub("continue"), hub("recent"), hub("watchlist"), hub("collections")]
|
||||
|
||||
@Test("Ohne Einstellungen bleibt die Reihenfolge des Servers")
|
||||
func defaultKeepsServerOrder() {
|
||||
let result = HubArrangement().apply(to: serverHubs)
|
||||
#expect(result.map(\.hubIdentifier) == ["continue", "recent", "watchlist", "collections"])
|
||||
}
|
||||
|
||||
@Test("Ausgeblendete Reihen verschwinden")
|
||||
func hiddenRowsAreRemoved() {
|
||||
let arrangement = HubArrangement(hidden: ["watchlist", "collections"])
|
||||
let result = arrangement.apply(to: serverHubs)
|
||||
#expect(result.map(\.hubIdentifier) == ["continue", "recent"])
|
||||
}
|
||||
|
||||
@Test("Die gespeicherte Reihenfolge wird angewendet")
|
||||
func storedOrderIsApplied() {
|
||||
let arrangement = HubArrangement(order: ["watchlist", "continue", "recent", "collections"])
|
||||
let result = arrangement.apply(to: serverHubs)
|
||||
#expect(result.map(\.hubIdentifier) == ["watchlist", "continue", "recent", "collections"])
|
||||
}
|
||||
|
||||
@Test("Neue Reihen vom Server landen am Ende statt zu verschwinden")
|
||||
func unknownRowsAppearAtTheEnd() {
|
||||
// Der Fall, der die einfachere Regel — nur Bekanntes anzeigen — zur
|
||||
// Falle macht: ein neu angelegtes Regal auf dem Server wäre in der App
|
||||
// nie aufgetaucht, ohne dass jemand den Grund gesehen hätte.
|
||||
let arrangement = HubArrangement(order: ["recent", "continue"])
|
||||
let result = arrangement.apply(to: serverHubs)
|
||||
#expect(result.map(\.hubIdentifier) == ["recent", "continue", "watchlist", "collections"])
|
||||
}
|
||||
|
||||
@Test("Mehrere neue Reihen behalten die Reihenfolge des Servers")
|
||||
func multipleUnknownRowsKeepServerOrder() {
|
||||
// Ohne stabile Rückfalllinie wäre ihre Anordnung zufällig — und
|
||||
// änderte sich bei jedem Start.
|
||||
let arrangement = HubArrangement(order: ["collections"])
|
||||
let result = arrangement.apply(to: serverHubs)
|
||||
#expect(result.map(\.hubIdentifier) == ["collections", "continue", "recent", "watchlist"])
|
||||
}
|
||||
|
||||
@Test("Gespeicherte Reihen, die es nicht mehr gibt, stören nicht")
|
||||
func staleOrderEntriesAreIgnored() {
|
||||
// Entsteht beim Löschen einer Mediathek auf dem Server.
|
||||
let arrangement = HubArrangement(order: ["geloescht", "recent", "continue"])
|
||||
let result = arrangement.apply(to: serverHubs)
|
||||
#expect(result.map(\.hubIdentifier) == ["recent", "continue", "watchlist", "collections"])
|
||||
}
|
||||
|
||||
@Test("Ein doppelter Eintrag in der Reihenfolge stürzt nicht ab")
|
||||
func duplicateOrderEntriesAreTolerated() {
|
||||
// Kann nach einem abgebrochenen Schreibvorgang entstehen. Ein
|
||||
// Dictionary aus doppelten Schlüsseln wäre ein harter Absturz.
|
||||
let arrangement = HubArrangement(order: ["recent", "recent", "continue"])
|
||||
let result = arrangement.apply(to: serverHubs)
|
||||
#expect(result.first?.hubIdentifier == "recent")
|
||||
#expect(result.count == 4)
|
||||
}
|
||||
|
||||
@Test("Ausblenden und Sortieren greifen zusammen")
|
||||
func hidingAndOrderingCombine() {
|
||||
let arrangement = HubArrangement(
|
||||
hidden: ["recent"],
|
||||
order: ["collections", "recent", "continue"]
|
||||
)
|
||||
let result = arrangement.apply(to: serverHubs)
|
||||
#expect(result.map(\.hubIdentifier) == ["collections", "continue", "watchlist"])
|
||||
}
|
||||
|
||||
// MARK: - Die Liste für die Einstellungen
|
||||
|
||||
@Test("Der Einstellungsbildschirm zeigt Gespeichertes zuerst")
|
||||
func settingsListPutsStoredFirst() {
|
||||
let arrangement = HubArrangement(order: ["watchlist", "continue"])
|
||||
let identifiers = arrangement.arrangedIdentifiers(from: serverHubs)
|
||||
#expect(identifiers == ["watchlist", "continue", "recent", "collections"])
|
||||
}
|
||||
|
||||
@Test("Der Einstellungsbildschirm zeigt auch ausgeblendete Reihen")
|
||||
func settingsListIncludesHidden() {
|
||||
// Sonst ließe sich eine einmal ausgeblendete Reihe nie wieder
|
||||
// einschalten — eine Einbahnstraße.
|
||||
let arrangement = HubArrangement(hidden: ["recent"])
|
||||
let identifiers = arrangement.arrangedIdentifiers(from: serverHubs)
|
||||
#expect(identifiers.contains("recent"))
|
||||
}
|
||||
|
||||
@Test("Verschwundene Reihen erscheinen nicht in den Einstellungen")
|
||||
func settingsListDropsStaleEntries() {
|
||||
let arrangement = HubArrangement(order: ["geloescht", "recent"])
|
||||
let identifiers = arrangement.arrangedIdentifiers(from: serverHubs)
|
||||
#expect(!identifiers.contains("geloescht"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user