Add Tonuino Manager website and Sparkle updates
This commit is contained in:
commit
1cac427ff2
44 files changed
+5861
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
|
||||
struct MusicFolder: Identifiable, Hashable, Sendable {
|
||||
let number: Int
|
||||
let url: URL
|
||||
var tracks: [MusicTrack]
|
||||
var customName: String? = nil
|
||||
var id: Int { number }
|
||||
var title: String { customName ?? tracks.first(where: { !$0.album.isEmpty })?.album ?? "Ordner \(String(format: "%02d", number))" }
|
||||
}
|
||||
|
||||
struct MusicTrack: Identifiable, Hashable, Sendable {
|
||||
let url: URL
|
||||
var title: String
|
||||
var artist: String
|
||||
var album: String
|
||||
var albumArtist: String = ""
|
||||
var trackNumber: String = ""
|
||||
var discNumber: String = ""
|
||||
var year: String = ""
|
||||
var genre: String = ""
|
||||
var acoustID: String = ""
|
||||
var musicBrainzRecordingID: String = ""
|
||||
var musicBrainzReleaseID: String = ""
|
||||
var id: String { url.path }
|
||||
var displayTitle: String { title.isEmpty ? url.deletingPathExtension().lastPathComponent : title }
|
||||
}
|
||||
|
||||
struct SystemMusicFolder: Identifiable, Hashable, Sendable {
|
||||
let name: String
|
||||
let url: URL
|
||||
var tracks: [MusicTrack]
|
||||
var id: String { name }
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import Foundation
|
||||
|
||||
enum PlaybackMode: Int, CaseIterable, Identifiable, Hashable {
|
||||
case hoerspiel = 1, album, party, single, audiobook, admin = 255
|
||||
case hoerspielRange = 7, albumRange, partyRange, audiobookSingle, repeatLast, quiz, memory, bluetooth, teapot, audiobookRange
|
||||
|
||||
var id: Int { rawValue }
|
||||
var title: String {
|
||||
switch self {
|
||||
case .hoerspiel: "Hörspiel"; case .album: "Album"; case .party: "Party"; case .single: "Einzelner Track"
|
||||
case .audiobook: "Hörbuch"; case .admin: "Admin-Karte"; case .hoerspielRange: "Hörspiel von-bis"
|
||||
case .albumRange: "Album von-bis"; case .partyRange: "Party von-bis"; case .audiobookSingle: "Hörbuch einzeln"
|
||||
case .repeatLast: "Letzte Karte wiederholen"; case .quiz: "Quiz-Spiel"; case .memory: "Memory-Spiel"
|
||||
case .bluetooth: "Bluetooth umschalten"; case .teapot: "Teekesselchen-Spiel"; case .audiobookRange: "Hörbuch von-bis"
|
||||
}
|
||||
}
|
||||
var needsFolder: Bool { ![.repeatLast, .bluetooth].contains(self) }
|
||||
var needsSpecial: Bool { [.single, .audiobookSingle, .hoerspielRange, .albumRange, .partyRange, .quiz, .audiobookRange].contains(self) }
|
||||
var needsSpecial2: Bool { [.hoerspielRange, .albumRange, .partyRange, .quiz, .audiobookRange].contains(self) }
|
||||
}
|
||||
|
||||
enum TonuinoModifier: Int, CaseIterable, Identifiable, Hashable {
|
||||
case sleepTimer = 1
|
||||
case freezeDance
|
||||
case fireWaterAir
|
||||
case toddlerMode
|
||||
case kindergartenMode
|
||||
case repeatSingle
|
||||
case bluetoothToggle
|
||||
case jukebox
|
||||
case pauseAfterTrack
|
||||
case standbyTimerToggle
|
||||
case endlessToggle
|
||||
|
||||
var id: Int { rawValue }
|
||||
var title: String {
|
||||
switch self {
|
||||
case .sleepTimer: "Einschlaf-Timer"
|
||||
case .freezeDance: "Stopptanz"
|
||||
case .fireWaterAir: "Feuer, Wasser, Luft"
|
||||
case .toddlerMode: "Kleinkindmodus"
|
||||
case .kindergartenMode: "Kindergartenmodus"
|
||||
case .repeatSingle: "Track wiederholen"
|
||||
case .bluetoothToggle: "Bluetooth umschalten"
|
||||
case .jukebox: "Jukebox"
|
||||
case .pauseAfterTrack: "Nach jedem Track pausieren"
|
||||
case .standbyTimerToggle: "Standby-Timer umschalten"
|
||||
case .endlessToggle: "Endlosmodus umschalten"
|
||||
}
|
||||
}
|
||||
|
||||
var firmwareNote: String? {
|
||||
switch self {
|
||||
case .bluetoothToggle: "Benötigt eine TonUINO-Firmware mit BT_MODULE."
|
||||
case .jukebox: "Benötigt eine TonUINO-Firmware mit MODIFICATION_CARD_JUKEBOX."
|
||||
case .pauseAfterTrack: "Benötigt eine TonUINO-Firmware mit MODIFICATION_CARD_PAUSE_AFTER_TRACK."
|
||||
case .standbyTimerToggle: "Benötigt TonUINO-TNG 3.3.2 oder neuer."
|
||||
case .endlessToggle: "Benötigt TonUINO-TNG 3.3.3 oder neuer."
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TonuinoCard: Equatable {
|
||||
var folder: UInt8
|
||||
var mode: PlaybackMode
|
||||
var special: UInt8 = 0
|
||||
var special2: UInt8 = 0
|
||||
var modifier: TonuinoModifier? = nil
|
||||
|
||||
var displayTitle: String { modifier?.title ?? mode.title }
|
||||
|
||||
func payload() -> Data {
|
||||
Data([0x13, 0x37, 0xB3, 0x47, 0x02, folder, UInt8(modifier?.rawValue ?? mode.rawValue), special, special2] + Array(repeating: 0, count: 7))
|
||||
}
|
||||
|
||||
static func decode(_ bytes: Data) -> TonuinoCard? {
|
||||
guard bytes.count >= 9, Array(bytes.prefix(4)) == [0x13, 0x37, 0xB3, 0x47], bytes[4] > 0, bytes[4] <= 2,
|
||||
let mode = PlaybackMode(rawValue: Int(bytes[6])) else { return nil }
|
||||
let folder = bytes[5]
|
||||
let modifier = folder == 0 ? TonuinoModifier(rawValue: Int(bytes[6])) : nil
|
||||
return TonuinoCard(folder: folder, mode: mode, special: bytes[7], special2: bytes[8], modifier: modifier)
|
||||
}
|
||||
|
||||
static func makeModifier(_ modifier: TonuinoModifier, special: UInt8 = 0) -> TonuinoCard {
|
||||
let fallbackMode = PlaybackMode(rawValue: modifier.rawValue) ?? .admin
|
||||
return TonuinoCard(folder: 0, mode: fallbackMode, special: special, modifier: modifier)
|
||||
}
|
||||
}
|
||||
|
||||
enum MifareClassicDump {
|
||||
static func make1K(for card: TonuinoCard) -> Data {
|
||||
var dump = Data(repeating: 0, count: 1024)
|
||||
|
||||
let uid = (0..<4).map { _ in UInt8.random(in: 0...255) }
|
||||
let bcc = uid.reduce(UInt8(0), ^)
|
||||
let manufacturerBlock = uid + [bcc, 0x08, 0x04, 0x00] + Array(repeating: UInt8(0), count: 8)
|
||||
dump.replaceSubrange(0..<16, with: manufacturerBlock)
|
||||
|
||||
// TonUINO stores its 16-byte card payload in MIFARE Classic block 4.
|
||||
dump.replaceSubrange(64..<80, with: card.payload())
|
||||
|
||||
// Use the standard transport configuration and default FF keys in each sector trailer.
|
||||
let sectorTrailer: [UInt8] = [
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0x07, 0x80, 0x69,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
||||
]
|
||||
for block in stride(from: 3, through: 63, by: 4) {
|
||||
let start = block * 16
|
||||
dump.replaceSubrange(start..<(start + 16), with: sectorTrailer)
|
||||
}
|
||||
|
||||
return dump
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user