Pure-Native SwiftUI-App für Moodlit. Pendant zur SvelteKit-Web-App auf moodlit.mana.how; konsumiert ManaCore + ManaTokens + ManaAuthUI aus den Schwester-Repos. Stack: - SwiftUI Universal (iOS 18 / macOS 15), Swift 6 strict concurrency - mana-swift-core + mana-swift-ui (lokale SPM-Pakete via XcodeGen) - Bundle ev.mana.moodlit, Team QP3GLU8PH3, App-Group group.ev.mana.moodlit Features: - 24 Mood-Presets als Swift-Konstanten (Port von default-moods.ts) - Custom-Moods + Sequenzen via MoodlitAPI (Actor mit JWT-Bearer-Calls über AuthenticatedTransport, automatischer 401-Retry) - MoodPlayerView mit Idle-Timer-Off, Status-Bar-Hidden, Timer-Auto- Close, Favorite-Toggle, Play/Pause, Auto-Hide-Controls - SequencePlayerView mit Crossfade-Rotation durch alle Sequence-Moods (Net new ggü. Web — dort ist Sequence-Playback nicht verkabelt) - AnimatedMoodView rendert alle 21 AnimationTypes als 30-fps Timeline- View mit sin/cos-modulierten Filter-Effekten - Cards-Pattern Auth-Gate: Presets ohne Login sichtbar, Custom- Creation triggert ManaAuthGate.require → Login-Sheet - Theme: ManaTheme.twilight Forward (Violett #7c3aed) Build verified: - xcodebuild iOS Simulator (iPhone 17) → BUILD SUCCEEDED - xcodebuild macOS → BUILD SUCCEEDED Offen (μ-7.1+): Apple-Dev-Portal-Setup (Bundle, Capabilities), TestFlight, Widget, Settings-UI (Brightness/Speed), Hex-Color-Picker mit Text-Input, Visual-Polish der per-Animation Effekte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
148 lines
4.2 KiB
Swift
148 lines
4.2 KiB
Swift
import Foundation
|
|
import ManaCore
|
|
|
|
/// HTTP-Client für `moodlit-api.mana.how`. Konsumiert
|
|
/// `AuthenticatedTransport` aus ManaCore — JWT-Refresh +
|
|
/// 401-Retry werden dort transparent gelöst.
|
|
///
|
|
/// Wire-Format-SOT: `Code/moodlit/apps/api/src/routes/*.ts`.
|
|
/// Decoder ist case-sensitive — Server schickt `camelCase` keys.
|
|
public actor MoodlitAPI {
|
|
private let transport: AuthenticatedTransport
|
|
private let decoder: JSONDecoder
|
|
private let encoder: JSONEncoder
|
|
|
|
public init(auth: AuthClient, session: URLSession = .shared) {
|
|
self.transport = AuthenticatedTransport(
|
|
baseURL: AppConfig.apiBaseURL,
|
|
auth: auth,
|
|
session: session
|
|
)
|
|
self.decoder = JSONDecoder()
|
|
self.encoder = JSONEncoder()
|
|
}
|
|
|
|
// MARK: - Moods
|
|
|
|
public struct MoodsResponse: Decodable, Sendable {
|
|
public let moods: [Mood]
|
|
}
|
|
|
|
public func listMoods() async throws -> [Mood] {
|
|
let (data, _) = try await transport.request(path: "/api/v1/moods")
|
|
return try decoder.decode(MoodsResponse.self, from: data).moods
|
|
}
|
|
|
|
public struct CreateMoodInput: Encodable, Sendable {
|
|
public let name: String
|
|
public let colors: [String]
|
|
public let animation: AnimationType
|
|
|
|
public init(name: String, colors: [String], animation: AnimationType) {
|
|
self.name = name
|
|
self.colors = colors
|
|
self.animation = animation
|
|
}
|
|
}
|
|
|
|
public func createMood(_ input: CreateMoodInput) async throws -> Mood {
|
|
let body = try encoder.encode(input)
|
|
let (data, _) = try await transport.request(
|
|
path: "/api/v1/moods",
|
|
method: "POST",
|
|
body: body
|
|
)
|
|
return try decoder.decode(Mood.self, from: data)
|
|
}
|
|
|
|
public func deleteMood(id: String) async throws {
|
|
_ = try await transport.request(
|
|
path: "/api/v1/moods/\(id)",
|
|
method: "DELETE"
|
|
)
|
|
}
|
|
|
|
// MARK: - Sequences
|
|
|
|
public struct SequencesResponse: Decodable, Sendable {
|
|
public let sequences: [MoodSequence]
|
|
}
|
|
|
|
public func listSequences() async throws -> [MoodSequence] {
|
|
let (data, _) = try await transport.request(path: "/api/v1/sequences")
|
|
return try decoder.decode(SequencesResponse.self, from: data).sequences
|
|
}
|
|
|
|
public struct CreateSequenceInput: Encodable, Sendable {
|
|
public let name: String
|
|
public let moodIds: [String]
|
|
public let durationSec: Int
|
|
public let transitionSec: Int?
|
|
|
|
public init(name: String, moodIds: [String], durationSec: Int = 30, transitionSec: Int? = nil) {
|
|
self.name = name
|
|
self.moodIds = moodIds
|
|
self.durationSec = durationSec
|
|
self.transitionSec = transitionSec
|
|
}
|
|
}
|
|
|
|
public func createSequence(_ input: CreateSequenceInput) async throws -> MoodSequence {
|
|
let body = try encoder.encode(input)
|
|
let (data, _) = try await transport.request(
|
|
path: "/api/v1/sequences",
|
|
method: "POST",
|
|
body: body
|
|
)
|
|
return try decoder.decode(MoodSequence.self, from: data)
|
|
}
|
|
|
|
public func deleteSequence(id: String) async throws {
|
|
_ = try await transport.request(
|
|
path: "/api/v1/sequences/\(id)",
|
|
method: "DELETE"
|
|
)
|
|
}
|
|
|
|
// MARK: - Preferences
|
|
|
|
public func getPreferences() async throws -> Preferences {
|
|
let (data, _) = try await transport.request(path: "/api/v1/preferences")
|
|
return try decoder.decode(Preferences.self, from: data)
|
|
}
|
|
|
|
public struct UpdatePreferencesInput: Encodable, Sendable {
|
|
public let animationSpeed: Preferences.AnimationSpeed?
|
|
public let brightness: Int?
|
|
public let autoTimerMinutes: Int?
|
|
public let autoMoodSwitch: Bool?
|
|
public let autoMoodSwitchInterval: Int?
|
|
public let favoriteIds: [String]?
|
|
|
|
public init(
|
|
animationSpeed: Preferences.AnimationSpeed? = nil,
|
|
brightness: Int? = nil,
|
|
autoTimerMinutes: Int? = nil,
|
|
autoMoodSwitch: Bool? = nil,
|
|
autoMoodSwitchInterval: Int? = nil,
|
|
favoriteIds: [String]? = nil
|
|
) {
|
|
self.animationSpeed = animationSpeed
|
|
self.brightness = brightness
|
|
self.autoTimerMinutes = autoTimerMinutes
|
|
self.autoMoodSwitch = autoMoodSwitch
|
|
self.autoMoodSwitchInterval = autoMoodSwitchInterval
|
|
self.favoriteIds = favoriteIds
|
|
}
|
|
}
|
|
|
|
public func updatePreferences(_ input: UpdatePreferencesInput) async throws -> Preferences {
|
|
let body = try encoder.encode(input)
|
|
let (data, _) = try await transport.request(
|
|
path: "/api/v1/preferences",
|
|
method: "PATCH",
|
|
body: body
|
|
)
|
|
return try decoder.decode(Preferences.self, from: data)
|
|
}
|
|
}
|