Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f98b995050
commit
a7cf6c5def
@@ -0,0 +1,102 @@
|
||||
import Foundation
|
||||
|
||||
/// Was auf dem Regal über der App steht, wenn man sie im Menü anwählt.
|
||||
///
|
||||
/// **Warum ein eigener Abzug und nicht `LibraryCache`.** Das Regal wird von
|
||||
/// einer **App-Erweiterung** gezeichnet, also von einem anderen Prozess mit
|
||||
/// eigener Sandbox. Der kommt weder an den Schlüsselbund noch an den
|
||||
/// Zwischenspeicher der App, und er soll auch gar nicht: er hat weniger als
|
||||
/// eine Sekunde Zeit, bevor tvOS ihn abbricht. Ein Netzabruf, eine Anmeldung,
|
||||
/// ein Blättern — alles ausgeschlossen.
|
||||
///
|
||||
/// Also legt die App hin, was das Regal braucht, und sonst nichts: Titel,
|
||||
/// Bildadresse, Kennung. Fertig aufbereitet, eine Datei, ein `JSONDecoder`.
|
||||
///
|
||||
/// **Zur Bildadresse.** Sie trägt den Zugangsschlüssel des Servers, weil
|
||||
/// Jellyfin ihn für Bilder verlangt und die Erweiterung ihn sonst nirgends
|
||||
/// herbekäme. Die Datei liegt in der App-Gruppe — erreichbar für die App und
|
||||
/// ihre Erweiterungen, für sonst niemanden. Der Schlüsselbund bleibt, wo er
|
||||
/// ist; hierher kommt nur, was ohnehin in jeder Bildanfrage steht.
|
||||
public struct TopShelfSnapshot: Codable, Sendable, Equatable {
|
||||
public struct Entry: Codable, Sendable, Equatable, Identifiable {
|
||||
public let id: String
|
||||
public let title: String
|
||||
public let subtitle: String?
|
||||
/// Das Plakat, in der Grösse, die das Regal zeigt.
|
||||
public let imageURL: URL?
|
||||
/// Wie weit gesehen — für den Balken auf dem Plakat.
|
||||
public let progress: Double?
|
||||
|
||||
public init(
|
||||
id: String, title: String, subtitle: String? = nil,
|
||||
imageURL: URL?, progress: Double? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.imageURL = imageURL
|
||||
self.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
public struct Section: Codable, Sendable, Equatable, Identifiable {
|
||||
public let id: String
|
||||
public let title: String
|
||||
public let entries: [Entry]
|
||||
|
||||
public init(id: String, title: String, entries: [Entry]) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.entries = entries
|
||||
}
|
||||
}
|
||||
|
||||
public let sections: [Section]
|
||||
public let savedAt: Date
|
||||
|
||||
public init(sections: [Section], savedAt: Date = .now) {
|
||||
self.sections = sections
|
||||
self.savedAt = savedAt
|
||||
}
|
||||
|
||||
/// Wie viele Reihen und wie viele Einträge darin höchstens stehen.
|
||||
///
|
||||
/// **Nicht aus Sparsamkeit.** Das Regal zeigt eine knappe Bildschirmbreite;
|
||||
/// alles darüber hinaus lädt die Erweiterung nur, um es nie zu zeigen —
|
||||
/// und sie hat dafür weniger als eine Sekunde.
|
||||
public static let sectionLimit = 3
|
||||
public static let entryLimit = 12
|
||||
}
|
||||
|
||||
/// Wo der Abzug liegt und wie er dorthin kommt.
|
||||
///
|
||||
/// **In der App-Gruppe, nicht im Caches-Ordner.** Jeder Prozess hat seinen
|
||||
/// eigenen; die App-Gruppe ist der einzige Ort, den App und Erweiterung
|
||||
/// gemeinsam sehen.
|
||||
public enum TopShelfStore {
|
||||
/// Gibt `nil` zurück, wenn die App-Gruppe fehlt — und das ist der Fall,
|
||||
/// bis sie im Entwicklerkonto eingetragen ist. Die Erweiterung zeigt dann
|
||||
/// nichts, statt abzustürzen.
|
||||
public static func location(group: String) -> URL? {
|
||||
FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: group)?
|
||||
.appending(path: "topshelf.json", directoryHint: .notDirectory)
|
||||
}
|
||||
|
||||
public static func write(_ snapshot: TopShelfSnapshot, group: String) {
|
||||
guard let ziel = location(group: group),
|
||||
let daten = try? JSONEncoder().encode(snapshot)
|
||||
else { return }
|
||||
// `.atomic`: die Erweiterung kann jederzeit lesen, auch mitten im
|
||||
// Schreiben. Eine halb geschriebene Datei ergäbe eine leere Regalreihe
|
||||
// statt eines Fehlers — und niemand käme darauf, warum.
|
||||
try? daten.write(to: ziel, options: .atomic)
|
||||
}
|
||||
|
||||
public static func read(group: String) -> TopShelfSnapshot? {
|
||||
guard let quelle = location(group: group),
|
||||
let daten = try? Data(contentsOf: quelle)
|
||||
else { return nil }
|
||||
return try? JSONDecoder().decode(TopShelfSnapshot.self, from: daten)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user