moodlit-native/Sources/Core/API/MoodlitAPI.swift
till bbfdff7e3c μ-7.0: Initial moodlit-native Skelett (Pure-Native iOS+macOS)
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>
2026-05-18 15:01:04 +02:00

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)
}
}