basics api client

This commit is contained in:
RSA 2026-08-28 16:18:11 +01:00
parent 8e08130776
commit a3f4586ced
15 changed files with 314 additions and 143 deletions

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "corporateLogos.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "splashImage.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "splashLogo.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,63 @@
//
// Color+Extensions.swift
// medmap
//
// Created by RSA on 28/08/2026.
//
import SwiftUI
// MARK: - Color + Hex Initializer
extension Color {
init(hex: String) {
let hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "#", with: "")
var rgbValue: UInt64 = 0
Scanner(string: hexString).scanHexInt64(&rgbValue)
let length = hexString.count
let r, g, b, a: Double
switch length {
case 6: // RRGGBB
r = Double((rgbValue & 0xFF0000) >> 16) / 255.0
g = Double((rgbValue & 0x00FF00) >> 8) / 255.0
b = Double(rgbValue & 0x0000FF) / 255.0
a = 1.0
case 8: // RRGGBBAA
r = Double((rgbValue & 0xFF000000) >> 24) / 255.0
g = Double((rgbValue & 0x00FF0000) >> 16) / 255.0
b = Double((rgbValue & 0x0000FF00) >> 8) / 255.0
a = Double(rgbValue & 0x000000FF) / 255.0
default:
r = 0
g = 0
b = 0
a = 1.0
}
self.init(.sRGB, red: r, green: g, blue: b, opacity: a)
}
}
// MARK: - Custom App Colors
extension Color {
static let appBackground = Color(hex: "#4078BC")
static let appBackgroundDark = Color(hex: "##4078BC")
static let appSecondary = Color(hex: "#F7F7F8")
static let appAccent = Color(hex: "#4A2EC2")
static let appTextPrimary = Color(hex: "#333333")
static let appBorder = Color(hex: "#E5E5E5")
// Add more as needed, e.g.:
// static let appError = Color(hex: "#FF3B30")
// static let appSuccess = Color(hex: "#34C759")
}

View File

@ -0,0 +1,86 @@
//
// Data+Extensions.swift
// FluxRead
//
// Created by RSA on 15/02/2026.
//
import Foundation
extension Data {
init<T>(copyingBufferOf array: [T]) {
self = array.withUnsafeBufferPointer(Data.init)
}
func toArray<T>(type: T.Type) -> [T] where T: ExpressibleByIntegerLiteral {
var array = [T](repeating: 0, count: self.count / MemoryLayout<T>.stride)
_ = array.withUnsafeMutableBytes { copyBytes(to: $0) }
return array
}
/// Saves the Data object to a file at the specified URL
/// - Parameters:
/// - url: The destination file URL
/// - options: Writing options (default: .atomic)
/// - Throws: Error if writing fails
func saveToFile(at url: URL, options: WritingOptions = .atomic) throws {
try self.write(to: url, options: options)
}
/// Saves the Data object to a file with a specified filename in a directory
/// - Parameters:
/// - filename: The name of the file to create
/// - directory: The directory URL where the file should be saved
/// - options: Writing options (default: .atomic)
/// - Returns: The URL of the saved file
/// - Throws: Error if writing fails
@discardableResult
func saveToFile(filename: String, in directory: URL, options: WritingOptions = .atomic) throws -> URL {
let fileURL = directory.appendingPathComponent(filename)
try self.write(to: fileURL, options: options)
return fileURL
}
/// Saves the Data object to the app's Documents directory
/// - Parameters:
/// - filename: The name of the file to create
/// - options: Writing options (default: .atomic)
/// - Returns: The URL of the saved file
/// - Throws: Error if writing fails or Documents directory not accessible
@discardableResult
func saveToDocuments(filename: String, options: WritingOptions = .atomic) throws -> URL {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw NSError(domain: "DataExtension", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not access Documents directory"])
}
return try saveToFile(filename: filename, in: documentsDirectory, options: options)
}
/// Saves the Data object to the app's temporary directory
/// - Parameters:
/// - filename: The name of the file to create
/// - options: Writing options (default: .atomic)
/// - Returns: The URL of the saved file
/// - Throws: Error if writing fails
@discardableResult
func saveToTemporary(filename: String, options: WritingOptions = .atomic) throws -> URL {
let temporaryDirectory = FileManager.default.temporaryDirectory
return try saveToFile(filename: filename, in: temporaryDirectory, options: options)
}
/// Saves the Data object to the app's Caches directory
/// - Parameters:
/// - filename: The name of the file to create
/// - options: Writing options (default: .atomic)
/// - Returns: The URL of the saved file
/// - Throws: Error if writing fails or Caches directory not accessible
@discardableResult
func saveToCaches(filename: String, options: WritingOptions = .atomic) throws -> URL {
guard let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
throw NSError(domain: "DataExtension", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not access Caches directory"])
}
return try saveToFile(filename: filename, in: cachesDirectory, options: options)
}
}

View File

@ -0,0 +1,22 @@
//
// Date+extensions.swift
// FluxRead
//
// Created by RSA on 30/11/2025.
//
import Foundation
extension Date {
static func firstDayOfMonth(year: Int, month: Int, calendar: Calendar = .current) -> Date? {
var components = DateComponents()
components.year = year
components.month = month
components.day = 1
components.hour = 0
components.minute = 0
components.second = 0
return calendar.date(from: components)
}
}

View File

@ -0,0 +1,49 @@
// Copyright © 2024 VOID Software SA. All rights reserved.
import SwiftUI
extension Font {
static let jakartaRegularName = "Plus Jakarta Sans Regular"
static let jakartaBold = "Plus Jakarta Sans Bold"
static let jakartaLight = "Plus Jakarta Sans Light"
static let jakartaMedium = "Plus Jakarta Sans Medium"
static let interRegular = "Inter Regular"
static let interBold = "Inter Bold"
static let redHatSemibold = "Red Hat Display SemiBold"
static let redHat = "Red Hat Display Regular"
static let jakartaRegular24 = Font.custom(jakartaRegularName, size: 24)
static let jakartaRegular64 = Font.custom(jakartaRegularName, size: 64)
static let jakartaRegular60 = Font.custom(jakartaRegularName, size: 60)
static let jakartaRegular56 = Font.custom(jakartaRegularName, size: 56)
static let jakartaRegular16 = Font.custom(jakartaRegularName, size: 16)
static let jakartaRegular14 = Font.custom(jakartaRegularName, size: 14)
static let jakartaRegular12 = Font.custom(jakartaRegularName, size: 12)
static let jakartaRegular6 = Font.custom(jakartaRegularName, size: 6)
static let jakartaRegular18 = Font.custom(jakartaRegularName, size: 18)
static let jakartaRegular40 = Font.custom(jakartaRegularName, size: 40)
static let jakartaBold12 = Font.custom(jakartaBold, size: 12)
static let jakartaBold14 = Font.custom(jakartaBold, size: 14)
static let jakartaBold16 = Font.custom(jakartaBold, size: 16)
static let jakartaBold18 = Font.custom(jakartaBold, size: 18)
static let jakartaBold22 = Font.custom(jakartaBold, size: 22)
static let jakartaBold24 = Font.custom(jakartaBold, size: 24)
static let jakartaBold64 = Font.custom(jakartaBold, size: 64)
static let jakartaBold40 = Font.custom(jakartaBold, size: 40)
static let interRegular14 = Font.custom(interRegular, size: 14)
static let interRegular16 = Font.custom(interRegular, size: 16)
static let interRegular12 = Font.custom(interRegular, size: 12)
static let interBold14 = Font.custom(interBold, size: 14)
static let redHatSemibold24 = Font.custom(redHatSemibold, size: 24)
static let redHatSemibold18 = Font.custom(redHatSemibold, size: 18)
static let redHat24 = Font.custom(redHat, size: 24)
static let redHat14 = Font.custom(redHat, size: 14)
static let redHat12 = Font.custom(redHat, size: 12)
static let jakartaMedium14 = Font.custom(jakartaMedium, size: 14)
static let jakartaMedium18 = Font.custom(jakartaMedium, size: 18)
static let jakartaMedium40 = Font.custom(jakartaMedium, size: 40)
}

View File

@ -0,0 +1,7 @@
//
// DashboardViewModel.swift
// medmap
//
// Created by RSA on 28/08/2026.
//

View File

@ -14,7 +14,7 @@ import UserNotifications
class ViewModel: ObservableObject {
var appStateManager: AppStateManager = DIContainer.shared.resolve()
@Published var isLoading: Bool = false

View File

@ -0,0 +1,7 @@
//
// DashboardView.swift
// medmap
//
// Created by RSA on 28/08/2026.
//

View File

@ -12,158 +12,39 @@ struct SplashScreenView: View {
@ObservedObject var viewModel: SplashScreenViewModel
@State private var glowPulse = false
@State private var logoScale = 0.85
@State private var logoOpacity = 0.0
@State private var ringRotate = 0.0
@State private var ringOpacity = 0.0
var body: some View {
ZStack {
// ── Deep black base ──────────────────────────────────────────
Color.black.ignoresSafeArea()
// ── Bottom-half warm particle field ──────────────────────────
SplashParticleField()
.ignoresSafeArea()
.opacity(0.90)
// ── Radial glow anchored to centre top ───────────────────────
Color.appBackground.ignoresSafeArea()
VStack(spacing: 0) {
RadialGradient(
colors: [
Color.orange.opacity(glowPulse ? 0.22 : 0.10),
Color.yellow.opacity(glowPulse ? 0.10 : 0.04),
Color.clear
],
center: .center,
startRadius: 0,
endRadius: 340
)
.frame(width: 680, height: 680)
.blur(radius: 48)
.scaleEffect(glowPulse ? 1.12 : 1.0)
.animation(
.easeInOut(duration: 3.8).repeatForever(autoreverses: true),
value: glowPulse
)
HStack {
Image("splashLogo")
.frame(width: 150, height: 180)
.padding(.leading, 70)
.padding(.top, 90)
Spacer()
}
Spacer()
Image("splashImage")
.padding(.top, 20)
Spacer()
Image("corporateLogos")
.padding(.bottom, 35)
}
.ignoresSafeArea()
// ── Orbiting arc ring around logo ────────────────────────────
ZStack {
// slow outer arc
Circle()
.trim(from: 0.0, to: 0.28)
.stroke(
LinearGradient(
colors: [Color.orange.opacity(0.45), Color.clear],
startPoint: .leading, endPoint: .trailing
),
style: StrokeStyle(lineWidth: 1.0, lineCap: .round)
)
.frame(width: 210, height: 210)
.rotationEffect(.degrees(ringRotate))
.opacity(ringOpacity)
// faster inner arc
Circle()
.trim(from: 0.55, to: 0.72)
.stroke(
LinearGradient(
colors: [Color.yellow.opacity(0.30), Color.clear],
startPoint: .leading, endPoint: .trailing
),
style: StrokeStyle(lineWidth: 0.6, lineCap: .round)
)
.frame(width: 175, height: 175)
.rotationEffect(.degrees(-ringRotate * 1.4))
.opacity(ringOpacity)
}
// ── Logo centred ─────────────────────────────────────────────
VStack(spacing: 0) {
Spacer()
Spacer()
}
}
.ignoresSafeArea()
.onAppear {
// Logo entrance
withAnimation(.spring(response: 0.75, dampingFraction: 0.72).delay(0.1)) {
logoScale = 1.0
logoOpacity = 1.0
}
// Glow breath
glowPulse = true
// Ring fade-in then continuous rotation
withAnimation(.easeIn(duration: 0.8).delay(0.4)) {
ringOpacity = 1.0
}
withAnimation(.linear(duration: 12).repeatForever(autoreverses: false).delay(0.4)) {
ringRotate = 360
}
}
}
}
// MARK: - Splash Particle Field
// Warm, slow-drifting particles — denser than the reader version, tinted orange/gold
struct SplashParticleField: View {
@State private var tick: Double = 0
private let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect()
private struct Particle {
let x, y, speed, phase: Double
let radius: CGFloat
let opacity: Double
let warm: Bool // warm (orange/gold) vs cool (white)
}
private let particles: [Particle] = (0..<120).map { i in
let seed = Double(i) * 1.618033
return Particle(
x: (seed * 0.7369).truncatingRemainder(dividingBy: 1.0),
y: (seed * 1.2345).truncatingRemainder(dividingBy: 1.0),
speed: Double.random(in: 8...28),
phase: Double.random(in: 0...Double.pi * 2),
radius: CGFloat(Double.random(in: 0.7...2.8)),
opacity: Double.random(in: 0.04...0.28),
warm: i % 3 != 0
)
}
var body: some View {
Canvas { ctx, size in
let t = tick
for p in particles {
let x = (p.x + t / p.speed).truncatingRemainder(dividingBy: 1.0) * size.width
let drift = sin(t / (p.speed * 0.6) + p.phase) * 22
let y = p.y * size.height + CGFloat(drift)
let pulse = 0.65 + 0.35 * sin(t / 2.5 + p.phase)
let alpha = p.opacity * pulse
let color: Color = p.warm
? Color(red: 1.0, green: 0.62, blue: 0.18).opacity(alpha)
: Color.white.opacity(alpha * 0.55)
ctx.fill(
Path(ellipseIn: CGRect(
x: x - p.radius, y: y - p.radius,
width: p.radius * 2, height: p.radius * 2
)),
with: .color(color)
)
}
}
.onReceive(timer) { _ in tick += 1.0 / 60.0 }
}
}