From e91e9cf5f065be78f21a1e071618d19a9a38fac8 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 10:14:11 -0700 Subject: [PATCH 01/21] =?UTF-8?q?sailor=20song=20(sage=20oc):=20released?= =?UTF-8?q?=20on=20AC=20=E2=80=94=20mp3=20+=20cover=20self-hosted=20at=20a?= =?UTF-8?q?ssets.aesthetic.computer/pop/sailor-song.*;=20DistroKid=20skipp?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pop/RELEASES.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pop/RELEASES.md b/pop/RELEASES.md index aad56fb314..48ac15b196 100644 --- a/pop/RELEASES.md +++ b/pop/RELEASES.md @@ -7,7 +7,7 @@ Status legend: **RELEASED** · **SUBMITTED** · **MASTERING** · **RENDER** · * --- -## sailor song (sage oc) — HOLD · awaiting Sage's word (delivered to her 2026-10-04) +## sailor song (sage oc) — RELEASED on AC 2026-10-06 (self-hosted, not on DistroKid) - **What:** @jeffrey's niece Sage (**sage oc**, O'Connor) singing Gigi Perez's *Sailor Song* with nylon guitar on a phone, remixed *under* her take by Aesthetic Dot Computer. **A cover** — not ours @@ -27,9 +27,13 @@ Status legend: **RELEASED** · **SUBMITTED** · **MASTERING** · **RENDER** · * syllable captions, strum ripple, kiss-glitch RGB split, held-"long" event; `--fia` stickers (Noto emoji, Apache-2.0; Anne Hathaway head CC BY-SA 2.0 — credit in `EYECONS-CREDITS.md` must accompany any public post of Fia's Cut or the reel). -- **Before any submission:** Sage's reply · parental consent (she is a minor) · mechanical licence · - decide the release artist (DistroKid artist "sage oc" vs. a feature) · brighten check per - MASTERING.md phone-proxy rule (v119 fixed the 52 Hz lane; re-measure). +- **Release (2026-10-06):** Sage + parents said yes (via @jeffrey). @jeffrey chose AC self-hosting over + DistroKid/Spotify. Live: https://assets.aesthetic.computer/pop/sailor-song.mp3 (the tagged v120 mp3 — + Sage performer, Gigi Perez composer, Aesthetic Dot Computer producer/engineer) + `pop/sailor-song.jpg` + (cover v120-video), sha256-verified against the CDN. Video not yet hosted (main cut is 312 MB). + **Open:** a cover still needs a mechanical licence for self-hosted streams/downloads — DistroKid's only + covers its own stores. Lossless master kept: `out/sage oc - sailor song.flac` (16/44.1, −11.6 LUFS, + phone loss −5.4 dB). --- -- 2.51.2 From 4c255bd9faedc0abad9694bbdd30fc6418a89ba5 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 10:34:23 -0700 Subject: [PATCH 02/21] Play all of /pop natively in the iPhone app; add sailor song to the catalog PopPlayer.swift: AVPlayer over pop.aesthetic.computer catalog.json with background audio, lock-screen / Control Center Now Playing + remote commands, AirPlay picker, the whole catalog looping. Opened by pop / pop at the prompt via the iOSApp bridge (pop:open); off the app, pop opens pop.aesthetic.computer. sailor-song gets a manifest, wrapper piece and section stills (CDN), and joins the catalog. --- apple/aesthetic-computer-Info.plist | 1 + .../project.pbxproj | 1146 +++++++++-------- apple/aesthetic.computer/ContentView.swift | 7 + apple/aesthetic.computer/PopPlayer.swift | 419 ++++++ .../aesthetic.computer/disks/pop/README.md | 7 + .../disks/pop/sailor-song.json | 64 + .../aesthetic.computer/disks/prompt.mjs | 19 +- .../aesthetic.computer/disks/sailor-song.mjs | 28 + .../releases/catalog.json | 16 + 9 files changed, 1135 insertions(+), 572 deletions(-) create mode 100644 apple/aesthetic.computer/PopPlayer.swift create mode 100644 system/public/aesthetic.computer/disks/pop/sailor-song.json create mode 100644 system/public/aesthetic.computer/disks/sailor-song.mjs diff --git a/apple/aesthetic-computer-Info.plist b/apple/aesthetic-computer-Info.plist index bea85c5b5f..dc631b3645 100644 --- a/apple/aesthetic-computer-Info.plist +++ b/apple/aesthetic-computer-Info.plist @@ -6,6 +6,7 @@ UIBackgroundModes + audio remote-notification fetch diff --git a/apple/aesthetic.computer.xcodeproj/project.pbxproj b/apple/aesthetic.computer.xcodeproj/project.pbxproj index 6299e5946c..6b355d5d06 100644 --- a/apple/aesthetic.computer.xcodeproj/project.pbxproj +++ b/apple/aesthetic.computer.xcodeproj/project.pbxproj @@ -1,571 +1,575 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000002 /* PrivacyInfo.xcprivacy */; }; - AC1250000000000000000003 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000004 /* Settings.bundle */; }; - 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */; }; - AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1A0C012E9A000000000002 /* LaunchPing.swift */; }; - 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */; }; - 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */; }; - 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */; }; - 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41F5CDE72B29356F00F7FF87 /* Messages.framework */; }; - 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */; }; - 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */; }; - 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 41F5CDBB2B2931AF00F7FF87 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 41F5CDE52B29356F00F7FF87; - remoteInfo = aesthetic; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */, - ); - name = "Embed Foundation Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - AC1250000000000000000002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - AC1250000000000000000004 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = wrapper.plug-in; path = Settings.bundle; sourceTree = ""; }; - 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "aesthetic-computer-Info.plist"; sourceTree = SOURCE_ROOT; }; - 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = aesthetic.computer.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = aesthetic_computerApp.swift; sourceTree = ""; }; - AC1A0C012E9A000000000002 /* LaunchPing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPing.swift; sourceTree = ""; }; - 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = aesthetic.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 41F5CDE72B29356F00F7FF87 /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = System/Library/Frameworks/Messages.framework; sourceTree = SDKROOT; }; - 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; - 41F5CDED2B29356F00F7FF87 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 41F5CDF12B29356F00F7FF87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "aesthetic.computer-release.entitlements"; sourceTree = ""; }; - 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = aesthetic.computer.entitlements; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 41F5CDC02B2931AF00F7FF87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 41F5CDE32B29356F00F7FF87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 41F5CDBA2B2931AF00F7FF87 = { - isa = PBXGroup; - children = ( - 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */, - 41F5CDE92B29356F00F7FF87 /* aesthetic */, - 41F5CDC42B2931AF00F7FF87 /* Products */, - 41F5CDDF2B29348800F7FF87 /* Frameworks */, - ); - sourceTree = ""; - }; - 41F5CDC42B2931AF00F7FF87 /* Products */ = { - isa = PBXGroup; - children = ( - 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */, - 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */, - ); - name = Products; - sourceTree = ""; - }; - 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */ = { - isa = PBXGroup; - children = ( - 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */, - 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */, - 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */, - 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */, - 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */, - AC1A0C012E9A000000000002 /* LaunchPing.swift */, - AC1250000000000000000002 /* PrivacyInfo.xcprivacy */, - AC1250000000000000000004 /* Settings.bundle */, - 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */, - 41F5CDCC2B2931B000F7FF87 /* Preview Content */, - ); - path = aesthetic.computer; - sourceTree = ""; - }; - 41F5CDCC2B2931B000F7FF87 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 41F5CDDF2B29348800F7FF87 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 41F5CDE72B29356F00F7FF87 /* Messages.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 41F5CDE92B29356F00F7FF87 /* aesthetic */ = { - isa = PBXGroup; - children = ( - 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */, - 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */, - 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */, - 41F5CDF12B29356F00F7FF87 /* Info.plist */, - ); - path = aesthetic; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */ = { - isa = PBXNativeTarget; - buildConfigurationList = 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */; - buildPhases = ( - 41F5CDBF2B2931AF00F7FF87 /* Sources */, - 41F5CDC02B2931AF00F7FF87 /* Frameworks */, - 41F5CDC12B2931AF00F7FF87 /* Resources */, - 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */, - ); - buildRules = ( - ); - dependencies = ( - 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */, - ); - name = aesthetic.computer; - packageProductDependencies = ( - ); - productName = aesthetic.computer; - productReference = 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */; - productType = "com.apple.product-type.application"; - }; - 41F5CDE52B29356F00F7FF87 /* aesthetic */ = { - isa = PBXNativeTarget; - buildConfigurationList = 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */; - buildPhases = ( - 41F5CDE22B29356F00F7FF87 /* Sources */, - 41F5CDE32B29356F00F7FF87 /* Frameworks */, - 41F5CDE42B29356F00F7FF87 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = aesthetic; - productName = aesthetic; - productReference = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; - productType = "com.apple.product-type.app-extension.messages"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 41F5CDBB2B2931AF00F7FF87 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1510; - LastUpgradeCheck = 1510; - TargetAttributes = { - 41F5CDC22B2931AF00F7FF87 = { - CreatedOnToolsVersion = 15.1; - }; - 41F5CDE52B29356F00F7FF87 = { - CreatedOnToolsVersion = 15.1; - }; - }; - }; - buildConfigurationList = 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 41F5CDBA2B2931AF00F7FF87; - packageReferences = ( - ); - productRefGroup = 41F5CDC42B2931AF00F7FF87 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */, - 41F5CDE52B29356F00F7FF87 /* aesthetic */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 41F5CDC12B2931AF00F7FF87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */, - 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */, - AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */, - AC1250000000000000000003 /* Settings.bundle in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 41F5CDE42B29356F00F7FF87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */, - 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 41F5CDBF2B2931AF00F7FF87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */, - 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */, - AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 41F5CDE22B29356F00F7FF87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 41F5CDE52B29356F00F7FF87 /* aesthetic */; - targetProxy = 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 41F5CDED2B29356F00F7FF87 /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 41F5CDCF2B2931B000F7FF87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 41F5CDD02B2931B000F7FF87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 41F5CDD22B2931B000F7FF87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - "CODE_SIGN_ENTITLEMENTS[sdk=*]" = aesthetic.computer/aesthetic.computer.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; - DEVELOPMENT_TEAM = FB5948YR3S; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "aesthetic-computer-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 41F5CDD32B2931B000F7FF87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = "aesthetic.computer/aesthetic.computer-release.entitlements"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; - DEVELOPMENT_TEAM = FB5948YR3S; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "aesthetic-computer-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 41F5CDF62B29357000F7FF87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_TEAM = FB5948YR3S; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = aesthetic/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 41F5CDF72B29357000F7FF87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_TEAM = FB5948YR3S; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = aesthetic/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 41F5CDCF2B2931B000F7FF87 /* Debug */, - 41F5CDD02B2931B000F7FF87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 41F5CDD22B2931B000F7FF87 /* Debug */, - 41F5CDD32B2931B000F7FF87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 41F5CDF62B29357000F7FF87 /* Debug */, - 41F5CDF72B29357000F7FF87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - - }; - rootObject = 41F5CDBB2B2931AF00F7FF87 /* Project object */; -} +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000002 /* PrivacyInfo.xcprivacy */; }; + AC1250000000000000000003 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000004 /* Settings.bundle */; }; + 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */; }; + AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1A0C012E9A000000000002 /* LaunchPing.swift */; }; + AC1A0C012E9A000000000003 /* PopPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1A0C012E9A000000000004 /* PopPlayer.swift */; }; + 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */; }; + 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */; }; + 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */; }; + 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41F5CDE72B29356F00F7FF87 /* Messages.framework */; }; + 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */; }; + 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */; }; + 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 41F5CDBB2B2931AF00F7FF87 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 41F5CDE52B29356F00F7FF87; + remoteInfo = aesthetic; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + AC1250000000000000000002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + AC1250000000000000000004 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = wrapper.plug-in; path = Settings.bundle; sourceTree = ""; }; + 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "aesthetic-computer-Info.plist"; sourceTree = SOURCE_ROOT; }; + 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = aesthetic.computer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = aesthetic_computerApp.swift; sourceTree = ""; }; + AC1A0C012E9A000000000002 /* LaunchPing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPing.swift; sourceTree = ""; }; + AC1A0C012E9A000000000004 /* PopPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopPlayer.swift; sourceTree = ""; }; + 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = aesthetic.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 41F5CDE72B29356F00F7FF87 /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = System/Library/Frameworks/Messages.framework; sourceTree = SDKROOT; }; + 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; + 41F5CDED2B29356F00F7FF87 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 41F5CDF12B29356F00F7FF87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "aesthetic.computer-release.entitlements"; sourceTree = ""; }; + 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = aesthetic.computer.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 41F5CDC02B2931AF00F7FF87 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 41F5CDE32B29356F00F7FF87 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 41F5CDBA2B2931AF00F7FF87 = { + isa = PBXGroup; + children = ( + 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */, + 41F5CDE92B29356F00F7FF87 /* aesthetic */, + 41F5CDC42B2931AF00F7FF87 /* Products */, + 41F5CDDF2B29348800F7FF87 /* Frameworks */, + ); + sourceTree = ""; + }; + 41F5CDC42B2931AF00F7FF87 /* Products */ = { + isa = PBXGroup; + children = ( + 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */, + 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */, + ); + name = Products; + sourceTree = ""; + }; + 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */ = { + isa = PBXGroup; + children = ( + 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */, + 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */, + 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */, + 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */, + 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */, + AC1A0C012E9A000000000002 /* LaunchPing.swift */, + AC1A0C012E9A000000000004 /* PopPlayer.swift */, + AC1250000000000000000002 /* PrivacyInfo.xcprivacy */, + AC1250000000000000000004 /* Settings.bundle */, + 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */, + 41F5CDCC2B2931B000F7FF87 /* Preview Content */, + ); + path = aesthetic.computer; + sourceTree = ""; + }; + 41F5CDCC2B2931B000F7FF87 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 41F5CDDF2B29348800F7FF87 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 41F5CDE72B29356F00F7FF87 /* Messages.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 41F5CDE92B29356F00F7FF87 /* aesthetic */ = { + isa = PBXGroup; + children = ( + 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */, + 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */, + 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */, + 41F5CDF12B29356F00F7FF87 /* Info.plist */, + ); + path = aesthetic; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */; + buildPhases = ( + 41F5CDBF2B2931AF00F7FF87 /* Sources */, + 41F5CDC02B2931AF00F7FF87 /* Frameworks */, + 41F5CDC12B2931AF00F7FF87 /* Resources */, + 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */, + ); + name = aesthetic.computer; + packageProductDependencies = ( + ); + productName = aesthetic.computer; + productReference = 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */; + productType = "com.apple.product-type.application"; + }; + 41F5CDE52B29356F00F7FF87 /* aesthetic */ = { + isa = PBXNativeTarget; + buildConfigurationList = 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */; + buildPhases = ( + 41F5CDE22B29356F00F7FF87 /* Sources */, + 41F5CDE32B29356F00F7FF87 /* Frameworks */, + 41F5CDE42B29356F00F7FF87 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = aesthetic; + productName = aesthetic; + productReference = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; + productType = "com.apple.product-type.app-extension.messages"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 41F5CDBB2B2931AF00F7FF87 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + 41F5CDC22B2931AF00F7FF87 = { + CreatedOnToolsVersion = 15.1; + }; + 41F5CDE52B29356F00F7FF87 = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 41F5CDBA2B2931AF00F7FF87; + packageReferences = ( + ); + productRefGroup = 41F5CDC42B2931AF00F7FF87 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */, + 41F5CDE52B29356F00F7FF87 /* aesthetic */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 41F5CDC12B2931AF00F7FF87 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */, + 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */, + AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */, + AC1250000000000000000003 /* Settings.bundle in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 41F5CDE42B29356F00F7FF87 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */, + 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 41F5CDBF2B2931AF00F7FF87 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */, + 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */, + AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */, + AC1A0C012E9A000000000003 /* PopPlayer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 41F5CDE22B29356F00F7FF87 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 41F5CDE52B29356F00F7FF87 /* aesthetic */; + targetProxy = 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 41F5CDED2B29356F00F7FF87 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 41F5CDCF2B2931B000F7FF87 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 41F5CDD02B2931B000F7FF87 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 41F5CDD22B2931B000F7FF87 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_ENTITLEMENTS[sdk=*]" = aesthetic.computer/aesthetic.computer.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; + DEVELOPMENT_TEAM = FB5948YR3S; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "aesthetic-computer-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 41F5CDD32B2931B000F7FF87 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "aesthetic.computer/aesthetic.computer-release.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; + DEVELOPMENT_TEAM = FB5948YR3S; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "aesthetic-computer-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 41F5CDF62B29357000F7FF87 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_TEAM = FB5948YR3S; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = aesthetic/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 41F5CDF72B29357000F7FF87 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_TEAM = FB5948YR3S; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = aesthetic/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 41F5CDCF2B2931B000F7FF87 /* Debug */, + 41F5CDD02B2931B000F7FF87 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 41F5CDD22B2931B000F7FF87 /* Debug */, + 41F5CDD32B2931B000F7FF87 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 41F5CDF62B29357000F7FF87 /* Debug */, + 41F5CDF72B29357000F7FF87 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = 41F5CDBB2B2931AF00F7FF87 /* Project object */; +} diff --git a/apple/aesthetic.computer/ContentView.swift b/apple/aesthetic.computer/ContentView.swift index e5875a84ad..fb4611638c 100644 --- a/apple/aesthetic.computer/ContentView.swift +++ b/apple/aesthetic.computer/ContentView.swift @@ -222,6 +222,11 @@ class Coordinator: NSObject, WKScriptMessageHandler, WKUIDelegate, WKNavigationD if enabled { AppDelegate.shared?.triggerSubscribe(in: webView) } else { AppDelegate.shared?.triggerUnsubscribe(in: webView) } } + case "pop:open": + guard message.frameInfo.isMainFrame, message.frameInfo.securityOrigin.protocol == "https", + message.frameInfo.securityOrigin.host == "aesthetic.computer" else { return } + let slug = (dictionary["body"] as? String).flatMap { $0.isEmpty ? nil : $0 } + Task { @MainActor in PopPlayer.shared.open(slug: slug) } case "url": if let urlString = dictionary["body"] as? String, let url = URL(string: urlString) { UIApplication.shared.open(url, options: [:], completionHandler: nil) @@ -384,6 +389,7 @@ struct StalledOverlay: View { struct ContentView: View { @StateObject private var monitor = AppNetworkMonitor() @StateObject private var bootStatus = BootStatus() + @ObservedObject private var pop = PopPlayer.shared @Environment(\.scenePhase) private var scenePhase @State private var lastBackgroundedAt: Date? = nil @State private var lastForceLiveTrigger: Int = 0 @@ -428,6 +434,7 @@ struct ContentView: View { .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 24 : 0) .background(Color(red: grey, green: grey, blue: grey)) .ignoresSafeArea(.keyboard, edges: .bottom) + .sheet(isPresented: $pop.isPresented) { PopPlayerView() } .onAppear { handleScenePhase(scenePhase) } .onDisappear { LaunchPing.remove(scene: usageScene) } .onChange(of: scenePhase) { newPhase in diff --git a/apple/aesthetic.computer/PopPlayer.swift b/apple/aesthetic.computer/PopPlayer.swift new file mode 100644 index 0000000000..c868ea0ab3 --- /dev/null +++ b/apple/aesthetic.computer/PopPlayer.swift @@ -0,0 +1,419 @@ +import SwiftUI +import AVFoundation +import AVKit +import MediaPlayer + +// MARK: - /pop player +// +// Plays every released /pop track natively, so music keeps going on the lock +// screen, in Control Center and over AirPlay after the web runtime sleeps. +// The catalog is the one pop.aesthetic.computer renders +// (pop/bin/publish-release-records.mjs); the last good copy is kept for +// launches without a network. Opened from the prompt with `pop` / `pop ` +// through the iOSApp bridge ("pop:open"). + +struct PopTrack: Decodable, Identifiable, Equatable { + let slug: String + let title: String + let artist: String + let album: String? + let duration: Double? + let cover: URL? + let audio: URL + var id: String { slug } +} + +private struct PopCatalog: Decodable { + let tracks: [PopTrack] +} + +@MainActor +final class PopPlayer: ObservableObject { + static let shared = PopPlayer() + + @Published var isPresented = false + @Published private(set) var tracks: [PopTrack] = [] + @Published private(set) var index: Int? = nil + @Published private(set) var isPlaying = false + @Published private(set) var elapsed: Double = 0 + @Published private(set) var duration: Double = 0 + @Published private(set) var loadError: String? = nil + @Published private(set) var artwork: [String: UIImage] = [:] + + var current: PopTrack? { index.map { tracks[$0] } } + + private let catalogURL = URL(string: "https://pop.aesthetic.computer/releases/catalog.json")! + private let catalogKey = "acPopCatalog" + private let player = AVPlayer() + private var timeObserver: Any? + private var endObserver: NSObjectProtocol? + private var pendingSlug: String? + private var loading = false + + private init() { + if let data = UserDefaults.standard.data(forKey: catalogKey), + let catalog = try? JSONDecoder().decode(PopCatalog.self, from: data) { + tracks = catalog.tracks + } + timeObserver = player.addPeriodicTimeObserver( + forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: .main + ) { [weak self] time in + MainActor.assumeIsolated { self?.tick(time) } + } + NotificationCenter.default.addObserver( + forName: AVAudioSession.interruptionNotification, object: nil, queue: .main + ) { [weak self] note in + let info = note.userInfo + MainActor.assumeIsolated { self?.interrupted(info) } + } + registerRemoteCommands() + } + + // MARK: opening + + func open(slug: String?) { + isPresented = true + pendingSlug = slug + if !tracks.isEmpty { playPending() } + refresh() + } + + func refresh() { + guard !loading else { return } + loading = true + Task { + defer { loading = false } + do { + let request = URLRequest(url: catalogURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 20) + let (data, _) = try await URLSession.shared.data(for: request) + let catalog = try JSONDecoder().decode(PopCatalog.self, from: data) + UserDefaults.standard.set(data, forKey: catalogKey) + let playing = current?.slug + tracks = catalog.tracks + index = playing.flatMap { slug in tracks.firstIndex { $0.slug == slug } } + loadError = nil + } catch { + if tracks.isEmpty { loadError = "Couldn’t load /pop. Check your connection." } + } + playPending() + } + } + + private func playPending() { + guard let slug = pendingSlug else { return } + pendingSlug = nil + if let i = tracks.firstIndex(where: { $0.slug == slug }) { play(at: i) } + } + + // MARK: transport + + func play(at i: Int) { + guard tracks.indices.contains(i) else { return } + let session = AVAudioSession.sharedInstance() + try? session.setCategory(.playback, mode: .default) + try? session.setActive(true) + + index = i + elapsed = 0 + duration = tracks[i].duration ?? 0 + let item = AVPlayerItem(url: tracks[i].audio) + if let endObserver { NotificationCenter.default.removeObserver(endObserver) } + endObserver = NotificationCenter.default.addObserver( + forName: .AVPlayerItemDidPlayToEndTime, object: item, queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { self?.next() } + } + player.replaceCurrentItem(with: item) + player.play() + isPlaying = true + updateNowPlaying() + loadArtwork(for: tracks[i]) + } + + func toggle() { + guard index != nil else { if !tracks.isEmpty { play(at: 0) }; return } + isPlaying ? pause() : resume() + } + + func resume() { + guard index != nil else { return } + try? AVAudioSession.sharedInstance().setActive(true) + player.play() + isPlaying = true + updateNowPlaying() + } + + func pause() { + player.pause() + isPlaying = false + updateNowPlaying() + } + + // The whole catalog loops: after the last track comes the first. + func next() { + guard !tracks.isEmpty else { return } + play(at: ((index ?? -1) + 1) % tracks.count) + } + + // Like a CD player: past three seconds, previous restarts the track. + func previous() { + guard !tracks.isEmpty else { return } + if elapsed > 3 { seek(to: 0); return } + play(at: ((index ?? 0) - 1 + tracks.count) % tracks.count) + } + + func seek(to seconds: Double) { + elapsed = seconds + player.seek(to: CMTime(seconds: seconds, preferredTimescale: 600)) { [weak self] _ in + Task { @MainActor in self?.updateNowPlaying() } + } + } + + private func tick(_ time: CMTime) { + elapsed = time.seconds.isFinite ? time.seconds : 0 + if let seconds = player.currentItem?.duration.seconds, seconds.isFinite, seconds > 0, + abs(seconds - duration) > 0.5 { + duration = seconds + updateNowPlaying() + } + } + + private func interrupted(_ info: [AnyHashable: Any]?) { + guard let raw = info?[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: raw) else { return } + switch type { + case .began: + isPlaying = false + updateNowPlaying() + case .ended: + let options = (info?[AVAudioSessionInterruptionOptionKey] as? UInt) + .map(AVAudioSession.InterruptionOptions.init) ?? [] + if options.contains(.shouldResume) { resume() } + @unknown default: + break + } + } + + // MARK: lock screen + Control Center + + private func registerRemoteCommands() { + let center = MPRemoteCommandCenter.shared() + center.playCommand.addTarget { [weak self] _ in + Task { @MainActor in self?.resume() } + return .success + } + center.pauseCommand.addTarget { [weak self] _ in + Task { @MainActor in self?.pause() } + return .success + } + center.togglePlayPauseCommand.addTarget { [weak self] _ in + Task { @MainActor in self?.toggle() } + return .success + } + center.nextTrackCommand.addTarget { [weak self] _ in + Task { @MainActor in self?.next() } + return .success + } + center.previousTrackCommand.addTarget { [weak self] _ in + Task { @MainActor in self?.previous() } + return .success + } + center.changePlaybackPositionCommand.addTarget { [weak self] event in + guard let event = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed } + let target = event.positionTime + Task { @MainActor in self?.seek(to: target) } + return .success + } + } + + private func updateNowPlaying() { + guard let track = current else { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + return + } + var info: [String: Any] = [ + MPMediaItemPropertyTitle: track.title, + MPMediaItemPropertyArtist: track.artist, + MPMediaItemPropertyPlaybackDuration: duration, + MPNowPlayingInfoPropertyElapsedPlaybackTime: elapsed, + MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, + MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue, + ] + if let album = track.album { info[MPMediaItemPropertyAlbumTitle] = album } + if let image = artwork[track.slug] { + info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + } + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + + func loadArtwork(for track: PopTrack) { + guard artwork[track.slug] == nil, let url = track.cover else { return } + Task { + guard let (data, _) = try? await URLSession.shared.data(from: url), + let image = UIImage(data: data) else { return } + artwork[track.slug] = image + if current == track { updateNowPlaying() } + } + } +} + +// MARK: - Views + +private let popGrey = Color(red: grey, green: grey, blue: grey) +private let popInk = Color(red: 1, green: 240/255, blue: 200/255) + +private func timecode(_ seconds: Double) -> String { + let s = max(0, Int(seconds.rounded(.down))) + return String(format: "%d:%02d", s / 60, s % 60) +} + +struct AirPlayButton: UIViewRepresentable { + func makeUIView(context: Context) -> AVRoutePickerView { + let view = AVRoutePickerView() + view.tintColor = .lightGray + view.activeTintColor = .systemYellow + return view + } + func updateUIView(_ view: AVRoutePickerView, context: Context) {} +} + +struct PopCover: View { + @ObservedObject var player: PopPlayer + let track: PopTrack + let size: CGFloat + + var body: some View { + ZStack { + Color.black.opacity(0.4) + if let image = player.artwork[track.slug] { + Image(uiImage: image).resizable().scaledToFill() + } + } + .frame(width: size, height: size) + .clipped() + .onAppear { player.loadArtwork(for: track) } + } +} + +struct PopPlayerView: View { + @ObservedObject var player = PopPlayer.shared + @State private var scrub: Double? = nil + + var body: some View { + VStack(spacing: 0) { + HStack { + Text("pop") + .font(.system(size: 20, weight: .bold, design: .monospaced)) + .foregroundColor(popInk) + Spacer() + AirPlayButton().frame(width: 36, height: 36) + Button { player.isPresented = false } label: { + Image(systemName: "chevron.down") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + .frame(width: 44, height: 44) + } + .accessibilityLabel("Close") + } + .padding(.horizontal, 16) + .padding(.top, 8) + + if let error = player.loadError, player.tracks.isEmpty { + Spacer() + Text(error).foregroundColor(.white).multilineTextAlignment(.center).padding() + Button("Retry") { player.refresh() }.foregroundColor(.yellow) + Spacer() + } else { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(Array(player.tracks.enumerated()), id: \.element.id) { i, track in + Button { player.play(at: i) } label: { row(track, playing: player.index == i) } + .buttonStyle(.plain) + } + } + } + } + + if let track = player.current { nowPlaying(track) } + } + .background(popGrey.ignoresSafeArea()) + .onAppear { if player.tracks.isEmpty { player.refresh() } } + } + + private func row(_ track: PopTrack, playing: Bool) -> some View { + HStack(spacing: 12) { + PopCover(player: player, track: track, size: 52) + VStack(alignment: .leading, spacing: 3) { + Text(track.title) + .font(.system(size: 17, weight: .semibold, design: .monospaced)) + .foregroundColor(playing ? .yellow : .white) + Text(track.artist) + .font(.system(size: 14, design: .monospaced)) + .foregroundColor(.gray) + } + .lineLimit(1) + Spacer() + if let duration = track.duration { + Text(timecode(duration)) + .font(.system(size: 14, design: .monospaced)) + .foregroundColor(.gray) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(playing ? Color.white.opacity(0.06) : Color.clear) + .contentShape(Rectangle()) + } + + private func nowPlaying(_ track: PopTrack) -> some View { + VStack(spacing: 10) { + HStack(spacing: 12) { + PopCover(player: player, track: track, size: 64) + VStack(alignment: .leading, spacing: 3) { + Text(track.title) + .font(.system(size: 18, weight: .bold, design: .monospaced)) + .foregroundColor(popInk) + Text(track.artist) + .font(.system(size: 14, design: .monospaced)) + .foregroundColor(.gray) + } + .lineLimit(1) + Spacer() + } + Slider( + value: Binding(get: { scrub ?? player.elapsed }, set: { scrub = $0 }), + in: 0...max(player.duration, 1), + onEditingChanged: { editing in + if !editing, let target = scrub { player.seek(to: target); scrub = nil } + } + ) + .accentColor(.yellow) + HStack { + Text(timecode(scrub ?? player.elapsed)) + Spacer() + Text("-" + timecode(player.duration - (scrub ?? player.elapsed))) + } + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(.gray) + HStack(spacing: 48) { + transport("backward.fill", "Previous") { player.previous() } + transport(player.isPlaying ? "pause.fill" : "play.fill", player.isPlaying ? "Pause" : "Play", size: 34) { + player.toggle() + } + transport("forward.fill", "Next") { player.next() } + } + } + .padding(16) + .background(Color.black.opacity(0.35).ignoresSafeArea(edges: .bottom)) + } + + private func transport(_ symbol: String, _ label: String, size: CGFloat = 24, action: @escaping () -> Void) -> some View { + Button(action: action) { + Image(systemName: symbol) + .font(.system(size: size)) + .foregroundColor(.white) + .frame(width: 56, height: 56) + } + .accessibilityLabel(label) + } +} diff --git a/system/public/aesthetic.computer/disks/pop/README.md b/system/public/aesthetic.computer/disks/pop/README.md index 47fcee01f3..2565d364c4 100644 --- a/system/public/aesthetic.computer/disks/pop/README.md +++ b/system/public/aesthetic.computer/disks/pop/README.md @@ -82,3 +82,10 @@ reading `out/.struct.json` + the campaign gens dir. `import` a sibling disk to share a wrapper: the loader hands modules a non-hierarchical base and `./other.mjs` fails to resolve, so the two names are two full wrappers over one manifest.) +- `sailor-song` ✓ (sage oc's cover, artist `sage oc`. Its section stills are + frames of the relit performance video, cropped above the caption row.) + +The AC iPhone app plays the whole catalog natively: `pop` (or `pop `) +at the prompt opens `apple/aesthetic.computer/PopPlayer.swift`, which reads +`pop.aesthetic.computer/releases/catalog.json`. A track reaches it once +`node pop/bin/publish-release-records.mjs` has been re-run with its manifest. diff --git a/system/public/aesthetic.computer/disks/pop/sailor-song.json b/system/public/aesthetic.computer/disks/pop/sailor-song.json new file mode 100644 index 0000000000..2cc6f67651 --- /dev/null +++ b/system/public/aesthetic.computer/disks/pop/sailor-song.json @@ -0,0 +1,64 @@ +{ + "slug": "sailor-song", + "title": "sailor song", + "artist": "sage oc", + "album": "sailor song", + "bpm": 120, + "key": "G# minor", + "meter": "4/4", + "duration": 152.53, + "audio": "/assets/pop/sailor-song.mp3", + "cover": "/assets/pop/sailor-song.jpg", + "sections": [ + { + "name": "strums", + "t": 0, + "illy": "/assets/pop/sailor-song/sec-0.jpg", + "code": "a" + }, + { + "name": "verse 1", + "t": 2.105, + "illy": "/assets/pop/sailor-song/sec-1.jpg", + "code": "b" + }, + { + "name": "chorus 1", + "t": 38.444, + "illy": "/assets/pop/sailor-song/sec-2.jpg", + "code": "c" + }, + { + "name": "verse 2", + "t": 69.124, + "illy": "/assets/pop/sailor-song/sec-3.jpg", + "code": "d" + }, + { + "name": "chorus 2", + "t": 84.898, + "illy": "/assets/pop/sailor-song/sec-4.jpg", + "code": "e" + }, + { + "name": "break", + "t": 115.313, + "illy": "/assets/pop/sailor-song/sec-5.jpg", + "code": "f" + }, + { + "name": "bridge", + "t": 124.783, + "illy": "/assets/pop/sailor-song/sec-6.jpg", + "code": "g" + }, + { + "name": "outro", + "t": 139.914, + "illy": "/assets/pop/sailor-song/sec-7.jpg", + "code": "h" + } + ], + "links": {}, + "credits": "sage oc — vocals + nylon guitar. song by Gigi Perez (\"Sailor Song\"). produced, engineered + arranged under her take by Aesthetic Dot Computer." +} diff --git a/system/public/aesthetic.computer/disks/prompt.mjs b/system/public/aesthetic.computer/disks/prompt.mjs index 3446519c5d..bb9058e1da 100644 --- a/system/public/aesthetic.computer/disks/prompt.mjs +++ b/system/public/aesthetic.computer/disks/prompt.mjs @@ -79,7 +79,7 @@ const forgetful = true; const BARE_KIDLISP_CODE = /^[0-9A-Za-z]{3,64}$/; -import { Android, MacOS, MetaBrowser, iOS } from "../lib/platform.mjs"; +import { AestheticIOSApp, Android, MacOS, MetaBrowser, iOS } from "../lib/platform.mjs"; import { validateHandle } from "../lib/text.mjs"; import { commandDescriptions } from "../lib/prompt-commands.mjs"; import { nopaint_adjust } from "../systems/nopaint.mjs"; @@ -2459,6 +2459,23 @@ async function halt($, text) { flashColor = [0, 0, 255]; makeFlash($); return true; + } else if (slug === "pop") { + // 🎵 Every /pop track. The iPhone app plays them natively (lock screen, + // AirPlay, background); elsewhere `pop ` opens that track's piece. + if (AestheticIOSApp) { + send({ type: "ios:send", content: { type: "pop:open", body: params[0] || "" } }); + flashColor = [255, 200, 0]; + makeFlash($); + return true; + } + if (params[0]) { + jump(params[0]); + return true; + } + const popUrl = "https://pop.aesthetic.computer"; + if (openExternalFromIframe(popUrl)) return true; + jump(`out:${popUrl}`); + return true; } else if (slug === "selfie") { jump("camera~me"); return true; diff --git a/system/public/aesthetic.computer/disks/sailor-song.mjs b/system/public/aesthetic.computer/disks/sailor-song.mjs new file mode 100644 index 0000000000..daf5abc00c --- /dev/null +++ b/system/public/aesthetic.computer/disks/sailor-song.mjs @@ -0,0 +1,28 @@ +// sailor-song, 2026.10.06 +// pop/sailor-song/ — sage oc's Sailor Song (Gigi Perez) cover, released on AC — see pop/RELEASES.md. +// Thin wrapper around lib/pop.mjs; see disks/pop/README.md for the recipe. + +import * as pop from "../lib/pop.mjs"; + +const MANIFEST_URL = "/aesthetic.computer/disks/pop/sailor-song.json"; +let manifest = null; + +async function boot($) { + if (!manifest) { + manifest = await fetch(MANIFEST_URL).then((r) => r.json()); + } + return pop.boot($, manifest); +} + +function paint($) { return pop.paint($); } +function sim($) { return pop.sim($); } +function act($) { return pop.act($); } +function leave($) { return pop.leave($); } +function receive($) { return pop.receive($); } // stream:* events from bios +function meta() { + return manifest + ? pop.meta(manifest) + : { title: "sailor song — sage oc", desc: "sage oc sings Gigi Perez's Sailor Song; produced by Aesthetic Dot Computer." }; +} + +export { boot, paint, sim, act, receive, leave, meta }; diff --git a/system/public/pop.aesthetic.computer/releases/catalog.json b/system/public/pop.aesthetic.computer/releases/catalog.json index 5fe4de9c8b..baec3472a3 100644 --- a/system/public/pop.aesthetic.computer/releases/catalog.json +++ b/system/public/pop.aesthetic.computer/releases/catalog.json @@ -120,6 +120,22 @@ "credits": "marimba lullaby for the pixsies body. all instruments + composition: aesthetic dot computer.", "releaseData": "/releases/marimbaba" }, + { + "slug": "sailor-song", + "title": "sailor song", + "artist": "sage oc", + "album": "sailor song", + "bpm": 120, + "key": "G# minor", + "meter": "4/4", + "duration": 152.53, + "cover": "https://assets.aesthetic.computer/pop/sailor-song.jpg", + "audio": "https://assets.aesthetic.computer/pop/sailor-song.mp3", + "piece": "https://aesthetic.computer/sailor-song", + "links": {}, + "credits": "sage oc — vocals + nylon guitar. song by Gigi Perez (\"Sailor Song\"). produced, engineered + arranged under her take by Aesthetic Dot Computer.", + "releaseData": null + }, { "slug": "trancenwaltz", "title": "trancenwaltz", -- 2.51.2 From 6ee4459e1374cc09fba2a1e77ee0c6668b8fd33b Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 10:36:08 -0700 Subject: [PATCH 03/21] Restore CRLF line endings in the iPhone app project file --- .../project.pbxproj | 1150 ++++++++--------- 1 file changed, 575 insertions(+), 575 deletions(-) diff --git a/apple/aesthetic.computer.xcodeproj/project.pbxproj b/apple/aesthetic.computer.xcodeproj/project.pbxproj index 6b355d5d06..6d4f44458a 100644 --- a/apple/aesthetic.computer.xcodeproj/project.pbxproj +++ b/apple/aesthetic.computer.xcodeproj/project.pbxproj @@ -1,575 +1,575 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000002 /* PrivacyInfo.xcprivacy */; }; - AC1250000000000000000003 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000004 /* Settings.bundle */; }; - 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */; }; - AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1A0C012E9A000000000002 /* LaunchPing.swift */; }; - AC1A0C012E9A000000000003 /* PopPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1A0C012E9A000000000004 /* PopPlayer.swift */; }; - 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */; }; - 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */; }; - 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */; }; - 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41F5CDE72B29356F00F7FF87 /* Messages.framework */; }; - 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */; }; - 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */; }; - 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 41F5CDBB2B2931AF00F7FF87 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 41F5CDE52B29356F00F7FF87; - remoteInfo = aesthetic; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */, - ); - name = "Embed Foundation Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - AC1250000000000000000002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - AC1250000000000000000004 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = wrapper.plug-in; path = Settings.bundle; sourceTree = ""; }; - 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "aesthetic-computer-Info.plist"; sourceTree = SOURCE_ROOT; }; - 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = aesthetic.computer.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = aesthetic_computerApp.swift; sourceTree = ""; }; - AC1A0C012E9A000000000002 /* LaunchPing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPing.swift; sourceTree = ""; }; - AC1A0C012E9A000000000004 /* PopPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopPlayer.swift; sourceTree = ""; }; - 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = aesthetic.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 41F5CDE72B29356F00F7FF87 /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = System/Library/Frameworks/Messages.framework; sourceTree = SDKROOT; }; - 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; - 41F5CDED2B29356F00F7FF87 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 41F5CDF12B29356F00F7FF87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "aesthetic.computer-release.entitlements"; sourceTree = ""; }; - 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = aesthetic.computer.entitlements; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 41F5CDC02B2931AF00F7FF87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 41F5CDE32B29356F00F7FF87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 41F5CDBA2B2931AF00F7FF87 = { - isa = PBXGroup; - children = ( - 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */, - 41F5CDE92B29356F00F7FF87 /* aesthetic */, - 41F5CDC42B2931AF00F7FF87 /* Products */, - 41F5CDDF2B29348800F7FF87 /* Frameworks */, - ); - sourceTree = ""; - }; - 41F5CDC42B2931AF00F7FF87 /* Products */ = { - isa = PBXGroup; - children = ( - 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */, - 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */, - ); - name = Products; - sourceTree = ""; - }; - 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */ = { - isa = PBXGroup; - children = ( - 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */, - 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */, - 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */, - 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */, - 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */, - AC1A0C012E9A000000000002 /* LaunchPing.swift */, - AC1A0C012E9A000000000004 /* PopPlayer.swift */, - AC1250000000000000000002 /* PrivacyInfo.xcprivacy */, - AC1250000000000000000004 /* Settings.bundle */, - 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */, - 41F5CDCC2B2931B000F7FF87 /* Preview Content */, - ); - path = aesthetic.computer; - sourceTree = ""; - }; - 41F5CDCC2B2931B000F7FF87 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 41F5CDDF2B29348800F7FF87 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 41F5CDE72B29356F00F7FF87 /* Messages.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 41F5CDE92B29356F00F7FF87 /* aesthetic */ = { - isa = PBXGroup; - children = ( - 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */, - 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */, - 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */, - 41F5CDF12B29356F00F7FF87 /* Info.plist */, - ); - path = aesthetic; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */ = { - isa = PBXNativeTarget; - buildConfigurationList = 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */; - buildPhases = ( - 41F5CDBF2B2931AF00F7FF87 /* Sources */, - 41F5CDC02B2931AF00F7FF87 /* Frameworks */, - 41F5CDC12B2931AF00F7FF87 /* Resources */, - 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */, - ); - buildRules = ( - ); - dependencies = ( - 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */, - ); - name = aesthetic.computer; - packageProductDependencies = ( - ); - productName = aesthetic.computer; - productReference = 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */; - productType = "com.apple.product-type.application"; - }; - 41F5CDE52B29356F00F7FF87 /* aesthetic */ = { - isa = PBXNativeTarget; - buildConfigurationList = 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */; - buildPhases = ( - 41F5CDE22B29356F00F7FF87 /* Sources */, - 41F5CDE32B29356F00F7FF87 /* Frameworks */, - 41F5CDE42B29356F00F7FF87 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = aesthetic; - productName = aesthetic; - productReference = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; - productType = "com.apple.product-type.app-extension.messages"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 41F5CDBB2B2931AF00F7FF87 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1510; - LastUpgradeCheck = 1510; - TargetAttributes = { - 41F5CDC22B2931AF00F7FF87 = { - CreatedOnToolsVersion = 15.1; - }; - 41F5CDE52B29356F00F7FF87 = { - CreatedOnToolsVersion = 15.1; - }; - }; - }; - buildConfigurationList = 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 41F5CDBA2B2931AF00F7FF87; - packageReferences = ( - ); - productRefGroup = 41F5CDC42B2931AF00F7FF87 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */, - 41F5CDE52B29356F00F7FF87 /* aesthetic */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 41F5CDC12B2931AF00F7FF87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */, - 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */, - AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */, - AC1250000000000000000003 /* Settings.bundle in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 41F5CDE42B29356F00F7FF87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */, - 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 41F5CDBF2B2931AF00F7FF87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */, - 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */, - AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */, - AC1A0C012E9A000000000003 /* PopPlayer.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 41F5CDE22B29356F00F7FF87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 41F5CDE52B29356F00F7FF87 /* aesthetic */; - targetProxy = 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 41F5CDED2B29356F00F7FF87 /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 41F5CDCF2B2931B000F7FF87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 41F5CDD02B2931B000F7FF87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 41F5CDD22B2931B000F7FF87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - "CODE_SIGN_ENTITLEMENTS[sdk=*]" = aesthetic.computer/aesthetic.computer.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; - DEVELOPMENT_TEAM = FB5948YR3S; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "aesthetic-computer-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 41F5CDD32B2931B000F7FF87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = "aesthetic.computer/aesthetic.computer-release.entitlements"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; - DEVELOPMENT_TEAM = FB5948YR3S; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "aesthetic-computer-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 41F5CDF62B29357000F7FF87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_TEAM = FB5948YR3S; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = aesthetic/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 41F5CDF72B29357000F7FF87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; - DEVELOPMENT_TEAM = FB5948YR3S; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = aesthetic/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = aesthetic; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 41F5CDCF2B2931B000F7FF87 /* Debug */, - 41F5CDD02B2931B000F7FF87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 41F5CDD22B2931B000F7FF87 /* Debug */, - 41F5CDD32B2931B000F7FF87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 41F5CDF62B29357000F7FF87 /* Debug */, - 41F5CDF72B29357000F7FF87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - - }; - rootObject = 41F5CDBB2B2931AF00F7FF87 /* Project object */; -} +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000002 /* PrivacyInfo.xcprivacy */; }; + AC1250000000000000000003 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = AC1250000000000000000004 /* Settings.bundle */; }; + 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */; }; + AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1A0C012E9A000000000002 /* LaunchPing.swift */; }; + AC1A0C012E9A000000000003 /* PopPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1A0C012E9A000000000004 /* PopPlayer.swift */; }; + 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */; }; + 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */; }; + 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */; }; + 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41F5CDE72B29356F00F7FF87 /* Messages.framework */; }; + 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */; }; + 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */; }; + 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 41F5CDBB2B2931AF00F7FF87 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 41F5CDE52B29356F00F7FF87; + remoteInfo = aesthetic; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 41F5CDF42B29356F00F7FF87 /* aesthetic.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + AC1250000000000000000002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + AC1250000000000000000004 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = wrapper.plug-in; path = Settings.bundle; sourceTree = ""; }; + 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "aesthetic-computer-Info.plist"; sourceTree = SOURCE_ROOT; }; + 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = aesthetic.computer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = aesthetic_computerApp.swift; sourceTree = ""; }; + AC1A0C012E9A000000000002 /* LaunchPing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPing.swift; sourceTree = ""; }; + AC1A0C012E9A000000000004 /* PopPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopPlayer.swift; sourceTree = ""; }; + 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = aesthetic.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 41F5CDE72B29356F00F7FF87 /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = System/Library/Frameworks/Messages.framework; sourceTree = SDKROOT; }; + 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; + 41F5CDED2B29356F00F7FF87 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 41F5CDF12B29356F00F7FF87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "aesthetic.computer-release.entitlements"; sourceTree = ""; }; + 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = aesthetic.computer.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 41F5CDC02B2931AF00F7FF87 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 41F5CDE32B29356F00F7FF87 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDE82B29356F00F7FF87 /* Messages.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 41F5CDBA2B2931AF00F7FF87 = { + isa = PBXGroup; + children = ( + 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */, + 41F5CDE92B29356F00F7FF87 /* aesthetic */, + 41F5CDC42B2931AF00F7FF87 /* Products */, + 41F5CDDF2B29348800F7FF87 /* Frameworks */, + ); + sourceTree = ""; + }; + 41F5CDC42B2931AF00F7FF87 /* Products */ = { + isa = PBXGroup; + children = ( + 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */, + 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */, + ); + name = Products; + sourceTree = ""; + }; + 41F5CDC52B2931AF00F7FF87 /* aesthetic.computer */ = { + isa = PBXGroup; + children = ( + 41CFB5D12B2A5D8D001954C9 /* aesthetic-computer-Info.plist */, + 41F5CDFB2B2937E300F7FF87 /* aesthetic.computer-release.entitlements */, + 41F5CDFC2B2937E300F7FF87 /* aesthetic.computer.entitlements */, + 41F5CDC62B2931AF00F7FF87 /* aesthetic_computerApp.swift */, + 41F5CDC82B2931AF00F7FF87 /* ContentView.swift */, + AC1A0C012E9A000000000002 /* LaunchPing.swift */, + AC1A0C012E9A000000000004 /* PopPlayer.swift */, + AC1250000000000000000002 /* PrivacyInfo.xcprivacy */, + AC1250000000000000000004 /* Settings.bundle */, + 41F5CDCA2B2931B000F7FF87 /* Assets.xcassets */, + 41F5CDCC2B2931B000F7FF87 /* Preview Content */, + ); + path = aesthetic.computer; + sourceTree = ""; + }; + 41F5CDCC2B2931B000F7FF87 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 41F5CDCD2B2931B000F7FF87 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 41F5CDDF2B29348800F7FF87 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 41F5CDE72B29356F00F7FF87 /* Messages.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 41F5CDE92B29356F00F7FF87 /* aesthetic */ = { + isa = PBXGroup; + children = ( + 41F5CDEA2B29356F00F7FF87 /* MessagesViewController.swift */, + 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */, + 41F5CDEF2B29356F00F7FF87 /* Assets.xcassets */, + 41F5CDF12B29356F00F7FF87 /* Info.plist */, + ); + path = aesthetic; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */; + buildPhases = ( + 41F5CDBF2B2931AF00F7FF87 /* Sources */, + 41F5CDC02B2931AF00F7FF87 /* Frameworks */, + 41F5CDC12B2931AF00F7FF87 /* Resources */, + 41F5CDF82B29357000F7FF87 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */, + ); + name = aesthetic.computer; + packageProductDependencies = ( + ); + productName = aesthetic.computer; + productReference = 41F5CDC32B2931AF00F7FF87 /* aesthetic.computer.app */; + productType = "com.apple.product-type.application"; + }; + 41F5CDE52B29356F00F7FF87 /* aesthetic */ = { + isa = PBXNativeTarget; + buildConfigurationList = 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */; + buildPhases = ( + 41F5CDE22B29356F00F7FF87 /* Sources */, + 41F5CDE32B29356F00F7FF87 /* Frameworks */, + 41F5CDE42B29356F00F7FF87 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = aesthetic; + productName = aesthetic; + productReference = 41F5CDE62B29356F00F7FF87 /* aesthetic.appex */; + productType = "com.apple.product-type.app-extension.messages"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 41F5CDBB2B2931AF00F7FF87 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + 41F5CDC22B2931AF00F7FF87 = { + CreatedOnToolsVersion = 15.1; + }; + 41F5CDE52B29356F00F7FF87 = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 41F5CDBA2B2931AF00F7FF87; + packageReferences = ( + ); + productRefGroup = 41F5CDC42B2931AF00F7FF87 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 41F5CDC22B2931AF00F7FF87 /* aesthetic.computer */, + 41F5CDE52B29356F00F7FF87 /* aesthetic */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 41F5CDC12B2931AF00F7FF87 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDCE2B2931B000F7FF87 /* Preview Assets.xcassets in Resources */, + 41F5CDCB2B2931B000F7FF87 /* Assets.xcassets in Resources */, + AC1250000000000000000001 /* PrivacyInfo.xcprivacy in Resources */, + AC1250000000000000000003 /* Settings.bundle in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 41F5CDE42B29356F00F7FF87 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDFD2B293C7B00F7FF87 /* Assets.xcassets in Resources */, + 41F5CDEE2B29356F00F7FF87 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 41F5CDBF2B2931AF00F7FF87 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDC92B2931AF00F7FF87 /* ContentView.swift in Sources */, + 41F5CDC72B2931AF00F7FF87 /* aesthetic_computerApp.swift in Sources */, + AC1A0C012E9A000000000001 /* LaunchPing.swift in Sources */, + AC1A0C012E9A000000000003 /* PopPlayer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 41F5CDE22B29356F00F7FF87 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 41F5CDEB2B29356F00F7FF87 /* MessagesViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 41F5CDF32B29356F00F7FF87 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 41F5CDE52B29356F00F7FF87 /* aesthetic */; + targetProxy = 41F5CDF22B29356F00F7FF87 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 41F5CDEC2B29356F00F7FF87 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 41F5CDED2B29356F00F7FF87 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 41F5CDCF2B2931B000F7FF87 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 41F5CDD02B2931B000F7FF87 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 41F5CDD22B2931B000F7FF87 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_ENTITLEMENTS[sdk=*]" = aesthetic.computer/aesthetic.computer.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; + DEVELOPMENT_TEAM = FB5948YR3S; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "aesthetic-computer-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 41F5CDD32B2931B000F7FF87 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "aesthetic.computer/aesthetic.computer-release.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_ASSET_PATHS = "\"aesthetic.computer/Preview Content\""; + DEVELOPMENT_TEAM = FB5948YR3S; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "aesthetic-computer-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSCameraUsageDescription = "For taking photos in `camera` and playing games like `handtime`."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "For input in pieces like `baktok`."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 41F5CDF62B29357000F7FF87 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_TEAM = FB5948YR3S; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = aesthetic/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 41F5CDF72B29357000F7FF87 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 5; + DEVELOPMENT_TEAM = FB5948YR3S; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = aesthetic/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = aesthetic; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = aesthetic.computer.aesthetic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 41F5CDBE2B2931AF00F7FF87 /* Build configuration list for PBXProject "aesthetic.computer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 41F5CDCF2B2931B000F7FF87 /* Debug */, + 41F5CDD02B2931B000F7FF87 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 41F5CDD12B2931B000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic.computer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 41F5CDD22B2931B000F7FF87 /* Debug */, + 41F5CDD32B2931B000F7FF87 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 41F5CDF52B29357000F7FF87 /* Build configuration list for PBXNativeTarget "aesthetic" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 41F5CDF62B29357000F7FF87 /* Debug */, + 41F5CDF72B29357000F7FF87 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = 41F5CDBB2B2931AF00F7FF87 /* Project object */; +} -- 2.51.2 From ab6f18c1191d4644cecef2b96a4a27a05f3c2b6d Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:01:07 -0700 Subject: [PATCH 04/21] Enable No Paint edit profiles for GPT Image 2 and Nano Banana 2.1 --- system/backend/nopaint-openrouter.mjs | 2 ++ system/tests/nopaint-openrouter.test.mjs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/system/backend/nopaint-openrouter.mjs b/system/backend/nopaint-openrouter.mjs index 131068ff77..00fd159d82 100644 --- a/system/backend/nopaint-openrouter.mjs +++ b/system/backend/nopaint-openrouter.mjs @@ -5,6 +5,8 @@ import {braincellsFromCost} from './easel-paid-credits.mjs'; export const profiles = { 'black-forest-labs/flux.2-klein-4b': {name:'FLUX.2 Klein 4B', options:{aspect_ratio:'1:1', output_format:'png', seed:true}}, 'google/gemini-3.1-flash-image': {name:'Nano Banana 2', options:{aspect_ratio:'1:1', resolution:'512'}}, + 'google/gemini-nano-banana-2.1': {name:'Nano Banana 2.1', options:{aspect_ratio:'1:1', resolution:'1K'}}, + 'openai/gpt-image-2': {name:'GPT Image 2', options:{aspect_ratio:'1:1', quality:'low'}}, 'openai/gpt-image-1-mini': {name:'GPT Image 1 Mini', options:{aspect_ratio:'1:1', quality:'low'}}, 'openai/gpt-image-2.5-flare': {name:'GPT Image 2.5 Flare', options:{aspect_ratio:'1:1', quality:'low'}}, }; diff --git a/system/tests/nopaint-openrouter.test.mjs b/system/tests/nopaint-openrouter.test.mjs index 96430e84ef..c40543d5a2 100644 --- a/system/tests/nopaint-openrouter.test.mjs +++ b/system/tests/nopaint-openrouter.test.mjs @@ -37,6 +37,22 @@ test('provider errors are not retried; external image URLs are not followed',asy await assert.rejects(generate({image,strength:.25,seed:3},offers[0]));assert.equal(calls,1); } }); +test('GPT Image 2 and Nano Banana 2.1 use their supported edit settings',async()=>{ + for(const [model,option,value] of [ + ['openai/gpt-image-2','quality','low'], + ['google/gemini-nano-banana-2.1','resolution','1K'], + ]) { + const [offer]=openRouterOffers(JSON.stringify([{model,usd:.05}])); + let request; + const generate=createOpenRouterProvider({key:'test-only',fetch:async(url,options)=>{ + request=JSON.parse(options.body);return Response.json({data:[{b64_json:image}]}); + }}); + await generate({image,strength:.5,seed:3},offer); + assert.equal(request.model,model);assert.equal(request[option],value); + assert.equal(request.aspect_ratio,'1:1');assert.equal(request.n,1); + assert.equal(request.input_references.length,1);assert.equal(request.seed,undefined); + } +}); test('gateway selects and bills by server quote; mismatched model never calls provider',async()=>{ const billed=[],generated=[]; const handler=createHandler({authorize:async()=>({sub:'owner',email_verified:true}),getHandleOrEmail:async()=>'@owner', -- 2.51.2 From 72fa93b48021ee484334319473767fcdbd6504fe Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:04:22 -0700 Subject: [PATCH 05/21] Restore saved Oskiewar fighters for signed-in web and iOS players --- .../netlify/functions/oskiewar-generation.mjs | 7 +- system/tests/oskiewar-generation.test.mjs | 15 +- xbox/live/account-deletion.mjs | 94 ++++++++++ xbox/live/account.mjs | 11 +- xbox/live/mac-test.html | 54 +++--- xbox/live/oskiewar-wizard.mjs | 7 +- xbox/live/tests/account-default-browser.mjs | 164 ++++++++++++++++++ xbox/live/tests/account.test.mjs | 16 +- xbox/tools/oskiewar-release.mjs | 2 +- 9 files changed, 338 insertions(+), 32 deletions(-) create mode 100644 xbox/live/account-deletion.mjs create mode 100644 xbox/live/tests/account-default-browser.mjs diff --git a/system/netlify/functions/oskiewar-generation.mjs b/system/netlify/functions/oskiewar-generation.mjs index d56a528b6c..a3407eed2b 100644 --- a/system/netlify/functions/oskiewar-generation.mjs +++ b/system/netlify/functions/oskiewar-generation.mjs @@ -1,5 +1,5 @@ import { createHash, randomUUID } from 'node:crypto'; -import { authorize } from '../../backend/authorization.mjs'; +import { authorize, handleFor } from '../../backend/authorization.mjs'; import { connect } from '../../backend/database.mjs'; import { pseudonym, frozenFields } from './oskiewar-consent.mjs'; import { gateRoutes, verifyGenerationCapability, readGrantedPhoto, generateAppearance, RECIPE } from '../../backend/oskiewar-generation.mjs'; @@ -11,7 +11,10 @@ const SAVED_MS = 24 * 60 * 60 * 1000; async function fighterAccount(userSub) { const { db } = await connect(); const profile = await db.collection('@handles').findOne({ _id: userSub }); - const handle = profile?.handle ? '@' + profile.handle.replace(/^@/, '') : null; + // Match the sign-in shell's handle lookup, including established aliases. + // The fighter and its consent still belong to the authenticated subject. + const name = profile?.handle || await handleFor(userSub); + const handle = name ? '@' + name.replace(/^@/, '') : null; const fighters = db.collection('oskiewar-fighters'); await fighters.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); return { db, handle, fighters }; diff --git a/system/tests/oskiewar-generation.test.mjs b/system/tests/oskiewar-generation.test.mjs index 588d457736..b7d2cd57ae 100644 --- a/system/tests/oskiewar-generation.test.mjs +++ b/system/tests/oskiewar-generation.test.mjs @@ -52,7 +52,8 @@ function collection(name) { }, }; } -mock.module('../backend/authorization.mjs', { exports: { authorize: async headers => headers?.authorization ? { sub: headers.authorization === 'Bearer other' ? 'auth0|other' : 'auth0|fixture' } : null } }); +const aliases = new Map(); +mock.module('../backend/authorization.mjs', { exports: { handleFor: async sub => aliases.get(sub), authorize: async headers => headers?.authorization ? { sub: headers.authorization === 'Bearer other' ? 'auth0|other' : 'auth0|fixture' } : null } }); mock.module('../backend/database.mjs', { exports: { connect: async () => ({ db: { collection } }) } }); const { handler } = await import('../netlify/functions/oskiewar-generation.mjs'); const { pseudonym } = await import('../netlify/functions/oskiewar-consent.mjs'); @@ -108,6 +109,18 @@ test('real bridge releases one result, refuses revoked grants, and never regener assert.equal(restored.handle, '@renamed'); assert.equal(restored.fighter.hash, fighter.hash, 'handle rename preserves ownership'); assert.equal(modelCalls, 1, 'restoring an accepted fighter never calls the model'); + stores.get('@handles').delete('auth0|fixture'); + aliases.set('auth0|fixture', 'renamed'); + const aliasRestored = JSON.parse((await handler(accountEvent)).body); + assert.equal(aliasRestored.handle, '@renamed', 'handle lookup matches the sign-in shell'); + assert.equal(aliasRestored.fighter.hash, fighter.hash, 'alias retains the same consent owner'); + aliases.set('auth0|other', 'renamed'); + const otherProfile = stores.get('@handles').get('auth0|other'); + stores.get('@handles').delete('auth0|other'); + assert.equal(JSON.parse((await handler({...accountEvent,headers:{authorization:'Bearer other'}})).body).status, 'empty', 'a shared handle label never shares a fighter'); + stores.get('@handles').set('auth0|fixture', profile); + stores.get('@handles').set('auth0|other', otherProfile); + aliases.clear(); delete process.env.OPENAI_API_KEY; assert.equal((await handler(accountEvent)).statusCode, 200, 'recovery needs no model credentials'); process.env.OPENAI_API_KEY = 'fixture'; diff --git a/xbox/live/account-deletion.mjs b/xbox/live/account-deletion.mjs new file mode 100644 index 0000000000..42f3f45588 --- /dev/null +++ b/xbox/live/account-deletion.mjs @@ -0,0 +1,94 @@ +// Account deletion stays in the web view; only explicit confirmation submits it. +const endpoint = 'https://aesthetic.computer/api/delete-erase-and-forget-me'; + +export function mountAccountDeletion({ account, onDeleted }) { + const button = document.createElement('button'); + button.type = 'button'; button.textContent = 'delete account'; + button.id = 'account-delete'; button.hidden = !account.signedIn; + document.querySelector('#account').append(button); + const dialog = document.createElement('dialog'); + dialog.setAttribute('aria-labelledby', 'account-deletion-title'); + dialog.style.cssText = 'box-sizing:border-box;width:calc(100% - 32px);max-width:26rem;max-height:90vh;overflow:auto;padding:24px;background:#f5f5f0;color:#111;border:1px solid #555;font:18px/1.5 sans-serif'; + dialog.innerHTML = `
+

Delete your AC account

+

Loading your account…

+ + + +
`; + document.body.append(dialog); + const form = dialog.querySelector('form'), note = dialog.querySelector('p'); + const field = dialog.querySelector('input'), label = dialog.querySelector('label'); + const submit = dialog.querySelector('[type="submit"]'), cancel = dialog.querySelector('[type="button"]'); + let revision = 0, ready = false, busy = false, completed = false; + const canSubmit = () => ready && !busy && !completed && account.signedIn && field.value === 'DELETE'; + const refresh = () => { submit.disabled = !canSubmit(); }; + function close() { + if (busy) return; + revision++; ready = false; dialog.close(); + } + dialog.addEventListener('close', () => { + revision++; ready = false; + globalThis.__oskiewarAccountOpen = false; + if (!button.hidden) button.focus(); + }); + dialog.addEventListener('cancel', event => { if (busy) event.preventDefault(); }); + cancel.addEventListener('click', close); + field.addEventListener('input', refresh); + addEventListener('oskiewar:account-change', () => { + button.hidden = !account.signedIn; + if (!account.signedIn && !completed) close(); + }); + async function request(method, token) { + const response = await fetch(endpoint + (method === 'GET' ? '?preview' : ''), { + method, headers: { authorization: 'Bearer ' + token }, + signal: AbortSignal.timeout(20000), + }); + const body = await response.json(); + if (!response.ok) throw Error(body.message || 'The account service did not answer.'); + return body; + } + button.addEventListener('click', async () => { + if (!account.signedIn) return; + const current = ++revision; + ready = false; completed = false; field.value = ''; refresh(); + label.hidden = false; submit.hidden = false; cancel.textContent = 'Cancel'; + note.textContent = 'Loading your account…'; + globalThis.__oskiewarAccountOpen = true; dialog.showModal(); + try { + const token = await account.bearer(); + if (!token) throw Error('Sign in again before deleting your account.'); + const preview = await request('GET', token); + if (current !== revision || !account.signedIn || !dialog.open) return; + const days = Number(preview.graceDays); + if (!Number.isFinite(days) || days < 0) throw Error('Could not read the deletion schedule.'); + note.textContent = `This deletes your entire Aesthetic Computer account, including its saved characters and content. Access stops immediately; deletion follows in ${days} days. Your email will receive a link to cancel before then.` + + (preview.braincells > 0 ? ` You will lose ${preview.braincells} braincells.` : '') + + ' Public blockchain records remain.'; + ready = true; refresh(); field.focus(); + } catch (error) { if (current === revision) note.textContent = error.message; } + }); + form.addEventListener('submit', async event => { + event.preventDefault(); + if (!canSubmit()) return; + busy = true; cancel.disabled = true; field.readOnly = true; refresh(); + try { + const token = await account.bearer(); + if (!token) throw Error('Sign in again before deleting your account.'); + const result = await request('POST', token); + completed = true; ready = false; + onDeleted(); + const date = result.purgeAfter && new Date(result.purgeAfter); + note.textContent = `Your account is locked${date && Number.isFinite(date.getTime()) ? ' and will be deleted on ' + date.toLocaleDateString() : ' for deletion'}.` + + (result.mailed ? ' Check your email for the link to keep it.' : ''); + label.hidden = true; submit.hidden = true; cancel.textContent = 'Done'; + } catch (error) { + ready = false; + note.textContent = error.message + ' Close this panel and check your account before trying again.'; + } finally { + busy = false; cancel.disabled = false; field.readOnly = false; refresh(); + } + }); +} diff --git a/xbox/live/account.mjs b/xbox/live/account.mjs index 86bc0ef35c..c824e42e6b 100644 --- a/xbox/live/account.mjs +++ b/xbox/live/account.mjs @@ -451,5 +451,14 @@ export default function mountAccount({ sfx = () => {}, probe = true } = {}) { if (!probe) account.ready = true; const ready = probe ? restore() : Promise.resolve(); - return { restore, ready, redirectPending }; + function clearSession() { + otp.forget(); + source = ""; who = ""; + publish({ signedIn: false }); + shut(); + // Clear the SPA cache without navigating away from the deletion receipt. + void spa?.logout?.({ openUrl: false })?.catch(() => {}); + } + + return { restore, ready, redirectPending, clearSession }; } diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html index 1bf56ca26d..2bec5d3da5 100644 --- a/xbox/live/mac-test.html +++ b/xbox/live/mac-test.html @@ -7,13 +7,8 @@ const offlineRuntime = !["https:", "http:"].includes(location.protocol); const inPackagedApp = globalThis.__oskiewarPackaged === true; const jevDemo = new URLSearchParams(location.search).has('jev-vs-jev'); - // The iOS app (apple/oskiewar) loads `?touch&app=ios`, and there the shell - // offers no sign-in and no "add yourself": App Store guideline 5.1.1(v) - // wants any app that makes an account to delete one in the app too, and - // that path does not exist yet. Sign-in comes back in a later version, - // with deletion beside it. Read once, from the address or from this tab's - // memory of it, because the room updater rewrites the address and a link - // followed inside the web view never carried the query to begin with. + // Keep the iOS host marker across room links and authentication redirects. + // Online iOS uses the same account and saved fighter as the website. const inIOSApp = (() => { let remembered = false; try { remembered = sessionStorage.getItem("oskiewar-app") === "ios"; } catch {} @@ -140,7 +135,7 @@ #account { position: fixed; z-index: 6; left: max(14px, env(safe-area-inset-left), var(--inset-left, 0px)); bottom: max(14px, env(safe-area-inset-bottom), var(--inset-bottom, 0px)); - display: flex; flex-direction: row; align-items: center; gap: 10px; } + display: flex; flex-direction: row; flex-wrap: wrap; max-width: calc(100% - 28px); align-items: center; gap: 10px; } .touch-enabled #account { top: max(12px, env(safe-area-inset-top), var(--inset-top, 0px)); bottom: auto; @@ -152,6 +147,8 @@ font: 700 19px/1 "Comic Relief", "Comic Sans MS", Arial, sans-serif; color: #f3f6ff; text-shadow: 2px 3px 0 rgba(4, 7, 18, .65); } #account-handle[hidden] { display: none; } + #account-delete { padding: 7px; border: 0; background: transparent; + color: #303849; font: 14px/1.3 Arial, sans-serif; text-decoration: underline; cursor: pointer; } #logout { appearance: none; padding: 7px 11px; border: 2px solid #6e768d; border-radius: 8px; background: #b0b8ca; color: #171b28; font: 700 15px/1 "Comic Relief", "Comic Sans MS", Arial, sans-serif; @@ -318,9 +315,8 @@ navigator.maxTouchPoints > 0 || matchMedia("(pointer: coarse)").matches; document.body.classList.toggle("touch-enabled", touchEnabled); document.body.classList.add("title-open"); - // No door in the app build (see `inIOSApp`): the corner button and its - // panel leave the document, so nothing can focus, tab to or raise them. - if (inIOSApp || inPackagedApp || offlineRuntime) + // Offline and packaged runtimes remove account controls. + if (inPackagedApp || offlineRuntime) for (const furniture of document.querySelectorAll("#account, #account-panel")) furniture.remove(); document.body.classList.toggle("social-preview", @@ -381,11 +377,9 @@ // `ready` says the shell has finished asking who is here — success or not. // Until then a piece should say nothing about accounts rather than guess // that nobody is signed in. - // The app build has no one to ask, so it is ready from the start: netplay - // waits on `ready` before it deals names, and nobody-signed-in is the - // whole answer there. + // Offline and packaged runtimes are ready as guests from the start. globalThis.__oskiewarAccount = { - ready: inIOSApp, signedIn: false, handle: "", colors: [], + ready: inPackagedApp || offlineRuntime, signedIn: false, handle: "", colors: [], // Replaced by `account.mjs` with the real token accessor. Answering null // until then is the honest state: nobody is signed in yet. bearer: async () => null, @@ -1753,6 +1747,7 @@ import createPhotoTheme from "/photo-theme.mjs"; import mountAccount from "/account.mjs"; import mountWizard from "/oskiewar-wizard.mjs"; + import { mountAccountDeletion } from "/account-deletion.mjs"; import { defaultCharacterEntry as isDefaultCharacterEntry, readRelease } from "/release-client.mjs"; const releaseAtLoad = readRelease(); import { mountWorkshop } from "/oskiewar-workshop.mjs"; @@ -1777,41 +1772,54 @@ globalThis.__oskiewarRenderSurface = renderSurface; globalThis.__oskiewarNextResolutionScale = nextResolutionScale; globalThis.__oskiewarInsetsChanged(); - // The app build mounts neither door (see `inIOSApp`); the game already - // plays a shell without them as anonymous. - const accountDoor = inIOSApp || inPackagedApp || offlineRuntime ? null : mountAccount({ + // Offline and packaged runtimes have no account service. + const accountDoor = inPackagedApp || offlineRuntime ? null : mountAccount({ sfx: (name, amount, pan) => oskiewarSfx.drum(name, amount, pan), probe: !socialPreview, }); + if (accountDoor && !socialPreview) + mountAccountDeletion({ account: globalThis.__oskiewarAccount, + onDeleted: () => accountDoor.clearSession() }); + // 🚪 The consent wall, mounted after the door it depends on. Kept out of a // social-preview render for the same reason the account probe is: that pass // draws a picture of the title and must not grow furniture a viewer of the // picture could never press. + if (accountDoor?.redirectPending) await accountDoor.ready; const defaultCharacterEntry = isDefaultCharacterEntry(new URL(location.href)); - if (!socialPreview && !inIOSApp && !inPackagedApp && !offlineRuntime) + const practiceUrl = () => { + const url = new URL(location.href); + url.pathname = "/"; url.hash = ""; + for (const key of [...url.searchParams.keys()]) + if (!["touch", "app", "renderer", "graphics", "frame", "voice"].includes(key)) + url.searchParams.delete(key); + url.searchParams.set("practice", ""); + return url.pathname + url.search; + }; + if (!socialPreview && !inPackagedApp && !offlineRuntime) globalThis.__oskiewarWizard = mountWizard({ sfx: (name, amount, pan) => oskiewarSfx.drum(name, amount, pan), bearer: () => globalThis.__oskiewarAccount.bearer(), - defaultHandle: "@jeffrey", + defaultHandle: () => globalThis.__oskiewarAccount.handle.toLowerCase(), onDefault: () => { if (defaultCharacterEntry && !globalThis.__oskiewarLocalPractice) { // During initial loading, select the private lane before the game // or a room bridge boots. A later sign-in starts a fresh page. if (globalThis.__oskiewarTouch?.screen === "game") { - location.replace("/?practice"); + location.replace(practiceUrl()); return; } globalThis.__oskiewarLocalPractice = true; globalThis.__oskiewarVersusCapable = false; globalThis.__oskiewarOpponent = "dummy"; - history.replaceState(null, "", "/?practice"); + history.replaceState(null, "", practiceUrl()); } }, enterPractice: () => { if (globalThis.__oskiewarLocalPractice || offlineRuntime) return; sessionStorage.setItem("oskiewar-enter-practice", "1"); - location.assign("/?practice"); + location.assign(practiceUrl()); }, }); const defaultFighterReady = Promise.resolve(accountDoor?.ready) diff --git a/xbox/live/oskiewar-wizard.mjs b/xbox/live/oskiewar-wizard.mjs index 370642a7d4..1d091bc73b 100644 --- a/xbox/live/oskiewar-wizard.mjs +++ b/xbox/live/oskiewar-wizard.mjs @@ -442,9 +442,10 @@ export default function mountWizard({ sfx = () => {}, bearer = async () => null, }); // A default is an already accepted account result, never a new grant or // acceptance. Fetch live authority before equipping; don't retain a photo or - // fighter across pages. Only the explicitly configured pilot handle opts in. + // fighter across pages. The current account supplies the expected handle. async function restoreSaved() { - if (!defaultHandle) return false; + const handle = typeof defaultHandle === "function" ? defaultHandle() : defaultHandle; + if (!handle) return false; const revision = ++authority; try { const token = await bearer(); @@ -452,7 +453,7 @@ export default function mountWizard({ sfx = () => {}, bearer = async () => null, if (!token) { clearFighter(); return false; } const result = await generationRequest(token, "account", {}, { silent: true }); if (revision !== authority) return false; - if (result.status !== "accepted" || result.handle !== defaultHandle || + if (result.status !== "accepted" || result.handle?.toLowerCase() !== handle.toLowerCase() || !Number.isFinite(result.validUntil) || result.validUntil <= Date.now()) { clearFighter(); return false; } diff --git a/xbox/live/tests/account-default-browser.mjs b/xbox/live/tests/account-default-browser.mjs new file mode 100644 index 0000000000..d73d89146d --- /dev/null +++ b/xbox/live/tests/account-default-browser.mjs @@ -0,0 +1,164 @@ +// Signed-in defaults and mobile account controls through the real shell. +// Auth0, saved characters and deletion responses are isolated fixtures. +// Run: node xbox/live/tests/account-default-browser.mjs +import assert from 'node:assert/strict'; +import { readFile, mkdir } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import puppeteer from 'puppeteer'; +const root = fileURLToPath(new URL('../../../', import.meta.url)).replace(/\/$/, ''); +const output = process.env.OSKIEWAR_TEST_OUTPUT || '/tmp/oskiewar-account-default'; +await mkdir(output, { recursive: true }); +const browser = await puppeteer.launch({ + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + headless: true, args: ['--mute-audio', '--no-first-run'], +}); +const pause = ms => new Promise(resolve => setTimeout(resolve, ms)); +async function open(path, { phone = false, failed = false, roomStatus = null, assetFailure = false, savedFighter = false } = {}) { + const context = await browser.createBrowserContext(); + const page = await context.newPage(); + const errors = [], discoveries = [], deletionRequests = []; + let deletionFails = false; + await page.setViewport(phone ? { width: 390, height: 844, deviceScaleFactor: 3, isMobile: true, hasTouch: true } + : { width: 1200, height: 700, deviceScaleFactor: 2 }); + page.on('pageerror', e => errors.push(e.message)); + await page.evaluateOnNewDocument(({ failed, roomStatus, savedFighter }) => { + speechSynthesis.speak = () => {}; + let signed = savedFighter; + window.__authCalls = []; + window.auth0 = { createAuth0Client: async () => ({ + checkSession: async () => { await new Promise(r => setTimeout(r, 900)); }, + handleRedirectCallback: async () => { + window.__authCalls.push('callback'); + await new Promise(r => setTimeout(r, 900)); + if (failed) throw new Error('invalid state'); + signed = true; + return { appState: { returnTo: '/' } }; + }, + getUser: async () => signed ? { sub: 'test-user' } : undefined, + getTokenSilently: async () => 'test-token', + }) }; + window.WebSocket = class { + static OPEN = 1; static CONNECTING = 0; static CLOSED = 3; + constructor() { this.readyState = 0; } + send() {} close() {} removeEventListener() {} + addEventListener(type, listener) { + if (type === 'message' && roomStatus !== null) setTimeout(() => listener({ + data: JSON.stringify({type: 'oskiewar:status', content: {live: roomStatus}}) + }), 100); + } + }; + }, { failed, roomStatus, savedFighter }); + await page.setRequestInterception(true); + page.on('request', async req => { + try { + const u = new URL(req.url()); + if (assetFailure && u.pathname.endsWith('/props.png')) + return req.respond({status:404,body:''}); + const json = body => req.respond({ status: 200, contentType: 'application/json', + headers: { 'access-control-allow-origin': '*' }, body: JSON.stringify(body) }); + if (u.pathname === '/oskiewar-open-room') { + discoveries.push(u.pathname); + return json({ room: 'daffo394' }); + } + if (u.pathname === '/api/oskiewar-generation') return json(savedFighter ? { + status:'accepted', handle:'@tester', validUntil:Date.now()+600000, + fighter:{version:1,recipe:'oskiewar-capsule-fighter-v1',hash:'a'.repeat(64), + appearance:{skin:'#f1d9c9',hair:'#4a3c31',shirt:'#f0f0f0',pants:'#1c4ea6',shoes:'#000000',hairStyle:'long',beard:false,glasses:false,sleeves:'long'}}, + } : {status:'empty',handle:'@tester'}); + if (req.method() === 'OPTIONS') return req.respond({status:204,headers:{'access-control-allow-origin':'*','access-control-allow-headers':'authorization,content-type','access-control-allow-methods':'GET,POST'}}); + if (u.pathname === '/api/delete-erase-and-forget-me') { + deletionRequests.push(req.method()); + if (deletionFails) return req.respond({status:503,contentType:'application/json',headers:{'access-control-allow-origin':'*'},body:JSON.stringify({message:'Service unavailable'})}); + if (req.method() === 'POST') return json({result:'Deleted!',purgeAfter:'2026-11-01',mailed:true}); + return json({graceDays:7,braincells:12,counts:{}}); + } + if (u.pathname === '/handle') return json({ handle: 'tester' }); + if (u.pathname === '/api/handle-colors') return json({ colors: [] }); + if (u.pathname.includes('/api/') || u.hostname === 'session-server.aesthetic.computer') + return json({}); + if (u.hostname !== 'oskiewar.com') return req.abort(); + let file; + if (u.pathname === '/' || /^\/[a-z]+[0-9]+$/.test(u.pathname)) file = root + '/xbox/live/mac-test.html'; + else if (u.pathname.startsWith('/aesthetic.computer/')) file = root + '/system/public' + u.pathname; + else if (u.pathname.startsWith('/ComicRelief')) file = root + '/system/public/papers.aesthetic.computer/foundry/fonts' + u.pathname; + else file = root + '/xbox/live' + u.pathname; + const body = await readFile(file); + const contentType = file.endsWith('.html') ? 'text/html' : /\.m?js$/.test(file) + ? 'application/javascript' : file.endsWith('.svg') ? 'image/svg+xml' : 'application/octet-stream'; + await req.respond({ status: 200, contentType, body }); + } catch { await req.respond({ status: 404, body: '' }); } + }); + await page.goto('https://oskiewar.com' + path, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => window.__oskiewarAccount?.ready && window.__oskiewarTouch && + window.__oskiewarGraphicsThemeStatus, + { timeout: 20000 }); + await pause(1100); // Exercise the 500 ms room URL updater after the first paint. + const pixels = await page.evaluate(() => { + const canvas = document.querySelector('canvas'); + const bounds = canvas.getBoundingClientRect(); + return { actual: [canvas.width, canvas.height], + expected: [Math.round(bounds.width * devicePixelRatio), + Math.round(bounds.height * devicePixelRatio)] }; + }); + assert.deepEqual(pixels.actual, pixels.expected, 'canvas maps to exact display pixels'); + assert.equal(await page.evaluate(() => __oskiewarGraphicsThemeStatus), + path.includes('renderer=canvas') && !assetFailure && !path.includes('graphics=flat') ? 'photorealistic' : 'flat'); + assert.deepEqual(errors, [], 'no browser exceptions'); + assert.deepEqual(discoveries, [], 'startup never discovers an unsolicited match'); + return { page, context, errors, deletionRequests, failDeletion: () => { deletionFails = true; } }; +} +async function state(page) { + return page.evaluate(() => ({ path: location.pathname + location.search, + screen: __oskiewarTouch.screen, handle: __oskiewarAccount.handle, + signedIn: __oskiewarAccount.signedIn, open: __oskiewarAccountOpen, + callbacks: __authCalls.length, room: __oskiewarRoundBridge?.name || '' })); +} +try { + for (const [path, phone] of [['/',false], ['/?touch&app=ios',true], ['/?code=fixture&state=fixture&app=ios',true]]) { + const {page,context,errors,deletionRequests,failDeletion}=await open(path,{phone,savedFighter:true}); + await page.waitForFunction(()=>globalThis.__oskiewarFighterAppearance?.handle==='@tester'); + assert.equal(await page.evaluate(()=>__oskiewarLocalPractice),true); + assert.equal(await page.evaluate(()=>__oskiewarFighterAppearance.appearance.shirt.join(',')),'240,240,240'); + assert.equal((await state(page)).signedIn,true); + assert.equal(await page.$eval('#account-delete',e=>e.hidden),false); + if(phone) assert.equal(await page.evaluate(()=>sessionStorage.getItem('oskiewar-app')),'ios'); + const start = await page.evaluate(() => { + const b = __oskiewarTouch.titleButton, view = __fightHost.gameView(); + return {x:(b.x+b.width/2)/view.width*innerWidth,y:(b.y+b.height/2)/view.height*innerHeight}; + }); + await page.mouse.click(start.x,start.y); + await page.waitForFunction(()=>__oskiewarTouch.screen==='game' && __oskiewarTouch.practiceFighter==='@tester' && __oskiewarTouch.practiceModel?.parts>0); + await page.screenshot({path:`${output}/saved-${phone?'ios':'web'}.png`}); + await page.reload({waitUntil:'domcontentloaded'}); + await page.waitForFunction(()=>globalThis.__oskiewarFighterAppearance?.handle==='@tester'); + assert.equal(await page.evaluate(()=>__oskiewarLocalPractice),true); + if(phone) { + await page.click('#account-delete'); + await page.waitForFunction(()=>document.querySelector('dialog p').textContent.includes('7 days')); + assert.deepEqual(deletionRequests,['GET']); + assert.equal(await page.$eval('dialog [type="submit"]',e=>e.disabled),true); + await page.screenshot({path:`${output}/delete-ios.png`}); + await page.click('dialog [type="button"]'); + assert.deepEqual(deletionRequests,['GET'],'cancel does not delete'); + if(path.includes('code=')) { + failDeletion(); await page.click('#account-delete'); + await page.waitForFunction(()=>document.querySelector('dialog p').textContent.includes('Service unavailable')); + await page.type('dialog input','DELETE'); + assert.equal(await page.$eval('dialog [type="submit"]',e=>e.disabled),true,'failed preview cannot submit'); + } else { + await page.click('#account-delete'); + await page.waitForFunction(()=>document.querySelector('dialog p').textContent.includes('7 days')); + await page.type('dialog input','DELETE'); + await page.click('dialog [type="submit"]'); + await page.waitForFunction(()=>document.querySelector('dialog p').textContent.includes('account is locked')); + assert.deepEqual(deletionRequests,['GET','GET','POST']); + assert.equal((await state(page)).signedIn,false); + assert.equal(await page.evaluate(()=>globalThis.__oskiewarFighterAppearance),null); + } + } + console.log(`PASS saved fighter: ${path}, reload, ${phone?'mobile account controls':'desktop'}`); + assert.deepEqual(errors, [], "no browser errors through account operations"); + await context.close(); + } +} finally { await browser.close(); } diff --git a/xbox/live/tests/account.test.mjs b/xbox/live/tests/account.test.mjs index ac4936b6c9..08dd3ba13a 100644 --- a/xbox/live/tests/account.test.mjs +++ b/xbox/live/tests/account.test.mjs @@ -16,6 +16,8 @@ function fixture({ url = 'https://oskiewar.com/', callback, native = null, const account = { ready: false, signedIn: false, handle: '', colors: [] }; const context = vm.createContext({ URL, console, location, + dispatchEvent: event => calls.push(["change", event.detail]), + CustomEvent: class { constructor(type, options) { this.type = type; this.detail = options.detail; } }, history: { replaceState(_state, _title, path) { location.href = new URL(path, location).href; } }, document: { querySelector(selector) { if (!elements.has(selector)) elements.set(selector, { @@ -29,6 +31,7 @@ function fixture({ url = 'https://oskiewar.com/', callback, native = null, otpSignIn: () => ({ session() { calls.push('otp'); if (failNative) throw new Error('storage unavailable'); return native; }, token: async () => 'native-token', + forget: () => calls.push('forget'), }), auth0: { createAuth0Client: async () => ({ checkSession: async () => { calls.push('silent'); }, @@ -109,7 +112,7 @@ test('normal restoration keeps the OTP session and does not handle a callback', await f.door.ready; assert.equal(f.door.redirectPending, false); assert.equal(f.account.handle, '@TESTER'); - assert.equal(f.calls.length, 1); + assert.equal(f.calls.filter(call => typeof call === 'string').length, 1); assert.equal(f.calls[0], 'otp'); }); @@ -138,3 +141,14 @@ test('sign-in from the title returns home even with a legacy room address', asyn const options = f.calls.find(call => call[0] === 'redirect')[1]; assert.equal(options.appState.returnTo, '/'); }); + + +test('clearing a deleted account immediately drops identity and announces sign-out', async () => { + const f = fixture({ native: { sub: 'native-user' } }); + await f.door.ready; + f.door.clearSession(); + assert.equal(f.account.signedIn, false); + assert.equal(f.account.handle, ''); + assert.equal(f.calls.includes('forget'), true); + assert.equal(f.calls.at(-1)[1].signedIn, false); +}); diff --git a/xbox/tools/oskiewar-release.mjs b/xbox/tools/oskiewar-release.mjs index 7c9ca4b18f..4fa01f8d7a 100644 --- a/xbox/tools/oskiewar-release.mjs +++ b/xbox/tools/oskiewar-release.mjs @@ -236,7 +236,7 @@ async function reconcile(receipt, { dryRun = false } = {}) { if (receipt.channels.web.status !== "current") { if (dryRun) console.log("would deploy web"); else try { - run("fish", ["lith/deploy.fish"]); + run("node", ["slab/bin/ac-deploy"]); await verifyWeb(hash, receipt.desired.runtimeHash); mark(receipt, "web", "current", "verified production bytes"); } catch (error) { mark(receipt, "web", "failed", error.message); } -- 2.51.2 From 2538b5be91a3792d3d365f63f1b5aa9c3effa654 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:08:18 -0700 Subject: [PATCH 06/21] Fix account preview setup and Xbox reachability checks --- system/backend/account-deletion-deps.mjs | 2 +- xbox/tools/oskiewar-release.mjs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/system/backend/account-deletion-deps.mjs b/system/backend/account-deletion-deps.mjs index 641429177c..042e96dbdc 100644 --- a/system/backend/account-deletion-deps.mjs +++ b/system/backend/account-deletion-deps.mjs @@ -110,7 +110,7 @@ async function stripeClient() { const key = dev ? process.env.STRIPE_API_TEST_PRIV_KEY : process.env.STRIPE_API_PRIV_KEY; if (!key) return null; const { default: Stripe } = await import("stripe"); - const stripe = Stripe(key); + const stripe = new Stripe(key); return { async forget(address) { const customers = await stripe.customers.list({ email: address, limit: 100 }); diff --git a/xbox/tools/oskiewar-release.mjs b/xbox/tools/oskiewar-release.mjs index 4fa01f8d7a..248b14b879 100644 --- a/xbox/tools/oskiewar-release.mjs +++ b/xbox/tools/oskiewar-release.mjs @@ -108,8 +108,22 @@ export function probeDevicePortal({ host, port }, timeout = 2000) { socket.once("connect", () => done(true, "")); socket.once("timeout", () => done(false, `${host}:${port} did not answer within ${timeout}ms`)); - socket.once("error", (error) => done(false, - `${host}:${port} ${error.code || error.message}`)); + socket.once("error", (error) => { + // macOS can deny Node's LAN connection while the curl transport used + // by live.mjs still reaches the console. Check that transport, bounded + // and without credentials, before declaring the console offline. + if (process.platform === "darwin" && error.code === "EHOSTUNREACH") { + socket.destroy(); + const seconds = String(timeout / 1000); + const probe = spawnSync("curl", ["--insecure", "--silent", + "--connect-timeout", seconds, "--max-time", seconds, + "--output", "/dev/null", "--write-out", "%{http_code}", + `https://${host}:${port}/`], { encoding: "utf8", timeout: timeout + 250 }); + if (probe.status === 0 && /^[1-5]\d\d$/.test(probe.stdout.trim())) + return done(true, ""); + } + done(false, `${host}:${port} ${error.code || error.message}`); + }); }); } function readReceipt() { -- 2.51.2 From 717e1f0229adb623152c75be405e0a70f326b316 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:26:52 -0700 Subject: [PATCH 07/21] Resolve Oskiewar fighters to the verified AC handle owner --- system/backend/oskiewar-identity.mjs | 27 +++++++++++ system/netlify/functions/oskiewar-consent.mjs | 9 +++- .../netlify/functions/oskiewar-generation.mjs | 45 +++++++++++-------- .../netlify/functions/oskiewar-submission.mjs | 6 ++- system/tests/oskiewar-consent.test.mjs | 4 +- system/tests/oskiewar-generation.test.mjs | 31 ++++++++----- system/tests/oskiewar-submission.test.mjs | 3 +- 7 files changed, 90 insertions(+), 35 deletions(-) create mode 100644 system/backend/oskiewar-identity.mjs diff --git a/system/backend/oskiewar-identity.mjs b/system/backend/oskiewar-identity.mjs new file mode 100644 index 0000000000..5df7aed714 --- /dev/null +++ b/system/backend/oskiewar-identity.mjs @@ -0,0 +1,27 @@ +import { handleFor, userEmailFromID } from './authorization.mjs'; +import { accountLocked } from './account-lock.mjs'; +import { connect } from './database.mjs'; + +// Password and email-code sign-ins can have separate Auth0 subjects. Resolve +// the AC handle's owner only after Auth0 confirms the same verified address. +// A cached handle label alone never grants access to another account's data. +export async function fighterIdentity(sub) { + const { db } = await connect(); + const handles = db.collection('@handles'); + const own = await handles.findOne({ _id: sub }); + if (own?.handle) return { sub, handle: own.handle }; + const label = await handleFor(sub); + const owner = label && await handles.findOne({ handle: label.replace(/^@/, '') }); + if (!owner) return { sub, handle: null }; + const [signedIn, primary] = await Promise.all([ + userEmailFromID(sub), userEmailFromID(owner._id), + ]); + if (!signedIn || !primary) + throw Object.assign(Error('Could not verify your AC account. Try again.'), { status: 503 }); + if (signedIn.email_verified !== true || primary.email_verified !== true || + !signedIn.email || signedIn.email.toLowerCase() !== primary.email?.toLowerCase()) + throw Object.assign(Error('This sign-in does not own that AC handle.'), { status: 403 }); + if (await accountLocked(owner._id)) + throw Object.assign(Error('This AC account is locked.'), { status: 401 }); + return { sub: owner._id, handle: owner.handle }; +} diff --git a/system/netlify/functions/oskiewar-consent.mjs b/system/netlify/functions/oskiewar-consent.mjs index 1a8886ea62..f626ac1e18 100644 --- a/system/netlify/functions/oskiewar-consent.mjs +++ b/system/netlify/functions/oskiewar-consent.mjs @@ -33,6 +33,7 @@ import { createHash } from "node:crypto"; import { authorize } from "../../backend/authorization.mjs"; +import { fighterIdentity } from "../../backend/oskiewar-identity.mjs"; const GATEWAY_TIMEOUT_MS = 6000; const SCHEMA_VERSION = 1; @@ -157,16 +158,20 @@ export async function handler(event) { if (!gateway || !salt || !gatewayToken) return fail(503, "The consent desk is not reachable right now."); + let identity; + try { identity = await fighterIdentity(user.sub); } + catch (error) { return fail(error.status || 503, error.status ? error.message : 'Could not verify your AC account. Try again.'); } + const request = { venue: "oskiewar", - subject: pseudonym(user.sub, salt), + subject: pseudonym(identity.sub, salt), operation_type: "DATA_OPERATION", frozen_fields: frozenFields(scope), // One explicit choice may retry safely. A new choice gets a new requestId, // so consent after withdrawal cannot revive the retired receipt. // Older clients retain their subject/scope idempotency. idempotency_key: createHash("sha256") - .update(JSON.stringify([pseudonym(user.sub, salt), frozenFields(scope), ...(body.requestId ? [body.requestId] : [])])) + .update(JSON.stringify([pseudonym(identity.sub, salt), frozenFields(scope), ...(body.requestId ? [body.requestId] : [])])) .digest("hex").slice(0, 32), operation_descriptor: { purpose: PURPOSE, diff --git a/system/netlify/functions/oskiewar-generation.mjs b/system/netlify/functions/oskiewar-generation.mjs index a3407eed2b..302b0536fa 100644 --- a/system/netlify/functions/oskiewar-generation.mjs +++ b/system/netlify/functions/oskiewar-generation.mjs @@ -1,5 +1,6 @@ import { createHash, randomUUID } from 'node:crypto'; -import { authorize, handleFor } from '../../backend/authorization.mjs'; +import { authorize } from '../../backend/authorization.mjs'; +import { fighterIdentity } from '../../backend/oskiewar-identity.mjs'; import { connect } from '../../backend/database.mjs'; import { pseudonym, frozenFields } from './oskiewar-consent.mjs'; import { gateRoutes, verifyGenerationCapability, readGrantedPhoto, generateAppearance, RECIPE } from '../../backend/oskiewar-generation.mjs'; @@ -8,12 +9,9 @@ const SAVED_MS = 24 * 60 * 60 * 1000; // AC owns the account binding. Handles are labels and may change; neither // handles nor Auth0 subjects are sent to REGARDE or the appearance model. -async function fighterAccount(userSub) { +async function fighterAccount(identity) { const { db } = await connect(); - const profile = await db.collection('@handles').findOne({ _id: userSub }); - // Match the sign-in shell's handle lookup, including established aliases. - // The fighter and its consent still belong to the authenticated subject. - const name = profile?.handle || await handleFor(userSub); + const name = identity.handle; const handle = name ? '@' + name.replace(/^@/, '') : null; const fighters = db.collection('oskiewar-fighters'); await fighters.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); @@ -57,24 +55,35 @@ export async function handler(event) { // Trusted host entry point for recovering an authenticated player's own job. export async function generateForUser(input, userSub) { if (!userSub) return respond(401, { message: 'Sign in to generate your fighter.' }); + const signInSub = userSub; + let identity; + try { identity = await fighterIdentity(userSub); userSub = identity.sub; } + catch (error) { return respond(error.status || 503, { message: error.status ? error.message : 'Could not verify your AC account. Try again.' }); } if (input?.action === 'withdraw') { const { REGARDE_GATEWAY_URL: gateway, REGARDE_SUBJECT_SALT: salt, REGARDE_GATEWAY_TOKEN: token } = process.env; if (!gateway || !salt || !token) return respond(503, { message: 'REGARDE is unavailable.' }); try { const url = new URL(gateRoutes(gateway).media); url.pathname = url.pathname.replace(/media$/, 'withdraw'); const ff = frozenFields({ source: ['appearance'], outputs: ['fighter_mesh'], distribution: ['private_preview', 'local_gameplay'], retention: 'bound_to_purpose_scope' }); - const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, - body: JSON.stringify({ subject: pseudonym(userSub, salt), venue: 'oskiewar', operation_type: 'DATA_OPERATION', - idempotency_key: randomUUID(), operation_descriptor: { purpose: 'oskiewar_fighter_generation', description: 'Withdraw my Oskiewar material.' }, - frozen_fields: { ...ff, operation_kind: 'WITHDRAW_CONSENT', retention_constraint: 'propagate_to_named_processors', data_handling_chain: ['oskiewar'] } }), - signal: AbortSignal.timeout(10000) }); - const result = await response.json(); - if (!response.ok || !['withdrawn', 'nothing-to-withdraw'].includes(result.outcome)) return respond(502, { message: 'Withdrawal was not confirmed. Try again.' }); + const owners = [...new Set([userSub, signInSub])]; + let outcome = 'nothing-to-withdraw'; + // Also retire material submitted by this sign-in before account linking. + for (const owner of owners) { + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ subject: pseudonym(owner, salt), venue: 'oskiewar', operation_type: 'DATA_OPERATION', + idempotency_key: randomUUID(), operation_descriptor: { purpose: 'oskiewar_fighter_generation', description: 'Withdraw my Oskiewar material.' }, + frozen_fields: { ...ff, operation_kind: 'WITHDRAW_CONSENT', retention_constraint: 'propagate_to_named_processors', data_handling_chain: ['oskiewar'] } }), + signal: AbortSignal.timeout(10000) }); + const result = await response.json(); + if (!response.ok || !['withdrawn', 'nothing-to-withdraw'].includes(result.outcome)) return respond(502, { message: 'Withdrawal was not confirmed. Try again.' }); + if (result.outcome === 'withdrawn') outcome = 'withdrawn'; + } const { db } = await connect(); - const subject = pseudonym(userSub, salt); - await db.collection('oskiewar-fighters').deleteOne({ _id: subject }); - await db.collection('oskiewar-generation-jobs').deleteMany({ owner: userSub }); - return respond(200, { status: result.outcome }); + for (const owner of owners) { + await db.collection('oskiewar-fighters').deleteOne({ _id: pseudonym(owner, salt) }); + await db.collection('oskiewar-generation-jobs').deleteMany({ owner }); + } + return respond(200, { status: outcome }); } catch { return respond(502, { message: 'Withdrawal was not confirmed. Try again.' }); } } if (!['generate', 'status', 'accept', 'account'].includes(input?.action) || @@ -85,7 +94,7 @@ export async function generateForUser(input, userSub) { let jobs, id; try { const subject = pseudonym(userSub, salt), routes = gateRoutes(gateway); - const account = await fighterAccount(userSub); + const account = await fighterAccount(identity); if (!account.handle) return respond(409, { code: 'handle_required', message: 'Choose your AC handle before making a fighter.' }); if (input.action === 'account') return await restoreFighter(account, subject, routes, token); const keysResponse = await fetch(routes.jwks, { signal: AbortSignal.timeout(10000) }); diff --git a/system/netlify/functions/oskiewar-submission.mjs b/system/netlify/functions/oskiewar-submission.mjs index 74b7f4c17b..b2a18a2e30 100644 --- a/system/netlify/functions/oskiewar-submission.mjs +++ b/system/netlify/functions/oskiewar-submission.mjs @@ -1,5 +1,6 @@ // Authenticated transport only. REGARDE owns capability and retention checks. import { authorize } from '../../backend/authorization.mjs'; +import { fighterIdentity } from '../../backend/oskiewar-identity.mjs'; import { pseudonym } from './oskiewar-consent.mjs'; const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }; @@ -17,12 +18,13 @@ export async function handler(event) { REGARDE_GATEWAY_TOKEN: token } = process.env; if (!gateway || !salt || !token) return fail(503, 'The submission desk is unavailable.'); try { + const identity = await fighterIdentity(user.sub); const url = new URL(gateway); url.pathname = url.pathname.replace(/\/gateway\/?$/, body.action === 'status' ? '/manifest' : '/submission'); if (!url.pathname.endsWith(body.action === 'status' ? '/manifest' : '/submission')) return fail(503, 'The submission desk is unconfigured.'); const upstream = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, - body: JSON.stringify({ capability: body.capability, subject: pseudonym(user.sub, salt), + body: JSON.stringify({ capability: body.capability, subject: pseudonym(identity.sub, salt), ...(body.action === 'status' ? {} : { files: body.files.map(file => ({ source: file?.source, base64: file?.base64 })) }) }), signal: AbortSignal.timeout(10000), }); @@ -30,5 +32,5 @@ export async function handler(event) { if (!upstream.ok) return fail(upstream.status, result.error || 'Submission refused.'); return { statusCode: body.action === 'status' ? 200 : 201, headers, body: JSON.stringify({ manifest: result.manifest, retention: result.retention, purge_at: result.purge_at }) }; - } catch { return fail(502, 'Submission did not complete. Check your connection before trying again.'); } + } catch (error) { return fail(error.status || 502, error.status ? error.message : 'Submission did not complete. Check your connection before trying again.'); } } diff --git a/system/tests/oskiewar-consent.test.mjs b/system/tests/oskiewar-consent.test.mjs index f5788d89b9..064286b196 100644 --- a/system/tests/oskiewar-consent.test.mjs +++ b/system/tests/oskiewar-consent.test.mjs @@ -8,7 +8,8 @@ import assert from "node:assert/strict"; import test, { mock } from "node:test"; mock.module('../backend/authorization.mjs', { exports: { authorize: async () => ({ sub: 'auth0|fixture' }) } }); -const { readScope, frozenFields, SEPARATE, handler } = await import("../netlify/functions/oskiewar-consent.mjs"); +mock.module('../backend/oskiewar-identity.mjs', { exports: { fighterIdentity: async () => ({ sub: 'auth0|primary', handle: 'fixture' }) } }); +const { readScope, frozenFields, SEPARATE, handler, pseudonym } = await import("../netlify/functions/oskiewar-consent.mjs"); // The narrowest answer the wall can send: look at me, make a picture, show // nobody. Every test below is this with one thing changed. @@ -35,6 +36,7 @@ test('explicit consent choices retry identically but a new choice gets a new rec await ask('11111111-1111-1111-1111-111111111111'); await ask('22222222-2222-2222-2222-222222222222'); assert.equal(sent[0].idempotency_key, sent[1].idempotency_key); + assert.equal(sent[0].subject, pseudonym('auth0|primary', 'fixture'), 'consent uses the verified handle owner'); assert.notEqual(sent[0].idempotency_key, sent[2].idempotency_key); assert.equal((await ask('bad')).statusCode, 400); assert.equal(sent.length, 3); diff --git a/system/tests/oskiewar-generation.test.mjs b/system/tests/oskiewar-generation.test.mjs index b7d2cd57ae..4c2b742b97 100644 --- a/system/tests/oskiewar-generation.test.mjs +++ b/system/tests/oskiewar-generation.test.mjs @@ -38,7 +38,7 @@ function collection(name) { if (!stores.has(name)) stores.set(name, new Map()); const rows = stores.get(name); return { - createIndex: async () => {}, findOne: async q => rows.get(q._id), + createIndex: async () => {}, findOne: async q => [...rows.values()].find(row => Object.entries(q).every(([key,value]) => row[key] === value)), deleteOne: async q => { const row = rows.get(q._id); if (row && (!q['fighter.hash'] || row.fighter?.hash === q['fighter.hash'])) rows.delete(q._id); }, deleteMany: async q => { for (const [id, row] of rows) if (row.owner === q.owner) rows.delete(id); }, insertOne: async row => { if(rows.has(row._id)) throw Object.assign(Error('duplicate'),{code:11000}); rows.set(row._id, row); }, @@ -53,7 +53,12 @@ function collection(name) { }; } const aliases = new Map(); -mock.module('../backend/authorization.mjs', { exports: { handleFor: async sub => aliases.get(sub), authorize: async headers => headers?.authorization ? { sub: headers.authorization === 'Bearer other' ? 'auth0|other' : 'auth0|fixture' } : null } }); +mock.module('../backend/authorization.mjs', { exports: { + handleFor: async sub => aliases.get(sub), + userEmailFromID: async sub => ({ email: sub === 'auth0|other' ? 'other@example.test' : 'fixture@example.test', email_verified: true }), + authorize: async headers => headers?.authorization ? { sub: headers.authorization === 'Bearer other' ? 'auth0|other' : headers.authorization === 'Bearer otp' ? 'email|fixture' : 'auth0|fixture' } : null, +} }); +mock.module('../backend/account-lock.mjs', { exports: { accountLocked: async () => false } }); mock.module('../backend/database.mjs', { exports: { connect: async () => ({ db: { collection } }) } }); const { handler } = await import('../netlify/functions/oskiewar-generation.mjs'); const { pseudonym } = await import('../netlify/functions/oskiewar-consent.mjs'); @@ -109,15 +114,15 @@ test('real bridge releases one result, refuses revoked grants, and never regener assert.equal(restored.handle, '@renamed'); assert.equal(restored.fighter.hash, fighter.hash, 'handle rename preserves ownership'); assert.equal(modelCalls, 1, 'restoring an accepted fighter never calls the model'); - stores.get('@handles').delete('auth0|fixture'); - aliases.set('auth0|fixture', 'renamed'); - const aliasRestored = JSON.parse((await handler(accountEvent)).body); + aliases.set('email|fixture', 'renamed'); + const aliasRestored = JSON.parse((await handler({...accountEvent, headers:{authorization:'Bearer otp'}})).body); assert.equal(aliasRestored.handle, '@renamed', 'handle lookup matches the sign-in shell'); assert.equal(aliasRestored.fighter.hash, fighter.hash, 'alias retains the same consent owner'); + assert.equal((await handler({...event('status'), headers:{authorization:'Bearer otp'}})).statusCode, 200, 'generation status shares the same consent identity'); aliases.set('auth0|other', 'renamed'); const otherProfile = stores.get('@handles').get('auth0|other'); stores.get('@handles').delete('auth0|other'); - assert.equal(JSON.parse((await handler({...accountEvent,headers:{authorization:'Bearer other'}})).body).status, 'empty', 'a shared handle label never shares a fighter'); + assert.equal((await handler({...accountEvent,headers:{authorization:'Bearer other'}})).statusCode, 403, 'a shared handle label never shares a fighter'); stores.get('@handles').set('auth0|fixture', profile); stores.get('@handles').set('auth0|other', otherProfile); aliases.clear(); @@ -161,14 +166,18 @@ test('withdrawal uses the signed-in subject and works without a generation token const saved={...process.env}, originalFetch=globalThis.fetch; Object.assign(process.env,{REGARDE_GATEWAY_URL:'https://gate.invalid/v0/gateway',REGARDE_SUBJECT_SALT:'fixture',REGARDE_GATEWAY_TOKEN:'deployer'}); delete process.env.OPENAI_API_KEY; - let sent; - globalThis.fetch=async(url,options)=>{assert.equal(String(url),'https://gate.invalid/v0/withdraw');sent=JSON.parse(options.body);return Response.json({outcome:'withdrawn'});}; + const sent=[]; + globalThis.fetch=async(url,options)=>{assert.equal(String(url),'https://gate.invalid/v0/withdraw');sent.push(JSON.parse(options.body));return Response.json({outcome:'withdrawn'});}; try { const result=await handler({httpMethod:'POST',headers:{authorization:'Bearer fixture'},body:JSON.stringify({action:'withdraw',subject:'another-person'})}); assert.equal(result.statusCode,200); - assert.equal(sent.subject,pseudonym('auth0|fixture','fixture')); - assert.equal(sent.frozen_fields.operation_kind,'WITHDRAW_CONSENT'); - }finally{globalThis.fetch=originalFetch;for(const key of ['REGARDE_GATEWAY_URL','REGARDE_SUBJECT_SALT','REGARDE_GATEWAY_TOKEN','OPENAI_API_KEY']){if(saved[key]===undefined)delete process.env[key];else process.env[key]=saved[key];}} + assert.equal(sent[0].subject,pseudonym('auth0|fixture','fixture')); + assert.equal(sent[0].frozen_fields.operation_kind,'WITHDRAW_CONSENT'); + aliases.set('email|fixture','fixture'); + const linked=await handler({httpMethod:'POST',headers:{authorization:'Bearer otp'},body:JSON.stringify({action:'withdraw'})}); + assert.equal(linked.statusCode,200); + assert.deepEqual(sent.slice(1).map(r=>r.subject),['auth0|fixture','email|fixture'].map(sub=>pseudonym(sub,'fixture')),'withdrawal covers both the handle owner and legacy email-code material'); + }finally{aliases.clear();globalThis.fetch=originalFetch;for(const key of ['REGARDE_GATEWAY_URL','REGARDE_SUBJECT_SALT','REGARDE_GATEWAY_TOKEN','OPENAI_API_KEY']){if(saved[key]===undefined)delete process.env[key];else process.env[key]=saved[key];}} }); test('shared character follows the game bones without mutating physics or restoring missing limbs', async () => { diff --git a/system/tests/oskiewar-submission.test.mjs b/system/tests/oskiewar-submission.test.mjs index d15b001dd6..c710234dde 100644 --- a/system/tests/oskiewar-submission.test.mjs +++ b/system/tests/oskiewar-submission.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import test, { mock } from 'node:test'; mock.module('../backend/authorization.mjs', { exports: { authorize: async headers => headers?.authorization ? { sub: 'auth0|fixture' } : null } }); +mock.module('../backend/oskiewar-identity.mjs', { exports: { fighterIdentity: async () => ({ sub: 'auth0|primary', handle: 'fixture' }) } }); const { handler } = await import('../netlify/functions/oskiewar-submission.mjs'); const { pseudonym } = await import('../netlify/functions/oskiewar-consent.mjs'); test('submission bridge authenticates, strips filenames/identity/retention, forwards capability, fails closed', async () => { @@ -15,7 +16,7 @@ test('submission bridge authenticates, strips filenames/identity/retention, forw assert.equal(seen, undefined); const result = await handler(event); assert.equal(result.statusCode, 201); assert.equal(seen.url, 'https://gate.invalid/v0/submission'); - assert.deepEqual(JSON.parse(seen.body), { subject: pseudonym('auth0|fixture', 'fixture'), capability: 'signed-capability', files: [{source:'appearance', base64:btoa('fixture')}] }); + assert.deepEqual(JSON.parse(seen.body), { subject: pseudonym('auth0|primary', 'fixture'), capability: 'signed-capability', files: [{source:'appearance', base64:btoa('fixture')}] }); assert.equal(seen.headers.Authorization, 'Bearer deployer'); globalThis.fetch = async () => Response.json({error:'expired'}, {status:403}); assert.equal((await handler(event)).statusCode, 403); -- 2.51.2 From ecd86b555204b079ee78af3a5bde9f1d020152dc Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:27:06 -0700 Subject: [PATCH 08/21] Cover verified Oskiewar sign-in aliases --- system/tests/oskiewar-identity.test.mjs | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 system/tests/oskiewar-identity.test.mjs diff --git a/system/tests/oskiewar-identity.test.mjs b/system/tests/oskiewar-identity.test.mjs new file mode 100644 index 0000000000..1336e3dcaf --- /dev/null +++ b/system/tests/oskiewar-identity.test.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import test, { mock } from 'node:test'; + +const handles = [{_id:'auth0|primary',handle:'tester'}]; +let alias = 'tester', locked = false, requests = []; +const profiles = new Map([ + ['auth0|primary',{email:'player@example.test',email_verified:true}], + ['email|otp',{email:'player@example.test',email_verified:true}], +]); +mock.module('../backend/database.mjs', {exports:{connect:async()=>({db:{collection:()=>({ + findOne:async query=>handles.find(row=>Object.entries(query).every(([key,value])=>row[key]===value)), +})}})}}); +mock.module('../backend/authorization.mjs', {exports:{ + handleFor:async()=>alias, + userEmailFromID:async sub=>{requests.push(sub);return profiles.get(sub);}, +}}); +mock.module('../backend/account-lock.mjs', {exports:{accountLocked:async()=>locked}}); +const {fighterIdentity}=await import('../backend/oskiewar-identity.mjs'); + +test('email-code sign-in recovers the handle owner only with matching verified addresses',async()=>{ + assert.deepEqual(await fighterIdentity('auth0|primary'),{sub:'auth0|primary',handle:'tester'}); + assert.deepEqual(requests,[],'the direct handle owner needs no alias lookup'); + assert.deepEqual(await fighterIdentity('email|otp'),{sub:'auth0|primary',handle:'tester'}); + for(const bad of [{email:'other@example.test',email_verified:true},{email:'player@example.test',email_verified:false},{email:'',email_verified:true}]){ + profiles.set('email|otp',bad); + await assert.rejects(fighterIdentity('email|otp'),{status:403}); + } + profiles.set('email|otp',{email:'player@example.test',email_verified:true}); + profiles.set('auth0|primary',{email:'player@example.test',email_verified:false}); + await assert.rejects(fighterIdentity('email|otp'),{status:403}); + profiles.set('auth0|primary',{email:'player@example.test',email_verified:true}); + locked=true; + await assert.rejects(fighterIdentity('email|otp'),{status:401}); + locked=false; profiles.delete('email|otp'); + await assert.rejects(fighterIdentity('email|otp'),{status:503}); + alias=undefined; + assert.deepEqual(await fighterIdentity('email|new'),{sub:'email|new',handle:null}); +}); -- 2.51.2 From 0abd0d0de23ac5eac17e9c6310be62edca57ea47 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:28:48 -0700 Subject: [PATCH 09/21] Publish reviewed new-media opportunities with RSS and calendar feeds Replace the personal public tracker with a community catalog, searchable by discipline, funding, fees and eligibility. Preserve uncertain cutoffs as date-only calendar entries and keep closed calls accessible in the archive. Verified: eight data/feed tests, browser interactions and responsive views, RSS XML parsing, deterministic generated assets. Personal tracker archived privately before replacement. --- system/netlify/functions/docs.js | 2 +- .../deadlines/.gitattributes | 2 + .../deadlines/app.mjs | 108 + .../deadlines/calendar.ics | 199 ++ .../deadlines/core.mjs | 166 + .../deadlines/feed.xml | 123 + .../deadlines/icon.svg | 1 + .../deadlines/index.html | 3003 +---------------- .../deadlines/opportunities.json | 197 ++ .../deadlines/style.css | 8 + utilities/deadlines/README.md | 21 + utilities/deadlines/build.mjs | 30 + utilities/deadlines/core.test.mjs | 86 + utilities/deadlines/template.html | 44 + 14 files changed, 1160 insertions(+), 2830 deletions(-) create mode 100644 system/public/papers.aesthetic.computer/deadlines/.gitattributes create mode 100644 system/public/papers.aesthetic.computer/deadlines/app.mjs create mode 100644 system/public/papers.aesthetic.computer/deadlines/calendar.ics create mode 100644 system/public/papers.aesthetic.computer/deadlines/core.mjs create mode 100644 system/public/papers.aesthetic.computer/deadlines/feed.xml create mode 100644 system/public/papers.aesthetic.computer/deadlines/icon.svg create mode 100644 system/public/papers.aesthetic.computer/deadlines/opportunities.json create mode 100644 system/public/papers.aesthetic.computer/deadlines/style.css create mode 100644 utilities/deadlines/README.md create mode 100644 utilities/deadlines/build.mjs create mode 100644 utilities/deadlines/core.test.mjs create mode 100644 utilities/deadlines/template.html diff --git a/system/netlify/functions/docs.js b/system/netlify/functions/docs.js index 3410439c44..b00f416a81 100644 --- a/system/netlify/functions/docs.js +++ b/system/netlify/functions/docs.js @@ -2928,7 +2928,7 @@ void draw() { }, deadlines: { sig: "deadlines", - desc: "Open papers conference & grant deadlines.", + desc: "Find new media open calls, residencies & artist funding.", url: "https://papers.aesthetic.computer/deadlines", done: true, }, diff --git a/system/public/papers.aesthetic.computer/deadlines/.gitattributes b/system/public/papers.aesthetic.computer/deadlines/.gitattributes new file mode 100644 index 0000000000..6553e9753c --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/.gitattributes @@ -0,0 +1,2 @@ +# iCalendar requires CRLF; folded text may end a physical line with a space. +*.ics -text whitespace=-blank-at-eol diff --git a/system/public/papers.aesthetic.computer/deadlines/app.mjs b/system/public/papers.aesthetic.computer/deadlines/app.mjs new file mode 100644 index 0000000000..5218e3a88b --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/app.mjs @@ -0,0 +1,108 @@ +import { validateCatalog, filterItems, renderCard, makeCalendar, deadlineState } from "./core.mjs"; + +const form = document.querySelector("#filters"); +const root = document.querySelector("#opportunities"); +const count = document.querySelector("#result-count"); +const empty = document.querySelector("#empty"); +const viewLinks = [...document.querySelectorAll("[data-view]")]; +const keys = ["q", "discipline", "kind", "paid", "free", "remote", "international", "sort"]; +const flags = ["paid", "free", "remote", "international"]; +const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; +let catalog, view = "upcoming"; + +function readURL() { + const p = new URLSearchParams(location.search); + view = ["upcoming", "rolling", "archive"].includes(p.get("view")) ? p.get("view") : "upcoming"; + for (const key of keys) { + const input = form.elements.namedItem(key); + if (flags.includes(key)) input.checked = p.get(key) === "1"; + else input.value = p.get(key) || (key === "sort" ? "deadline" : ""); + } +} +function filters() { + const data = new FormData(form); + return {view, ...Object.fromEntries(keys.map((key) => [key, flags.includes(key) ? data.has(key) : data.get(key)]))}; +} +function writeURL() { + const f = filters(), params = new URLSearchParams(); + for (const [key, value] of Object.entries(f)) { + if (!value || (key === "view" && value === "upcoming") || (key === "sort" && value === "deadline")) continue; + params.set(key, value === true ? "1" : value); + } + history.replaceState(null, "", location.pathname + (params.size ? "?" + params : "") + location.hash); +} +function render({updateURL = true, focusHash = false} = {}) { + if (!catalog) return; + const items = filterItems(catalog.opportunities, filters()); + const expanded = [...root.querySelectorAll("article:has(details[open])")].map((x) => x.id); + root.innerHTML = items.map((item) => renderCard(item, new Date(), timeZone)).join(""); + for (const id of expanded) { + const details = document.getElementById(id)?.querySelector("details"); + if (details) details.open = true; + } + count.textContent = `${items.length} ${items.length === 1 ? "opportunity" : "opportunities"}`; + empty.hidden = items.length > 0; + for (const link of viewLinks) { + if (link.dataset.view === view) link.setAttribute("aria-current", "page"); + else link.removeAttribute("aria-current"); + } + if (updateURL) writeURL(); + if (focusHash && location.hash) { + const target = document.getElementById(location.hash.slice(1)); + if (target?.classList.contains("opportunity")) { target.querySelector("details").open = true; target.scrollIntoView(); } + } +} +function revealHash() { + if (!catalog || !location.hash) return; + const item = catalog.opportunities.find((x) => "#" + x.id === location.hash); + if (!item) return; + if (!filterItems(catalog.opportunities, filters()).some((x) => x.id === item.id)) { + form.reset(); + view = deadlineState(item) === "closed" ? "archive" : "upcoming"; + } + render({focusHash: true}); +} +function reset() { + form.reset(); view = "upcoming"; + history.replaceState(null, "", location.pathname); + // Reset values are applied after the reset event finishes. + queueMicrotask(() => render()); +} +form.addEventListener("submit", (event) => event.preventDefault()); +form.addEventListener("input", () => render()); +form.addEventListener("change", () => render()); +form.elements.namedItem("sort").addEventListener("change", () => render()); +form.addEventListener("reset", () => { view = "upcoming"; queueMicrotask(() => render()); }); +document.querySelector("#clear-empty").addEventListener("click", reset); +for (const link of viewLinks) link.addEventListener("click", (event) => { + if (!catalog || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + event.preventDefault(); view = link.dataset.view; render(); +}); +window.addEventListener("popstate", () => { readURL(); render({updateURL: false, focusHash: true}); }); +window.addEventListener("hashchange", revealHash); +root.addEventListener("click", (event) => { + const button = event.target.closest("[data-calendar]"); + if (!button || !catalog) return; + const item = catalog.opportunities.find((x) => x.id === button.dataset.calendar); + if (!item?.deadline) return; + const url = URL.createObjectURL(new Blob([makeCalendar([item], catalog.updated)], {type: "text/calendar;charset=utf-8"})); + const link = document.createElement("a"); + link.href = url; link.download = `${item.id}.ics`; link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); +}); +async function load() { + try { + const response = await fetch(new URL("./opportunities.json", import.meta.url), {cache: "no-cache"}); + if (!response.ok) throw new Error(`Catalog returned ${response.status}`); + catalog = validateCatalog(await response.json()); + readURL(); render({updateURL: false}); revealHash(); + form.hidden = false; document.querySelector("#sort-label").hidden = false; + document.querySelector("#load-error").hidden = true; + } catch (error) { + console.error("Deadlines catalog:", error.message); + document.querySelector("#load-error").hidden = false; + } +} +document.querySelector("#retry").addEventListener("click", load); +document.addEventListener("visibilitychange", () => { if (!document.hidden && catalog) render({updateURL: false}); }); +await load(); diff --git a/system/public/papers.aesthetic.computer/deadlines/calendar.ics b/system/public/papers.aesthetic.computer/deadlines/calendar.ics new file mode 100644 index 0000000000..0980d465be --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/calendar.ics @@ -0,0 +1,199 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Aesthetic Computer//Deadlines//EN +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:AC — New media deadlines +X-PUBLISHED-TTL:PT12H +BEGIN:VEVENT +UID:interplay-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART:20261009T065900Z +SUMMARY:Interplay 2027 — deadline +DESCRIPTION:A 15–45 minute live online performance at the intersection of + performance and technology.\n\nArtist fee per selected artist or group.\n + Application fee not stated\n\nEligibility: International\, all disciplines + and career stages. Priority for projects led by women\, femmes and gender + -diverse artists.\n\nDeadline: 2026-10-08. 23:59 Pacific\n\nSelected artis + ts provide a technical breakdown\, a five-minute highlight edit and develo + pment-process documentation. Live performance must be central to the work. + \n\nOfficial call: https://interplay-arts.com/open_call.html\nChecked 2026 + -10-06 +URL:https://interplay-arts.com/open_call.html +LOCATION:Online +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:djerassi-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART;VALUE=DATE:20261009 +DTEND;VALUE=DATE:20261010 +SUMMARY:Djerassi Artist Residencies — deadline (check time) +DESCRIPTION:One month of studio time in the Santa Cruz Mountains for compos + ers\, media artists and other disciplines.\n\nResidency provided at no cos + t. Cash stipend not confirmed\; travel is self-funded.\n$35 application fe + e · reconfirm in form\n\nEligibility: National and international artists. + Apply in one discipline\; attendance for the full residency is required.\ + n\nDeadline: 2026-10-09. Cutoff time not confirmed. This calendar entry is + all-day\; verify the cutoff with the organizer.\n\nNo shortened or partia + l residencies. Collaborators and couples must apply individually. Full for + m requirements\, including references\, have not been verified.\n\nOfficia + l call: https://djerassi.org/apply/application-guidelines/\nChecked 2026-1 + 0-06 +URL:https://djerassi.org/apply/application-guidelines/ +LOCATION:Woodside\, California\, US +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:smallforms-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART;VALUE=DATE:20261011 +DTEND;VALUE=DATE:20261012 +SUMMARY:smallforms — Solo Sets — deadline (check time) +DESCRIPTION:Solo experimental music and sound performances\, with an option + al recording session for a digital release.\n\nConfirmed artist fee. Trave + l and accommodation support not stated.\nApplication fee not stated\n\nEli + gibility: Musicians\, sound artists and performers. One artist per proposa + l\; no geographic restriction stated.\n\nDeadline: 2026-10-11. Cutoff time + not stated. This calendar entry is all-day\; verify the cutoff with the o + rganizer.\n\nIn-person solo performance. Recording-session releases use th + e smallforms label\; confirm release terms if selected.\n\nOfficial call: + https://smallforms.org/open-call-2027/\nChecked 2026-10-06 +URL:https://smallforms.org/open-call-2027/ +LOCATION:Vienna\, Austria +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:eyebeam-agency-2026@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART:20261012T035900Z +SUMMARY:Eyebeam — AGENCY — deadline +DESCRIPTION:A virtual residency for early-stage work exploring agency throu + gh art and technology.\n\nStipend\, mentorship\, technical consultation an + d weekly residency programming.\nFree to apply\n\nEligibility: Individuals + based in the US outside NYC\, remaining US-based throughout. No full-time + degree enrollment or overlapping residency commitments.\n\nDeadline: 2026 + -10-11. 23:59 Eastern\n\nWeekly cohort participation and independent proje + ct work. The live application specifies 3–4 cohort hours weekly plus ind + ependent work\; confirm the schedule before accepting.\n\nOfficial call: h + ttps://eyebeam.org/articles/agency-virtual-residency\nChecked 2026-10-06 +URL:https://eyebeam.org/articles/agency-virtual-residency +LOCATION:Online · US outside New York City +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:music-for-your-inbox-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART;VALUE=DATE:20261015 +DTEND;VALUE=DATE:20261016 +SUMMARY:Music for Your Inbox — deadline (check time) +DESCRIPTION:Unpremiered experimental sound and music films for an online se + ries and library screenings.\n\nHonorarium. Limited production support is + available for co-produced films.\nApplication fee not stated\n\nEligibilit + y: Artists and artist teams\; no geographic restriction stated. Music or s + ound must be central. Films must be 10–20 minutes and not previously pre + miered.\n\nDeadline: 2026-10-15. 23:59 PST as published\; daylight-saving + ambiguity — submit early. This calendar entry is all-day\; verify the cu + toff with the organizer.\n\nOne year of exclusive presentation rights. Ord + inary concert documentation is excluded. In-person attendance is not requi + red. The organizer discourages creating a new project solely for this call + .\n\nOfficial call: https://www.musicforyourinbox.com/apply\nChecked 2026- + 10-06 +URL:https://www.musicforyourinbox.com/apply +LOCATION:Online · Los Angeles screenings +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:lendhauer-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART;VALUE=DATE:20261102 +DTEND;VALUE=DATE:20261103 +SUMMARY:Lendhauer — Urban Ecologies — deadline (check time) +DESCRIPTION:Public artworks exploring coexistence\, control and interdepend + ence at Klagenfurt’s Lendhafen.\n\nUp to €2\,500 gross artist fee\, + €4\,000 gross production and €1\,000 travel\, per selected project.\nA + pplication fee not stated\n\nEligibility: Individuals and collectives in A + ustria and abroad\, including artists\, designers\, performers and interdi + sciplinary practitioners.\n\nDeadline: 2026-11-02. Cutoff time not stated. + This calendar entry is all-day\; verify the cutoff with the organizer.\n\ + nThe proposal must engage with the actual harbor and its social\, ecologic + al and material conditions. Timing is agreed with selected artists. Fee is + per project\, not per collaborator.\n\nOfficial call: https://www.lendhaf + en.at/open-call-2027-urban-ecologies-coexistence-and-control/\nChecked 202 + 6-10-06 +URL:https://www.lendhafen.at/open-call-2027-urban-ecologies-coexistence-and + -control/ +LOCATION:Klagenfurt\, Austria +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:spot-flux-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART;VALUE=DATE:20261105 +DTEND;VALUE=DATE:20261106 +SUMMARY:SPOT FLUX — Sound Art — deadline (check time) +DESCRIPTION:Existing sound works\, music films and performances for SPOT Fe + stival’s experimental program.\n\nDanish Musicians’ Union rate per per + former\, with approved technical and material costs. Exact fee not stated. + \nApplication fee not stated\n\nEligibility: The work and/or artists must + have a connection to Denmark. Existing works only\; the artist must attend + the presentation.\n\nDeadline: 2026-11-05. 14:00 as published\; time zone + not explicitly stated. This calendar entry is all-day\; verify the cutoff + with the organizer.\n\nTravel\, accommodation and preparation expenses ar + e not covered. The Café Stage is an open\, busy space\; quiet cannot be g + uaranteed.\n\nOfficial call: https://spotfestival.dk/spot-flux-2027-open-c + all/\nChecked 2026-10-06 +URL:https://spotfestival.dk/spot-flux-2027-open-call/ +LOCATION:Aarhus\, Denmark +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:amsterdam-light-festival-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART:20261207T225900Z +SUMMARY:Amsterdam Light Festival — deadline +DESCRIPTION:Light artworks for the canals of Amsterdam\, with technical and + financial support to realize selected concepts.\n\nArtist fee plus produc + tion and travel costs. General-call production ceiling is not specified in + the FAQ.\nApplication fee not confirmed\n\nEligibility: The general open + call is worldwide\, including people new to light art. The separate Amarte + call is restricted to Netherlands-based makers.\n\nDeadline: 2026-12-07. + 23:59 CET\n\nWork must withstand two months outdoors and avoid nuisance to + residents\, including sound\, smoke and lasers. Full briefing and contrac + t terms need review.\n\nOfficial call: https://amsterdamlightfestival.com/ + en/open-call\nChecked 2026-10-06 +URL:https://amsterdamlightfestival.com/en/open-call +LOCATION:Amsterdam\, Netherlands +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:openfeed-2026-2027@papers.aesthetic.computer +DTSTAMP:20261006T000000Z +LAST-MODIFIED:20261006T000000Z +DTSTART;VALUE=DATE:20270228 +DTEND;VALUE=DATE:20270301 +SUMMARY:OPENFEED — deadline (check time) +DESCRIPTION:A virtual residency spanning interactive installations\, softwa + re art\, sound\, electronics and participatory work.\n\nHardware\, computi + ng\, software and mentorship. Cash support and exact program dates are not + confirmed.\nFree to apply · form checked Oct 5\n\nEligibility: Internati + onal artists working across digital\, electronic and experimental media. I + ndividual and participatory practices welcome.\n\nDeadline: 2027-02-28. Ro + lling review within the October 1–February 28 application window. This c + alendar entry is all-day\; verify the cutoff with the organizer.\n\nDevelo + pment may combine an online edition with an on-site presentation. Confirm + resources\, dates and any attendance requirements before accepting.\n\nOff + icial call: https://www.feed.art/open-feed\nChecked 2026-10-06 +URL:https://www.feed.art/open-feed +LOCATION:Online · FEED Media Art Center\, US +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR diff --git a/system/public/papers.aesthetic.computer/deadlines/core.mjs b/system/public/papers.aesthetic.computer/deadlines/core.mjs new file mode 100644 index 0000000000..bf1de4a0cb --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/core.mjs @@ -0,0 +1,166 @@ +// Shared by the public board and the feed builder. No personal application data. +export const BASE = "https://papers.aesthetic.computer/deadlines/"; +export const DISCIPLINES = ["Creative coding", "Digital art", "Sound", "Performance", "Moving image", "Games", "Open source", "Research"]; +export const KINDS = ["Open call", "Commission", "Performance", "Residency", "Fellowship", "Grant", "Research"]; +export const escapeHTML = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&", "<":"<", ">":">", '"':""", "'":"'"})[c]); +const day = 86_400_000; +const isoDate = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s) && !Number.isNaN(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s; + +function fields(value, required, optional = []) { + if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected an object"); + for (const key of required) if (!(key in value)) throw new Error(`Missing field: ${key}`); + for (const key of Object.keys(value)) if (![...required, ...optional].includes(key)) throw new Error(`Unapproved public field: ${key}`); +} +function text(value, label) { + if (typeof value !== "string" || !value.trim() || value.length > 1800) throw new Error(`Invalid ${label}`); + if (/<[^>]+>|vscode:|\/Users\/|draftPath|aesthetic-computer-vault/i.test(value)) throw new Error(`Private path or markup in ${label}`); +} +function sourceURL(value) { + const url = new URL(value); + if (url.protocol !== "https:" || url.username || url.password || /^(localhost|127\.|\[::1\])/.test(url.hostname)) throw new Error("Public sources must use HTTPS"); +} +export function validateCatalog(catalog) { + fields(catalog, ["version", "updated", "opportunities"]); + if (catalog.version !== 1 || !isoDate(catalog.updated) || !Array.isArray(catalog.opportunities)) throw new Error("Invalid catalog metadata"); + const ids = new Set(); + for (const item of catalog.opportunities) { + fields(item, ["id", "title", "kind", "disciplines", "summary", "location", "remote", "dates", "deadline", "support", "fee", "eligibility", "requirements", "terms", "source", "checked", "published"], ["sources"]); + if (!/^[a-z][a-z0-9-]+$/.test(item.id) || ids.has(item.id)) throw new Error(`Invalid or duplicate id: ${item.id}`); + ids.add(item.id); + for (const key of ["title", "summary", "location", "dates", "requirements", "terms"]) text(item[key], key); + if (!KINDS.includes(item.kind) || !Array.isArray(item.disciplines) || !item.disciplines.length || item.disciplines.some((d) => !DISCIPLINES.includes(d))) throw new Error(`Invalid classification: ${item.id}`); + if (typeof item.remote !== "boolean") throw new Error("Invalid remote flag"); + for (const key of ["checked", "published"]) if (!isoDate(item[key]) || item[key] > catalog.updated) throw new Error(`Invalid ${key}: ${item.id}`); + sourceURL(item.source); + if (item.sources !== undefined && !Array.isArray(item.sources)) throw new Error("Invalid sources"); + item.sources?.forEach(sourceURL); + fields(item.support, ["paid", "text", "detail"]); + if (typeof item.support.paid !== "boolean") throw new Error("Invalid funding flag"); + text(item.support.text, "support"); text(item.support.detail, "support detail"); + fields(item.fee, ["status", "text"]); + if (!["free", "paid", "unknown"].includes(item.fee.status)) throw new Error("Invalid fee status"); + text(item.fee.text, "fee"); + fields(item.eligibility, ["international", "text"]); + if (typeof item.eligibility.international !== "boolean") throw new Error("Invalid eligibility flag"); + text(item.eligibility.text, "eligibility"); + if (item.deadline !== null) { + fields(item.deadline, ["date", "at", "note"], ["rolling"]); + if (item.deadline.rolling !== undefined && typeof item.deadline.rolling !== "boolean") throw new Error("Invalid rolling flag"); + if (!isoDate(item.deadline.date)) throw new Error(`Invalid deadline date: ${item.id}`); + text(item.deadline.note, "deadline note"); + if (item.deadline.at !== null && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(item.deadline.at) || !Number.isFinite(Date.parse(item.deadline.at)) || item.deadline.at.slice(0, 10) !== item.deadline.date)) throw new Error(`Invalid deadline instant: ${item.id}`); + } + } + return catalog; +} + +export function deadlineState(item, now = new Date()) { + if (!item.deadline) return "rolling"; + // A date-only deadline is kept visible through that date everywhere on Earth. + // Its exact cutoff is never inferred or represented as a midnight timestamp. + const cutoff = item.deadline.at ? Date.parse(item.deadline.at) : Date.parse(item.deadline.date + "T00:00:00Z") + 36 * 3_600_000; + return now.getTime() >= cutoff ? "closed" : item.deadline.rolling ? "rolling" : "open"; +} + +export function filterItems(items, filters = {}, now = new Date()) { + const query = (filters.q || "").normalize("NFKD").toLowerCase(); + return items.filter((item) => { + const state = deadlineState(item, now); + if (filters.view === "archive" ? state !== "closed" : filters.view === "rolling" ? state !== "rolling" : state === "closed") return false; + if (filters.discipline && !item.disciplines.includes(filters.discipline)) return false; + if (filters.kind && item.kind !== filters.kind) return false; + if (filters.paid && !item.support.paid) return false; + if (filters.free && item.fee.status !== "free") return false; + if (filters.remote && !item.remote) return false; + if (filters.international && !item.eligibility.international) return false; + const haystack = [item.title, item.summary, item.location, item.kind, ...item.disciplines, item.eligibility.text, item.support.text].join(" ").normalize("NFKD").toLowerCase(); + return !query || haystack.includes(query); + }).sort((a, b) => { + if (filters.sort === "newest") return b.published.localeCompare(a.published) || a.title.localeCompare(b.title); + const instant = (x) => !x.deadline ? Number.MAX_SAFE_INTEGER : x.deadline.at + ? Date.parse(x.deadline.at) : Date.parse(x.deadline.date) + 36 * 3_600_000; + const cmp = instant(a) - instant(b); + return filters.view === "archive" ? -cmp : cmp; + }); +} + +export function calendarDate(date) { + return new Intl.DateTimeFormat("en", {month: "short", day: "numeric", year: "numeric", timeZone: "UTC"}).format(new Date(date + "T12:00:00Z")); +} +export function dateParts(item, timeZone = "UTC") { + if (!item.deadline) return {month: "ANY", number: "∞", year: "Rolling"}; + const date = new Date(item.deadline.at || item.deadline.date + "T12:00:00Z"); + const parts = new Intl.DateTimeFormat("en", {month: "short", day: "numeric", year: "numeric", timeZone: item.deadline.at ? timeZone : "UTC"}).formatToParts(date); + return {month: parts.find((p) => p.type === "month").value, number: parts.find((p) => p.type === "day").value, year: parts.find((p) => p.type === "year").value}; +} +export function countdown(item, now = new Date()) { + const state = deadlineState(item, now); + if (state === "closed") return "Closed"; + if (state === "rolling") return "Rolling"; + if (item.deadline.at) { + const hours = Math.ceil((Date.parse(item.deadline.at) - now.getTime()) / 3_600_000); + if (hours < 24) return hours <= 1 ? "Under 1 hour" : `${hours} hours left`; + return `${Math.ceil(hours / 24)} days left`; + } + const remaining = Math.round((Date.parse(item.deadline.date) - Date.parse(now.toISOString().slice(0, 10))) / day); + if (remaining <= 0) return "Check cutoff"; + return remaining === 1 ? "Tomorrow · check time" : `${remaining} days · check time`; +} + +export function renderCard(item, now = new Date(), timeZone = "UTC") { + const e = escapeHTML, p = dateParts(item, timeZone), state = deadlineState(item, now); + const soon = item.deadline && state !== "closed" && Date.parse(item.deadline.date) - now < 7 * day; + const deadline = item.deadline?.at + ? `${new Intl.DateTimeFormat("en", {dateStyle: "medium", timeStyle: "short", timeZone}).format(new Date(item.deadline.at))} (${timeZone}). Source: ${item.deadline.note}.` + : item.deadline ? `${calendarDate(item.deadline.date)}. ${item.deadline.note}.` : "Rolling applications. No fixed deadline published."; + const sources = [item.source, ...(item.sources || [])].map((url, i) => `${i ? "Additional source" : "Official call"} ↗`).join(" · "); + return `
+
${e(p.month)}${e(p.number)}${e(p.year)}
+
${e(item.kind)}${e(countdown(item, now))}
+

${e(item.title)}

+

${e(item.summary)}

${e(item.location)} ${e(item.dates)}

+
    ${item.disciplines.map((d) => `
  • ${e(d)}
  • `).join("")}
+
+

${e(item.support.text)}

${e(item.fee.text)}

+
Eligibility & details
+

Who can apply

${e(item.eligibility.text)}

Support

${e(item.support.detail)}

+

Application

${e(item.requirements)}

Terms

${e(item.terms)}

+

Deadline: ${e(deadline)}

${sources} · Checked ${e(calendarDate(item.checked))}

${item.deadline ? `` : ""}
+
+
`; +} + +export const escapeICS = (s) => String(s).replace(/\\/g, "\\\\").replace(/\r?\n/g, "\\n").replace(/;/g, "\\;").replace(/,/g, "\\,"); +export function foldICS(line) { + const encoder = new TextEncoder(); + let result = "", current = "", bytes = 0; + for (const c of line) { + const length = encoder.encode(c).length; + if (bytes + length > 75) { result += current + "\r\n"; current = " "; bytes = 1; } + current += c; bytes += length; + } + return result + current; +} +const compact = (date) => new Date(date).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); +export function makeCalendar(items) { + const lines = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Aesthetic Computer//Deadlines//EN", "CALSCALE:GREGORIAN", "METHOD:PUBLISH", "X-WR-CALNAME:AC — New media deadlines", "X-PUBLISHED-TTL:PT12H"]; + for (const item of items.filter((x) => x.deadline)) { + lines.push("BEGIN:VEVENT", `UID:${item.id}@papers.aesthetic.computer`, `DTSTAMP:${compact(item.published + "T00:00:00Z")}`, `LAST-MODIFIED:${compact(item.checked + "T00:00:00Z")}`); + if (item.deadline.at) lines.push(`DTSTART:${compact(item.deadline.at)}`); + else { + lines.push(`DTSTART;VALUE=DATE:${item.deadline.date.replaceAll("-", "")}`); + lines.push(`DTEND;VALUE=DATE:${new Date(Date.parse(item.deadline.date) + day).toISOString().slice(0, 10).replaceAll("-", "")}`); + } + const description = `${item.summary}\n\n${item.support.detail}\n${item.fee.text}\n\nEligibility: ${item.eligibility.text}\n\nDeadline: ${item.deadline.date}. ${item.deadline.note}${item.deadline.at ? "" : ". This calendar entry is all-day; verify the cutoff with the organizer."}\n\n${item.terms}\n\nOfficial call: ${item.source}\nChecked ${item.checked}`; + lines.push(`SUMMARY:${escapeICS(item.title + " — deadline" + (item.deadline.at ? "" : " (check time)"))}`, `DESCRIPTION:${escapeICS(description)}`, `URL:${item.source}`, `LOCATION:${escapeICS(item.location)}`, "TRANSP:TRANSPARENT", "END:VEVENT"); + } + lines.push("END:VCALENDAR"); + return lines.map(foldICS).join("\r\n") + "\r\n"; +} + +export function makeRSS(catalog) { + const e = escapeHTML; + return `\nAC — New media deadlines${BASE}Reviewed opportunities for creative coding, digital art, experimental sound and new media.en${new Date(catalog.updated + "T12:00:00Z").toUTCString()} +${[...catalog.opportunities].sort((a, b) => b.published.localeCompare(a.published)).map((item) => `${e(item.title)}${BASE}#${item.id}${BASE}#${item.id}${new Date(item.published + "T12:00:00Z").toUTCString()}${e(`${item.summary}\n\nDeadline: ${item.deadline ? item.deadline.date + ". " + item.deadline.note : "Rolling"}\n${item.support.detail}\n${item.fee.text}\n\nEligibility: ${item.eligibility.text}\n\n${item.terms}\n\nOfficial call: ${item.source}\nChecked ${item.checked}`)}${item.disciplines.map((d) => `${e(d)}`).join("")}`).join("\n")} +\n`; +} diff --git a/system/public/papers.aesthetic.computer/deadlines/feed.xml b/system/public/papers.aesthetic.computer/deadlines/feed.xml new file mode 100644 index 0000000000..1e6a21c19a --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/feed.xml @@ -0,0 +1,123 @@ + +AC — New media deadlineshttps://papers.aesthetic.computer/deadlines/Reviewed opportunities for creative coding, digital art, experimental sound and new media.enTue, 06 Oct 2026 12:00:00 GMT +Interplay 2027https://papers.aesthetic.computer/deadlines/#interplay-2027https://papers.aesthetic.computer/deadlines/#interplay-2027Tue, 06 Oct 2026 12:00:00 GMTA 15–45 minute live online performance at the intersection of performance and technology. + +Deadline: 2026-10-08. 23:59 Pacific +Artist fee per selected artist or group. +Application fee not stated + +Eligibility: International, all disciplines and career stages. Priority for projects led by women, femmes and gender-diverse artists. + +Selected artists provide a technical breakdown, a five-minute highlight edit and development-process documentation. Live performance must be central to the work. + +Official call: https://interplay-arts.com/open_call.html +Checked 2026-10-06SoundPerformanceCreative coding +Djerassi Artist Residencieshttps://papers.aesthetic.computer/deadlines/#djerassi-2027https://papers.aesthetic.computer/deadlines/#djerassi-2027Tue, 06 Oct 2026 12:00:00 GMTOne month of studio time in the Santa Cruz Mountains for composers, media artists and other disciplines. + +Deadline: 2026-10-09. Cutoff time not confirmed +Residency provided at no cost. Cash stipend not confirmed; travel is self-funded. +$35 application fee · reconfirm in form + +Eligibility: National and international artists. Apply in one discipline; attendance for the full residency is required. + +No shortened or partial residencies. Collaborators and couples must apply individually. Full form requirements, including references, have not been verified. + +Official call: https://djerassi.org/apply/application-guidelines/ +Checked 2026-10-06SoundDigital artPerformance +smallforms — Solo Setshttps://papers.aesthetic.computer/deadlines/#smallforms-2027https://papers.aesthetic.computer/deadlines/#smallforms-2027Tue, 06 Oct 2026 12:00:00 GMTSolo experimental music and sound performances, with an optional recording session for a digital release. + +Deadline: 2026-10-11. Cutoff time not stated +Confirmed artist fee. Travel and accommodation support not stated. +Application fee not stated + +Eligibility: Musicians, sound artists and performers. One artist per proposal; no geographic restriction stated. + +In-person solo performance. Recording-session releases use the smallforms label; confirm release terms if selected. + +Official call: https://smallforms.org/open-call-2027/ +Checked 2026-10-06SoundPerformance +Eyebeam — AGENCYhttps://papers.aesthetic.computer/deadlines/#eyebeam-agency-2026https://papers.aesthetic.computer/deadlines/#eyebeam-agency-2026Tue, 06 Oct 2026 12:00:00 GMTA virtual residency for early-stage work exploring agency through art and technology. + +Deadline: 2026-10-11. 23:59 Eastern +Stipend, mentorship, technical consultation and weekly residency programming. +Free to apply + +Eligibility: Individuals based in the US outside NYC, remaining US-based throughout. No full-time degree enrollment or overlapping residency commitments. + +Weekly cohort participation and independent project work. The live application specifies 3–4 cohort hours weekly plus independent work; confirm the schedule before accepting. + +Official call: https://eyebeam.org/articles/agency-virtual-residency +Checked 2026-10-06Digital artCreative codingResearch +Music for Your Inboxhttps://papers.aesthetic.computer/deadlines/#music-for-your-inbox-2027https://papers.aesthetic.computer/deadlines/#music-for-your-inbox-2027Tue, 06 Oct 2026 12:00:00 GMTUnpremiered experimental sound and music films for an online series and library screenings. + +Deadline: 2026-10-15. 23:59 PST as published; daylight-saving ambiguity — submit early +Honorarium. Limited production support is available for co-produced films. +Application fee not stated + +Eligibility: Artists and artist teams; no geographic restriction stated. Music or sound must be central. Films must be 10–20 minutes and not previously premiered. + +One year of exclusive presentation rights. Ordinary concert documentation is excluded. In-person attendance is not required. The organizer discourages creating a new project solely for this call. + +Official call: https://www.musicforyourinbox.com/apply +Checked 2026-10-06SoundMoving imagePerformance +Lendhauer — Urban Ecologieshttps://papers.aesthetic.computer/deadlines/#lendhauer-2027https://papers.aesthetic.computer/deadlines/#lendhauer-2027Tue, 06 Oct 2026 12:00:00 GMTPublic artworks exploring coexistence, control and interdependence at Klagenfurt’s Lendhafen. + +Deadline: 2026-11-02. Cutoff time not stated +Up to €2,500 gross artist fee, €4,000 gross production and €1,000 travel, per selected project. +Application fee not stated + +Eligibility: Individuals and collectives in Austria and abroad, including artists, designers, performers and interdisciplinary practitioners. + +The proposal must engage with the actual harbor and its social, ecological and material conditions. Timing is agreed with selected artists. Fee is per project, not per collaborator. + +Official call: https://www.lendhafen.at/open-call-2027-urban-ecologies-coexistence-and-control/ +Checked 2026-10-06Digital artSoundPerformanceResearch +SPOT FLUX — Sound Arthttps://papers.aesthetic.computer/deadlines/#spot-flux-2027https://papers.aesthetic.computer/deadlines/#spot-flux-2027Tue, 06 Oct 2026 12:00:00 GMTExisting sound works, music films and performances for SPOT Festival’s experimental program. + +Deadline: 2026-11-05. 14:00 as published; time zone not explicitly stated +Danish Musicians’ Union rate per performer, with approved technical and material costs. Exact fee not stated. +Application fee not stated + +Eligibility: The work and/or artists must have a connection to Denmark. Existing works only; the artist must attend the presentation. + +Travel, accommodation and preparation expenses are not covered. The Café Stage is an open, busy space; quiet cannot be guaranteed. + +Official call: https://spotfestival.dk/spot-flux-2027-open-call/ +Checked 2026-10-06SoundPerformanceMoving image +Amsterdam Light Festivalhttps://papers.aesthetic.computer/deadlines/#amsterdam-light-festival-2027https://papers.aesthetic.computer/deadlines/#amsterdam-light-festival-2027Tue, 06 Oct 2026 12:00:00 GMTLight artworks for the canals of Amsterdam, with technical and financial support to realize selected concepts. + +Deadline: 2026-12-07. 23:59 CET +Artist fee plus production and travel costs. General-call production ceiling is not specified in the FAQ. +Application fee not confirmed + +Eligibility: The general open call is worldwide, including people new to light art. The separate Amarte call is restricted to Netherlands-based makers. + +Work must withstand two months outdoors and avoid nuisance to residents, including sound, smoke and lasers. Full briefing and contract terms need review. + +Official call: https://amsterdamlightfestival.com/en/open-call +Checked 2026-10-06Digital artCreative coding +OPENFEEDhttps://papers.aesthetic.computer/deadlines/#openfeed-2026-2027https://papers.aesthetic.computer/deadlines/#openfeed-2026-2027Tue, 06 Oct 2026 12:00:00 GMTA virtual residency spanning interactive installations, software art, sound, electronics and participatory work. + +Deadline: 2027-02-28. Rolling review within the October 1–February 28 application window +Hardware, computing, software and mentorship. Cash support and exact program dates are not confirmed. +Free to apply · form checked Oct 5 + +Eligibility: International artists working across digital, electronic and experimental media. Individual and participatory practices welcome. + +Development may combine an online edition with an on-site presentation. Confirm resources, dates and any attendance requirements before accepting. + +Official call: https://www.feed.art/open-feed +Checked 2026-10-06Digital artCreative codingSoundPerformance +Sequoia Open Source Fellowshiphttps://papers.aesthetic.computer/deadlines/#sequoia-open-sourcehttps://papers.aesthetic.computer/deadlines/#sequoia-open-sourceTue, 06 Oct 2026 12:00:00 GMTLiving-expense support for developers sustaining important open-source projects. + +Deadline: Rolling +Six to twelve months of support, equity-free. Amount is individualized rather than a published fixed award. +Free to apply · form checked Oct 5 + +Eligibility: Open-source developers; no geographic restriction stated in the reviewed application. A small cohort is selected each year. + +No equity or required travel. Confirm the support amount and work expectations with the organizer. + +Official call: https://sequoiacap.com/oss +Checked 2026-10-05Open sourceCreative coding + diff --git a/system/public/papers.aesthetic.computer/deadlines/icon.svg b/system/public/papers.aesthetic.computer/deadlines/icon.svg new file mode 100644 index 0000000000..0fbd89c976 --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/icon.svg @@ -0,0 +1 @@ + diff --git a/system/public/papers.aesthetic.computer/deadlines/index.html b/system/public/papers.aesthetic.computer/deadlines/index.html index 0a4637a782..6e293018a3 100644 --- a/system/public/papers.aesthetic.computer/deadlines/index.html +++ b/system/public/papers.aesthetic.computer/deadlines/index.html @@ -1,2838 +1,183 @@ - + - - - - - deadlines · papers · Aesthetic Computer - - - - - - - - - - - - - - - - - - + + + + Deadlines · Aesthetic Computer + + + + + + + + + + + - - AC - - -
- ← papers - · - platter + +
+
+ +

deadlines.

New media opportunities.

+
+
+ +
10 opportunities
+ + +
+
Oct92026
+
Performance3 days left
+

Interplay 2027

+

A 15–45 minute live online performance at the intersection of performance and technology.

Online February 10–12, 2027

+
  • Sound
  • Performance
  • Creative coding
- - - -
-

deadlines.

-
Conference, journal, grant, and residency deadlines tracked for Aesthetic Computer papers and adjacent opportunities. Submission state lives in SUBMISSIONS.md; draft state lives in SCORE.md.
-
+

CAD $1,500

Application fee not stated

+
Eligibility & details
+

Who can apply

International, all disciplines and career stages. Priority for projects led by women, femmes and gender-diverse artists.

Support

Artist fee per selected artist or group.

+

Application

Bio, proposal, duration, three descriptive words, up to five minutes of support footage, and 3–5 promotional images.

Terms

Selected artists provide a technical breakdown, a five-minute highlight edit and development-process documentation. Live performance must be central to the work.

+

Deadline: Oct 9, 2026, 6:59 AM (UTC). Source: 23:59 Pacific.

Official call ↗ · Checked Oct 6, 2026

+
+
+
+
Oct92026
+
Residency3 days · check time
+

Djerassi Artist Residencies

+

One month of studio time in the Santa Cruz Mountains for composers, media artists and other disciplines.

Woodside, California, US 2027 · one month

+
  • Sound
  • Digital art
  • Performance
- -
- -
+
+
Oct122026
+
Residency6 days left
+

Eyebeam — AGENCY

+

A virtual residency for early-stage work exploring agency through art and technology.

Online · US outside New York City November 4, 2026–February 12, 2027

+
  • Digital art
  • Creative coding
  • Research
+
+

$2,000

Free to apply

+
Eligibility & details
+

Who can apply

Individuals based in the US outside NYC, remaining US-based throughout. No full-time degree enrollment or overlapping residency commitments.

Support

Stipend, mentorship, technical consultation and weekly residency programming.

+

Application

Written answers, CV, work samples and a personal 1–2 minute introduction video, via Submittable.

Terms

Weekly cohort participation and independent project work. The live application specifies 3–4 cohort hours weekly plus independent work; confirm the schedule before accepting.

+

Deadline: Oct 12, 2026, 3:59 AM (UTC). Source: 23:59 Eastern.

Official call ↗ · Checked Oct 6, 2026

+
+
+
+
Oct112026
+
Performance5 days · check time
+

smallforms — Solo Sets

+

Solo experimental music and sound performances, with an optional recording session for a digital release.

Vienna, Austria March–December 2027 · 30–60 minute set

+
  • Sound
  • Performance
+
+

€400

Application fee not stated

+
Eligibility & details
+

Who can apply

Musicians, sound artists and performers. One artist per proposal; no geographic restriction stated.

Support

Confirmed artist fee. Travel and accommodation support not stated.

+

Application

Audio/video links, a proposal of at most 600 characters, and a short CV. State whether you also want a recording session. Apply by email via the official call.

Terms

In-person solo performance. Recording-session releases use the smallforms label; confirm release terms if selected.

+

Deadline: Oct 11, 2026. Cutoff time not stated.

Official call ↗ · Checked Oct 6, 2026

+
+
+
+
Oct152026
+
Open call9 days · check time
+

Music for Your Inbox

+

Unpremiered experimental sound and music films for an online series and library screenings.

Online · Los Angeles screenings 2027 season · films complete by May 1

+
  • Sound
  • Moving image
  • Performance
+
+

$600

Application fee not stated

+
Eligibility & details
+

Who can apply

Artists and artist teams; no geographic restriction stated. Music or sound must be central. Films must be 10–20 minutes and not previously premiered.

Support

Honorarium. Limited production support is available for co-produced films.

+

Application

A film proposal and samples demonstrating the proposed work. Finished or nearly finished projects are best suited to the available support.

Terms

One year of exclusive presentation rights. Ordinary concert documentation is excluded. In-person attendance is not required. The organizer discourages creating a new project solely for this call.

+

Deadline: Oct 15, 2026. 23:59 PST as published; daylight-saving ambiguity — submit early.

Official call ↗ · Checked Oct 6, 2026

+
+
+
+
Nov22026
+
Commission27 days · check time
+

Lendhauer — Urban Ecologies

+

Public artworks exploring coexistence, control and interdependence at Klagenfurt’s Lendhafen.

Klagenfurt, Austria June–September 2027

+
  • Digital art
  • Sound
  • Performance
  • Research
+
+

Up to €2,500 + costs

Application fee not stated

+
Eligibility & details
+

Who can apply

Individuals and collectives in Austria and abroad, including artists, designers, performers and interdisciplinary practitioners.

Support

Up to €2,500 gross artist fee, €4,000 gross production and €1,000 travel, per selected project.

+

Application

One PDF, at most 15 MB: project proposal up to three pages, production estimate, bio/CV and portfolio up to ten pages. Apply by email via the official call.

Terms

The proposal must engage with the actual harbor and its social, ecological and material conditions. Timing is agreed with selected artists. Fee is per project, not per collaborator.

+

Deadline: Nov 2, 2026. Cutoff time not stated.

Official call ↗ · Checked Oct 6, 2026

+
+
+
+
Nov52026
+
Performance30 days · check time
+

SPOT FLUX — Sound Art

+

Existing sound works, music films and performances for SPOT Festival’s experimental program.

Aarhus, Denmark May 7–8, 2027

+
  • Sound
  • Performance
  • Moving image
+
+

Performer fee + production

Application fee not stated

+
Eligibility & details
+

Who can apply

The work and/or artists must have a connection to Denmark. Existing works only; the artist must attend the presentation.

Support

Danish Musicians’ Union rate per performer, with approved technical and material costs. Exact fee not stated.

+

Application

Application form, work documentation and an estimated budget including rights costs. The work must be practical to set up and take down quickly.

Terms

Travel, accommodation and preparation expenses are not covered. The Café Stage is an open, busy space; quiet cannot be guaranteed.

+

Deadline: Nov 5, 2026. 14:00 as published; time zone not explicitly stated.

Official call ↗ · Checked Oct 6, 2026

+
+
+
+
Dec72026
+
Commission63 days left
+

Amsterdam Light Festival

+

Light artworks for the canals of Amsterdam, with technical and financial support to realize selected concepts.

Amsterdam, Netherlands November 25, 2027–January 16, 2028

+
  • Digital art
  • Creative coding
+
+

€4,000 + costs

Application fee not confirmed

+
Eligibility & details
+

Who can apply

The general open call is worldwide, including people new to light art. The separate Amarte call is restricted to Netherlands-based makers.

Support

Artist fee plus production and travel costs. General-call production ceiling is not specified in the FAQ.

+

Application

Read the artist briefing, prepare a concept using the submission template (up to three pages), and submit through the linked portal.

Terms

Work must withstand two months outdoors and avoid nuisance to residents, including sound, smoke and lasers. Full briefing and contract terms need review.

+

Deadline: Dec 7, 2026, 10:59 PM (UTC). Source: 23:59 CET.

Official call ↗ · Additional source ↗ · Checked Oct 6, 2026

+
+
+
+
Feb282027
+
ResidencyRolling
+

OPENFEED

+

A virtual residency spanning interactive installations, software art, sound, electronics and participatory work.

Online · FEED Media Art Center, US 2026–27 · program schedule to be confirmed

+
  • Digital art
  • Creative coding
  • Sound
  • Performance
+
+

Tools + mentorship

Free to apply · form checked Oct 5

+
Eligibility & details
+

Who can apply

International artists working across digital, electronic and experimental media. Individual and participatory practices welcome.

Support

Hardware, computing, software and mentorship. Cash support and exact program dates are not confirmed.

+

Application

Artist and project information with work samples through the official application form.

Terms

Development may combine an online edition with an on-site presentation. Confirm resources, dates and any attendance requirements before accepting.

+

Deadline: Feb 28, 2027. Rolling review within the October 1–February 28 application window.

Official call ↗ · Checked Oct 6, 2026

+
+
+
+
ANY∞Rolling
+
FellowshipRolling
+

Sequoia Open Source Fellowship

+

Living-expense support for developers sustaining important open-source projects.

Remote 6–12 months

+
  • Open source
  • Creative coding
- - +

Living-expense support

Free to apply · form checked Oct 5

+
Eligibility & details
+

Who can apply

Open-source developers; no geographic restriction stated in the reviewed application. A small cohort is selected each year.

Support

Six to twelve months of support, equity-free. Amount is individualized rather than a published fixed award.

+

Application

Eight short application fields covering the project, contribution history, impact and need for support.

Terms

No equity or required travel. Confirm the support amount and work expectations with the organizer.

+

Deadline: Rolling applications. No fixed deadline published.

Official call ↗ · Checked Oct 5, 2026

+
+
+ +
+

Reviewed 2026-10-06 Open data Papers

Sources checked by Aesthetic Computer. Confirm terms with the organizer.
“Free to apply” includes confirmed free applications only. Exact cutoff times display in your time zone.

+
diff --git a/system/public/papers.aesthetic.computer/deadlines/opportunities.json b/system/public/papers.aesthetic.computer/deadlines/opportunities.json new file mode 100644 index 0000000000..e5064df519 --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/opportunities.json @@ -0,0 +1,197 @@ +{ + "version": 1, + "updated": "2026-10-06", + "opportunities": [ + { + "id": "interplay-2027", + "title": "Interplay 2027", + "kind": "Performance", + "disciplines": ["Sound", "Performance", "Creative coding"], + "summary": "A 15–45 minute live online performance at the intersection of performance and technology.", + "location": "Online", + "remote": true, + "dates": "February 10–12, 2027", + "deadline": {"date": "2026-10-08", "at": "2026-10-08T23:59:00-07:00", "note": "23:59 Pacific"}, + "support": {"paid": true, "text": "CAD $1,500", "detail": "Artist fee per selected artist or group."}, + "fee": {"status": "unknown", "text": "Application fee not stated"}, + "eligibility": {"international": true, "text": "International, all disciplines and career stages. Priority for projects led by women, femmes and gender-diverse artists."}, + "requirements": "Bio, proposal, duration, three descriptive words, up to five minutes of support footage, and 3–5 promotional images.", + "terms": "Selected artists provide a technical breakdown, a five-minute highlight edit and development-process documentation. Live performance must be central to the work.", + "source": "https://interplay-arts.com/open_call.html", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "djerassi-2027", + "title": "Djerassi Artist Residencies", + "kind": "Residency", + "disciplines": ["Sound", "Digital art", "Performance"], + "summary": "One month of studio time in the Santa Cruz Mountains for composers, media artists and other disciplines.", + "location": "Woodside, California, US", + "remote": false, + "dates": "2027 · one month", + "deadline": {"date": "2026-10-09", "at": null, "note": "Cutoff time not confirmed"}, + "support": {"paid": false, "text": "Room, board + workspace", "detail": "Residency provided at no cost. Cash stipend not confirmed; travel is self-funded."}, + "fee": {"status": "paid", "text": "$35 application fee · reconfirm in form"}, + "eligibility": {"international": true, "text": "National and international artists. Apply in one discipline; attendance for the full residency is required."}, + "requirements": "Recent work samples, with formats and limits depending on discipline. Review the full application before preparing materials.", + "terms": "No shortened or partial residencies. Collaborators and couples must apply individually. Full form requirements, including references, have not been verified.", + "source": "https://djerassi.org/apply/application-guidelines/", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "smallforms-2027", + "title": "smallforms — Solo Sets", + "kind": "Performance", + "disciplines": ["Sound", "Performance"], + "summary": "Solo experimental music and sound performances, with an optional recording session for a digital release.", + "location": "Vienna, Austria", + "remote": false, + "dates": "March–December 2027 · 30–60 minute set", + "deadline": {"date": "2026-10-11", "at": null, "note": "Cutoff time not stated"}, + "support": {"paid": true, "text": "€400", "detail": "Confirmed artist fee. Travel and accommodation support not stated."}, + "fee": {"status": "unknown", "text": "Application fee not stated"}, + "eligibility": {"international": true, "text": "Musicians, sound artists and performers. One artist per proposal; no geographic restriction stated."}, + "requirements": "Audio/video links, a proposal of at most 600 characters, and a short CV. State whether you also want a recording session. Apply by email via the official call.", + "terms": "In-person solo performance. Recording-session releases use the smallforms label; confirm release terms if selected.", + "source": "https://smallforms.org/open-call-2027/", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "eyebeam-agency-2026", + "title": "Eyebeam — AGENCY", + "kind": "Residency", + "disciplines": ["Digital art", "Creative coding", "Research"], + "summary": "A virtual residency for early-stage work exploring agency through art and technology.", + "location": "Online · US outside New York City", + "remote": true, + "dates": "November 4, 2026–February 12, 2027", + "deadline": {"date": "2026-10-11", "at": "2026-10-11T23:59:00-04:00", "note": "23:59 Eastern"}, + "support": {"paid": true, "text": "$2,000", "detail": "Stipend, mentorship, technical consultation and weekly residency programming."}, + "fee": {"status": "free", "text": "Free to apply"}, + "eligibility": {"international": false, "text": "Individuals based in the US outside NYC, remaining US-based throughout. No full-time degree enrollment or overlapping residency commitments."}, + "requirements": "Written answers, CV, work samples and a personal 1–2 minute introduction video, via Submittable.", + "terms": "Weekly cohort participation and independent project work. The live application specifies 3–4 cohort hours weekly plus independent work; confirm the schedule before accepting.", + "source": "https://eyebeam.org/articles/agency-virtual-residency", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "music-for-your-inbox-2027", + "title": "Music for Your Inbox", + "kind": "Open call", + "disciplines": ["Sound", "Moving image", "Performance"], + "summary": "Unpremiered experimental sound and music films for an online series and library screenings.", + "location": "Online · Los Angeles screenings", + "remote": true, + "dates": "2027 season · films complete by May 1", + "deadline": {"date": "2026-10-15", "at": null, "note": "23:59 PST as published; daylight-saving ambiguity — submit early"}, + "support": {"paid": true, "text": "$600", "detail": "Honorarium. Limited production support is available for co-produced films."}, + "fee": {"status": "unknown", "text": "Application fee not stated"}, + "eligibility": {"international": true, "text": "Artists and artist teams; no geographic restriction stated. Music or sound must be central. Films must be 10–20 minutes and not previously premiered."}, + "requirements": "A film proposal and samples demonstrating the proposed work. Finished or nearly finished projects are best suited to the available support.", + "terms": "One year of exclusive presentation rights. Ordinary concert documentation is excluded. In-person attendance is not required. The organizer discourages creating a new project solely for this call.", + "source": "https://www.musicforyourinbox.com/apply", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "lendhauer-2027", + "title": "Lendhauer — Urban Ecologies", + "kind": "Commission", + "disciplines": ["Digital art", "Sound", "Performance", "Research"], + "summary": "Public artworks exploring coexistence, control and interdependence at Klagenfurt’s Lendhafen.", + "location": "Klagenfurt, Austria", + "remote": false, + "dates": "June–September 2027", + "deadline": {"date": "2026-11-02", "at": null, "note": "Cutoff time not stated"}, + "support": {"paid": true, "text": "Up to €2,500 + costs", "detail": "Up to €2,500 gross artist fee, €4,000 gross production and €1,000 travel, per selected project."}, + "fee": {"status": "unknown", "text": "Application fee not stated"}, + "eligibility": {"international": true, "text": "Individuals and collectives in Austria and abroad, including artists, designers, performers and interdisciplinary practitioners."}, + "requirements": "One PDF, at most 15 MB: project proposal up to three pages, production estimate, bio/CV and portfolio up to ten pages. Apply by email via the official call.", + "terms": "The proposal must engage with the actual harbor and its social, ecological and material conditions. Timing is agreed with selected artists. Fee is per project, not per collaborator.", + "source": "https://www.lendhafen.at/open-call-2027-urban-ecologies-coexistence-and-control/", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "spot-flux-2027", + "title": "SPOT FLUX — Sound Art", + "kind": "Performance", + "disciplines": ["Sound", "Performance", "Moving image"], + "summary": "Existing sound works, music films and performances for SPOT Festival’s experimental program.", + "location": "Aarhus, Denmark", + "remote": false, + "dates": "May 7–8, 2027", + "deadline": {"date": "2026-11-05", "at": null, "note": "14:00 as published; time zone not explicitly stated"}, + "support": {"paid": true, "text": "Performer fee + production", "detail": "Danish Musicians’ Union rate per performer, with approved technical and material costs. Exact fee not stated."}, + "fee": {"status": "unknown", "text": "Application fee not stated"}, + "eligibility": {"international": false, "text": "The work and/or artists must have a connection to Denmark. Existing works only; the artist must attend the presentation."}, + "requirements": "Application form, work documentation and an estimated budget including rights costs. The work must be practical to set up and take down quickly.", + "terms": "Travel, accommodation and preparation expenses are not covered. The Café Stage is an open, busy space; quiet cannot be guaranteed.", + "source": "https://spotfestival.dk/spot-flux-2027-open-call/", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "amsterdam-light-festival-2027", + "title": "Amsterdam Light Festival", + "kind": "Commission", + "disciplines": ["Digital art", "Creative coding"], + "summary": "Light artworks for the canals of Amsterdam, with technical and financial support to realize selected concepts.", + "location": "Amsterdam, Netherlands", + "remote": false, + "dates": "November 25, 2027–January 16, 2028", + "deadline": {"date": "2026-12-07", "at": "2026-12-07T23:59:00+01:00", "note": "23:59 CET"}, + "support": {"paid": true, "text": "€4,000 + costs", "detail": "Artist fee plus production and travel costs. General-call production ceiling is not specified in the FAQ."}, + "fee": {"status": "unknown", "text": "Application fee not confirmed"}, + "eligibility": {"international": true, "text": "The general open call is worldwide, including people new to light art. The separate Amarte call is restricted to Netherlands-based makers."}, + "requirements": "Read the artist briefing, prepare a concept using the submission template (up to three pages), and submit through the linked portal.", + "terms": "Work must withstand two months outdoors and avoid nuisance to residents, including sound, smoke and lasers. Full briefing and contract terms need review.", + "source": "https://amsterdamlightfestival.com/en/open-call", + "sources": ["https://amsterdamlightfestival.com/en/faq-open-call"], + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "openfeed-2026-2027", + "title": "OPENFEED", + "kind": "Residency", + "disciplines": ["Digital art", "Creative coding", "Sound", "Performance"], + "summary": "A virtual residency spanning interactive installations, software art, sound, electronics and participatory work.", + "location": "Online · FEED Media Art Center, US", + "remote": true, + "dates": "2026–27 · program schedule to be confirmed", + "deadline": {"date": "2027-02-28", "at": null, "rolling": true, "note": "Rolling review within the October 1–February 28 application window"}, + "support": {"paid": false, "text": "Tools + mentorship", "detail": "Hardware, computing, software and mentorship. Cash support and exact program dates are not confirmed."}, + "fee": {"status": "free", "text": "Free to apply · form checked Oct 5"}, + "eligibility": {"international": true, "text": "International artists working across digital, electronic and experimental media. Individual and participatory practices welcome."}, + "requirements": "Artist and project information with work samples through the official application form.", + "terms": "Development may combine an online edition with an on-site presentation. Confirm resources, dates and any attendance requirements before accepting.", + "source": "https://www.feed.art/open-feed", + "checked": "2026-10-06", + "published": "2026-10-06" + }, + { + "id": "sequoia-open-source", + "title": "Sequoia Open Source Fellowship", + "kind": "Fellowship", + "disciplines": ["Open source", "Creative coding"], + "summary": "Living-expense support for developers sustaining important open-source projects.", + "location": "Remote", + "remote": true, + "dates": "6–12 months", + "deadline": null, + "support": {"paid": true, "text": "Living-expense support", "detail": "Six to twelve months of support, equity-free. Amount is individualized rather than a published fixed award."}, + "fee": {"status": "free", "text": "Free to apply · form checked Oct 5"}, + "eligibility": {"international": true, "text": "Open-source developers; no geographic restriction stated in the reviewed application. A small cohort is selected each year."}, + "requirements": "Eight short application fields covering the project, contribution history, impact and need for support.", + "terms": "No equity or required travel. Confirm the support amount and work expectations with the organizer.", + "source": "https://sequoiacap.com/oss", + "checked": "2026-10-05", + "published": "2026-10-06" + } + ] +} diff --git a/system/public/papers.aesthetic.computer/deadlines/style.css b/system/public/papers.aesthetic.computer/deadlines/style.css new file mode 100644 index 0000000000..821a574528 --- /dev/null +++ b/system/public/papers.aesthetic.computer/deadlines/style.css @@ -0,0 +1,8 @@ +:root{color-scheme:light dark;--paper:#f5f3ef;--ink:#242424;--muted:#62605e;--line:#cbc7c1;--pink:#cf2869;--green:#216549;--green-bg:#e2eee6;--accent-bg:#f9dee9;--input:#fffdf9;--focus:#7038bd;--hover:#eeeae4;font-family:ui-monospace,"SFMono-Regular",Consolas,"Liberation Mono",monospace;font-size:15px} +*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink)}[hidden]{display:none!important}a{color:inherit;text-decoration-thickness:1px;text-underline-offset:4px}button,input,select{font:inherit;color:inherit}button,a,input,select,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:3px solid var(--focus);outline-offset:4px}button,select,input{border-radius:0}button{background:transparent;border:0}p{line-height:1.55}.page{max-width:1240px;margin:auto;padding:0 48px}.skip{position:fixed;top:-80px;left:12px;padding:12px;background:var(--ink);color:var(--paper);z-index:5}.skip:focus{top:12px}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.topline{display:flex;justify-content:space-between;gap:24px;align-items:center;padding:30px 0}.brand{display:flex;gap:12px;align-items:center;text-decoration:none;font-size:16px;letter-spacing:-.6px}.brand img{flex:none}.suggest{font-size:13px;white-space:nowrap}.masthead{display:flex;align-items:flex-end;justify-content:space-between;gap:30px;padding:40px 0 35px}h1{font-weight:600;font-size:clamp(54px,8vw,100px);letter-spacing:-8px;line-height:.95;margin:0}h1 span{color:var(--pink)}.intro{font-size:20px;letter-spacing:-.5px;margin:18px 0 0}.subscribe{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:14px 22px;max-width:390px;font-size:13px;padding-bottom:4px}.download-calendar{color:var(--muted);font-size:12px}.filters{border:1px solid var(--line);background:var(--input)}.search-row{display:grid;grid-template-columns:1fr 200px 176px}.search-label{display:flex;align-items:center;padding:0 16px;gap:10px;min-width:0}.search-label svg{width:19px;height:19px;stroke:var(--muted);fill:none;stroke-width:1.7;flex:none}.search-label input{border:0;background:transparent;width:100%;height:56px;min-width:0;font-size:14px}.search-label input:focus-visible{outline-offset:-3px}.select-wrap{border-left:1px solid var(--line)}select{width:100%;border:0;background:transparent;padding:0 34px 0 14px;height:56px;font-size:13px;cursor:pointer}.filter-row{border-top:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:12px 16px;gap:10px;font-size:12px}.checks{display:flex;flex-wrap:wrap;gap:14px 20px}.checks label{display:flex;align-items:center;gap:7px;cursor:pointer;min-height:24px}.checks input{accent-color:var(--pink);margin:0;width:16px;height:16px}.reset{font-size:12px;color:var(--muted);padding:7px;text-decoration:underline;text-underline-offset:3px}.list-toolbar{display:flex;justify-content:space-between;align-items:center;gap:16px;border-bottom:2px solid var(--ink);margin-top:26px;min-height:56px}#views{display:flex;align-self:stretch;gap:24px}#views a{display:flex;align-items:center;text-decoration:none;font-size:14px;color:var(--muted);border-bottom:3px solid transparent;margin-bottom:-2px}#views a[aria-current]{color:var(--ink);border-color:var(--pink)}.list-info{display:flex;align-items:center;gap:14px;font-size:12px;color:var(--muted)}.list-info select{width:auto;height:42px;font-size:12px}#opportunities:focus{outline:none}.opportunity{display:grid;grid-template-columns:70px minmax(0,1fr) 205px;column-gap:30px;position:relative;padding:28px 0 0;border-bottom:1px solid var(--line);scroll-margin-top:20px}.date-block{text-align:center;align-self:start;padding-top:2px}.month{display:block;font-size:12px;letter-spacing:1px;text-transform:uppercase}.day{display:block;font-size:41px;line-height:1.25;letter-spacing:-2px}.year{display:block;color:var(--muted);font-size:11px}.row-meta{display:flex;align-items:center;flex-wrap:wrap;gap:12px;font-size:11px;color:var(--muted);margin:0 0 10px}.countdown{padding:3px 6px;background:var(--hover);color:var(--muted)}.countdown.soon{background:var(--accent-bg);color:var(--pink)}h2{font-size:21px;line-height:1.25;letter-spacing:-.7px;font-weight:600;margin:0 0 8px}h2 a{text-decoration:none}h2 a:hover{text-decoration:underline}h2 a span{color:var(--pink);font-size:18px}.summary{margin:0;font-family:system-ui,-apple-system,sans-serif;font-size:15px;line-height:1.5;max-width:600px}.location{font-family:system-ui,-apple-system,sans-serif;font-size:12px;color:var(--muted);margin:10px 0}.location span{padding:0 3px}.tags{display:flex;flex-wrap:wrap;gap:7px;padding:0;margin:12px 0 0;list-style:none;font-size:10px;color:var(--muted)}.tags li{border:1px solid var(--line);padding:3px 6px}.support{align-self:start;padding-top:30px;position:relative}.amount{font-size:16px;line-height:1.5;margin:0 0 8px;letter-spacing:-.5px;overflow-wrap:anywhere}.amount.funded{color:var(--green)}.fee{font-size:11px;color:var(--muted);margin:0;line-height:1.6}.permalink{position:absolute;top:0;right:0;color:var(--muted);font-size:12px;text-decoration:none;padding:4px}.opportunity:target{background:var(--accent-bg);box-shadow:18px 0 var(--accent-bg),-18px 0 var(--accent-bg)}details{grid-column:2/4;margin:14px 0 18px;font-size:12px}summary{cursor:pointer;width:max-content;padding:6px 0;color:var(--muted);font-size:12px}summary:hover{color:var(--ink)}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px 32px;padding:12px 0 6px}.detail-grid h3{font-size:12px;margin:12px 0 6px}.detail-grid p{font-family:system-ui,-apple-system,sans-serif;font-size:14px;margin:0 0 15px}.detail-bottom{grid-column:1/-1;border-top:1px dashed var(--line);padding-top:16px}.detail-bottom p{font-size:12px;margin:0 0 10px}.checked{color:var(--muted)}.add-calendar{border:1px solid var(--line);padding:9px 12px;margin:4px 0;font-size:12px}.add-calendar:hover{background:var(--hover)}.closed .date-block{color:var(--muted)}.empty{padding:70px 10px;text-align:center}.empty p{font-size:19px}.empty button{border:1px solid var(--ink);padding:12px 18px}.notice{padding:16px;background:var(--accent-bg);font-size:13px}.notice button{text-decoration:underline}footer{display:flex;flex-wrap:wrap;justify-content:space-between;column-gap:40px;padding:26px 0 38px;color:var(--muted);font-size:11px}footer p{margin:8px 0}footer span{padding:0 5px} +@media(prefers-color-scheme:dark){:root{--paper:#191b20;--ink:#f1eee9;--muted:#adaaa6;--line:#44464b;--pink:#ff86b4;--green:#9bd6b1;--green-bg:#203a2d;--accent-bg:#392735;--input:#202329;--focus:#c4a0ff;--hover:#2e3137}} +@media(min-width:1450px){.page{padding:0 52px}.masthead{padding-top:52px;padding-bottom:44px}.opportunity{padding-top:32px}} +@media(max-width:850px){.page{padding:0 26px}.masthead{align-items:flex-start;flex-direction:column;gap:24px;padding:28px 0}.subscribe{justify-content:flex-start;max-width:none}.search-row{grid-template-columns:1fr 1fr}.search-label{grid-column:1/-1;border-bottom:1px solid var(--line)}.search-label+.select-wrap{border-left:0}.opportunity{grid-template-columns:58px minmax(0,1fr) 160px;column-gap:20px}.amount{font-size:14px}.list-info{gap:6px}h1{letter-spacing:-5px}.checks{gap:10px 16px}} +@media(max-width:570px){.page{padding:0 18px}.topline{padding:20px 0;gap:12px}.brand{font-size:13px;gap:8px}.brand img{width:24px;height:24px}.suggest{font-size:11px}.masthead{padding:26px 0 24px}.intro{font-size:17px}.subscribe{gap:13px;font-size:11px}.download-calendar{font-size:11px}.search-label input{font-size:13px}.filter-row{align-items:flex-start;padding:10px 12px}.checks{display:grid;grid-template-columns:1fr 1fr;gap:7px 13px;font-size:11px}.reset{padding:7px 0}.list-toolbar{align-items:flex-start;flex-wrap:wrap;padding-top:0;gap:0;margin-top:18px}#views{gap:24px;height:46px;flex-basis:100%}#views a{font-size:13px}.list-info{width:100%;justify-content:space-between;font-size:11px;padding:2px 0}.list-info select{font-size:11px;padding-right:0;height:38px}.opportunity{grid-template-columns:46px minmax(0,1fr);column-gap:16px;padding-top:23px}h2{font-size:19px}.day{font-size:34px}.month{font-size:10px}.year{font-size:10px}.row-meta{gap:7px;font-size:10px}.summary{font-size:14px}.location{font-size:12px}.support{grid-column:2;padding-top:13px;display:flex;flex-wrap:wrap;gap:5px 12px;align-items:baseline}.amount{font-size:14px;margin:0}.fee{font-size:10px}.permalink{display:none}.tags{font-size:9px;gap:5px;margin-top:9px}details{grid-column:2;margin:9px 0 18px}.detail-grid{grid-template-columns:1fr;gap:0}.detail-bottom{grid-column:1}footer{font-size:10px;padding-bottom:26px;gap:0}.date-block{padding-top:0}select{height:46px;font-size:12px;padding-left:12px}} +@media(prefers-reduced-motion:no-preference){a,button{transition:color .12s,background .12s}} +@media print{.filters,.subscribe,.suggest,.list-toolbar,footer,.add-calendar{display:none!important}.page{padding:0}.opportunity{break-inside:avoid}.opportunity details{display:block}.opportunity details>*{display:block}.opportunity details>summary{display:none}body{background:white;color:black}} diff --git a/utilities/deadlines/README.md b/utilities/deadlines/README.md new file mode 100644 index 0000000000..e08e238785 --- /dev/null +++ b/utilities/deadlines/README.md @@ -0,0 +1,21 @@ +# Deadlines + +The public board at `https://papers.aesthetic.computer/deadlines/` serves reviewed new-media opportunities. Its JSON catalog, RSS feed and calendar are static assets on the existing Papers host. + +Edit `system/public/papers.aesthetic.computer/deadlines/opportunities.json`, then run: + +```sh +node utilities/deadlines/build.mjs +node --test utilities/deadlines/core.test.mjs +node utilities/deadlines/build.mjs --check +``` + +Commit the catalog and generated `index.html`, `feed.xml` and `calendar.ics` together. Publish through the repository's standard lith deployment. `template.html` owns the page shell; `core.mjs` shares validation and formatting between the browser and builder. + +Use Exa to discover candidates, then verify the official call before adding one. Keep stable IDs across edits. Record the review date, fee, funding, eligibility, requirements, material rights or travel conditions, and source. Unknown fees never count as free. Private application notes, draft locations and applicant statuses do not belong in this catalog; the schema rejects unapproved fields and local paths. No search service runs in the visitor's browser, and no automatic scouting schedule is configured. + +Only set `deadline.at` when the organizer's date, time and zone establish an unambiguous instant. Otherwise set it to `null`, retain the stated date, and explain the uncertainty in `deadline.note`. Calendar exports make these all-day entries marked “check time.” Date-only entries stay visible through the source date in every time zone, then archive; this is a display grace period, not an extension. A deadline can have `rolling: true` for rolling review with a final closing date. Use `deadline: null` for an open-ended call. + +The browser refreshes expiry on load and when the page becomes visible. Exact cutoff times display in the visitor's time zone. RSS retains publication dates and stable permalinks; calendar events retain stable UIDs, so corrections update subscriptions instead of duplicating events. Closed entries remain in the source catalog and are reachable in Archive. The generated HTML is a dated fallback when JavaScript or the catalog request is unavailable. + +The former personal tracker is archived privately in the adjacent vault under `grants/deadlines-personal/`. diff --git a/utilities/deadlines/build.mjs b/utilities/deadlines/build.mjs new file mode 100644 index 0000000000..8ad99050d9 --- /dev/null +++ b/utilities/deadlines/build.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { resolve, dirname } from "node:path"; +import { validateCatalog, filterItems, renderCard, makeCalendar, makeRSS, DISCIPLINES, KINDS } from "../../system/public/papers.aesthetic.computer/deadlines/core.mjs"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const dir = resolve(root, "system/public/papers.aesthetic.computer/deadlines"); +const catalog = validateCatalog(JSON.parse(await readFile(resolve(dir, "opportunities.json"), "utf8"))); +const now = new Date(catalog.updated + "T12:00:00Z"); +const items = filterItems(catalog.opportunities, {}, now); +const template = await readFile(resolve(root, "utilities/deadlines/template.html"), "utf8"); +const files = { + "index.html": template.replace("", items.map((x) => renderCard(x, now)).join("\n")) + .replaceAll("{{UPDATED}}", catalog.updated) + .replaceAll("{{COUNT}}", String(items.length)) + .replace("", DISCIPLINES.map((x) => ``).join("")) + .replace("", KINDS.map((x) => ``).join("")), + "feed.xml": makeRSS(catalog), + "calendar.ics": makeCalendar(catalog.opportunities, catalog.updated), +}; +let stale = false; +for (const [name, content] of Object.entries(files)) { + if (process.argv.includes("--check")) { + const existing = await readFile(resolve(dir, name), "utf8").catch(() => ""); + if (existing !== content) { console.error(`Stale generated file: ${name}`); stale = true; } + } else await writeFile(resolve(dir, name), content); +} +if (stale) process.exitCode = 1; +else console.log(`${process.argv.includes("--check") ? "Verified" : "Built"} ${catalog.opportunities.length} opportunities, RSS and calendar.`); diff --git a/utilities/deadlines/core.test.mjs b/utilities/deadlines/core.test.mjs new file mode 100644 index 0000000000..68021bdab9 --- /dev/null +++ b/utilities/deadlines/core.test.mjs @@ -0,0 +1,86 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { validateCatalog, filterItems, deadlineState, dateParts, makeCalendar, makeRSS, foldICS, escapeHTML } from "../../system/public/papers.aesthetic.computer/deadlines/core.mjs"; + +const catalog = JSON.parse(await readFile(new URL("../../system/public/papers.aesthetic.computer/deadlines/opportunities.json", import.meta.url), "utf8")); +const find = (id) => catalog.opportunities.find((x) => x.id === id); +const now = new Date("2026-10-06T19:00:00Z"); + +test("public schema rejects private fields, paths and malformed dates", () => { + assert.equal(validateCatalog(catalog), catalog); + for (const mutate of [ + (x) => { x.opportunities[0].draftPath = "private"; }, + (x) => { x.opportunities[0].summary = "/Users/example/application"; }, + (x) => { x.opportunities[0].source = "javascript:alert(1)"; }, + (x) => { x.opportunities[0].deadline.date = "2026-02-30"; }, + (x) => { x.opportunities[1].id = x.opportunities[0].id; }, + (x) => { x.opportunities[0].fee.status = "probably free"; }, + ]) { + const copy = structuredClone(catalog); mutate(copy); + assert.throws(() => validateCatalog(copy)); + } +}); + +test("verified cutoffs expire at the actual instant across time zones", () => { + const item = find("interplay-2027"); + assert.equal(deadlineState(item, new Date("2026-10-09T06:58:59Z")), "open"); + assert.equal(deadlineState(item, new Date("2026-10-09T06:59:00Z")), "closed"); + assert.equal(dateParts(item, "America/Los_Angeles").number, "8"); + assert.equal(dateParts(item, "Europe/London").number, "9"); +}); + +test("unknown cutoffs retain their source date and eventually archive", () => { + const item = find("djerassi-2027"); + assert.equal(dateParts(item, "Pacific/Auckland").number, "9"); + assert.equal(dateParts(item, "Pacific/Honolulu").number, "9"); + assert.equal(deadlineState(item, new Date("2026-10-10T11:59:59Z")), "open"); + assert.equal(deadlineState(item, new Date("2026-10-10T12:00:00Z")), "closed"); +}); + +test("rolling review with a closing date appears until the window ends", () => { + const ids = filterItems(catalog.opportunities, {view: "rolling"}, now).map((x) => x.id); + assert.deepEqual(ids, ["openfeed-2026-2027", "sequoia-open-source"]); + const future = new Date("2027-03-02T12:00:00Z"); + assert.deepEqual(filterItems(catalog.opportunities, {view: "rolling"}, future).map((x) => x.id), ["sequoia-open-source"]); + assert(filterItems(catalog.opportunities, {view: "archive"}, future).some((x) => x.id === "openfeed-2026-2027")); +}); + +test("unknown fees do not pass the free filter; combined filters and search work", () => { + const items = filterItems(catalog.opportunities, {free: true, paid: true, remote: true}, now); + assert.deepEqual(items.map((x) => x.id), ["eyebeam-agency-2026", "sequoia-open-source"]); + assert.deepEqual(filterItems(catalog.opportunities, {q: "VIENNA", discipline: "Sound"}, now).map((x) => x.id), ["smallforms-2027"]); + assert.deepEqual(filterItems(catalog.opportunities, {q: "nothing matches"}, now), []); +}); + +test("deadline ordering compares absolute instants, not offset strings", () => { + const base = find("interplay-2027"); + const items = [ + {...base, id: "later", deadline: {date: "2026-10-08", at: "2026-10-08T23:00:00-07:00"}}, + {...base, id: "earlier", deadline: {date: "2026-10-09", at: "2026-10-09T01:00:00+01:00"}}, + ]; + assert.deepEqual(filterItems(items, {}, now).map((x) => x.id), ["earlier", "later"]); +}); + +test("calendar exports exact UTC instants and honest all-day placeholders", () => { + const ics = makeCalendar(catalog.opportunities); + assert.match(ics, /DTSTART:20261009T065900Z/); + assert.match(ics, /DTSTART;VALUE=DATE:20261009\r\nDTEND;VALUE=DATE:20261010/); + assert.match(ics, /UID:djerassi-2027@papers.aesthetic.computer/); + assert.match(ics, /\(check time\)/); + assert(!ics.includes("UID:sequoia-open-source")); + assert.equal((ics.match(/BEGIN:VEVENT/g) || []).length, 9); + for (const line of ics.split("\r\n")) assert(Buffer.byteLength(line) <= 75); + const unicode = "DESCRIPTION:" + "é💻,;".repeat(50); + assert.equal(foldICS(unicode).replace(/\r\n /g, ""), unicode); +}); + +test("RSS has stable public permalinks and escaped text", () => { + const copy = structuredClone(catalog); + copy.opportunities[0].title = 'Art & "sound"'; + const xml = makeRSS(copy); + assert.match(xml, /Art & "sound"<\/title>/); + assert.match(xml, /<guid isPermaLink="true">https:\/\/papers.aesthetic.computer\/deadlines\/#interplay-2027<\/guid>/); + assert(!xml.includes("/Users/") && !xml.includes("draftPath")); + assert.equal(escapeHTML('<a href="x">&'), "<a href="x">&"); +}); diff --git a/utilities/deadlines/template.html b/utilities/deadlines/template.html new file mode 100644 index 0000000000..ebaffbc230 --- /dev/null +++ b/utilities/deadlines/template.html @@ -0,0 +1,44 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <meta name="theme-color" content="#f5f3ef"> + <title>Deadlines · Aesthetic Computer + + + + + + + + + + + + + + +
+
+ +

deadlines.

New media opportunities.

+
+
+ +
{{COUNT}} opportunities
+ + +
+ +
+

Reviewed {{UPDATED}} Open data Papers

Sources checked by Aesthetic Computer. Confirm terms with the organizer.
“Free to apply” includes confirmed free applications only. Exact cutoff times display in your time zone.

+
+ + -- 2.51.2 From dc700682005ed4496f7f88a2a63c75f5ad03acd2 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:32:52 -0700 Subject: [PATCH 10/21] Resolve deadlines assets from the existing slashless route Papers serves /deadlines directly, without redirecting to /deadlines/. Use rooted asset and feed URLs so both entry points load the same board. Add a regression check for both route forms. --- .../deadlines/index.html | 16 ++++++++-------- utilities/deadlines/core.test.mjs | 9 +++++++++ utilities/deadlines/template.html | 16 ++++++++-------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/system/public/papers.aesthetic.computer/deadlines/index.html b/system/public/papers.aesthetic.computer/deadlines/index.html index 6e293018a3..75b5d7d90e 100644 --- a/system/public/papers.aesthetic.computer/deadlines/index.html +++ b/system/public/papers.aesthetic.computer/deadlines/index.html @@ -11,18 +11,18 @@ - - - - - + + + + + -

Reviewed 2026-10-06 Open data Papers

Sources checked by Aesthetic Computer. Confirm terms with the organizer.
“Free to apply” includes confirmed free applications only. Exact cutoff times display in your time zone.

+

Reviewed 2026-10-06 Open data Papers

Sources checked by Aesthetic Computer. Confirm terms with the organizer.
“Free to apply” includes confirmed free applications only. Exact cutoff times display in your time zone.

diff --git a/utilities/deadlines/core.test.mjs b/utilities/deadlines/core.test.mjs index 68021bdab9..7a791ca633 100644 --- a/utilities/deadlines/core.test.mjs +++ b/utilities/deadlines/core.test.mjs @@ -7,6 +7,15 @@ const catalog = JSON.parse(await readFile(new URL("../../system/public/papers.ae const find = (id) => catalog.opportunities.find((x) => x.id === id); const now = new Date("2026-10-06T19:00:00Z"); +test("page assets and feeds work on the existing route without a trailing slash", async () => { + const html = await readFile(new URL("./template.html", import.meta.url), "utf8"); + const assets = [...html.matchAll(/(?:href|src)="([^"\s]+\.(?:mjs|css|json|ics|xml|svg))"/g)].map((x) => x[1]); + assert(assets.length >= 8); + for (const base of ["https://papers.aesthetic.computer/deadlines", "https://papers.aesthetic.computer/deadlines/"]) { + for (const asset of assets) assert(new URL(asset, base).pathname.startsWith("/deadlines/"), asset); + } +}); + test("public schema rejects private fields, paths and malformed dates", () => { assert.equal(validateCatalog(catalog), catalog); for (const mutate of [ diff --git a/utilities/deadlines/template.html b/utilities/deadlines/template.html index ebaffbc230..406297df1c 100644 --- a/utilities/deadlines/template.html +++ b/utilities/deadlines/template.html @@ -11,18 +11,18 @@ - - - - - + + + + +
- -

deadlines.

New media opportunities.

+ +

deadlines.

New media opportunities.

@@ -38,7 +38,7 @@
-

Reviewed {{UPDATED}} Open data Papers

Sources checked by Aesthetic Computer. Confirm terms with the organizer.
“Free to apply” includes confirmed free applications only. Exact cutoff times display in your time zone.

+

Reviewed {{UPDATED}} Open data Papers

Sources checked by Aesthetic Computer. Confirm terms with the organizer.
“Free to apply” includes confirmed free applications only. Exact cutoff times display in your time zone.

-- 2.51.2 From 63e55cd2a74c26bf36da2258b36c63a798adfc88 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:36:09 -0700 Subject: [PATCH 11/21] Refresh deadline results after native form reset completes The reset event microtask can run before the browser restores control values. Render on the next task so Reset and Clear filters restore the full list. Add a browser regression check for both public URL forms. --- .../deadlines/app.mjs | 5 +-- utilities/deadlines/README.md | 1 + utilities/deadlines/check-browser.mjs | 43 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 utilities/deadlines/check-browser.mjs diff --git a/system/public/papers.aesthetic.computer/deadlines/app.mjs b/system/public/papers.aesthetic.computer/deadlines/app.mjs index 5218e3a88b..265140f956 100644 --- a/system/public/papers.aesthetic.computer/deadlines/app.mjs +++ b/system/public/papers.aesthetic.computer/deadlines/app.mjs @@ -65,14 +65,13 @@ function revealHash() { function reset() { form.reset(); view = "upcoming"; history.replaceState(null, "", location.pathname); - // Reset values are applied after the reset event finishes. - queueMicrotask(() => render()); } form.addEventListener("submit", (event) => event.preventDefault()); form.addEventListener("input", () => render()); form.addEventListener("change", () => render()); form.elements.namedItem("sort").addEventListener("change", () => render()); -form.addEventListener("reset", () => { view = "upcoming"; queueMicrotask(() => render()); }); +// Native button resets finish after event microtasks; read the new values next task. +form.addEventListener("reset", () => { view = "upcoming"; setTimeout(() => render(), 0); }); document.querySelector("#clear-empty").addEventListener("click", reset); for (const link of viewLinks) link.addEventListener("click", (event) => { if (!catalog || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; diff --git a/utilities/deadlines/README.md b/utilities/deadlines/README.md index e08e238785..6ccac7215b 100644 --- a/utilities/deadlines/README.md +++ b/utilities/deadlines/README.md @@ -8,6 +8,7 @@ Edit `system/public/papers.aesthetic.computer/deadlines/opportunities.json`, the node utilities/deadlines/build.mjs node --test utilities/deadlines/core.test.mjs node utilities/deadlines/build.mjs --check +node utilities/deadlines/check-browser.mjs https://papers.aesthetic.computer/deadlines/ ``` Commit the catalog and generated `index.html`, `feed.xml` and `calendar.ics` together. Publish through the repository's standard lith deployment. `template.html` owns the page shell; `core.mjs` shares validation and formatting between the browser and builder. diff --git a/utilities/deadlines/check-browser.mjs b/utilities/deadlines/check-browser.mjs new file mode 100644 index 0000000000..d11ff29af7 --- /dev/null +++ b/utilities/deadlines/check-browser.mjs @@ -0,0 +1,43 @@ +// Run against a local preview or the public board after publication. +import { chromium } from "playwright"; +import assert from "node:assert/strict"; + +const base = process.argv[2]; +if (!base) throw new Error("Usage: node utilities/deadlines/check-browser.mjs "); +const browser = await chromium.launch({headless: true, channel: "chrome"}); +try { + const page = await browser.newPage({viewport: {width: 1440, height: 1000}}); + const errors = []; + page.on("pageerror", (error) => errors.push(error.message)); + const count = () => page.locator("article.opportunity").count(); + const waitCount = (expected) => page.waitForFunction((n) => document.querySelectorAll("article.opportunity").length === n, expected); + for (const url of [base.replace(/\/$/, ""), base.replace(/\/$/, "") + "/"]) { + await page.goto(url, {waitUntil: "domcontentloaded"}); + await page.locator("#filters").waitFor({state: "visible", timeout: 10000}); + const all = await count(); + assert(all > 0); + assert(await page.locator("link[rel=stylesheet]").evaluate((el) => el.sheet !== null)); + await page.getByRole("searchbox").fill("no opportunity will match this text"); + await waitCount(0); + await page.getByRole("button", {name: "Reset", exact: true}).click(); + await waitCount(all); + assert.equal(await page.getByRole("searchbox").inputValue(), ""); + await page.getByRole("checkbox", {name: "Free to apply", exact: true}).check(); + assert(await count() <= all); + await page.getByRole("button", {name: "Reset", exact: true}).click(); + await waitCount(all); + assert.equal(await page.getByRole("checkbox", {name: "Free to apply", exact: true}).isChecked(), false); + assert(!new URL(page.url()).searchParams.has("free")); + await page.getByRole("searchbox").fill("no opportunity will match this text"); + await waitCount(0); + await page.getByRole("button", {name: "Clear filters", exact: true}).click(); + await waitCount(all); + assert(!new URL(page.url()).search); + } + await page.setViewportSize({width: 390, height: 844}); + assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); + assert.deepEqual(errors, []); + console.log("Browser verified both entry routes, assets, search, reset, clear filters and mobile width."); +} finally { + await browser.close(); +} -- 2.51.2 From 4d73f4c9fc96d0575887eadd72f5aee965001de0 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 6 Oct 2026 11:43:35 -0700 Subject: [PATCH 12/21] Keep saved Oskiewar characters in freeskate and move account actions behind the handle --- xbox/live/account-deletion.mjs | 36 ++++++++++++- xbox/live/mac-test.html | 51 ++++++++++++++----- xbox/live/oskiewar.js | 25 ++++++--- xbox/live/tests/account-default-browser.mjs | 56 ++++++++++++++------- 4 files changed, 130 insertions(+), 38 deletions(-) diff --git a/xbox/live/account-deletion.mjs b/xbox/live/account-deletion.mjs index 42f3f45588..c785ebf0fc 100644 --- a/xbox/live/account-deletion.mjs +++ b/xbox/live/account-deletion.mjs @@ -2,11 +2,40 @@ const endpoint = 'https://aesthetic.computer/api/delete-erase-and-forget-me'; export function mountAccountDeletion({ account, onDeleted }) { + const row = document.querySelector('#account'); + const handle = document.querySelector('#account-handle'); + const logout = document.querySelector('#logout'); + const settings = document.createElement('dialog'); + settings.id = 'account-settings'; + settings.setAttribute('aria-labelledby', 'account-settings-title'); + settings.innerHTML = '

'; + document.body.append(settings); + const settingsTitle = settings.querySelector('h2'); + const settingsActions = settings.querySelector('div'); + const done = settings.querySelector('button'); + function syncSettings() { + settingsTitle.textContent = handle.textContent; + if (account.signedIn && account.handle) settingsActions.prepend(logout); + else { row.append(logout); settings.close(); } + } + function openSettings() { + if (!account.signedIn) return; + syncSettings(); + globalThis.__oskiewarAccountOpen = true; + settings.showModal(); + done.focus(); + } + handle.addEventListener('click', openSettings); + done.addEventListener('click', () => settings.close()); + settings.addEventListener('close', () => { + if (!dialog.open) globalThis.__oskiewarAccountOpen = false; + }); const button = document.createElement('button'); button.type = 'button'; button.textContent = 'delete account'; button.id = 'account-delete'; button.hidden = !account.signedIn; - document.querySelector('#account').append(button); + settings.append(button); const dialog = document.createElement('dialog'); + dialog.id = 'account-deletion'; dialog.setAttribute('aria-labelledby', 'account-deletion-title'); dialog.style.cssText = 'box-sizing:border-box;width:calc(100% - 32px);max-width:26rem;max-height:90vh;overflow:auto;padding:24px;background:#f5f5f0;color:#111;border:1px solid #555;font:18px/1.5 sans-serif'; dialog.innerHTML = ` @@ -32,15 +61,17 @@ export function mountAccountDeletion({ account, onDeleted }) { dialog.addEventListener('close', () => { revision++; ready = false; globalThis.__oskiewarAccountOpen = false; - if (!button.hidden) button.focus(); + if (account.signedIn) openSettings(); }); dialog.addEventListener('cancel', event => { if (busy) event.preventDefault(); }); cancel.addEventListener('click', close); field.addEventListener('input', refresh); addEventListener('oskiewar:account-change', () => { + syncSettings(); button.hidden = !account.signedIn; if (!account.signedIn && !completed) close(); }); + syncSettings(); async function request(method, token) { const response = await fetch(endpoint + (method === 'GET' ? '?preview' : ''), { method, headers: { authorization: 'Bearer ' + token }, @@ -56,6 +87,7 @@ export function mountAccountDeletion({ account, onDeleted }) { ready = false; completed = false; field.value = ''; refresh(); label.hidden = false; submit.hidden = false; cancel.textContent = 'Cancel'; note.textContent = 'Loading your account…'; + settings.close(); globalThis.__oskiewarAccountOpen = true; dialog.showModal(); try { const token = await account.bearer(); diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html index 2bec5d3da5..f3635fb5bb 100644 --- a/xbox/live/mac-test.html +++ b/xbox/live/mac-test.html @@ -39,6 +39,7 @@ { const harness = new URLSearchParams(location.search); globalThis.__oskiewarLocalPractice = harness.has("practice"); + globalThis.__oskiewarPracticeOpponent = harness.has("practice") ? "dummy" : "freeskate"; globalThis.__oskiewarVersusCapable = !offlineRuntime && !harness.has("social-preview") && !harness.has("practice") && !harness.has("replay-oven") && !harness.has("self-play") && !harness.has("workshop") && !harness.has("map"); } @@ -143,12 +144,22 @@ /* The silent auth check answers well after the first paint. In a row that is fine — the handle appears to the left of a button that has not moved — where the old stacked layout made the door jump a second in. */ - #account-handle { white-space: nowrap; + #account-handle { appearance: none; border: 0; background: transparent; padding: 7px 0; cursor: pointer; white-space: nowrap; font: 700 19px/1 "Comic Relief", "Comic Sans MS", Arial, sans-serif; color: #f3f6ff; text-shadow: 2px 3px 0 rgba(4, 7, 18, .65); } #account-handle[hidden] { display: none; } - #account-delete { padding: 7px; border: 0; background: transparent; - color: #303849; font: 14px/1.3 Arial, sans-serif; text-decoration: underline; cursor: pointer; } + #account-handle:focus-visible { outline: 3px solid #e6cd5c; outline-offset: 3px; } + #account-settings { width: calc(100% - 32px); max-width: 25rem; padding: 22px; + border: 3px solid #6e768d; border-radius: 14px; background: #171b28; + color: #f3f6ff; font: 17px/1.35 "Comic Relief", Arial, sans-serif; } + #account-settings::backdrop { background: rgba(4, 7, 18, .82); } + #account-settings h2 { margin: 0 0 22px; font-size: 26px; } + #account-settings-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 14px; } + #account-settings button { font: inherit; cursor: pointer; } + #account-settings-close { margin-left: auto; padding: 7px 11px; border: 1px solid #6e768d; + border-radius: 8px; background: #3b4256; color: #f3f6ff; } + #account-delete { display: block; margin-top: 28px; padding: 8px 0; border: 0; + background: transparent; color: #ffb7ad; text-decoration: underline; } #logout { appearance: none; padding: 7px 11px; border: 2px solid #6e768d; border-radius: 8px; background: #b0b8ca; color: #171b28; font: 700 15px/1 "Comic Relief", "Comic Sans MS", Arial, sans-serif; @@ -166,9 +177,14 @@ box-shadow: 4px 6px 0 rgba(20, 25, 38, .4); } #logout:active { transform: translate(1px, 2px); box-shadow: 1px 1px 0 rgba(20, 25, 38, .35); } - body:not(.title-open) #account, body.social-preview #account { display: none; } + body:not(.title-open):not(.menu-open) #account, body.social-preview #account { display: none; } body.midi-out #account { display: none; } - body:not(.title-open) #logout, body.social-preview #logout { display: none; } + body.social-preview #logout { display: none; } + #game-menu { position: fixed; z-index: 6; left: 50%; transform: translateX(-50%); + bottom: max(14px, env(safe-area-inset-bottom)); padding: 9px 14px; + border: 2px solid #6e768d; border-radius: 9px; background: #b0b8ca; + color: #171b28; font: 700 15px/1.3 "Comic Relief", Arial, sans-serif; } + body:not(.game-open) #game-menu, body.social-preview #game-menu { display: none; } /* Scoring from a DAW is anonymous dummy play, so the account door is shut and the corner it occupied is free for the midi lamp. */ body.midi-out #logout { display: none; } @@ -228,14 +244,15 @@ - + +
- +