Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251import SwiftUIimport WebKitimport UniformTypeIdentifiersimport ACWaveform
struct ContentView: View { let session: Session let host: SessionHost private var draft: String { get { session.composer } nonmutating set { guard !session.viewingHistory else { return } session.composer = newValue host.setDraft(newValue, threadID: session.currentSessionID) } } // Straight onto the notebook; /new or Settings opens the chooser. @State private var showHome = false @State private var showHelp = false @State private var showSource = false @State private var showImport = false @State private var showExport = false @State private var fileNotice: String? @State private var showSettings = false @State private var signInArrived = false @State private var showVolume = false @State private var braincells = Braincells() @StateObject private var preview = PiecePreview() @StateObject private var oskiewar = OskiewarAttachment() @State private var expandedPreview = false @State private var previewHidden = false @State private var notebookHeight: CGFloat = 24 @State private var notebookTop: CGFloat = 12 /// Slab's "complete" is an attention colour that clears once you return to /// the window; here, once you touch the draft again. @State private var attended = true @State private var writing = true @State private var composerHeight: CGFloat = 48 @State private var sheetSize = CGSize(width: 840, height: 680) @AppStorage("aesel.uiScale") private var uiScale = 1.0 @Environment(\.openURL) private var openURL @Environment(\.colorScheme) private var colorScheme @Environment(\.accessibilityReduceMotion) private var reduceMotion
/// One ruled row of the sheet, as the desktop's --notebook-line-height. private let row: CGFloat = 24 private let paperTop: CGFloat = 12 private let edgeInset = PreviewBounds.edgeInset /// The corner stays fixed; notebook prose flows around its frame. @State private var previewBounds = PreviewBounds() private var previewSize: CGSize { CGSize(width: previewBounds.width, height: previewBounds.height) } private var previewInset: CGFloat { previewBounds.right } private var previewVisible: Bool { session.accountReady && (session.previewURL != nil || !oskiewar.room.isEmpty) && !previewHidden && sheetSize.width >= 420 && sheetSize.height >= 300 } private var previewBlockHeight: CGFloat { paperTop } private var hasNotebook: Bool { session.displayedEntries.contains { $0.kind != .edit } || session.fatal != nil } // WebKit reserves a paint row for descenders; the next editor row shares it. private var transcriptHeight: CGFloat { hasNotebook ? max(row, notebookHeight) - row : 0 } private var notebookExclusion: [String: CGFloat] { guard previewVisible else { return [:] } let width = previewBounds.width + 8 + previewBounds.right + 16 - edgeInset let available = sheetSize.width - edgeInset * 2 return ["width": available - width < 220 ? available : width, "height": max(0, previewBounds.top + previewBounds.height + 8 + 16 - notebookTop), "top": max(0, previewBounds.top - 16 - notebookTop)] } private var speakerVisible: Bool { preview.hasAudio || preview.volume == 0 || showVolume } private var compact: Bool { sheetSize.width < 340 || sheetSize.height < 240 }
/// Slab's palette for what the session is doing right now. private var paint: Paint { let status = attended && session.status == "ready" ? "blank" : session.status return Paint.slab(status: status, busy: session.busy, failed: session.health == .failed || session.fatal != nil, colorScheme: colorScheme) }
private func accountAction(_ title: String, signUp: Bool = false, action: @escaping () -> Void) -> some View { Button(action: action) { Text(title) .font(.system(size: 20, weight: .regular, design: .monospaced)) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } .buttonStyle(CurtainAccountButtonStyle(signUp: signUp)) }
private var accountEntry: some View { GeometryReader { geometry in ScrollView { VStack(spacing: 30) { VStack(spacing: 8) { if geometry.size.height >= 360 { AeselDonkey(busy: true, failed: false).accessibilityHidden(true) } Text("aesel") .font(.system(size: 40, weight: .medium, design: .serif)) .tracking(-1.5) } VStack(spacing: 18) { if session.signedIn { accountAction("Choose your @handle") { openURL(URL(string: "https://aesthetic.computer/handle")!) } Button("I’ve chosen a handle") { host.restore() } .font(Paint.font(14)) Button("Use another account") { host.signOut() } .font(Paint.font(13)).foregroundStyle(paint.ink.opacity(0.65)) } else { HStack(spacing: 12) { accountAction("Log in") { host.signIn() } .keyboardShortcut(.defaultAction) accountAction("I'm new", signUp: true) { host.signIn(signUp: true) } } .disabled(session.showSignIn) if host.accessToken() != nil { Button("Retry account verification") { host.restore() } .font(Paint.font(14)) } } if !session.accessNotice.isEmpty && !["An AC account and @handle are required.", "Claim an AC @handle to use Aesel."].contains(session.accessNotice) { Text(session.accessNotice).font(Paint.font(13)) .foregroundStyle(paint.ink.opacity(0.8)) .multilineTextAlignment(.center) } } } .frame(maxWidth: 310) .padding(24) .frame(maxWidth: .infinity, minHeight: geometry.size.height) } .scrollIndicators(.hidden) } .background(paint.bg) }
private var styledSheet: some View { ZStack { if !session.accountReady { accountEntry .scaleEffect(session.showSignIn && !reduceMotion ? 0.975 : 1) .opacity(session.showSignIn ? 0.65 : 1) .animation(.easeInOut(duration: 0.24), value: session.showSignIn) .transition(.opacity) } else { Group { if expandedPreview { expandedPiece } else { sheet } } .allowsHitTesting(session.accountReady) .accessibilityHidden(!session.accountReady) .transition(.opacity) } } // Reveal the notebook gently; lock it immediately when account access ends. .animation(session.accountReady ? .easeInOut(duration: reduceMotion ? 0.12 : 0.35) : nil, value: session.accountReady) .overlay { if session.accountReady && showHome { AeselHomeView(session: session, host: host) { showHome = false } } } .overlay(alignment: .bottomLeading) { connectionNotices } .overlay { if session.accountReady && showSettings { settingsPane } } .font(Paint.font()) .buttonStyle(AeselButtonStyle()) .foregroundStyle(paint.ink) .background { AeselCloth().ignoresSafeArea() } .background(HostCarrier(host: host).frame(width: 0, height: 0)) .aeselWindowTitle(oskiewar.room.isEmpty ? session.route : "Oskiewar · " + oskiewar.room, paper: paint.bg) .environment(\.paint, paint) .coordinateSpace(name: "aesel-ui") .animation(.easeInOut(duration: 0.5), value: paint) .overlay { if session.showSignIn { signInPanel.transition(.opacity) } } .animation(.easeOut(duration: reduceMotion ? 0.12 : 0.22), value: session.showSignIn) }
private var signInPanel: some View { GeometryReader { geometry in let layout = SignInPanelLayout(available: geometry.size) ZStack { Color.black.opacity(0.25).ignoresSafeArea() .onTapGesture { session.showSignIn = false } ZStack(alignment: .topTrailing) { ZStack { SignInCarrier(host: host, viewport: SignInPanelLayout.canvas) .frame(width: SignInPanelLayout.canvas.width, height: SignInPanelLayout.canvas.height) .scaleEffect(layout.scale) .frame(width: layout.size.width, height: layout.size.height) if session.signInLoading { ProgressView().padding(16).background(paint.bg, in: RoundedRectangle(cornerRadius: 8)) } if let error = session.signInError { VStack(spacing: 18) { Text(error).multilineTextAlignment(.center) Button("Retry") { host.retrySignIn() } }.padding(24).frame(maxWidth: .infinity, maxHeight: .infinity).background(paint.bg) } } Button { session.showSignIn = false } label: { Image(systemName: "xmark").font(.system(size: 12, weight: .medium)) .frame(width: 28, height: 28) .background(paint.bg.opacity(0.95), in: Circle()) } .accessibilityLabel("Close sign-in") .keyboardShortcut(.cancelAction) .padding(6) } .frame(width: layout.size.width, height: layout.size.height) .background(paint.bg) .clipShape(RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.2), radius: 18, y: 6) .buttonStyle(AeselButtonStyle()) .foregroundStyle(paint.ink) .scaleEffect(signInArrived || reduceMotion ? 1 : 0.94) .offset(y: signInArrived || reduceMotion ? 0 : 12) .opacity(signInArrived ? 1 : 0) } .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear { withAnimation(reduceMotion ? .easeOut(duration: 0.12) : .spring(response: 0.38, dampingFraction: 0.9)) { signInArrived = true } } .onDisappear { signInArrived = false } }
private var managedSheet: some View { styledSheet .onChange(of: session.accountReady) { _, ready in if !ready { showSettings = false showHome = false showHelp = false showSource = false showImport = false showExport = false showVolume = false expandedPreview = false writing = false } } .onChange(of: session.selectedRevision) { writing = false } .onChange(of: session.currentSessionID) { previewHidden = false; expandedPreview = false; attended = true } .onChange(of: preview.failure) { host.automation.previewFailure = preview.failure } .onChange(of: session.busy) { if session.busy { attended = false } else if !oskiewar.room.isEmpty { oskiewar.finish(source: session.source) } } .onChange(of: session.entries) { if session.busy, let response = session.entries.last(where: { $0.kind == .ac }) { oskiewar.mirror(response.text) } } .onChange(of: writing) { if writing { attended = true } } .onChange(of: draft) { attended = true } .onChange(of: session.showSignIn) { if !session.showSignIn { host.cancelSignIn() } } .onChange(of: session.signedIn) { if session.signedIn { Task { await braincells.load() } } } .task { braincells.start(token: { host.accessToken() }, credited: { host.refreshCredits() }) } .onDisappear { host.automation.stop(); oskiewar.disconnect() } .onAppear { host.automation.inspect = { automationState } host.automation.perform = { action, params in try await automationAction(action, params) } host.automation.preview = preview.view host.automation.retryPreview = { preview.reload() } host.automation.start() if let connection = ProcessInfo.processInfo.environment["AESEL_OSKIEWAR"], connection.hasPrefix("oskiewar://") { draft = connection oskiewar.connect(connection) } #if DEBUG if ProcessInfo.processInfo.environment["AESEL_NOTEBOOK_PREVIEW"] == "1" { showHome = false showSettings = ProcessInfo.processInfo.environment["AESEL_SETTINGS_PREVIEW"] == "1" } #endif } }
var body: some View { managedSheet .alert("Account locked", isPresented: Binding(get: { session.accountDeleted }, set: { session.accountDeleted = $0 })) { Button("Done") { session.accountDeleted = false } } message: { Text("Your Aesthetic Computer account is locked and will be deleted\(session.accountDeletionDate.map { " on " + $0.formatted(date: .long, time: .omitted) } ?? " in 14 days").\(session.accountDeletionMailed ? " We emailed you a link to keep it." : "") Your local notebooks remain on this device.") } .sheet(item: Binding(get: { session.approval }, set: { value in if value == nil, let pending = session.approval { host.respondToApproval(id: pending.id, decision: "decline") } })) { request in VStack(alignment: .leading, spacing: 16) { Text(request.title).font(Paint.title(20)) ScrollView { Text(request.detail).font(Paint.font(15)).textSelection(.enabled) } if let scope = request.alwaysScope { Text(scope).font(Paint.font(13)) } HStack { Button(request.canAccept ? "Deny" : "Dismiss") { host.respondToApproval(id: request.id, decision: "decline") } Spacer() if request.canAccept { Button("Allow once") { host.respondToApproval(id: request.id, decision: "accept") } } if let label = request.alwaysLabel { Button(label) { host.respondToApproval(id: request.id, decision: "always") } } } }.padding(24).frame(minWidth: 300, minHeight: 200) .background(paint.bg).foregroundStyle(paint.ink).buttonStyle(AeselButtonStyle()) } .onChange(of: session.exportJSON) { if session.exportJSON != nil { showExport = true } } .fileExporter(isPresented: $showExport, document: NotebookDocument(text: session.exportJSON ?? ""), contentType: .json, defaultFilename: "Aesel Notebook") { result in session.exportJSON = nil if case .failure(let error) = result { fileNotice = error.localizedDescription } } .fileImporter(isPresented: $showImport, allowedContentTypes: [.json]) { result in do { let url = try result.get() let scoped = url.startAccessingSecurityScopedResource() defer { if scoped { url.stopAccessingSecurityScopedResource() } } let size = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 guard size <= 8 * 1024 * 1024 else { throw CocoaError(.fileReadTooLarge) } host.importNotebook(try String(contentsOf: url, encoding: .utf8)) } catch { fileNotice = error.localizedDescription } } .alert("Notebook file", isPresented: Binding(get: { fileNotice != nil }, set: { if !$0 { fileNotice = nil } })) { Button("OK") { fileNotice = nil } } message: { Text(fileNotice ?? "") } .sheet(isPresented: $showSource) { AeselSourceView(session: session, host: host).environment(\.paint, paint) } .sheet(isPresented: $showHelp) { help }
}
// MARK: - The sheet
/// A fixed title strip above one scrolling document, starting on its first row. private var sheet: some View { VStack(spacing: 0) { notebookHeader GeometryReader { geometry in ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: 0) { Color.clear.frame(height: previewBlockHeight) AeselNotebook(session: session, automation: host.automation, paint: paint, exclusion: notebookExclusion, height: $notebookHeight) { openURL($0) } .frame(height: hasNotebook ? max(row, notebookHeight) : 0) .clipped() .padding(.bottom, hasNotebook ? -row : 0) .allowsHitTesting(hasNotebook) .onGeometryChange(for: CGFloat.self) { $0.frame(in: .named("notebook-scroll")).minY } action: { notebookTop = $0 } if session.fatal != nil { Button("/retry") { host.restore() } .foregroundStyle(paint.you).frame(height: row).padding(.horizontal, edgeInset) } prompt(minHeight: max(48, geometry.size.height - paperTop - transcriptHeight - row)).id("prompt") } .frame(maxWidth: .infinity, minHeight: geometry.size.height, alignment: .topLeading) .background(alignment: .top) { AeselRuling(spacing: row, topInset: paperTop) } } .coordinateSpace(name: "notebook-scroll") .scrollBounceBehavior(.always, axes: .vertical) .aeselKeyboardScrolling() .mask(alignment: .top) { VStack(spacing: 0) { let fade = min(1, max(0, (paperTop - notebookTop) / row)) LinearGradient(colors: [.black.opacity(1 - fade), .black], startPoint: .top, endPoint: .bottom) .frame(height: row) Rectangle().fill(.black) } } .overlay(alignment: .topTrailing) { if previewVisible { // Down the sheet at the desktop's 80-point inset, // passing behind the corner piece. ACWaveformStrip(preview.waveform, span: ACWaveformHistory.span(bpm: preview.tempo), suppressed: preview.volume == 0 || reduceMotion) .frame(width: 16).frame(maxHeight: .infinity) .padding(.trailing, 72) .allowsHitTesting(false) .accessibilityHidden(true) previewBox(container: geometry.size) } } .onGeometryChange(for: CGSize.self) { $0.size } action: { sheetSize = $0 } // Scrolling reflows prose around the fixed preview and changes // notebookHeight. Only editing may bring the input back into view. .onChange(of: draft) { if writing { proxy.scrollTo("prompt", anchor: .bottom) } } .onChange(of: writing) { if writing { withAnimation { proxy.scrollTo("prompt", anchor: .bottom) } } } .onChange(of: session.busy) { if session.busy { withAnimation { proxy.scrollTo("prompt", anchor: .bottom) } } } } } } }
@ViewBuilder private var notebookHeader: some View { #if os(macOS) AeselTitlebarAccessory { headerControls.environment(\.paint, paint) } .frame(height: 32 / uiScale) #else headerControls.frame(height: 32) #endif }
private var headerControls: some View { GeometryReader { geometry in #if os(macOS) let proxWidth: CGFloat = 40 #else let proxWidth: CGFloat = 0 #endif HStack(spacing: 12) { title(availableWidth: max(0, geometry.size.width - edgeInset * 2 - 24 - 48 - proxWidth - (speakerVisible ? 36 : 0))) if speakerVisible { speakerControl } Spacer(minLength: 8) .frame(height: 32) .background { AeselWindowDragArea() } #if os(macOS) AeselProxAnchor(prox: host.prox).frame(width: 28, height: 28) .accessibilityHidden(true) #endif versionLabel } .padding(.horizontal, edgeInset) .frame(height: 32) } }
private var speakerControl: some View { Button { showVolume.toggle() } label: { Image(systemName: preview.volume == 0 ? "speaker.slash.fill" : "speaker.wave.2.fill") .font(.system(size: 12)).frame(width: 24, height: 24) } .buttonStyle(AeselButtonStyle()) .accessibilityLabel("Volume") .popover(isPresented: $showVolume) { VStack(spacing: 12) { HStack(spacing: 10) { Button { preview.volume = preview.volume == 0 ? 1 : 0 } label: { Image(systemName: preview.volume == 0 ? "speaker.slash.fill" : "speaker.wave.2.fill") }.accessibilityLabel(preview.volume == 0 ? "Unmute" : "Mute") Slider(value: $preview.volume, in: 0...1).tint(paint.accent).accessibilityLabel("Volume") } HStack(spacing: 8) { Image(systemName: "metronome").accessibilityHidden(true) Text("\(Int(preview.tempo)) BPM").font(Paint.font(13)).monospacedDigit() Spacer() Button("Sync") { preview.syncClock() }.font(Paint.font(12)) .accessibilityLabel("Restart on the shared 120 BPM clock") .help("Restart on the shared 120 BPM beat") } Slider(value: $preview.tempo, in: 30...240, step: 1) .tint(paint.accent).accessibilityLabel("Metronome rate") .accessibilityValue("\(Int(preview.tempo)) beats per minute") .disabled(preview.clockRate == nil) } .padding(14).frame(width: 240) .foregroundStyle(paint.ink).background(paint.bg) .buttonStyle(AeselButtonStyle()).presentationCompactAdaptation(.popover) } }
private var connectionNotices: some View { VStack(alignment: .leading, spacing: 6) { ForEach(session.notices.keys.sorted(), id: \.self) { scope in if let notice = session.notices[scope] { HStack(spacing: 10) { Text(notice.text).font(Paint.font(13)).fixedSize(horizontal: false, vertical: true) if let action = notice.action { Button(action == "publish" ? "Retry" : "Sign in") { if action == "publish" { host.publish() } else { host.signIn() } } } Button { session.notices[scope] = nil } label: { Image(systemName: "xmark").font(.system(size: 10)).frame(width: 24, height: 24) }.accessibilityLabel("Dismiss status") } .padding(.horizontal, 10).padding(.vertical, 6) .background(paint.bg, in: RoundedRectangle(cornerRadius: 8)) .overlay { RoundedRectangle(cornerRadius: 8).stroke(paint.ink.opacity(0.3), lineWidth: 1) } .task(id: notice.id) { guard !notice.working else { return } try? await Task.sleep(for: .seconds(15)) if !Task.isCancelled && session.notices[scope]?.id == notice.id { session.notices[scope] = nil } } } } }.padding(12).frame(maxWidth: 460, alignment: .leading) }
private var titleURL: URL? { session.viewingHistory ? nil : session.shareURL }
@ViewBuilder private func title(availableWidth: CGFloat) -> some View { if !oskiewar.room.isEmpty { HStack(spacing: 6) { OskiewarHeadIcon().frame(width: 22, height: 24) .opacity(oskiewar.status.hasPrefix("Connected") ? 1 : 0.5) AeselTitle(text: "Oskiewar · " + oskiewar.room, size: compact ? 12 : 16, maximumWidth: max(0, availableWidth - 28), horizontalInset: 0) } .frame(height: row) .help(oskiewar.status + (oskiewar.editStatus.isEmpty ? "" : "\n" + oskiewar.editStatus)) .accessibilityElement(children: .ignore) .accessibilityLabel("Oskiewar, " + oskiewar.room + ". " + oskiewar.status) .contextMenu { Button("Disconnect Oskiewar") { oskiewar.disconnect() } } } else if !session.route.isEmpty { Button { if let url = titleURL { openURL(url) } } label: { AeselTitle(text: session.route, colors: session.handleColors, size: compact ? 12 : 16, maximumWidth: availableWidth, horizontalInset: 0, hoverSound: { AeselHoverSound.play(project: session.route, revision: session.currentRevision, control: "title") }) } .buttonStyle(AeselButtonStyle()) .disabled(titleURL == nil) .accessibilityLabel(titleURL == nil ? "Publishing piece" : "Open piece in browser") .frame(height: row) } }
/// The corner piece keeps the original desktop wood grain and resize edges. private func previewBox(container: CGSize) -> some View { ZStack(alignment: .topTrailing) { if !oskiewar.room.isEmpty { artifactPreview .frame(width: previewSize.width, height: previewSize.height) .clipped() } else if let url = session.previewURL { PieceView(url: url, source: session.displayedSource, sessionID: session.currentSessionID, revision: session.displayedRevision, preview: preview) .frame(width: previewSize.width, height: previewSize.height, alignment: .topTrailing) .clipped() .overlay { AeselPreviewInset() } }
} .padding(4) .background { AeselWoodFrame() .shadow(color: Color(red: 1, green: 0.176, blue: 0.333).opacity(0.5), radius: 0, x: 1.5, y: 1.5) } .overlay { PreviewResizeOverlay(bounds: $previewBounds, container: container) } .padding(.top, previewBounds.top).padding(.trailing, previewBounds.right) .animation(nil, value: previewBounds) .onChange(of: container) { if container.width >= 420 && container.height >= 300 { previewBounds = previewBounds.fitted(in: container) } } }
private var artifactPreview: some View { TimelineView(.animation(minimumInterval: 1.0 / 30)) { clock in OskiewarArtifactPreview(artifact: oskiewar.artifact, time: clock.date.timeIntervalSince1970) }.padding(8).background(Color(red: 0.16, green: 0.13, blue: 0.22)) }
private var expandedPiece: some View { ZStack(alignment: .topTrailing) { if !oskiewar.room.isEmpty { artifactPreview } else if let url = session.previewURL { PieceView(url: url, source: session.displayedSource, sessionID: session.currentSessionID, revision: session.displayedRevision, preview: preview) .frame(maxWidth: .infinity, maxHeight: .infinity) } Button { expandedPreview = false } label: { Image(systemName: "arrow.down.right.and.arrow.up.left") .font(.system(size: 14)).padding(10).background(paint.deep.opacity(0.85)) } .buttonStyle(AeselButtonStyle()).padding(5) .accessibilityLabel("Return to aesel") } }
/// The empty paper remains editable below the conversation. @ViewBuilder private func prompt(minHeight: CGFloat) -> some View { if session.viewingHistory { VStack(alignment: .leading, spacing: 8) { if !session.historicalTranscriptAvailable { Text("Transcript checkpoint unavailable for this older version.").font(Paint.font(13)) } Button { host.selectRevision(session.currentRevision) } label: { Label("Return to v\(session.currentRevision) to write", systemImage: "lock.fill") }.font(Paint.font(15)).foregroundStyle(paint.dim) }.padding(.horizontal, edgeInset).padding(.vertical, row) } else { editablePrompt(minHeight: minHeight) } }
private func editablePrompt(minHeight: CGFloat) -> some View { HStack(alignment: .top, spacing: 8) { AeselComposer(text: Binding(get: { draft }, set: { draft = $0 }), height: $composerHeight, focused: $writing, color: paint.userInk, submit: send) .frame(height: max(composerHeight, minHeight)) if session.busy { Button { host.stop() } label: { RoundedRectangle(cornerRadius: 1.5).fill(.white).frame(width: 8, height: 8) .frame(width: row, height: row) .background(Color(red: 0.94, green: 0.325, blue: 0.314), in: Circle()) .overlay { Circle().stroke(.white.opacity(0.25), lineWidth: 0.5) } .shadow(color: .black.opacity(0.3), radius: 1, y: 1) } .buttonStyle(AeselStopButtonStyle()) .accessibilityLabel("Stop") } } .buttonStyle(AeselButtonStyle()) .overlay(alignment: .bottomTrailing) { if !session.busy && !draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Button(action: send) { Color.clear.frame(width: row, height: row).contentShape(Rectangle()) } .accessibilityLabel("Send") } } .padding(.leading, edgeInset) .padding(.trailing, previewVisible && notebookTop + transcriptHeight < previewBounds.top + previewBounds.height + 8 ? previewSize.width + previewInset + 24 : edgeInset) .padding(.bottom, row) }
/// The piece version opens settings from the fixed title strip. private var versionLabel: some View { Button { openSettings() } label: { AeselTitle(text: "v\(session.displayedRevision)", size: compact ? 12 : 16, horizontalInset: 0, hoverAnchor: .trailing, hoverSound: { AeselHoverSound.play(project: session.route, revision: session.currentRevision, control: "version") }) } .buttonStyle(AeselButtonStyle()) .frame(height: 32) .accessibilityLabel("Piece version \(session.displayedRevision). Settings") }
// MARK: - Settings
private func openSettings() { writing = false braincells.notice = "" host.refreshCredits() host.refreshProviders() withAnimation(.easeOut(duration: 0.18)) { showSettings = true } }
private func closeSettings(then action: (() -> Void)? = nil) { withAnimation(.easeIn(duration: 0.15)) { showSettings = false } if let action { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: action) } }
private func usd(_ value: Double) -> String { value.formatted(.currency(code: "USD")) }
/// The desktop's #provider-menu: a centred card over the dimmed sheet, /// 15pt Helvetica, hairline sections, with the account, the automatic /// model, braincells and the way to buy more. private var settingsPane: some View { GeometryReader { geometry in ZStack { Color.black.opacity(0.4).ignoresSafeArea().onTapGesture { closeSettings() } VStack(spacing: 0) { HStack { Spacer() Button { closeSettings() } label: { Image(systemName: "xmark").font(.system(size: 13, weight: .semibold)) .frame(width: 30, height: 30).background(paint.ink.opacity(0.08), in: RoundedRectangle(cornerRadius: 6)) }.accessibilityLabel("Close settings") }.padding(.horizontal, 18).padding(.top, 12) ScrollView { VStack(alignment: .leading, spacing: 16) { let fields = geometry.size.width < 420 ? AnyLayout(VStackLayout(alignment: .leading, spacing: 12)) : AnyLayout(HStackLayout(alignment: .top, spacing: 12)) fields { settingsField("Provider", bordered: false) { AeselProviderPicker(session: session, host: host) } if session.provider != "ac" { settingsField("Model") { AeselModelPicker(session: session, host: host) } } } if !session.reportedModel.isEmpty { Text(session.reportedModel).font(Paint.font(13)).foregroundStyle(paint.dim) .textSelection(.enabled) } if !session.providerNotice.isEmpty { Text(session.providerNotice).font(Paint.font(13)).foregroundStyle(paint.dim) } if session.hostOperationID != nil && !session.busy { settingsItem("Reconnect to current turn") { host.resumeHostTurn() } } Rectangle().fill(paint.ink.opacity(0.16)).frame(height: 1) if session.signedIn { AeselTitle(text: session.handle.isEmpty ? "Aesthetic Computer account" : "@\(session.handle)", colors: session.handleColors, size: 26, maximumWidth: max(180, min(520, geometry.size.width - 80)), horizontalInset: 4) .padding(.bottom, 4) HStack(spacing: 16) { accountAction("Log out") { closeSettings { host.signOut() } } Spacer(minLength: 0) Button { openURL(URL(string: "https://aesthetic.computer/delete-erase-and-forget-me")!) } label: { Label("Manage account", systemImage: "arrow.up.right") } .accessibilityHint("Opens account management in your browser") } } else { settingsItem("Sign in to Aesthetic Computer") { closeSettings { host.signIn() } } } if !session.accountNotice.isEmpty { Text(session.accountNotice).font(Paint.font(13)) } if session.signedIn { Rectangle().fill(paint.ink.opacity(0.16)).frame(height: 1) VStack(alignment: .leading, spacing: 8) { if let balance = session.braincells { HStack(spacing: 8) { Text("🧠").accessibilityHidden(true) Text(balance.formatted(.number.precision(.fractionLength(0)))) } .font(Paint.font(24)) .accessibilityElement(children: .ignore) .accessibilityLabel("\(balance.formatted(.number.precision(.fractionLength(0)))) braincells remaining") if let dollars = session.braincellDollars { Text("\(usd(dollars)) equivalent").font(Paint.font(13)).foregroundStyle(paint.dim) } if let daily = session.freeDollars, let purchased = session.purchasedDollars { Text("Daily \(usd(daily)) · Purchased \(usd(purchased))") .font(Paint.font(13)).foregroundStyle(paint.dim) } } else { Text(session.creditsStatus).foregroundStyle(paint.dim) } buyButton if !braincells.storeStatus.isEmpty { Text(braincells.storeStatus).font(Paint.font(13)).foregroundStyle(paint.dim) } if !braincells.notice.isEmpty { Text(braincells.notice).font(Paint.font(13)) } } } Rectangle().fill(paint.ink.opacity(0.16)).frame(height: 1) AeselVersionList(session: session, host: host, onSelect: { closeSettings() }) }.padding(.horizontal, 20).padding(.bottom, 20) } } .frame(maxWidth: 560).frame(maxHeight: min(560, geometry.size.height * 0.85)) .background(paint.bg) .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay { RoundedRectangle(cornerRadius: 12).stroke(paint.ink.opacity(0.3), lineWidth: 1) } .shadow(color: .black.opacity(0.25), radius: 20, y: 10).padding(20) }.font(Paint.font(15)).buttonStyle(AeselButtonStyle()).transition(.opacity) } }
/// The App Store purchase: one big green button with Apple's price. private var buyButton: some View { Button { Task { await braincells.buy() } } label: { HStack(spacing: 10) { Text("🧠").font(.system(size: 28)).accessibilityHidden(true) VStack(alignment: .leading, spacing: 2) { Text(braincells.busy ? "Opening the App Store…" : "Add 1,000,000") .font(Paint.font(18)).fontWeight(.semibold) Text(braincells.busy ? "" : "\(braincells.price) · App Store").font(Paint.font(13)).opacity(0.85) } Spacer() } .padding(EdgeInsets(top: 14, leading: 16, bottom: 14, trailing: 16)) .frame(maxWidth: .infinity, minHeight: 64) .foregroundStyle(.white) .background(Color(rgb: 0x1f9d55)) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.25), lineWidth: 1) } } .disabled(braincells.busy || braincells.product == nil) .opacity(braincells.product == nil ? 0.55 : 1) .accessibilityLabel("Add one million braincells for \(braincells.price) through the App Store") }
private func settingsField<Content: View>(_ label: String, bordered: Bool = true, @ViewBuilder content: () -> Content) -> some View { VStack(alignment: .leading, spacing: 5) { Text(label).font(Paint.font(13)) content() .frame(maxWidth: .infinity, minHeight: 36, alignment: .leading) .padding(.horizontal, bordered ? 8 : 0) .overlay { if bordered { RoundedRectangle(cornerRadius: 6).stroke(paint.ink.opacity(0.45), lineWidth: 1) } } } .padding(.top, 14) }
private func settingsItem(_ label: String, enabled: Bool = true, action: @escaping () -> Void) -> some View { Button(action: action) { Text(label).frame(maxWidth: .infinity, alignment: .leading) .padding(EdgeInsets(top: 9, leading: 10, bottom: 9, trailing: 10)) .contentShape(Rectangle()) } .disabled(!enabled) .opacity(enabled ? 1 : 0.5) }
private var settingsDivider: some View { Rectangle().fill(paint.ink.opacity(0.18)).frame(height: 1) .padding(.top, 16).padding(.bottom, 12) }
private var help: some View { VStack(alignment: .leading, spacing: 24) { AeselWordmark() Text("describe a picture, a sound, or a little world. aesel writes it while you watch.") Text("/new start a piece\n/publish share your piece\n/open open in your browser\n/login your AC account\n/logout log out") .foregroundStyle(paint.dim) Button("/close") { showHelp = false }.foregroundStyle(paint.you) Spacer() } .font(Paint.font(22)).padding(24) .frame(maxWidth: .infinity, alignment: .leading) .background { AeselCloth().ignoresSafeArea() } .aeselHelpSheet() }
private func send() { guard !session.viewingHistory else { return } let text = draft.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return } if text.hasPrefix("oskiewar://") { oskiewar.connect(text); draft = ""; return } if text == "/shirt star", !oskiewar.room.isEmpty { oskiewar.applyStar(); draft = ""; return } if text == "/disconnect" { oskiewar.disconnect(); draft = ""; return } switch Session.command(for: text) { case "new", "home": writing = false; showHome = true case "publish": host.publish() case "login": host.signIn() case "logout": host.signOut() case "stop": host.stop() case "open": if let url = session.shareURL { openURL(url) } case "settings": openSettings() case "buy": openSettings(); Task { await braincells.buy() } case "help": showHelp = true default: if session.provider == "ac" && !session.signedIn { host.signIn(); return } guard session.canStartTurn else { openSettings(); return } if oskiewar.room.isEmpty { host.ask(text) } else { oskiewar.begin(source: session.source); host.ask(text, context: oskiewar.artifactContext) } } draft = "" }
private var automationState: [String: Any] { #if os(macOS) let canResize = true #else let canResize = false #endif let controls: [(String, String, Bool)] = [ ("settings.open", "Open settings", !showSettings), ("settings.close", "Close settings", showSettings), ("home.open", "Show saved threads", !showHome), ("home.close", "Return to notebook", showHome), ("help.open", "Open help", !showHelp), ("help.close", "Close help", showHelp), ("composer.set", "Set message draft", !session.viewingHistory), ("composer.clear", "Clear message draft", !session.viewingHistory), ("composer.send", "Send message; may run inference and publish", session.canStartTurn && !draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty), ("turn.stop", "Stop current turn", session.busy), ("turn.reconnect", "Check current host turn without resending it", session.hostOperationID != nil && !session.busy), ("session.retry", "Restore failed session", session.fatal != nil), ("session.new", "Start a blank piece", !session.busy), ("session.resume", "Resume a saved thread", !session.busy), ("preview.expand", "Expand preview", previewVisible && !expandedPreview), ("preview.collapse", "Return to notebook", expandedPreview), ("preview.hide", "Hide preview", previewVisible), ("preview.show", "Show preview", previewHidden), ("preview.reload", "Reload embedded preview", previewVisible), ("preview.resize", "Resize corner preview", canResize && previewVisible && !expandedPreview), ("preview.retry", "Retry failed preview", host.automation.previewFailure != nil), ("window.resize", "Resize app window without focusing it", canResize), ("ui.scale", "Set UI size from 0.7 to 1.75; does not resize the window", canResize), ("piece.open", "Open public piece in browser", titleURL != nil), ("piece.publish", "Publish current source", session.signedIn && !session.busy && !session.viewingHistory), ("account.signin", "Open AC sign-in", !session.signedIn), ("account.signup", "Create an AC account", !session.signedIn), ("account.check", "Verify saved AC account", !session.accountReady), ("account.handle", "Choose an AC handle", session.signedIn && !session.accountReady), ("account.signin.close", "Close sign-in", session.showSignIn), ("account.signin.retry", "Retry sign-in", session.showSignIn && session.signInError != nil), ("account.signout", "Log out of AC", session.signedIn), ("balance.refresh", "Refresh balance", session.signedIn), ("credits.buy", "Open App Store purchase confirmation", session.signedIn && !braincells.busy && braincells.product != nil), ("source.open", "Open source and revisions", !session.busy && !session.viewingHistory), ("version.select", "View a saved version", !session.busy && session.hostOperationID == nil), ("provider.select", "Choose connected provider", !session.busy && session.hostOperationID == nil), ("model.select", "Choose provider model", session.provider != "ac" && !session.busy && session.hostOperationID == nil) ] return ["schema": 1, "surface": !session.accountReady ? "account" : expandedPreview ? "preview" : showSettings ? "settings" : showHome ? "home" : "notebook", "overlays": ["settings": session.accountReady && showSettings, "home": session.accountReady && showHome, "help": session.accountReady && showHelp, "signin": session.showSignIn], "piece": ["route": session.route, "version": session.displayedRevision, "currentVersion": session.currentRevision, "readOnly": session.viewingHistory, "sourceBytes": session.displayedSource.utf8.count, "sourceHash": PiecePreview.sourceHash(session.displayedSource), "previewURL": session.previewURL?.absoluteString ?? "", "shareURL": session.shareURL?.absoluteString ?? ""], "oskiewar": ["room": oskiewar.room, "status": oskiewar.status, "update": oskiewar.editStatus, "artifact": oskiewar.artifact.wire], "composer": ["characters": draft.count, "placeholder": "", "focused": writing], "title": ["opacity": 1, "linked": oskiewar.room.isEmpty && titleURL != nil, "text": oskiewar.room.isEmpty ? session.route : "Oskiewar · " + oskiewar.room], "footer": "v\(session.displayedRevision)", "session": ["id": session.currentSessionID, "busy": session.busy, "status": session.status, "signedIn": session.signedIn, "accountReady": session.accountReady, "entryCount": session.displayedEntries.count, "totalEntryCount": session.entries.count, "history": session.history.map { ["id": $0.id, "title": $0.title, "route": $0.route] }], "provider": ["selected": session.provider, "ready": session.providerReady, "model": session.model, "reportedModel": session.reportedModel, "operationID": session.hostOperationID ?? "", "choices": session.providers.map { ["id": $0.id, "available": $0.available, "notice": $0.notice] }, "models": session.modelChoices.map { ["id": $0.id, "title": $0.title] }], "audio": ["volume": preview.volume, "tempo": preview.tempo, "clockRate": preview.clockRate ?? 0, "hasAudio": preview.hasAudio], "preview": ["visible": previewVisible, "expanded": expandedPreview, "width": previewBounds.width, "height": previewBounds.height, "right": previewBounds.right, "top": previewBounds.top], "layout": ["uiScale": uiScale, "compact": compact, "width": sheetSize.width, "height": sheetSize.height, "row": row, "paperTop": paperTop, "previewBlockHeight": previewBlockHeight, "composerHeight": composerHeight], "notebook": ["visible": session.accountReady && !expandedPreview && !showHome && !showSettings && !showHelp && !session.showSignIn], "controls": controls.map { ["id": $0.0, "label": $0.1, "enabled": $0.2 && (session.accountReady || $0.0.hasPrefix("account.") || ["window.resize", "ui.scale"].contains($0.0))] }] }
private func automationAction(_ action: String, _ params: [String: Any]) async throws { guard session.accountReady || (action.hasPrefix("account.") || ["window.resize", "ui.scale"].contains(action)) else { throw NSError(domain: "Aesel", code: 401, userInfo: [NSLocalizedDescriptionKey: "An AC login and @handle are required."]) } guard params["expectedSessionID"] as? String == session.currentSessionID else { throw AeselAutomation.error("Stale or missing expectedSessionID; inspect state before acting") } let controls = automationState["controls"] as? [[String: Any]] ?? [] guard let control = controls.first(where: { $0["id"] as? String == action }) else { throw AeselAutomation.error("Unknown control: \(action)") } guard control["enabled"] as? Bool == true else { throw AeselAutomation.error("Control is disabled: \(action)") } switch action { case "settings.open": openSettings() case "settings.close": closeSettings() case "home.open": showHome = true case "home.close": showHome = false case "help.open": showHelp = true case "help.close": showHelp = false case "composer.set": guard let text = params["text"] as? String, text.utf8.count <= 32768 else { throw AeselAutomation.error("text is required, at most 32768 bytes") } draft = text case "composer.clear": draft = "" case "composer.send": send() case "turn.stop": host.stop() case "turn.reconnect": host.resumeHostTurn() case "session.retry": host.restore() case "session.new": host.newSession(medium: "piece"); showHome = false case "session.resume": guard let id = params["sessionId"] as? String, session.history.contains(where: { $0.id == id }) else { throw AeselAutomation.error("Known sessionId is required") } host.resumeSession(id: id); showHome = false case "preview.expand": expandedPreview = true; writing = false case "preview.collapse": expandedPreview = false case "preview.hide": previewHidden = true; expandedPreview = false case "preview.show": previewHidden = false case "preview.resize": #if os(macOS) guard let width = params["width"] as? Double, let height = params["height"] as? Double, (96...2400).contains(width), (72...1800).contains(height), let window = host.automation.notebook?.window ?? host.automation.preview?.window ?? host.attachable.window else { throw AeselAutomation.error("Preview dimensions are out of range") } var bounds = previewBounds bounds.width = width; bounds.height = height previewBounds = bounds.fitted(in: sheetSize) #else throw AeselAutomation.error("Automation resizing is macOS-only") #endif case "preview.reload": host.automation.preview?.reload() case "preview.retry": host.automation.retryPreview?() case "ui.scale": guard let scale = params["scale"] as? Double, scale.isFinite, (0.7...1.75).contains(scale) else { throw AeselAutomation.error("UI scale must be 0.7–1.75") } uiScale = scale case "window.resize": #if os(macOS) guard let width = params["width"] as? Double, let height = params["height"] as? Double, (220...2400).contains(width), (160...1800).contains(height), let window = host.automation.notebook?.window ?? host.automation.preview?.window ?? host.attachable.window else { throw AeselAutomation.error("Window width 220–2400 and height 160–1800 are required") } window.setContentSize(NSSize(width: width, height: height)) #else throw AeselAutomation.error("Window resizing is macOS-only") #endif case "piece.open": if let url = titleURL { openURL(url) } case "piece.publish": host.publish() case "account.check": host.restore() case "account.handle": openURL(URL(string: "https://aesthetic.computer/handle")!) case "account.signin": host.signIn() case "account.signin.retry": host.retrySignIn() case "account.signup": host.signIn(signUp: true) case "account.signin.close": session.showSignIn = false case "account.signout": host.signOut() case "balance.refresh": host.refreshCredits() case "credits.buy": await braincells.buy() case "version.select": guard let version = params["version"] as? Int, session.revisions.contains(where: { $0.id == version }) else { throw AeselAutomation.error("Saved version required") } host.selectRevision(version); closeSettings() case "source.open": showSource = true case "provider.select": guard let id = params["provider"] as? String, session.providers.contains(where: { $0.id == id && $0.available }) else { throw AeselAutomation.error("Connected provider required") } host.setProvider(id) case "model.select": guard let id = params["model"] as? String, session.modelChoices.contains(where: { $0.id == id }) else { throw AeselAutomation.error("Known model required") } host.setModel(id: id) default: throw AeselAutomation.error("Unsupported control: \(action)") } }}
/// Puts the hidden session webview in the hierarchy without drawing it.private struct HostCarrier: AeselWebViewRepresentable { let host: SessionHost
func makeWebView(context: Context) -> WKWebView { host.attachable } func updateWebView(_ view: WKWebView, context: Context) {}}
private struct SignInCarrier: AeselWebViewRepresentable { let host: SessionHost let viewport: CGSize @Environment(\.colorScheme) private var colorScheme func makeWebView(context: Context) -> WKWebView { host.signInView } func updateWebView(_ view: WKWebView, context: Context) { ApplePlatform.setAppearance(view, colorScheme: colorScheme) view.pageZoom = min(0.85, max(0.5, min(viewport.width / 440, viewport.height / 720))) }}
// A live game attachment, owned by this window. Presence expires in the game// if the window closes, the process stops, or the network drops.@MainActorprivate final class OskiewarAttachment: ObservableObject { @Published var status = "" @Published var editStatus = "" @Published var artifact = OskiewarArtifact(shape: "star", color: "#fac83c") private var caption = "" private var captionUntil = Date.distantPast private var pendingArtifact = "" private var sourceAtStart: String? var artifactContext: String { """ You are editing an isolated Oskiewar shirt-symbol artifact, connected to room \(room). Current artifact: \(artifact.wire) Use the existing source-edit tool to write valid piece JavaScript with exactly one descriptor comment: /* ac-artifact {"version":1,"kind":"shirt-symbol","shape":"star","color":"#fac83c"} */ Supported shapes: drawing (freely composed circles, strokes and triangles), custom (outline), star, heart, flower, rainbow. Color is a six-digit hex; rainbow uses six fixed rainbow bands. For any new outline use shape "custom" plus "points":[[x,y],...] with 3 to 10 vertices, integer coordinates from -9 to 9, y up. Order them around a simple, non-self-intersecting polygon. The compact descriptor must fit 320 ASCII bytes. Example: {"version":1,"kind":"shirt-symbol","shape":"custom","color":"#ff6699","points":[[0,8],[7,-6],[-7,-6]]}. For custom drawings use shape "drawing" and "draw": an array of up to 12 commands. Circle [0,x,y,r,c]; round stroke [1,x1,y1,x2,y2,width,c]; filled triangle [2,x1,y1,x2,y2,x3,y3,c]. All values integers, positions -16 to 16, circle radius 1 to 9, stroke width 1 to 6. Color index c is 0=artifact color, 1=dark ink, 2=white. Later commands draw on top. Example smiley: {"version":1,"kind":"shirt-symbol","shape":"drawing","color":"#ffcc44","draw":[[0,0,0,8,0],[0,-3,2,1,1],[0,3,2,1,1],[1,-4,-2,0,-4,1,1],[1,0,-4,4,-2,1,1]]}. You CAN custom draw and make smileys using these primitives. Do not say custom shapes are unavailable. Optional "animation":"spin", "pulse", or "float" animates the graphic in both TV and game on a shared two-second loop. Omit animation for a still image. Keep a minimal paint export for valid piece syntax. The native TV renders the descriptor alone, without a shirt or scene. On completion this descriptor is automatically sent to the game. Only the game's acknowledgment confirms application. Briefly describe your actual edit in ordinary text; the butterfly mirrors an excerpt of that response. Do not claim the game applied it. Do not create an unrelated piece, publish it, or add UI. If a request needs another artifact type or geometry, explain that it is not supported yet instead of pretending it changed the game. """ } func begin(source: String) { sourceAtStart = source; caption = ""; editStatus = "Editing" } func mirror(_ text: String) { let plain = text.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }.joined(separator: " ") let ascii = String(plain.unicodeScalars.filter { $0.value >= 32 && $0.value <= 126 }.map(Character.init)) guard !ascii.isEmpty else { return } caption = "Aesel: " + String(ascii.prefix(109)) + (ascii.count > 109 ? "..." : "") captionUntil = Date().addingTimeInterval(10) } func finish(source: String) { guard let previous = sourceAtStart else { return } sourceAtStart = nil guard source != previous else { editStatus = "No artifact change"; return } guard let value = OskiewarArtifact.read(source) else { editStatus = "No supported artifact in this response"; return } artifact = value; pendingArtifact = value.wire; editStatus = "Sending to game" } func applyStar() { artifact = OskiewarArtifact(shape: "star", color: "#fac83c") pendingArtifact = artifact.wire; editStatus = "Sending to game" mirror("Changing the shirt symbol to a star.") } private var socket: URLSessionWebSocketTask? private var retry: Task<Void, Never>? private var receiver: Task<Void, Never>? private var heartbeat: Task<Void, Never>? private(set) var room = ""
func disconnect() { receiver?.cancel(); heartbeat?.cancel(); retry?.cancel() caption = ""; pendingArtifact = ""; sourceAtStart = nil socket?.cancel(with: .goingAway, reason: nil) socket = nil; receiver = nil; heartbeat = nil; retry = nil; room = ""; status = "Oskiewar disconnected" }
private func lost(_ task: URLSessionWebSocketTask) { guard socket === task, retry == nil else { return } status = "Reconnecting to Oskiewar…" retry = Task { [weak self] in try? await Task.sleep(nanoseconds: 2_000_000_000) guard !Task.isCancelled, let self, self.socket === task else { return } let wire = self.pendingArtifact, source = self.sourceAtStart, caption = self.caption, until = self.captionUntil self.retry = nil self.connect("oskiewar://" + self.room) self.pendingArtifact = wire; self.sourceAtStart = source; self.caption = caption; self.captionUntil = until } } func connect(_ text: String) { let name = String(text.dropFirst("oskiewar://".count)).lowercased() guard name.range(of: "^[a-z0-9-]{5,24}$", options: .regularExpression) != nil else { status = "Invalid Oskiewar connection string"; return } disconnect(); room = name; status = "Connecting to Oskiewar…" var url = URLComponents(string: "wss://session-server.aesthetic.computer/oskiewar-live")! url.queryItems = [URLQueryItem(name: "match", value: "ow-" + name), URLQueryItem(name: "role", value: "agent")] let task = URLSession.shared.webSocketTask(with: url.url!) socket = task; task.resume() receiver = Task { [weak self] in do { while !Task.isCancelled { let message = try await task.receive() guard let self, self.socket === task else { return } guard case .string(let text) = message, let data = text.data(using: .utf8), let value = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue } if value["type"] as? String == "oskiewar:error" { self.status = "Oskiewar connection rejected"; self.heartbeat?.cancel(); return } if value["type"] as? String == "oskiewar:state", let state = value["content"] as? [String: Any], let aesel = state["aesel"] as? [String: Any], let wire = aesel["artifact"] as? String, !wire.isEmpty { if wire == self.pendingArtifact { self.editStatus = "Live in game" } if self.pendingArtifact.isEmpty, let data = wire.data(using: .utf8), let current = try? JSONDecoder().decode(OskiewarArtifact.self, from: data), current.valid { self.artifact = current; self.editStatus = "Live in game" } } if value["type"] as? String == "oskiewar:state" && self.heartbeat == nil { self.status = "Connected · oskiewar://" + name self.heartbeat = Task { [weak self] in var pulse = 0 do { while !Task.isCancelled { pulse = (pulse + 1) % 64 guard let self, self.socket === task else { return } var flags: [String: Any] = ["aeselPulse": pulse] if pulse % 2 == 0 && !self.pendingArtifact.isEmpty { flags["aeselArtifact"] = self.pendingArtifact } else { flags["aeselCaption"] = Date() < self.captionUntil ? self.caption : "" } let data = try JSONSerialization.data(withJSONObject: ["type": "oskiewar:flags", "content": flags]) if data.count <= 512 { try await task.send(.string(String(decoding: data, as: UTF8.self))) } else { self.editStatus = "Drawing is too large to send" } try await Task.sleep(nanoseconds: 1_000_000_000) } } catch { if !Task.isCancelled { self?.lost(task) } } } } } } catch { if !Task.isCancelled { self?.lost(task) } } } }}
private struct OskiewarHeadIcon: View { var body: some View { Canvas { context, size in let face = CGRect(x: 1, y: 4, width: size.width - 2, height: size.height - 5) context.fill(Path(ellipseIn: face), with: .color(Color(red: 0.95, green: 0.69, blue: 0.78))) context.stroke(Path(ellipseIn: face), with: .color(Color(red: 0.13, green: 0.09, blue: 0.17)), lineWidth: 1.5) for x in [size.width * 0.33, size.width * 0.67] { context.fill(Path(ellipseIn: CGRect(x: x - 1.5, y: size.height * 0.48, width: 3, height: 4)), with: .color(.black)) } var mouth = Path(); mouth.move(to: CGPoint(x: size.width * 0.37, y: size.height * 0.76)) mouth.addQuadCurve(to: CGPoint(x: size.width * 0.63, y: size.height * 0.76), control: CGPoint(x: size.width * 0.5, y: size.height * 0.85)) context.stroke(mouth, with: .color(.black), lineWidth: 1) var bow = Path(); bow.move(to: CGPoint(x: 5, y: 1)); bow.addLine(to: CGPoint(x: 17, y: 6)) bow.addLine(to: CGPoint(x: 17, y: 1)); bow.addLine(to: CGPoint(x: 5, y: 6)); bow.closeSubpath() context.fill(bow, with: .color(Color(red: 0.4, green: 0.85, blue: 0.7))) }.accessibilityHidden(true) }}
private struct OskiewarArtifact: Codable, Equatable { var version = 1 var kind = "shirt-symbol" var shape: String var color: String var points: [[Int]]? = nil var draw: [[Int]]? = nil var animation: String? = nil var valid: Bool { (animation == nil || ["spin", "pulse", "float"].contains(animation!)) && version == 1 && kind == "shirt-symbol" && ["star", "heart", "flower", "rainbow", "custom", "drawing"].contains(shape) && color.range(of: "^#[0-9a-fA-F]{6}$", options: .regularExpression) != nil && (shape != "custom" || (points.map { (3...10).contains($0.count) && $0.allSatisfy { $0.count == 2 && $0.allSatisfy { (-9...9).contains($0) } } } ?? false)) && (shape != "drawing" || (draw.map { commands in (1...12).contains(commands.count) && commands.allSatisfy { c in guard let op = c.first, (0...2).contains(op), c.count == [5,7,8][op], c.allSatisfy({ (-16...16).contains($0) }), let ink = c.last, (0...2).contains(ink) else { return false } return (op != 0 || (1...9).contains(c[3])) && (op != 1 || (1...6).contains(c[5])) } } ?? false)) } var wire: String { let encoder = JSONEncoder(); encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] return String(decoding: (try? encoder.encode(self)) ?? Data(), as: UTF8.self) } static func read(_ source: String) -> Self? { let marker = "/* ac-artifact " guard let start = source.range(of: marker), let end = source.range(of: "*/", range: start.upperBound..<source.endIndex), source.range(of: marker, range: end.upperBound..<source.endIndex) == nil else { return nil } let json = String(source[start.upperBound..<end.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines) guard json.utf8.count <= 320, let data = json.data(using: .utf8), let value = try? JSONDecoder().decode(Self.self, from: data), value.valid else { return nil } return value }}
private struct OskiewarArtifactPreview: View { let artifact: OskiewarArtifact var time: Double = 0 var body: some View { Canvas { input, size in var context = input let phase = time.truncatingRemainder(dividingBy: 2) * .pi context.translateBy(x: size.width / 2, y: size.height / 2) if artifact.animation == "spin" { context.rotate(by: .radians(-phase)) } if artifact.animation == "pulse" { let k = 0.85 + sin(phase) * 0.15; context.scaleBy(x: k, y: k) } if artifact.animation == "float" { context.translateBy(x: 0, y: -sin(phase) * min(size.width, size.height) / 10) } context.translateBy(x: -size.width / 2, y: -size.height / 2) let scale = min(size.width, size.height) / 20 let center = CGPoint(x: size.width / 2, y: size.height / 2) func point(_ x: Double, _ y: Double) -> CGPoint { CGPoint(x: center.x + x * scale, y: center.y - y * scale) } func ink(_ hex: String) -> Color { let rgb = Int(hex.dropFirst(), radix: 16) ?? 0xfac83c return Color(red: Double((rgb >> 16) & 255)/255, green: Double((rgb >> 8) & 255)/255, blue: Double(rgb & 255)/255) } if artifact.shape == "drawing", let commands = artifact.draw { let colors = [ink(artifact.color), Color(red: 24/255, green: 18/255, blue: 26/255), Color.white] for c in commands { let color = colors[c.last!] if c[0] == 0 { let p = point(Double(c[1]), Double(c[2])), r = Double(c[3]) * scale context.fill(Path(ellipseIn: CGRect(x: p.x-r, y: p.y-r, width: r*2, height: r*2)), with: .color(color)) } else { var path = Path(); path.move(to: point(Double(c[1]), Double(c[2]))) path.addLine(to: point(Double(c[3]), Double(c[4]))) if c[0] == 1 { context.stroke(path, with: .color(color), style: StrokeStyle(lineWidth: Double(c[5])*scale, lineCap: .round)) } else { path.addLine(to: point(Double(c[5]), Double(c[6]))); path.closeSubpath(); context.fill(path, with: .color(color)) } } } } else if artifact.shape == "custom", let points = artifact.points { var path = Path() for (i, p) in points.enumerated() { let position = point(Double(p[0]), Double(p[1])) if i == 0 { path.move(to: position) } else { path.addLine(to: position) } } path.closeSubpath(); context.fill(path, with: .color(ink(artifact.color))) } else if artifact.shape == "rainbow" { let colors = ["#f4535b", "#f99b41", "#fad34c", "#58bf79", "#5199eb", "#a66ddb"] for band in 0..<6 { let outer = 8 - Double(band) * 0.85, inner = outer - 0.85 var path = Path() for i in 0...12 { let a = Double(i) * .pi / 12; let p = point(cos(a)*outer, sin(a)*outer-3); if i == 0 { path.move(to: p) } else { path.addLine(to: p) } } for i in (0...12).reversed() { let a = Double(i) * .pi / 12; path.addLine(to: point(cos(a)*inner, sin(a)*inner-3)) } path.closeSubpath(); context.fill(path, with: .color(ink(colors[band]))) } } else if artifact.shape == "flower" { for i in 0..<6 { let a = Double(i) * .pi / 3, p = point(sin(a)*3, cos(a)*3) context.fill(Path(ellipseIn: CGRect(x: p.x-2*scale, y: p.y-2*scale, width: 4*scale, height: 4*scale)), with: .color(.white)) } context.fill(Path(ellipseIn: CGRect(x: center.x-2*scale, y: center.y-2*scale, width: 4*scale, height: 4*scale)), with: .color(ink(artifact.color))) } else { var path = Path(); let count = artifact.shape == "star" ? 10 : 32 for i in 0..<count { let a = Double(i) * .pi * 2 / Double(count) let r = i % 2 == 0 ? 6.0 : 2.6 let p = artifact.shape == "star" ? point(sin(a)*r, cos(a)*r) : point(16*pow(sin(a),3)*0.38, (13*cos(a)-5*cos(2*a)-2*cos(3*a)-cos(4*a))*0.38) if i == 0 { path.move(to: p) } else { path.addLine(to: p) } } path.closeSubpath(); context.fill(path, with: .color(ink(artifact.color))) } }.accessibilityLabel("Isolated " + artifact.shape + " shirt symbol") }}