iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

build: fix release recipe and unblock archive builds

- Pass BUNDLE_ID to xcodegen and xcodebuild in release recipe so archives have a bundle identifier
- Only persist the build-number bump on successful upload; restore project.yml on failure so retries reuse the same number
- Drop #if DEBUG guard around PreviewData so Release archives can compile #Preview blocks that reference it

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+288 -287
+280 -282
Grain/Utilities/PreviewData.swift
··· 1 - #if DEBUG 2 - import UIKit 1 + import UIKit 3 2 4 - // MARK: - Shared preview content used across all #Preview blocks 3 + // MARK: - Shared preview content used across all #Preview blocks 5 4 6 - enum PreviewData { 7 - // MARK: - Profiles 5 + enum PreviewData { 6 + // MARK: - Profiles 8 7 9 - static let profile = GrainProfileDetailed( 10 - cid: "cid1", 11 - did: "did:plc:prevuser1", 12 - handle: "yuki.grain.social", 13 - displayName: "Yuki Tanaka", 14 - description: "Analog photographer based in Tokyo 🇯🇵\nLeica M6 · Mamiya RB67 · Kodak Portra\n#35mm #film #analog #streetphoto", 15 - avatar: nil, 16 - cameras: ["Leica M6", "Mamiya RB67"], 17 - followersCount: 2847, 18 - followsCount: 412, 19 - galleryCount: 63, 20 - viewer: ActorViewerState(following: nil, followedBy: "at://preview/follow/1") 21 - ) 8 + static let profile = GrainProfileDetailed( 9 + cid: "cid1", 10 + did: "did:plc:prevuser1", 11 + handle: "yuki.grain.social", 12 + displayName: "Yuki Tanaka", 13 + description: "Analog photographer based in Tokyo 🇯🇵\nLeica M6 · Mamiya RB67 · Kodak Portra\n#35mm #film #analog #streetphoto", 14 + avatar: nil, 15 + cameras: ["Leica M6", "Mamiya RB67"], 16 + followersCount: 2847, 17 + followsCount: 412, 18 + galleryCount: 63, 19 + viewer: ActorViewerState(following: nil, followedBy: "at://preview/follow/1") 20 + ) 22 21 23 - static let profile2 = GrainProfile( 24 - cid: "cid2", did: "did:plc:prevuser2", 25 - handle: "marcus.grain.social", displayName: "Marcus Webb" 26 - ) 22 + static let profile2 = GrainProfile( 23 + cid: "cid2", did: "did:plc:prevuser2", 24 + handle: "marcus.grain.social", displayName: "Marcus Webb" 25 + ) 27 26 28 - static let profile3 = GrainProfile( 29 - cid: "cid3", did: "did:plc:prevuser3", 30 - handle: "sofia.grain.social", displayName: "Sofia Reyes" 31 - ) 27 + static let profile3 = GrainProfile( 28 + cid: "cid3", did: "did:plc:prevuser3", 29 + handle: "sofia.grain.social", displayName: "Sofia Reyes" 30 + ) 32 31 33 - // MARK: - Bundle image URL helper 32 + // MARK: - Bundle image URL helper 34 33 35 - static func bundleImageURL(_ name: String, ext: String = "jpg") -> String { 36 - Bundle.main.url(forResource: name, withExtension: ext)?.absoluteString ?? "" 37 - } 34 + static func bundleImageURL(_ name: String, ext: String = "jpg") -> String { 35 + Bundle.main.url(forResource: name, withExtension: ext)?.absoluteString ?? "" 36 + } 38 37 39 - // MARK: - Photos (file:// URLs → real images in preview via Nuke LazyImage) 38 + // MARK: - Photos (file:// URLs → real images in preview via Nuke LazyImage) 40 39 41 - static let photos: [GrainPhoto] = [ 42 - GrainPhoto( 43 - uri: "at://did:plc:prevuser1/social.grain.photo/p1", 44 - cid: "cid", 45 - thumb: bundleImageURL("Portland_Japanese_Garden_maple"), 46 - fullsize: bundleImageURL("Portland_Japanese_Garden_maple"), 47 - alt: "Japanese Garden, Portland", 48 - aspectRatio: AspectRatio(width: 4, height: 3) 49 - ), 50 - GrainPhoto( 51 - uri: "at://did:plc:prevuser1/social.grain.photo/p2", 52 - cid: "cid", 53 - thumb: bundleImageURL("Mount_Hood_reflected_in_Mirror_Lake,_Oregon"), 54 - fullsize: bundleImageURL("Mount_Hood_reflected_in_Mirror_Lake,_Oregon"), 55 - alt: "Mirror Lake, Oregon", 56 - aspectRatio: AspectRatio(width: 3, height: 2) 57 - ), 58 - GrainPhoto( 59 - uri: "at://did:plc:prevuser1/social.grain.photo/p3", 60 - cid: "cid", 61 - thumb: bundleImageURL("Mt_Herschel,_Antarctica,_Jan_2006"), 62 - fullsize: bundleImageURL("Mt_Herschel,_Antarctica,_Jan_2006"), 63 - alt: "Mt. Herschel, Antarctica", 64 - aspectRatio: AspectRatio(width: 4, height: 3) 65 - ), 66 - GrainPhoto( 67 - uri: "at://did:plc:prevuser1/social.grain.photo/p4", 68 - cid: "cid", 69 - thumb: bundleImageURL("Penguin_in_Antarctica_jumping_out_of_the_water"), 70 - fullsize: bundleImageURL("Penguin_in_Antarctica_jumping_out_of_the_water"), 71 - alt: "Penguin launching from ice shelf", 72 - aspectRatio: AspectRatio(width: 1, height: 1) 73 - ), 74 - GrainPhoto( 75 - uri: "at://did:plc:prevuser1/social.grain.photo/p5", 76 - cid: "cid", 77 - thumb: bundleImageURL("Union_Bank_Tower,_Portland_(2024)-L1006272"), 78 - fullsize: bundleImageURL("Union_Bank_Tower,_Portland_(2024)-L1006272"), 79 - alt: "Union Bank Tower, Portland", 80 - aspectRatio: AspectRatio(width: 2, height: 3) 81 - ), 82 - GrainPhoto( 83 - uri: "at://did:plc:prevuser1/social.grain.photo/p6", 84 - cid: "cid", 85 - thumb: bundleImageURL("ACE_EMD_F40PH_Fremont_-_San_Jose"), 86 - fullsize: bundleImageURL("ACE_EMD_F40PH_Fremont_-_San_Jose"), 87 - alt: "ACE train, Fremont–San Jose", 88 - aspectRatio: AspectRatio(width: 16, height: 9) 89 - ), 90 - GrainPhoto( 91 - uri: "at://did:plc:prevuser1/social.grain.photo/p7", 92 - cid: "cid", 93 - thumb: bundleImageURL("C-141_Starlifter_contrail"), 94 - fullsize: bundleImageURL("C-141_Starlifter_contrail"), 95 - alt: "C-141 Starlifter contrail", 96 - aspectRatio: AspectRatio(width: 16, height: 9) 97 - ), 98 - GrainPhoto( 99 - uri: "at://did:plc:prevuser1/social.grain.photo/p8", 100 - cid: "cid", 101 - thumb: bundleImageURL("Endeavour_after_STS-126_on_SCA_over_Mojave_from_above"), 102 - fullsize: bundleImageURL("Endeavour_after_STS-126_on_SCA_over_Mojave_from_above"), 103 - alt: "Space Shuttle Endeavour over Mojave", 104 - aspectRatio: AspectRatio(width: 4, height: 3) 105 - ), 106 - ] 40 + static let photos: [GrainPhoto] = [ 41 + GrainPhoto( 42 + uri: "at://did:plc:prevuser1/social.grain.photo/p1", 43 + cid: "cid", 44 + thumb: bundleImageURL("Portland_Japanese_Garden_maple"), 45 + fullsize: bundleImageURL("Portland_Japanese_Garden_maple"), 46 + alt: "Japanese Garden, Portland", 47 + aspectRatio: AspectRatio(width: 4, height: 3) 48 + ), 49 + GrainPhoto( 50 + uri: "at://did:plc:prevuser1/social.grain.photo/p2", 51 + cid: "cid", 52 + thumb: bundleImageURL("Mount_Hood_reflected_in_Mirror_Lake,_Oregon"), 53 + fullsize: bundleImageURL("Mount_Hood_reflected_in_Mirror_Lake,_Oregon"), 54 + alt: "Mirror Lake, Oregon", 55 + aspectRatio: AspectRatio(width: 3, height: 2) 56 + ), 57 + GrainPhoto( 58 + uri: "at://did:plc:prevuser1/social.grain.photo/p3", 59 + cid: "cid", 60 + thumb: bundleImageURL("Mt_Herschel,_Antarctica,_Jan_2006"), 61 + fullsize: bundleImageURL("Mt_Herschel,_Antarctica,_Jan_2006"), 62 + alt: "Mt. Herschel, Antarctica", 63 + aspectRatio: AspectRatio(width: 4, height: 3) 64 + ), 65 + GrainPhoto( 66 + uri: "at://did:plc:prevuser1/social.grain.photo/p4", 67 + cid: "cid", 68 + thumb: bundleImageURL("Penguin_in_Antarctica_jumping_out_of_the_water"), 69 + fullsize: bundleImageURL("Penguin_in_Antarctica_jumping_out_of_the_water"), 70 + alt: "Penguin launching from ice shelf", 71 + aspectRatio: AspectRatio(width: 1, height: 1) 72 + ), 73 + GrainPhoto( 74 + uri: "at://did:plc:prevuser1/social.grain.photo/p5", 75 + cid: "cid", 76 + thumb: bundleImageURL("Union_Bank_Tower,_Portland_(2024)-L1006272"), 77 + fullsize: bundleImageURL("Union_Bank_Tower,_Portland_(2024)-L1006272"), 78 + alt: "Union Bank Tower, Portland", 79 + aspectRatio: AspectRatio(width: 2, height: 3) 80 + ), 81 + GrainPhoto( 82 + uri: "at://did:plc:prevuser1/social.grain.photo/p6", 83 + cid: "cid", 84 + thumb: bundleImageURL("ACE_EMD_F40PH_Fremont_-_San_Jose"), 85 + fullsize: bundleImageURL("ACE_EMD_F40PH_Fremont_-_San_Jose"), 86 + alt: "ACE train, Fremont–San Jose", 87 + aspectRatio: AspectRatio(width: 16, height: 9) 88 + ), 89 + GrainPhoto( 90 + uri: "at://did:plc:prevuser1/social.grain.photo/p7", 91 + cid: "cid", 92 + thumb: bundleImageURL("C-141_Starlifter_contrail"), 93 + fullsize: bundleImageURL("C-141_Starlifter_contrail"), 94 + alt: "C-141 Starlifter contrail", 95 + aspectRatio: AspectRatio(width: 16, height: 9) 96 + ), 97 + GrainPhoto( 98 + uri: "at://did:plc:prevuser1/social.grain.photo/p8", 99 + cid: "cid", 100 + thumb: bundleImageURL("Endeavour_after_STS-126_on_SCA_over_Mojave_from_above"), 101 + fullsize: bundleImageURL("Endeavour_after_STS-126_on_SCA_over_Mojave_from_above"), 102 + alt: "Space Shuttle Endeavour over Mojave", 103 + aspectRatio: AspectRatio(width: 4, height: 3) 104 + ), 105 + ] 107 106 108 - // MARK: - Galleries 107 + // MARK: - Galleries 109 108 110 - static let gallery1 = GrainGallery( 111 - uri: "at://did:plc:prevuser1/social.grain.gallery/r1", 112 - cid: "cid", 113 - title: "Golden Hour, Kyoto", 114 - description: "Shot on Leica M6 with Kodak Portra 400 during autumn in Kyoto. #analog #japan #35mm #film", 115 - cameras: ["Leica M6"], 116 - creator: GrainProfile( 117 - cid: "cid", did: "did:plc:prevuser1", 118 - handle: "yuki.grain.social", displayName: "Yuki Tanaka" 119 - ), 120 - items: photos, 121 - favCount: 184, 122 - commentCount: 12, 123 - indexedAt: "2025-01-10T18:30:00Z" 124 - ) 109 + static let gallery1 = GrainGallery( 110 + uri: "at://did:plc:prevuser1/social.grain.gallery/r1", 111 + cid: "cid", 112 + title: "Golden Hour, Kyoto", 113 + description: "Shot on Leica M6 with Kodak Portra 400 during autumn in Kyoto. #analog #japan #35mm #film", 114 + cameras: ["Leica M6"], 115 + creator: GrainProfile( 116 + cid: "cid", did: "did:plc:prevuser1", 117 + handle: "yuki.grain.social", displayName: "Yuki Tanaka" 118 + ), 119 + items: photos, 120 + favCount: 184, 121 + commentCount: 12, 122 + indexedAt: "2025-01-10T18:30:00Z" 123 + ) 125 124 126 - static let gallery2 = GrainGallery( 127 - uri: "at://did:plc:prevuser2/social.grain.gallery/r2", 128 - cid: "cid", 129 - title: "Lower East Side", 130 - description: "Sunday morning light on Orchard St. #nyc #street #leica", 131 - cameras: ["Leica Q3"], 132 - creator: GrainProfile( 133 - cid: "cid", did: "did:plc:prevuser2", 134 - handle: "marcus.grain.social", displayName: "Marcus Webb" 135 - ), 136 - items: Array(photos.prefix(2)), 137 - favCount: 97, 138 - commentCount: 5, 139 - indexedAt: "2025-01-08T12:00:00Z" 140 - ) 125 + static let gallery2 = GrainGallery( 126 + uri: "at://did:plc:prevuser2/social.grain.gallery/r2", 127 + cid: "cid", 128 + title: "Lower East Side", 129 + description: "Sunday morning light on Orchard St. #nyc #street #leica", 130 + cameras: ["Leica Q3"], 131 + creator: GrainProfile( 132 + cid: "cid", did: "did:plc:prevuser2", 133 + handle: "marcus.grain.social", displayName: "Marcus Webb" 134 + ), 135 + items: Array(photos.prefix(2)), 136 + favCount: 97, 137 + commentCount: 5, 138 + indexedAt: "2025-01-08T12:00:00Z" 139 + ) 141 140 142 - static let gallery3 = GrainGallery( 143 - uri: "at://did:plc:prevuser3/social.grain.gallery/r3", 144 - cid: "cid", 145 - title: "Oaxaca Market", 146 - description: "Colors, light, and life. Shot on Fuji Velvia 50. #mexico #analog #color", 147 - cameras: ["Nikon FM2"], 148 - creator: GrainProfile( 149 - cid: "cid", did: "did:plc:prevuser3", 150 - handle: "sofia.grain.social", displayName: "Sofia Reyes" 151 - ), 152 - items: Array(photos.prefix(3)), 153 - favCount: 231, 154 - commentCount: 18, 155 - indexedAt: "2025-01-05T09:00:00Z" 156 - ) 141 + static let gallery3 = GrainGallery( 142 + uri: "at://did:plc:prevuser3/social.grain.gallery/r3", 143 + cid: "cid", 144 + title: "Oaxaca Market", 145 + description: "Colors, light, and life. Shot on Fuji Velvia 50. #mexico #analog #color", 146 + cameras: ["Nikon FM2"], 147 + creator: GrainProfile( 148 + cid: "cid", did: "did:plc:prevuser3", 149 + handle: "sofia.grain.social", displayName: "Sofia Reyes" 150 + ), 151 + items: Array(photos.prefix(3)), 152 + favCount: 231, 153 + commentCount: 18, 154 + indexedAt: "2025-01-05T09:00:00Z" 155 + ) 157 156 158 - static let galleries: [GrainGallery] = [gallery1, gallery2, gallery3] 157 + static let galleries: [GrainGallery] = [gallery1, gallery2, gallery3] 159 158 160 - // MARK: - Comments 159 + // MARK: - Comments 161 160 162 - static let comments: [GrainComment] = [ 163 - GrainComment( 164 - uri: "at://did:plc:prevuser2/social.grain.comment/c1", 165 - cid: "cid", 166 - author: profile2, 167 - text: "The light in the third frame is unreal. What film stock did you use?", 168 - createdAt: "2025-01-10T19:00:00Z" 169 - ), 170 - GrainComment( 171 - uri: "at://did:plc:prevuser3/social.grain.comment/c2", 172 - cid: "cid", 173 - author: profile3, 174 - text: "Portra always delivers in that golden hour window 🙌", 175 - replyTo: "at://did:plc:prevuser2/social.grain.comment/c1", 176 - createdAt: "2025-01-10T19:15:00Z" 177 - ), 178 - GrainComment( 179 - uri: "at://did:plc:prevuser2/social.grain.comment/c3", 180 - cid: "cid", 181 - author: profile2, 182 - text: "Worth every penny shooting on film. Love this series.", 183 - createdAt: "2025-01-10T20:00:00Z" 184 - ), 185 - ] 161 + static let comments: [GrainComment] = [ 162 + GrainComment( 163 + uri: "at://did:plc:prevuser2/social.grain.comment/c1", 164 + cid: "cid", 165 + author: profile2, 166 + text: "The light in the third frame is unreal. What film stock did you use?", 167 + createdAt: "2025-01-10T19:00:00Z" 168 + ), 169 + GrainComment( 170 + uri: "at://did:plc:prevuser3/social.grain.comment/c2", 171 + cid: "cid", 172 + author: profile3, 173 + text: "Portra always delivers in that golden hour window 🙌", 174 + replyTo: "at://did:plc:prevuser2/social.grain.comment/c1", 175 + createdAt: "2025-01-10T19:15:00Z" 176 + ), 177 + GrainComment( 178 + uri: "at://did:plc:prevuser2/social.grain.comment/c3", 179 + cid: "cid", 180 + author: profile2, 181 + text: "Worth every penny shooting on film. Love this series.", 182 + createdAt: "2025-01-10T20:00:00Z" 183 + ), 184 + ] 186 185 187 - // MARK: - PhotoItems (UIImage-based, for editor/grid/strip previews) 186 + // MARK: - PhotoItems (UIImage-based, for editor/grid/strip previews) 188 187 189 - static var photoItems: [PhotoItem] { 190 - let stockImages: [(name: String, alt: String)] = [ 191 - ("Portland_Japanese_Garden_maple", "Japanese Garden, Portland"), 192 - ("Mount_Hood_reflected_in_Mirror_Lake,_Oregon", "Mirror Lake, Oregon"), 193 - ("Mt_Herschel,_Antarctica,_Jan_2006", "Mt. Herschel, Antarctica"), 194 - ("Penguin_in_Antarctica_jumping_out_of_the_water", "Penguin launching from ice shelf"), 195 - ("Union_Bank_Tower,_Portland_(2024)-L1006272", "Union Bank Tower, Portland"), 196 - ("ACE_EMD_F40PH_Fremont_-_San_Jose", "ACE train, Fremont–San Jose"), 197 - ("C-141_Starlifter_contrail", "C-141 Starlifter contrail"), 198 - ("Endeavour_after_STS-126_on_SCA_over_Mojave_from_above", "Space Shuttle Endeavour over Mojave"), 199 - ] 200 - let fallbackColors: [([CGColor], String)] = [ 201 - ([UIColor.systemBrown.cgColor, UIColor.systemOrange.cgColor], ""), 202 - ([UIColor.systemMint.cgColor, UIColor.systemTeal.cgColor], ""), 203 - ([UIColor.systemCyan.cgColor, UIColor.systemBlue.cgColor], ""), 204 - ([UIColor.systemGray.cgColor, UIColor.systemGray3.cgColor], ""), 205 - ([UIColor.systemPink.cgColor, UIColor.systemRed.cgColor], "Market stalls, morning light"), 206 - ([UIColor.systemGreen.cgColor, UIColor.systemMint.cgColor], ""), 207 - ([UIColor.systemOrange.cgColor, UIColor.systemYellow.cgColor], ""), 208 - ] 209 - var items: [PhotoItem] = stockImages.compactMap { entry in 210 - guard let path = Bundle.main.url(forResource: entry.name, withExtension: "jpg")?.path, 211 - let fullImage = UIImage(contentsOfFile: path) else { return nil } 212 - let thumb = PhotoItem.makeThumbnail(from: fullImage) 213 - var item = PhotoItem(thumbnail: thumb, source: .camera(fullImage, metadata: nil)) 214 - item.alt = entry.alt 215 - return item 216 - } 217 - // Pad with gradient fallbacks if fewer than 15 items total 218 - for (colors, label) in fallbackColors { 219 - let thumb = gradientThumb(colors: colors) 220 - var item = PhotoItem(thumbnail: thumb, source: .camera(thumb, metadata: nil)) 221 - item.alt = label 222 - items.append(item) 223 - } 224 - return items 188 + static var photoItems: [PhotoItem] { 189 + let stockImages: [(name: String, alt: String)] = [ 190 + ("Portland_Japanese_Garden_maple", "Japanese Garden, Portland"), 191 + ("Mount_Hood_reflected_in_Mirror_Lake,_Oregon", "Mirror Lake, Oregon"), 192 + ("Mt_Herschel,_Antarctica,_Jan_2006", "Mt. Herschel, Antarctica"), 193 + ("Penguin_in_Antarctica_jumping_out_of_the_water", "Penguin launching from ice shelf"), 194 + ("Union_Bank_Tower,_Portland_(2024)-L1006272", "Union Bank Tower, Portland"), 195 + ("ACE_EMD_F40PH_Fremont_-_San_Jose", "ACE train, Fremont–San Jose"), 196 + ("C-141_Starlifter_contrail", "C-141 Starlifter contrail"), 197 + ("Endeavour_after_STS-126_on_SCA_over_Mojave_from_above", "Space Shuttle Endeavour over Mojave"), 198 + ] 199 + let fallbackColors: [([CGColor], String)] = [ 200 + ([UIColor.systemBrown.cgColor, UIColor.systemOrange.cgColor], ""), 201 + ([UIColor.systemMint.cgColor, UIColor.systemTeal.cgColor], ""), 202 + ([UIColor.systemCyan.cgColor, UIColor.systemBlue.cgColor], ""), 203 + ([UIColor.systemGray.cgColor, UIColor.systemGray3.cgColor], ""), 204 + ([UIColor.systemPink.cgColor, UIColor.systemRed.cgColor], "Market stalls, morning light"), 205 + ([UIColor.systemGreen.cgColor, UIColor.systemMint.cgColor], ""), 206 + ([UIColor.systemOrange.cgColor, UIColor.systemYellow.cgColor], ""), 207 + ] 208 + var items: [PhotoItem] = stockImages.compactMap { entry in 209 + guard let path = Bundle.main.url(forResource: entry.name, withExtension: "jpg")?.path, 210 + let fullImage = UIImage(contentsOfFile: path) else { return nil } 211 + let thumb = PhotoItem.makeThumbnail(from: fullImage) 212 + var item = PhotoItem(thumbnail: thumb, source: .camera(fullImage, metadata: nil)) 213 + item.alt = entry.alt 214 + return item 215 + } 216 + // Pad with gradient fallbacks if fewer than 15 items total 217 + for (colors, label) in fallbackColors { 218 + let thumb = gradientThumb(colors: colors) 219 + var item = PhotoItem(thumbnail: thumb, source: .camera(thumb, metadata: nil)) 220 + item.alt = label 221 + items.append(item) 225 222 } 223 + return items 224 + } 226 225 227 - // MARK: - Story authors 226 + // MARK: - Story authors 228 227 229 - static let storyAuthors: [GrainStoryAuthor] = [ 230 - GrainStoryAuthor(profile: GrainProfile(cid: "c1", did: "did:plc:prevuser1", handle: "yuki.grain.social", displayName: "Yuki"), storyCount: 3, latestAt: "2025-01-10T18:00:00Z"), 231 - GrainStoryAuthor(profile: GrainProfile(cid: "c2", did: "did:plc:prevuser2", handle: "marcus.grain.social", displayName: "Marcus"), storyCount: 1, latestAt: "2025-01-10T15:00:00Z"), 232 - GrainStoryAuthor(profile: GrainProfile(cid: "c3", did: "did:plc:prevuser3", handle: "sofia.grain.social", displayName: "Sofia"), storyCount: 2, latestAt: "2025-01-10T12:00:00Z"), 233 - GrainStoryAuthor(profile: GrainProfile(cid: "c4", did: "did:plc:prevuser4", handle: "kai.grain.social", displayName: "Kai"), storyCount: 1, latestAt: "2025-01-10T10:00:00Z"), 234 - GrainStoryAuthor(profile: GrainProfile(cid: "c5", did: "did:plc:prevuser5", handle: "leo.grain.social", displayName: "Leo"), storyCount: 4, latestAt: "2025-01-10T08:00:00Z"), 235 - ] 228 + static let storyAuthors: [GrainStoryAuthor] = [ 229 + GrainStoryAuthor(profile: GrainProfile(cid: "c1", did: "did:plc:prevuser1", handle: "yuki.grain.social", displayName: "Yuki"), storyCount: 3, latestAt: "2025-01-10T18:00:00Z"), 230 + GrainStoryAuthor(profile: GrainProfile(cid: "c2", did: "did:plc:prevuser2", handle: "marcus.grain.social", displayName: "Marcus"), storyCount: 1, latestAt: "2025-01-10T15:00:00Z"), 231 + GrainStoryAuthor(profile: GrainProfile(cid: "c3", did: "did:plc:prevuser3", handle: "sofia.grain.social", displayName: "Sofia"), storyCount: 2, latestAt: "2025-01-10T12:00:00Z"), 232 + GrainStoryAuthor(profile: GrainProfile(cid: "c4", did: "did:plc:prevuser4", handle: "kai.grain.social", displayName: "Kai"), storyCount: 1, latestAt: "2025-01-10T10:00:00Z"), 233 + GrainStoryAuthor(profile: GrainProfile(cid: "c5", did: "did:plc:prevuser5", handle: "leo.grain.social", displayName: "Leo"), storyCount: 4, latestAt: "2025-01-10T08:00:00Z"), 234 + ] 236 235 237 - // MARK: - Notifications 236 + // MARK: - Notifications 238 237 239 - static let notifications: [GrainNotification] = [ 240 - GrainNotification( 241 - uri: "at://did:plc:prevuser2/social.grain.notification/n1", 242 - reason: "gallery-favorite", 243 - createdAt: "2025-01-10T19:30:00Z", 244 - author: profile2, 245 - galleryUri: gallery1.uri, 246 - galleryTitle: gallery1.title, 247 - galleryThumb: "" 248 - ), 249 - GrainNotification( 250 - uri: "at://did:plc:prevuser3/social.grain.notification/n2", 251 - reason: "gallery-comment", 252 - createdAt: "2025-01-10T19:00:00Z", 253 - author: profile3, 254 - galleryUri: gallery1.uri, 255 - galleryTitle: gallery1.title, 256 - galleryThumb: "", 257 - commentText: "The light in the third frame is unreal. What film stock?" 258 - ), 259 - GrainNotification( 260 - uri: "at://did:plc:prevuser4/social.grain.notification/n3", 261 - reason: "follow", 262 - createdAt: "2025-01-10T18:00:00Z", 263 - author: GrainProfile(cid: "c4", did: "did:plc:prevuser4", handle: "kai.grain.social", displayName: "Kai Müller") 264 - ), 265 - GrainNotification( 266 - uri: "at://did:plc:prevuser2/social.grain.notification/n4", 267 - reason: "gallery-favorite", 268 - createdAt: "2025-01-09T12:00:00Z", 269 - author: profile2, 270 - galleryUri: gallery2.uri, 271 - galleryTitle: gallery2.title, 272 - galleryThumb: "" 273 - ), 274 - GrainNotification( 275 - uri: "at://did:plc:prevuser5/social.grain.notification/n5", 276 - reason: "gallery-comment-mention", 277 - createdAt: "2025-01-09T10:00:00Z", 278 - author: GrainProfile(cid: "c5", did: "did:plc:prevuser5", handle: "leo.grain.social", displayName: "Leo Park"), 279 - galleryUri: gallery1.uri, 280 - galleryTitle: gallery1.title, 281 - galleryThumb: "", 282 - commentText: "Tagged you in a comment: @yuki.grain.social beautiful work!" 283 - ), 284 - ] 238 + static let notifications: [GrainNotification] = [ 239 + GrainNotification( 240 + uri: "at://did:plc:prevuser2/social.grain.notification/n1", 241 + reason: "gallery-favorite", 242 + createdAt: "2025-01-10T19:30:00Z", 243 + author: profile2, 244 + galleryUri: gallery1.uri, 245 + galleryTitle: gallery1.title, 246 + galleryThumb: "" 247 + ), 248 + GrainNotification( 249 + uri: "at://did:plc:prevuser3/social.grain.notification/n2", 250 + reason: "gallery-comment", 251 + createdAt: "2025-01-10T19:00:00Z", 252 + author: profile3, 253 + galleryUri: gallery1.uri, 254 + galleryTitle: gallery1.title, 255 + galleryThumb: "", 256 + commentText: "The light in the third frame is unreal. What film stock?" 257 + ), 258 + GrainNotification( 259 + uri: "at://did:plc:prevuser4/social.grain.notification/n3", 260 + reason: "follow", 261 + createdAt: "2025-01-10T18:00:00Z", 262 + author: GrainProfile(cid: "c4", did: "did:plc:prevuser4", handle: "kai.grain.social", displayName: "Kai Müller") 263 + ), 264 + GrainNotification( 265 + uri: "at://did:plc:prevuser2/social.grain.notification/n4", 266 + reason: "gallery-favorite", 267 + createdAt: "2025-01-09T12:00:00Z", 268 + author: profile2, 269 + galleryUri: gallery2.uri, 270 + galleryTitle: gallery2.title, 271 + galleryThumb: "" 272 + ), 273 + GrainNotification( 274 + uri: "at://did:plc:prevuser5/social.grain.notification/n5", 275 + reason: "gallery-comment-mention", 276 + createdAt: "2025-01-09T10:00:00Z", 277 + author: GrainProfile(cid: "c5", did: "did:plc:prevuser5", handle: "leo.grain.social", displayName: "Leo Park"), 278 + galleryUri: gallery1.uri, 279 + galleryTitle: gallery1.title, 280 + galleryThumb: "", 281 + commentText: "Tagged you in a comment: @yuki.grain.social beautiful work!" 282 + ), 283 + ] 285 284 286 - // MARK: - Image generation 285 + // MARK: - Image generation 287 286 288 - static func gradientThumb( 289 - colors: [CGColor], 290 - size: CGSize = CGSize(width: 300, height: 300) 291 - ) -> UIImage { 292 - UIGraphicsImageRenderer(size: size).image { ctx in 293 - let cgCtx = ctx.cgContext 294 - let colorSpace = CGColorSpaceCreateDeviceRGB() 295 - guard let gradient = CGGradient( 296 - colorsSpace: colorSpace, 297 - colors: colors as CFArray, 298 - locations: nil 299 - ) else { return } 300 - cgCtx.drawLinearGradient( 301 - gradient, 302 - start: .zero, 303 - end: CGPoint(x: size.width, y: size.height), 304 - options: [] 305 - ) 306 - } 287 + static func gradientThumb( 288 + colors: [CGColor], 289 + size: CGSize = CGSize(width: 300, height: 300) 290 + ) -> UIImage { 291 + UIGraphicsImageRenderer(size: size).image { ctx in 292 + let cgCtx = ctx.cgContext 293 + let colorSpace = CGColorSpaceCreateDeviceRGB() 294 + guard let gradient = CGGradient( 295 + colorsSpace: colorSpace, 296 + colors: colors as CFArray, 297 + locations: nil 298 + ) else { return } 299 + cgCtx.drawLinearGradient( 300 + gradient, 301 + start: .zero, 302 + end: CGPoint(x: size.width, y: size.height), 303 + options: [] 304 + ) 307 305 } 308 306 } 309 - #endif 307 + }
+7 -4
justfile
··· 84 84 release: 85 85 #!/usr/bin/env bash 86 86 set -euo pipefail 87 - # Read current build number and bump 87 + # Compute next build number (don't write yet — only bump on successful upload) 88 88 current=$(grep 'CURRENT_PROJECT_VERSION' project.yml | head -1 | sed 's/.*"\([0-9]*\)"/\1/') 89 89 next=$((current + 1)) 90 + echo "Preparing build $next (current: $current)" 90 91 sed -i '' "s/CURRENT_PROJECT_VERSION: \"$current\"/CURRENT_PROJECT_VERSION: \"$next\"/" project.yml 91 - echo "Bumped build number: $current → $next" 92 - xcodegen generate 92 + # Restore on any failure so the next attempt reuses the same number 93 + trap 'sed -i "" "s/CURRENT_PROJECT_VERSION: \"$next\"/CURRENT_PROJECT_VERSION: \"$current\"/" project.yml; BUNDLE_ID={{bundle_id}} xcodegen generate >/dev/null 2>&1 || true' ERR 94 + BUNDLE_ID={{bundle_id}} xcodegen generate 93 95 echo "Archiving..." 94 - set -o pipefail && xcodebuild archive -scheme Grain -destination 'generic/platform=iOS' -archivePath /tmp/Grain.xcarchive CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM={{team_id}} -allowProvisioningUpdates 2>&1 | xcbeautify 96 + set -o pipefail && xcodebuild archive -scheme Grain -destination 'generic/platform=iOS' -archivePath /tmp/Grain.xcarchive CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM={{team_id}} PRODUCT_BUNDLE_IDENTIFIER={{bundle_id}} -allowProvisioningUpdates 2>&1 | xcbeautify 95 97 echo "Uploading to App Store Connect..." 96 98 cat > /tmp/ExportOptions.plist << 'PLIST' 97 99 <?xml version="1.0" encoding="UTF-8"?> ··· 108 110 </plist> 109 111 PLIST 110 112 xcodebuild -exportArchive -archivePath /tmp/Grain.xcarchive -exportOptionsPlist /tmp/ExportOptions.plist -exportPath /tmp/GrainExport -allowProvisioningUpdates 113 + trap - ERR 111 114 echo "Build $next uploaded successfully!"
+1 -1
project.yml
··· 44 44 INFOPLIST_FILE: Grain/Info.plist 45 45 PRODUCT_BUNDLE_IDENTIFIER: ${BUNDLE_ID} 46 46 MARKETING_VERSION: "1.0.0" 47 - CURRENT_PROJECT_VERSION: "38" 47 + CURRENT_PROJECT_VERSION: "41" 48 48 CODE_SIGN_STYLE: Automatic 49 49 DEVELOPMENT_ASSET_PATHS: "\"$(SRCROOT)/Grain/Preview Content\"" 50 50 dependencies: