// // ContentView.swift // Name a Color! // // Created by Travis Harbin on 8/30/26. // import SwiftUI #if os(macOS) import AppKit #else import UIKit #endif struct Item: Codable, Identifiable { let id: Int let name: String let color: String let timestamp: Int } struct WarningAlertModel: Identifiable { let id = UUID() let title: String let message: String } extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) let value = UInt64(hex, radix: 16) ?? 0 self.init( red: Double((value >> 16) & 0xFF) / 255, green: Double((value >> 8) & 0xFF) / 255, blue: Double(value & 0xFF) / 255 ) } //ai func func toHex() -> String? { #if canImport(UIKit) let color = UIColor(self) guard let color = color.cgColor.converted( to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil ) else { return nil } let components = color.components ?? [] guard components.count >= 3 else { return nil } let r = Int(round(components[0] * 255)) let g = Int(round(components[1] * 255)) let b = Int(round(components[2] * 255)) return String(format: "%02X%02X%02X", r, g, b) #elseif canImport(AppKit) let color = NSColor(self) guard let color = color.usingColorSpace(.sRGB) else { return nil } let r = Int(round(color.redComponent * 255)) let g = Int(round(color.greenComponent * 255)) let b = Int(round(color.blueComponent * 255)) return String(format: "%02X%02X%02X", r, g, b) #endif } } extension String { func base64Decoded() -> String? { guard let data = Data(base64Encoded: self) else { return nil } return String(data: data, encoding: .utf8) } } struct ContentView: View { @State private var items: [Item] = [] @State private var total: Int = 255 * 255 * 255 @State private var selectedColor: Color = Color.blue @State private var selectedName: String = "" @State private var activeWarning: WarningAlertModel? func updateItems() async throws { let url = URL(string: "https://nameacolor.lncvrt.xyz/get.php")! let (data, _) = try await URLSession.shared.data(from: url) items = try JSONDecoder().decode([Item].self, from: data) } var body: some View { VStack { HStack { Spacer() Text("Name a Color!") .font(.title) .fontWeight(.bold) Spacer() Button { items = [] Task { do { try await updateItems() } catch { print("failed to fetch items:", error) } } } label: { Image(systemName: "arrow.clockwise") .font(.system(size: 14, weight: .semibold)) .frame(width: 32, height: 32) } .buttonStyle(.plain) .background(.gray.opacity(0.12)) .clipShape(Circle()) .help("Refresh") } Text("\(items.count.formatted())/\(total.formatted()) colors named (\((total - items.count).formatted()) left)") Text("Name your own color!") .font(.title2) .fontWeight(.bold) .padding(.top) HStack { ColorPicker("Color", selection: $selectedColor, supportsOpacity: true) .padding() TextField("Name", text: $selectedName) Button("Name it!") { Task { let url = URL(string: "https://nameacolor.lncvrt.xyz/upload.php?color=" + (selectedColor.toHex() ?? "FFFFFF") + "&name=" + selectedName)! let (data, _) = try await URLSession.shared.data(from: url) let response = String(data: data, encoding: .utf8) if response == "-1" { activeWarning = WarningAlertModel(title: "Failed to upload color", message: "Invalid color") } else if response == "-2" { activeWarning = WarningAlertModel(title: "Failed to upload color", message: "Name too short") } else if response == "-3" { activeWarning = WarningAlertModel(title: "Failed to upload color", message: "Name too long") } else if response == "-4" { activeWarning = WarningAlertModel(title: "Failed to upload color", message: "Color already taken") } else if response == "-5" { activeWarning = WarningAlertModel(title: "Failed to upload color", message: "Name already taken") } else { activeWarning = WarningAlertModel(title: "uploaded", message: "kaboom") } } } .buttonStyle(.bordered) } Text("Colors that were named") .font(.title2) .fontWeight(.bold) .padding(.top) ScrollView { LazyVGrid( columns: [GridItem(.adaptive(minimum: 210), spacing: 14)], spacing: 14 ) { ForEach(items) { item in VStack(alignment: .leading, spacing: 12) { Color(hex: item.color) .frame(height: 100) .clipShape(RoundedRectangle(cornerRadius: 12)) VStack(alignment: .leading, spacing: 4) { Text(item.name.base64Decoded() ?? "???") .font(.headline) .foregroundStyle(.primary) Text("#" + item.color.uppercased()) .font(.system(.subheadline, design: .monospaced)) .foregroundStyle(.secondary) .textSelection(.enabled) } } .padding(12) .frame(maxWidth: .infinity, alignment: .leading) .background(.gray.opacity(0.12)) .clipShape(RoundedRectangle(cornerRadius: 16)) .overlay { RoundedRectangle(cornerRadius: 16) .stroke(.primary.opacity(0.06), lineWidth: 1) } } } .padding() .task { do { try await updateItems() } catch { print("failed to fetch items:", error) } } } } .alert(item: $activeWarning) { warning in Alert( title: Text(warning.title), message: Text(warning.message), dismissButton: .default(Text("OK")) ) } .padding() } } #Preview { ContentView() }