├── .gitignore ├── LICENSE ├── NefEditorClient.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── NefEditorClient ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 120-1.png │ │ ├── 120.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40-1.png │ │ ├── 40-2.png │ │ ├── 40.png │ │ ├── 58-1.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 76.png │ │ ├── 80-1.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── Contents.json │ │ ├── nef@128.png │ │ ├── nef@16.png │ │ ├── nef@256.png │ │ ├── nef@32.png │ │ └── nef@512.png │ ├── Contents.json │ ├── badges │ │ ├── Contents.json │ │ ├── bow-actions-badge.imageset │ │ │ ├── Contents.json │ │ │ └── bow-actions-badge.png │ │ ├── bow-platform-badge.imageset │ │ │ ├── Contents.json │ │ │ └── bow-platform-badge.png │ │ └── nef-playgrounds-badge.imageset │ │ │ ├── Contents.json │ │ │ └── nef-playgrounds-badge.png │ ├── bow-arch-background.imageset │ │ ├── Contents.json │ │ └── card-bow-arch-16_9 (1).pdf │ ├── bow-arch-brand.imageset │ │ ├── Contents.json │ │ └── bow-arch (1).pdf │ ├── bow-background.imageset │ │ ├── Contents.json │ │ └── card-bow-16_9 (1).pdf │ ├── bow-brand.imageset │ │ ├── Contents.json │ │ └── bow-brand (1).pdf │ ├── bow-lite-background.imageset │ │ ├── Contents.json │ │ └── bow-lite-card.pdf │ ├── bow-lite-brand.imageset │ │ ├── Contents.json │ │ └── bow-lite-brand.pdf │ ├── bow-openapi-brand.imageset │ │ ├── Contents.json │ │ └── bow-open-api (1).pdf │ ├── bow-openapi.imageset │ │ ├── Contents.json │ │ ├── bowopenapi120.png │ │ ├── bowopenapi40.png │ │ └── bowopenapi80.png │ ├── card.colorset │ │ └── Contents.json │ ├── form.colorset │ │ └── Contents.json │ ├── fortyseven.imageset │ │ ├── Contents.json │ │ └── fortyseven.pdf │ ├── github-icon.imageset │ │ ├── Contents.json │ │ ├── github-dark@2x.png │ │ ├── github-dark@3x.png │ │ ├── github-light@2x.png │ │ └── github-light@3x.png │ ├── mainBackground.colorset │ │ └── Contents.json │ ├── nef-icon.imageset │ │ ├── 256.png │ │ ├── 512.png │ │ ├── Contents.json │ │ ├── nef@256.png │ │ └── nef@512.png │ ├── nef.imageset │ │ ├── @2x.png │ │ ├── @3x.png │ │ └── Contents.json │ ├── nefColor.colorset │ │ └── Contents.json │ ├── shadow.colorset │ │ └── Contents.json │ ├── splash-launchimage.imageset │ │ ├── Contents.json │ │ └── launchscreen-h@2x.png │ └── splash-logo.imageset │ │ ├── Contents.json │ │ └── header-image.png ├── Authentication │ ├── State │ │ ├── AuthenticationInfo.swift │ │ └── AuthenticationState.swift │ └── View │ │ └── SignInButton.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Catalog │ ├── Component │ │ └── CatalogComponent.swift │ ├── Extensions │ │ └── URLQueryItem+Recipe.swift │ ├── Logic │ │ ├── CatalogAction.swift │ │ └── CatalogDispatcher.swift │ ├── State │ │ ├── Catalog.swift │ │ ├── CatalogItem.swift │ │ ├── CatalogSection.swift │ │ ├── Dependency.swift │ │ ├── FeaturedRecipe.swift │ │ ├── Recipe.swift │ │ └── TagViewModel.swift │ └── View │ │ ├── CatalogItemGridView.swift │ │ ├── CatalogSectionView.swift │ │ ├── FeaturedRecipeView.swift │ │ ├── RecipeCatalogView.swift │ │ ├── RegularRecipeView.swift │ │ ├── TagCloud.swift │ │ └── TagView.swift ├── Credits │ ├── Component │ │ └── CreditsComponent.swift │ ├── Logic │ │ ├── CreditsAction.swift │ │ └── CreditsDispatcher.swift │ ├── State │ │ └── Library.swift │ └── View │ │ └── CreditsView.swift ├── DeepLink │ ├── Extensions │ │ └── URL+Recipe.swift │ ├── Logic │ │ ├── DeepLinkAction.swift │ │ └── DeepLinkDispatcher.swift │ └── State │ │ └── DeepLinkState.swift ├── Detail │ ├── Component │ │ └── CatalogDetailComponent.swift │ ├── Logic │ │ ├── CatalogDetailAction.swift │ │ └── CatalogDetailDispatcher.swift │ └── View │ │ ├── CatalogItemDetailView.swift │ │ ├── DependencyListView.swift │ │ └── DependencyView.swift ├── Edit │ ├── Component │ │ └── EditComponent.swift │ ├── Logic │ │ ├── EditAction.swift │ │ └── EditDispatcher.swift │ ├── State │ │ └── EditState.swift │ └── View │ │ └── EditRecipeMetadataView.swift ├── Extensions │ ├── Array+Extensions.swift │ ├── Bundle+Extensions.swift │ └── URLSession+IO.swift ├── FAQ │ ├── Component │ │ └── FAQComponent.swift │ ├── Logic │ │ ├── FAQAction.swift │ │ └── FAQDispatcher.swift │ └── View │ │ └── FAQView.swift ├── Generation │ ├── Component │ │ └── GenerationComponent.swift │ ├── Logic │ │ ├── GenerationAction.swift │ │ └── GenerationDispatcher.swift │ ├── State │ │ └── GenerationState.swift │ └── View │ │ ├── GenerationErrorView.swift │ │ ├── GenerationPlaygroundBookView.swift │ │ ├── GenerationSigninView.swift │ │ ├── GenerationSuccessView.swift │ │ └── GenerationView.swift ├── GitHubExtensions │ └── Data+Equatable.swift ├── Info.plist ├── Main │ ├── Component │ │ ├── AppComponent.swift │ │ └── AppModalComponent.swift │ ├── Dependencies │ │ └── AppDependencies.swift │ ├── Logic │ │ ├── AppAction.swift │ │ ├── AppDispatcher.swift │ │ └── MainDispatcher.swift │ ├── State │ │ ├── AppModalState.swift │ │ ├── AppState.swift │ │ ├── ICloudStatus.swift │ │ └── PanelState.swift │ └── View │ │ ├── AppModalView.swift │ │ └── AppView.swift ├── NefEditorClient.entitlements ├── NefEditorClientRelease.entitlements ├── Persistence │ ├── ICloudPersistence.swift │ ├── Persistence.swift │ └── UserPreferences.swift ├── Preview Content │ ├── Preview Assets.xcassets │ │ └── Contents.json │ └── PreviewData.swift ├── Requirements │ ├── Component │ │ └── RepositoryDetailComponent.swift │ ├── Logic │ │ ├── RepositoryDetailAction.swift │ │ └── RepositoryDetailDispatcher.swift │ ├── State │ │ ├── RepositoryDetailState.swift │ │ └── Requirement.swift │ └── View │ │ ├── EmptyRequirementsView.swift │ │ ├── ErrorRequirementsView.swift │ │ ├── LoadingRequirementsView.swift │ │ ├── RepositoryDetailView.swift │ │ ├── RequirementListView.swift │ │ └── RequirementView.swift ├── SceneDelegate.swift ├── Search │ ├── Component │ │ └── SearchComponent.swift │ ├── Logic │ │ ├── SearchAction.swift │ │ └── SearchDispatcher.swift │ ├── State │ │ └── SearchState.swift │ └── View │ │ ├── EmptySearchView.swift │ │ ├── ErrorSearchView.swift │ │ ├── InitialSearchView.swift │ │ ├── LoadingSearchView.swift │ │ ├── RepositoryGridView.swift │ │ ├── RepositoryView.swift │ │ └── SearchView.swift ├── Theme │ ├── ActionButtonStyle.swift │ ├── ActivityIndicator.swift │ ├── ActivityTextView.swift │ ├── ActivityViewController.swift │ ├── AnimationView.swift │ ├── AvatarView.swift │ ├── CardView.swift │ ├── ColorPalette.swift │ ├── GridView.swift │ ├── Image+System.swift │ ├── KeyboardPadding.swift │ ├── LibraryButtonStyle.swift │ ├── LoadingView.swift │ ├── LottieAnimation.swift │ ├── NavigationBarButtonStyle.swift │ ├── Rectangle+Separator.swift │ ├── SearchBar.swift │ ├── SectionTitle.swift │ ├── TextButtonStyle.swift │ ├── TextStyle.swift │ ├── URLImage.swift │ ├── View+Conditional.swift │ ├── View+Hover.swift │ ├── View+Modal.swift │ └── ViewStyle.swift ├── ViewModifiers │ └── ScaledFont.swift └── WhatsNew │ ├── Component │ └── WhatsNewComponent.swift │ ├── Logic │ ├── WhatsNewAction.swift │ └── WhatsNewDispatcher.swift │ └── View │ ├── BadgeGeneratorCard.swift │ └── WhatsNewView.swift ├── NefEditorClientTests ├── AppDispatcherTests.swift ├── Info.plist └── NefEditorClientTests.swift ├── README.md ├── github.yaml └── nef.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | GitHub/** 2 | NefAPI/** 3 | **/Animations/** 4 | 5 | ## User settings 6 | xcuserdata/ 7 | .DS_Store 8 | 9 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 10 | build/ 11 | DerivedData/ 12 | 13 | ## Obj-C/Swift specific 14 | *.hmap 15 | 16 | ## App packaging 17 | *.ipa 18 | *.dSYM.zip 19 | *.dSYM 20 | 21 | ## Playgrounds 22 | timeline.xctimeline 23 | playground.xcworkspace 24 | 25 | # Swift Package Manager 26 | # 27 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 28 | Packages/ 29 | Package.pins 30 | *.xcodeproj 31 | .swiftpm 32 | .build 33 | -------------------------------------------------------------------------------- /NefEditorClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NefEditorClient.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NefEditorClient.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Bow", 6 | "repositoryURL": "https://github.com/bow-swift/bow.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "a33173c8a111cbb492fe0ae0f16b58a0adb945d2", 10 | "version": null 11 | } 12 | }, 13 | { 14 | "package": "BowArch", 15 | "repositoryURL": "https://github.com/bow-swift/bow-arch.git", 16 | "state": { 17 | "branch": "master", 18 | "revision": "9fbbcc703f196ba9286522365e71b2019ac30c7e", 19 | "version": null 20 | } 21 | }, 22 | { 23 | "package": "Lottie", 24 | "repositoryURL": "https://github.com/airbnb/lottie-ios.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "4e5877425dae5c10792fc9d22d53dc6bf6824dc1", 28 | "version": "3.1.8" 29 | } 30 | }, 31 | { 32 | "package": "nef-editor-server", 33 | "repositoryURL": "https://github.com/47deg/nef-editor-server", 34 | "state": { 35 | "branch": "master", 36 | "revision": "edadad0f37f90fd4a4b7cf17ee4551d2b132f4a5", 37 | "version": null 38 | } 39 | }, 40 | { 41 | "package": "RxSwift", 42 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "002d325b0bdee94e7882e1114af5ff4fe1e96afa", 46 | "version": "5.1.1" 47 | } 48 | }, 49 | { 50 | "package": "SwiftCheck", 51 | "repositoryURL": "https://github.com/bow-swift/SwiftCheck.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "748359f9a95edf94d0c4664102f104f56b1ff1fb", 55 | "version": "0.12.1" 56 | } 57 | }, 58 | { 59 | "package": "ZIPFoundation", 60 | "repositoryURL": "https://github.com/weichsel/ZIPFoundation.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "ec32d62d412578542c0ffb7a6ce34d3e64b43b94", 64 | "version": "0.9.11" 65 | } 66 | } 67 | ] 68 | }, 69 | "version": 1 70 | } 71 | -------------------------------------------------------------------------------- /NefEditorClient/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 7 | // Override point for customization after application launch. 8 | return true 9 | } 10 | 11 | // MARK: UISceneSession Lifecycle 12 | 13 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 14 | // Called when a new scene session is being created. 15 | // Use this method to select a configuration to create the new scene with. 16 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/120-1.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-1.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-2.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/58-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/58-1.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/80-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/80-1.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120-1.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "40-1.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "58-1.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40-2.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "80-1.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "nef@16.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "nef@32.png", 119 | "idiom" : "mac", 120 | "scale" : "1x", 121 | "size" : "32x32" 122 | }, 123 | { 124 | "filename" : "nef@128.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "128x128" 128 | }, 129 | { 130 | "filename" : "nef@256.png", 131 | "idiom" : "mac", 132 | "scale" : "1x", 133 | "size" : "256x256" 134 | }, 135 | { 136 | "filename" : "nef@512.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "512x512" 140 | } 141 | ], 142 | "info" : { 143 | "author" : "xcode", 144 | "version" : 1 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@128.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@16.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@256.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@32.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@512.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/badges/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/badges/bow-actions-badge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "bow-actions-badge.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/badges/bow-actions-badge.imageset/bow-actions-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/badges/bow-actions-badge.imageset/bow-actions-badge.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/badges/bow-platform-badge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "bow-platform-badge.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/badges/bow-platform-badge.imageset/bow-platform-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/badges/bow-platform-badge.imageset/bow-platform-badge.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/badges/nef-playgrounds-badge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "nef-playgrounds-badge.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/badges/nef-playgrounds-badge.imageset/nef-playgrounds-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/badges/nef-playgrounds-badge.imageset/nef-playgrounds-badge.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-arch-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "card-bow-arch-16_9 (1).pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-arch-background.imageset/card-bow-arch-16_9 (1).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-arch-background.imageset/card-bow-arch-16_9 (1).pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-arch-brand.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bow-arch (1).pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-arch-brand.imageset/bow-arch (1).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-arch-brand.imageset/bow-arch (1).pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "card-bow-16_9 (1).pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-background.imageset/card-bow-16_9 (1).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-background.imageset/card-bow-16_9 (1).pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-brand.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bow-brand (1).pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-brand.imageset/bow-brand (1).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-brand.imageset/bow-brand (1).pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-lite-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bow-lite-card.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-lite-background.imageset/bow-lite-card.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-lite-background.imageset/bow-lite-card.pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-lite-brand.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bow-lite-brand.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-lite-brand.imageset/bow-lite-brand.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-lite-brand.imageset/bow-lite-brand.pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-openapi-brand.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bow-open-api (1).pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-openapi-brand.imageset/bow-open-api (1).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi-brand.imageset/bow-open-api (1).pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-openapi.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bowopenapi40.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "bowopenapi80.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "bowopenapi120.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi120.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi40.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi80.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/card.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "255", 9 | "green" : "255", 10 | "red" : "255" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0.118", 45 | "green" : "0.110", 46 | "red" : "0.110" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/form.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "247", 9 | "green" : "242", 10 | "red" : "242" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "247", 27 | "green" : "242", 28 | "red" : "242" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "30", 45 | "green" : "28", 46 | "red" : "28" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/fortyseven.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "fortyseven.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/fortyseven.imageset/fortyseven.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/fortyseven.imageset/fortyseven.pdf -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/github-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "github-dark@2x.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "github-light@2x.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "github-dark@3x.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "github-light@3x.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@2x.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@3x.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@2x.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@3x.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/mainBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.969", 9 | "green" : "0.949", 10 | "red" : "0.949" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.969", 27 | "green" : "0.949", 28 | "red" : "0.949" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0", 45 | "green" : "0", 46 | "red" : "0" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef-icon.imageset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/256.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef-icon.imageset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/512.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "idiom" : "universal", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "filename" : "256.png", 19 | "idiom" : "universal", 20 | "scale" : "2x" 21 | }, 22 | { 23 | "appearances" : [ 24 | { 25 | "appearance" : "luminosity", 26 | "value" : "dark" 27 | } 28 | ], 29 | "filename" : "nef@256.png", 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "filename" : "512.png", 35 | "idiom" : "universal", 36 | "scale" : "3x" 37 | }, 38 | { 39 | "appearances" : [ 40 | { 41 | "appearance" : "luminosity", 42 | "value" : "dark" 43 | } 44 | ], 45 | "filename" : "nef@512.png", 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@256.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@512.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef.imageset/@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef.imageset/@2x.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef.imageset/@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef.imageset/@3x.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nef.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/nefColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "125", 9 | "green" : "6", 10 | "red" : "57" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "125", 27 | "green" : "6", 28 | "red" : "57" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "255", 45 | "green" : "87", 46 | "red" : "160" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/shadow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0", 9 | "green" : "0", 10 | "red" : "0" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "1.000", 45 | "green" : "0.341", 46 | "red" : "0.627" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/splash-launchimage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "launchscreen-h@2x.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/splash-launchimage.imageset/launchscreen-h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/splash-launchimage.imageset/launchscreen-h@2x.png -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/splash-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "header-image.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Assets.xcassets/splash-logo.imageset/header-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/splash-logo.imageset/header-image.png -------------------------------------------------------------------------------- /NefEditorClient/Authentication/State/AuthenticationInfo.swift: -------------------------------------------------------------------------------- 1 | struct AuthenticationInfo: Equatable { 2 | let user: String 3 | let identityToken: String 4 | let authorizationCode: String 5 | } 6 | -------------------------------------------------------------------------------- /NefEditorClient/Authentication/State/AuthenticationState.swift: -------------------------------------------------------------------------------- 1 | enum AuthenticationState: Equatable { 2 | case authenticated(token: String) 3 | case unauthenticated 4 | } 5 | -------------------------------------------------------------------------------- /NefEditorClient/Authentication/View/SignInButton.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import SwiftUI 3 | import AuthenticationServices 4 | 5 | struct SignInButton: UIViewRepresentable { 6 | let style: ASAuthorizationAppleIDButton.Style 7 | let onSignIn: (Either) -> Void 8 | 9 | class Coordinator: NSObject, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate { 10 | let onSignIn: (Either) -> Void 11 | 12 | init(onSignIn: @escaping (Either) -> Void) { 13 | self.onSignIn = onSignIn 14 | } 15 | 16 | @objc func didTapButton() { 17 | let request = ASAuthorizationAppleIDProvider().createRequest() 18 | let authorizationController = ASAuthorizationController(authorizationRequests: [request]) 19 | authorizationController.presentationContextProvider = self 20 | authorizationController.delegate = self 21 | authorizationController.performRequests() 22 | } 23 | 24 | func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { 25 | let lastController = UIApplication.shared.windows.last?.rootViewController 26 | return lastController!.view.window! 27 | } 28 | 29 | func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { 30 | guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential, 31 | let identityTokenRaw = credentials.identityToken, 32 | let authorizationCodeRaw = credentials.authorizationCode, 33 | let identityToken = String(data: identityTokenRaw, encoding: .utf8), 34 | let authorizationCode = String(data: authorizationCodeRaw, encoding: .utf8) else { 35 | onSignIn(.left(ASAuthorizationError(ASAuthorizationError.failed))) 36 | return 37 | } 38 | 39 | let info = AuthenticationInfo( 40 | user: credentials.user, 41 | identityToken: identityToken, 42 | authorizationCode: authorizationCode) 43 | 44 | onSignIn(.right(info)) 45 | } 46 | 47 | func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { 48 | onSignIn(.left(error)) 49 | } 50 | } 51 | 52 | func makeCoordinator() -> SignInButton.Coordinator { 53 | Coordinator(onSignIn: onSignIn) 54 | } 55 | 56 | func makeUIView(context: Context) -> ASAuthorizationAppleIDButton { 57 | let button = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn, 58 | authorizationButtonStyle: style) 59 | button.addTarget(context.coordinator, 60 | action: #selector(Coordinator.didTapButton), 61 | for: .touchUpInside) 62 | return button 63 | } 64 | 65 | func updateUIView(_ button: ASAuthorizationAppleIDButton, context: Context) -> Void {} 66 | } 67 | -------------------------------------------------------------------------------- /NefEditorClient/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/Component/CatalogComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias CatalogComponent = StoreComponent 4 | 5 | func catalogComponent(catalog: Catalog, selectedItem: CatalogItem?) -> CatalogComponent { 6 | CatalogComponent( 7 | initialState: (catalog, selectedItem)) { state, handle in 8 | RecipeCatalogView(catalog: state.0, selectedItem: state.1, handle: handle) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/Extensions/URLQueryItem+Recipe.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array where Element == URLQueryItem { 4 | 5 | var recipe: Recipe? { 6 | guard let title = value(of: "name"), 7 | let dependency = dependency else { return nil } 8 | 9 | return .init(title: title, 10 | description: value(of: "description") ?? "", 11 | dependencies: [dependency]) 12 | } 13 | 14 | // MARK: Recipe 15 | private var dependency: Dependency? { 16 | guard let repository = value(of: "name"), 17 | let url = value(of: "url"), 18 | let owner = value(of: "owner"), 19 | let avatar = value(of: "avatar"), 20 | let requirement = requirement else { return nil } 21 | 22 | return .init(repository: repository, 23 | owner: owner, 24 | url: url, 25 | avatar: avatar, 26 | requirement: requirement, 27 | products: products) 28 | } 29 | 30 | private var requirement: Requirement? { 31 | if let branch = value(of: "branch") { 32 | return .branch(.init(name: branch)) 33 | } else if let tag = value(of: "tag") { 34 | return .version(.init(name: tag)) 35 | } else { 36 | return nil 37 | } 38 | } 39 | 40 | private var products: Dependency.Products { 41 | let products = values(of: "products").map { $0.trimmingCharacters(in: .whitespaces) } 42 | return products.count > 0 ? .selected(names: products) : .all 43 | } 44 | 45 | // MARK: URLQueryItem 46 | private func value(of key: String) -> String? { 47 | first { $0.name == key }?.value 48 | } 49 | 50 | private func values(of key: String) -> [String] { 51 | filter { $0.name == "\(key)[]" } 52 | .compactMap(\.value) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/Logic/CatalogAction.swift: -------------------------------------------------------------------------------- 1 | enum CatalogAction: Equatable { 2 | case addRecipe 3 | case duplicate(item: CatalogItem) 4 | case remove(item: CatalogItem) 5 | case select(item: CatalogItem) 6 | } 7 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/Logic/CatalogDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowArch 3 | import Foundation 4 | 5 | typealias CatalogDispatcher = StateDispatcher 6 | 7 | let catalogDispatcher = CatalogDispatcher.pure { action in 8 | 9 | switch action { 10 | case .addRecipe: 11 | return addRecipe() 12 | 13 | case .select(item: let item): 14 | return select(item: item) 15 | 16 | case .duplicate(item: let item): 17 | return duplicate(item: item) 18 | 19 | case .remove(item: let item): 20 | return remove(item: item) 21 | } 22 | } 23 | 24 | func addRecipe() -> State { 25 | .modify { state in 26 | state.copy(modalState: .edit(.newRecipe)) 27 | }^ 28 | } 29 | 30 | func duplicate(item: CatalogItem) -> State { 31 | .modify { state in 32 | let recipe = item.recipe.copy(id: UUID(), title: item.recipe.title + " copy") 33 | let newItem = CatalogItem.regular(recipe) 34 | let newCatalog = state.catalog.appending(newItem) 35 | return state.copy(catalog: newCatalog, selectedItem: newItem) 36 | }^ 37 | } 38 | 39 | func remove(item: CatalogItem) -> State { 40 | .modify { state in 41 | if item.isEditable { 42 | let newCatalog = state.catalog.removing(item) 43 | let selectedItem = state.selectedItem == item ? nil : state.selectedItem 44 | return state.copy(catalog: newCatalog, selectedItem: selectedItem) 45 | } else { 46 | return state 47 | } 48 | }^ 49 | } 50 | 51 | func select(item: CatalogItem) -> State { 52 | .modify { state in 53 | if state.selectedItem == item { 54 | return state.copy(selectedItem: .some(nil)) 55 | } else { 56 | return state.copy(selectedItem: item) 57 | } 58 | }^ 59 | } 60 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/State/Catalog.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | 3 | struct Catalog: Equatable { 4 | let featured: CatalogSection 5 | let userCreated: CatalogSection 6 | 7 | var sections: [CatalogSection] { 8 | [featured, userCreated] 9 | } 10 | 11 | static var initial: Catalog { 12 | let bow = CatalogItem.featured( 13 | FeaturedRecipe( 14 | recipe: Recipe( 15 | title: "FP with Bow", 16 | description: "Get all modules in Bow 0.8.0 to write FP in Swift", 17 | dependencies: [ 18 | Dependency( 19 | repository: "bow", 20 | owner: "bow-swift", 21 | url: "https://github.com/bow-swift/bow", 22 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4", 23 | requirement: .version(Tag(name: "0.8.0")), 24 | products: .all) 25 | ]), 26 | backgroundImage: "bow-background", 27 | textColor: .white) 28 | ) 29 | 30 | let bowArch = CatalogItem.featured( 31 | FeaturedRecipe( 32 | recipe: Recipe( 33 | title: "Architecture", 34 | description: "Try Bow Arch to explore the possibilities of Functional Architecture", 35 | dependencies: [ 36 | Dependency( 37 | repository: "bow-arch", 38 | owner: "bow-swift", 39 | url: "https://github.com/bow-swift/bow-arch", 40 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4", 41 | requirement: .branch(Branch(name: "0.1.0")), 42 | products: .all) 43 | ]), 44 | backgroundImage: "bow-arch-background", 45 | textColor: .black) 46 | ) 47 | 48 | let bowLite = CatalogItem.featured( 49 | FeaturedRecipe( 50 | recipe: Recipe( 51 | title: "FP with Bow Lite", 52 | description: "Play with the lightweight version of Bow", 53 | dependencies: [ 54 | Dependency( 55 | repository: "bow-lite", 56 | owner: "bow-swift", 57 | url: "https://github.com/bow-swift/bow-lite", 58 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4", 59 | requirement: .version(Tag(name: "0.1.0")), 60 | products: .all) 61 | ]), 62 | backgroundImage: "bow-lite-background", 63 | textColor: .white) 64 | ) 65 | 66 | let featured = CatalogSection( 67 | title: "Featured", 68 | items: [bow, bowArch, bowLite]) 69 | let myRecipes = CatalogSection( 70 | title: "My recipes", 71 | action: CatalogSectionAction(icon: "plus", action: .addRecipe), 72 | items: []) 73 | 74 | return Catalog(featured: featured, userCreated: myRecipes) 75 | } 76 | 77 | func userCreated(_ recipes: [Recipe]) -> Catalog { 78 | Catalog(featured: featured, 79 | userCreated: CatalogSection(title: self.userCreated.title, 80 | action: self.userCreated.action, 81 | items: recipes.map(CatalogItem.regular))) 82 | } 83 | 84 | func appending(_ item: CatalogItem) -> Catalog { 85 | Catalog(featured: featured, userCreated: userCreated.appending(item)) 86 | } 87 | 88 | func replacing(_ item: CatalogItem, by newItem: CatalogItem) -> Catalog { 89 | Catalog(featured: featured, userCreated: userCreated.replacing(item, by: newItem)) 90 | } 91 | 92 | func removing(_ item: CatalogItem) -> Catalog { 93 | Catalog(featured: featured, userCreated: userCreated.removing(item)) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/State/CatalogItem.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import Foundation 3 | 4 | enum CatalogItem: Equatable, Identifiable { 5 | case featured(FeaturedRecipe) 6 | case regular(Recipe) 7 | 8 | private func fold( 9 | _ ifFeatured: (FeaturedRecipe) -> A, 10 | _ ifRegular: (Recipe) -> A 11 | ) -> A { 12 | switch self { 13 | case .featured(let featured): return ifFeatured(featured) 14 | case .regular(let recipe): return ifRegular(recipe) 15 | } 16 | } 17 | 18 | private func fold( 19 | _ ifFeatured: KeyPath, 20 | _ ifRegular: KeyPath 21 | ) -> A { 22 | self.fold({ $0[keyPath: ifFeatured] }, 23 | { $0[keyPath: ifRegular] }) 24 | } 25 | 26 | var title: String { 27 | self.fold(\.recipe.title, 28 | \.title) 29 | } 30 | 31 | var description: String { 32 | self.fold(\.recipe.description, 33 | \.description) 34 | } 35 | 36 | var dependencies: [Dependency] { 37 | self.fold(\.recipe.dependencies, 38 | \.dependencies) 39 | } 40 | 41 | var isEditable: Bool { 42 | self.fold(constant(false), constant(true)) 43 | } 44 | 45 | var recipe: Recipe { 46 | self.fold(\.recipe, \.self) 47 | } 48 | 49 | var id: UUID { 50 | self.fold(\.id, \.id) 51 | } 52 | 53 | func appending(dependency: Dependency) -> CatalogItem { 54 | self.fold( 55 | CatalogItem.featured, 56 | { recipe in 57 | CatalogItem.regular(recipe.appending(dependency: dependency)) 58 | } 59 | ) 60 | } 61 | 62 | func removing(dependency: Dependency) -> CatalogItem { 63 | self.fold( 64 | CatalogItem.featured, 65 | { recipe in 66 | CatalogItem.regular(recipe.removing(dependency: dependency)) 67 | } 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/State/CatalogSection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CatalogSection: Equatable { 4 | let title: String 5 | let action: CatalogSectionAction? 6 | let items: [CatalogItem] 7 | 8 | init(title: String, action: CatalogSectionAction? = nil, items: [CatalogItem]) { 9 | self.title = title 10 | self.action = action 11 | self.items = items 12 | } 13 | 14 | func copy(items: [CatalogItem]) -> CatalogSection { 15 | CatalogSection( 16 | title: self.title, 17 | action: self.action, 18 | items: items) 19 | } 20 | 21 | func appending(_ item: CatalogItem) -> CatalogSection { 22 | copy(items: self.items + [item]) 23 | } 24 | 25 | func replacing(_ item: CatalogItem, by newItem: CatalogItem) -> CatalogSection { 26 | copy(items: self.items.map { current in 27 | (current == item) ? newItem : current 28 | }) 29 | } 30 | 31 | func removing(_ item: CatalogItem) -> CatalogSection { 32 | copy(items: self.items.filter { current in 33 | current != item 34 | }) 35 | } 36 | } 37 | 38 | struct CatalogSectionAction: Equatable { 39 | let icon: String 40 | let action: CatalogAction 41 | } 42 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/State/Dependency.swift: -------------------------------------------------------------------------------- 1 | struct Dependency: Equatable, Identifiable { 2 | enum Products: Equatable { 3 | case all 4 | case selected(names: [String]) 5 | } 6 | 7 | let repository: String 8 | let owner: String 9 | let url: String 10 | let avatar: String 11 | let requirement: Requirement 12 | let products: Products 13 | 14 | var id: String { 15 | "\(url):\(requirement.id)" 16 | } 17 | } 18 | 19 | // MARK: - Codable 20 | 21 | extension Dependency: Codable { } 22 | 23 | extension Dependency.Products: Codable { 24 | private enum CodingKeys: String, CodingKey { 25 | case all 26 | case selected 27 | } 28 | 29 | public func encode(to encoder: Encoder) throws { 30 | var container = encoder.container(keyedBy: CodingKeys.self) 31 | 32 | switch self { 33 | case .all: 34 | try container.encode([String](), forKey: .all) 35 | case .selected(let products): 36 | try container.encode(products, forKey: .selected) 37 | } 38 | } 39 | 40 | public init(from decoder: Decoder) throws { 41 | let container = try decoder.container(keyedBy: CodingKeys.self) 42 | 43 | if let products = try? container.decode([String].self, forKey: .selected) { 44 | self = .selected(names: products) 45 | } else { 46 | self = .all 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/State/FeaturedRecipe.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct FeaturedRecipe: Equatable, Identifiable { 4 | let recipe: Recipe 5 | let backgroundImage: String 6 | let textColor: Color 7 | 8 | var id: UUID { 9 | recipe.id 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/State/Recipe.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Recipe: Equatable, Identifiable, Codable { 4 | let id: UUID 5 | let title: String 6 | let description: String 7 | let dependencies: [Dependency] 8 | 9 | init(id: UUID = UUID(), 10 | title: String, 11 | description: String, 12 | dependencies: [Dependency]) { 13 | self.id = id 14 | self.title = title 15 | self.description = description 16 | self.dependencies = dependencies 17 | } 18 | 19 | func copy( 20 | id: UUID? = nil, 21 | title: String? = nil, 22 | description: String? = nil, 23 | dependencies: [Dependency]? = nil 24 | ) -> Recipe { 25 | Recipe( 26 | id: id ?? self.id, 27 | title: title ?? self.title, 28 | description: description ?? self.description, 29 | dependencies: dependencies ?? self.dependencies) 30 | } 31 | 32 | func appending(dependency: Dependency) -> Recipe { 33 | if !contains(dependency: dependency) { 34 | return copy(dependencies: self.dependencies + [dependency]) 35 | } else { 36 | return replacing(dependency: dependency) 37 | } 38 | } 39 | 40 | func removing(dependency: Dependency) -> Recipe { 41 | copy(dependencies: self.dependencies.filter { dep in 42 | dep != dependency 43 | }) 44 | } 45 | 46 | private func contains(dependency: Dependency) -> Bool { 47 | self.dependencies.first { dep in 48 | dep.url == dependency.url 49 | } != nil 50 | } 51 | 52 | private func replacing(dependency: Dependency) -> Recipe { 53 | copy( 54 | dependencies: self.dependencies.map { dep in 55 | (dep.url == dependency.url) 56 | ? dependency 57 | : dep 58 | } 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/State/TagViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct TagViewModel { 4 | let text: String 5 | let foregroundColor: Color 6 | let backgroundColor: Color 7 | 8 | init(text: String, foregroundColor: Color = .gray, backgroundColor: Color = Color.gray.opacity(0.2)) { 9 | self.text = text 10 | self.foregroundColor = foregroundColor 11 | self.backgroundColor = backgroundColor 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/View/CatalogItemGridView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CatalogItemGridView: View { 4 | let items: [CatalogItem] 5 | let selectedItem: CatalogItem? 6 | let columns: Int 7 | let handle: (CatalogAction) -> Void 8 | 9 | var body: some View { 10 | GridView( 11 | rows: self.rows, 12 | columns: self.columns) { row, column in 13 | self.viewForItem(row: row, column: column) 14 | .aspectRatio(16/9, contentMode: .fit) 15 | .onTapGesture { 16 | if let item = self.item(row: row, column: column) { 17 | self.handle(.select(item: item)) 18 | } 19 | } 20 | .safeHoverEffect() 21 | } 22 | } 23 | 24 | private var rows: Int { 25 | Int(ceil(Double(items.count) / Double(columns))) 26 | } 27 | 28 | private func indexFor(row: Int, column: Int) -> Int { 29 | row * self.columns + column 30 | } 31 | 32 | private func item(row: Int, column: Int) -> CatalogItem? { 33 | items[safe: indexFor(row: row, column: column)] 34 | } 35 | 36 | private func viewForItem(row: Int, column: Int) -> some View { 37 | if let item = item(row: row, column: column) { 38 | switch item { 39 | case .regular(let recipe): 40 | return AnyView( 41 | RegularRecipeView(recipe: recipe, isSelected: item == selectedItem) 42 | .contextMenu { 43 | self.duplicateButton(for: item) 44 | self.removeButton(for: item) 45 | }) 46 | case .featured(let featured): 47 | return AnyView( 48 | FeaturedRecipeView(featured: featured, isSelected: item == selectedItem) 49 | .contextMenu { 50 | self.duplicateButton(for: item) 51 | }) 52 | } 53 | } else { 54 | return AnyView(Color.clear) 55 | } 56 | } 57 | 58 | private func duplicateButton(for item: CatalogItem) -> some View { 59 | Button(action: { self.handle(.duplicate(item: item)) }) { 60 | Text("Duplicate recipe") 61 | Image.duplicate 62 | } 63 | } 64 | 65 | private func removeButton(for item: CatalogItem) -> some View { 66 | Button(action: { self.handle(.remove(item: item)) }) { 67 | Text("Remove recipe") 68 | Image.trash 69 | } 70 | } 71 | } 72 | 73 | #if DEBUG 74 | struct RecipeGridView_Previews: PreviewProvider { 75 | static var previews: some View { 76 | Group { 77 | ScrollView { 78 | CatalogItemGridView(items: sampleFeaturedRecipes, selectedItem: nil, columns: 2) { _ in } 79 | } 80 | 81 | ScrollView { 82 | CatalogItemGridView(items: sampleRecipes, selectedItem: nil, columns: 3) { _ in } 83 | } 84 | } 85 | } 86 | } 87 | #endif 88 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/View/CatalogSectionView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CatalogSectionView: View { 4 | let section: CatalogSection 5 | let selectedItem: CatalogItem? 6 | let columns: Int 7 | let handle: (CatalogAction) -> Void 8 | 9 | var body: some View { 10 | VStack(alignment: .leading) { 11 | sectionTitle(for: self.section) 12 | .padding(.top, 16) 13 | .padding(.horizontal, 16) 14 | 15 | if section.items.isEmpty { 16 | Text("There are no recipes yet.") 17 | .activityStyle() 18 | .multilineTextAlignment(.center) 19 | .frame(maxWidth: .infinity) 20 | .padding(24) 21 | } else { 22 | CatalogItemGridView( 23 | items: section.items, 24 | selectedItem: self.selectedItem, 25 | columns: self.columns, 26 | handle: self.handle) 27 | .padding(.horizontal, 16) 28 | } 29 | } 30 | } 31 | 32 | func sectionTitle(for section: CatalogSection) -> some View { 33 | guard let action = section.action else { 34 | return SectionTitle(title: section.title) 35 | } 36 | 37 | return SectionTitle(title: section.title, 38 | action: .init(icon: Image(systemName: action.icon), 39 | handle: self.handle(action.action))) 40 | } 41 | } 42 | 43 | #if DEBUG 44 | struct CatalogSectionView_Previews: PreviewProvider { 45 | static var previews: some View { 46 | ScrollView { 47 | CatalogSectionView(section: sampleRecipesSection, selectedItem: nil, columns: 2) { _ in } 48 | } 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/View/FeaturedRecipeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct FeaturedRecipeView: View { 4 | @Environment(\.colorScheme) var colorScheme 5 | 6 | let featured: FeaturedRecipe 7 | let isSelected: Bool 8 | 9 | var body: some View { 10 | ZStack(alignment: .topLeading) { 11 | Image(featured.backgroundImage) 12 | .resizable() 13 | .mask(RoundedRectangle(cornerRadius: 8)) 14 | .if(colorScheme == .light, 15 | then: { 16 | $0.shadow( 17 | color: self.isSelected ? 18 | Color.shadow.opacity(0.6) : 19 | Color.shadow.opacity(0.2), 20 | radius: self.isSelected ? 6 : 2, 21 | x: 1, 22 | y: 1) 23 | }) 24 | .if(colorScheme == .dark && isSelected, 25 | then: { 26 | $0.overlay( 27 | RoundedRectangle(cornerRadius: 8) 28 | .stroke(Color.white, lineWidth: 2) 29 | ) 30 | }) 31 | 32 | 33 | VStack(alignment: .leading, spacing: 4) { 34 | 35 | Text(featured.recipe.title) 36 | .titleStyle() 37 | .foregroundColor(featured.textColor) 38 | 39 | Text(featured.recipe.description) 40 | .font(.callout) 41 | .lineLimit(2) 42 | .foregroundColor(featured.textColor) 43 | 44 | Spacer() 45 | 46 | TagCloud(tags: featured.tags(textColor: featured.textColor)) 47 | } 48 | .padding() 49 | } 50 | } 51 | } 52 | 53 | private extension FeaturedRecipe { 54 | func tags(textColor: Color) -> [TagViewModel] { 55 | self.recipe.dependencies.map { dependency in 56 | TagViewModel( 57 | text: dependency.repository, 58 | foregroundColor: textColor, 59 | backgroundColor: textColor.opacity(0.2)) 60 | } 61 | } 62 | } 63 | 64 | #if DEBUG 65 | struct FeaturedRecipeView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | FeaturedRecipeView(featured: sampleFeaturedRecipe, isSelected: false) 68 | .aspectRatio(16/9, contentMode: .fit) 69 | .previewLayout(.fixed(width: 300, height: 300)) 70 | .padding() 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/View/RecipeCatalogView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct RecipeCatalogView: View { 4 | let catalog: Catalog 5 | let selectedItem: CatalogItem? 6 | let handle: (CatalogAction) -> Void 7 | 8 | var body: some View { 9 | GeometryReader { geometry in 10 | ScrollView(showsIndicators: false) { 11 | VStack { 12 | ForEach(self.catalog.sections, id: \.title) { section in 13 | CatalogSectionView( 14 | section: section, 15 | selectedItem: self.selectedItem, 16 | columns: self.columns(for: geometry.size), 17 | handle: self.handle) 18 | } 19 | } 20 | } 21 | .transition(.identity) 22 | .animation(nil) 23 | } 24 | } 25 | 26 | private func columns(for size: CGSize) -> Int { 27 | let minimumCardWidth: CGFloat = Card.minimumWidth 28 | return max(Int(floor(size.width / minimumCardWidth)), 1) 29 | } 30 | } 31 | 32 | #if DEBUG 33 | struct RecipeCatalogView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | RecipeCatalogView(catalog: sampleCatalog, selectedItem: nil) { _ in } 36 | .previewLayout(.fixed(width: 910, height: 1024)) 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/View/RegularRecipeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct RegularRecipeView: View { 4 | let recipe: Recipe 5 | let isSelected: Bool 6 | 7 | var body: some View { 8 | CardView(isSelected: isSelected) { 9 | VStack(alignment: .leading, spacing: 8) { 10 | Text(self.recipe.title) 11 | .titleStyle() 12 | Text(self.recipe.description) 13 | .activityStyle() 14 | 15 | Spacer() 16 | 17 | TagCloud( 18 | tags: self.recipe.tags, 19 | layout: TagCloud.Layouts.multiline(spacing: 4, lines: 2)) 20 | }.padding() 21 | } 22 | } 23 | } 24 | 25 | private extension Recipe { 26 | var tags: [TagViewModel] { 27 | self.dependencies.map { dependency in 28 | TagViewModel(text: dependency.repository) 29 | } 30 | } 31 | } 32 | 33 | #if DEBUG 34 | struct RegularRecipeView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | RegularRecipeView(recipe: sampleRecipe, isSelected: false) 37 | .aspectRatio(16/9, contentMode: .fit) 38 | .frame(maxWidth: 350, maxHeight: 300) 39 | .previewLayout(.sizeThatFits) 40 | .padding() 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /NefEditorClient/Catalog/View/TagView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct TagView: View { 4 | let tag: TagViewModel 5 | 6 | var body: some View { 7 | Text(tag.text) 8 | .lineLimit(1) 9 | .font(.system(.callout, design: .monospaced)) 10 | .foregroundColor(tag.foregroundColor) 11 | .padding(4) 12 | .background( 13 | RoundedRectangle(cornerRadius: 4) 14 | .fill(tag.backgroundColor) 15 | ) 16 | } 17 | } 18 | 19 | #if DEBUG 20 | struct TagView_Previews: PreviewProvider { 21 | static var previews: some View { 22 | Group { 23 | TagView(tag: sampleBowTag) 24 | TagView(tag: sampleNefTag) 25 | } 26 | .previewLayout(.sizeThatFits) 27 | .padding(16) 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /NefEditorClient/Credits/Component/CreditsComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias CreditsComponent = StoreComponent 4 | 5 | func creditsComponent() -> CreditsComponent { 6 | CreditsComponent(initialState: ()) { _, handle in 7 | CreditsView(handle: handle) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Credits/Logic/CreditsAction.swift: -------------------------------------------------------------------------------- 1 | enum CreditsAction { 2 | case librarySelected(Library) 3 | case dismissCredits 4 | } 5 | -------------------------------------------------------------------------------- /NefEditorClient/Credits/Logic/CreditsDispatcher.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Bow 3 | import BowArch 4 | import BowEffects 5 | 6 | typealias CreditsDispatcher = StateDispatcher 7 | 8 | let creditsDispatcher = CreditsDispatcher.effectful { input in 9 | switch input { 10 | case .librarySelected(let library): 11 | return open(library: library) 12 | case .dismissCredits: 13 | return EnvIO.pure(dismissModal())^ 14 | } 15 | } 16 | 17 | func open(library: Library) -> EnvIO> { 18 | EnvIO.later(.main) { 19 | if let url = library.url { 20 | UIApplication.shared.open(url, completionHandler: nil) 21 | } 22 | }.as(.modify(id)^)^ 23 | } 24 | -------------------------------------------------------------------------------- /NefEditorClient/Credits/State/Library.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Library { 4 | case bow 5 | case bowArch 6 | case bowOpenAPI 7 | case bowLite 8 | 9 | var url: URL? { 10 | switch self { 11 | case .bow: 12 | return URL(string: "https://bow-swift.io") 13 | case .bowArch: 14 | return URL(string: "https://arch.bow-swift.io") 15 | case .bowOpenAPI: 16 | return URL(string: "https://openapi.bow-swift.io") 17 | case .bowLite: 18 | return URL(string: "https://github.com/bow-swift/bow-lite") 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NefEditorClient/Credits/View/CreditsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CreditsView: View { 4 | let handle: (CreditsAction) -> Void 5 | 6 | var body: some View { 7 | VStack { 8 | Spacer() 9 | self.version() 10 | self.sponsor() 11 | Spacer() 12 | self.librarySection() 13 | }.padding() 14 | .navigationBarTitle("Credits", displayMode: .inline) 15 | .navigationBarItems(leading: 16 | Button("Cancel") { 17 | self.handle(.dismissCredits) 18 | }.navigationBarButtonStyle() 19 | ) 20 | } 21 | 22 | var appVersion: String { 23 | if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { 24 | return "Version \(version)" 25 | } else { 26 | return "" 27 | } 28 | } 29 | 30 | func version() -> some View { 31 | Group { 32 | Image.appIcon.resizable() 33 | .frame(width: 256, height: 256) 34 | .mask(RoundedRectangle(cornerRadius: 16)) 35 | 36 | Text("nef Playgrounds").largeTitleStyle() 37 | 38 | Text(appVersion) 39 | .activityStyle() 40 | } 41 | } 42 | 43 | func sponsor() -> some View { 44 | HStack { 45 | Text("Proudly sponsored by") 46 | Image.fortySeven.resizable() 47 | .frame(width: 24, height: 24) 48 | }.padding() 49 | } 50 | 51 | func librarySection() -> some View { 52 | Group { 53 | Text("This application is powered by:") 54 | .font(.caption) 55 | .padding(.bottom, 16) 56 | 57 | HStack(alignment: .center, spacing: 4) { 58 | libraryView(image: .bowArch, name: "Bow Arch", library: .bowArch) 59 | 60 | libraryView(image: .bow, name: "Bow", library: .bow) 61 | 62 | libraryView(image: .bowLite, name: "Bow Lite", library: .bowLite) 63 | 64 | libraryView(image: .bowOpenAPI, name: "Bow OpenAPI", library: .bowOpenAPI) 65 | } 66 | } 67 | } 68 | 69 | func libraryView(image: Image, name: String, library: Library) -> some View { 70 | Button(action: { self.handle(.librarySelected(library)) }) { 71 | HStack { 72 | Spacer() 73 | 74 | VStack { 75 | image.resizable() 76 | .frame(width: 40, height: 40) 77 | .mask(RoundedRectangle(cornerRadius: 8)) 78 | 79 | Text(name) 80 | .font(.callout) 81 | } 82 | 83 | Spacer() 84 | } 85 | }.buttonStyle(LibraryButtonStyle()) 86 | } 87 | } 88 | 89 | #if DEBUG 90 | struct CreditsView_Previews: PreviewProvider { 91 | static var previews: some View { 92 | NavigationView { 93 | CreditsView { _ in } 94 | }.navigationViewStyle(StackNavigationViewStyle()) 95 | .previewLayout(.fixed(width: 500, height: 500)) 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /NefEditorClient/DeepLink/Extensions/URL+Recipe.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | func schemeRecipe(incomingURL: URL) -> Recipe? { 4 | guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true), 5 | let action = incomingURL.lastPathComponent.isEmpty ? components.host : incomingURL.lastPathComponent, 6 | let params = components.queryItems else { return nil } 7 | 8 | switch action { 9 | case "recipe": 10 | return params.recipe 11 | default: 12 | return nil 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /NefEditorClient/DeepLink/Logic/DeepLinkAction.swift: -------------------------------------------------------------------------------- 1 | import BowOptics 2 | 3 | enum DeepLinkAction: AutoPrism { 4 | case generateRecipe(Recipe) 5 | case regularInitialization 6 | } 7 | -------------------------------------------------------------------------------- /NefEditorClient/DeepLink/Logic/DeepLinkDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | import BowArch 4 | 5 | typealias DeepLinkDispatcher = StateDispatcher 6 | 7 | let deepLinkDispatcher = DeepLinkDispatcher.workflow { action in 8 | switch action { 9 | case .generateRecipe(let recipe): 10 | return initialLoad(recipe) 11 | case .regularInitialization: 12 | return [initialLoad()] 13 | } 14 | } 15 | 16 | func addNewRecipe(_ recipe: Recipe) -> State { 17 | .modify { state in 18 | state.addRecipe(recipe) 19 | }^ 20 | } 21 | 22 | func clearDeepLink() -> State { 23 | .modify { state in 24 | state.copy(deepLinkState: DeepLinkState.none) 25 | }^ 26 | } 27 | 28 | func generatePlayground(newRecipe recipe: Recipe) -> EnvIO> { 29 | EnvIO.pure( 30 | clearDeepLink() 31 | .followedBy(addNewRecipe(recipe)) 32 | .followedBy(generatePlayground(for: .regular(recipe)))^ 33 | )^ 34 | } 35 | 36 | func initialLoad(_ recipe: Recipe) -> [EnvIO>] { 37 | [ 38 | initialLoad(), 39 | generatePlayground(newRecipe: recipe), 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /NefEditorClient/DeepLink/State/DeepLinkState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum DeepLinkState: Equatable { 4 | case none 5 | case recipe(Recipe) 6 | } 7 | -------------------------------------------------------------------------------- /NefEditorClient/Detail/Component/CatalogDetailComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias CatalogDetailComponent = StoreComponent 4 | 5 | func catalogDetailComponent(state: CatalogItem) -> CatalogDetailComponent { 6 | CatalogDetailComponent( 7 | initialState: state, 8 | render: CatalogItemDetailView.init) 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Detail/Logic/CatalogDetailAction.swift: -------------------------------------------------------------------------------- 1 | enum CatalogDetailAction { 2 | case edit(item: CatalogItem) 3 | case searchDependency 4 | case remove(Dependency) 5 | case dismissDetail 6 | case generatePlayground(for: CatalogItem) 7 | } 8 | -------------------------------------------------------------------------------- /NefEditorClient/Detail/Logic/CatalogDetailDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowArch 3 | 4 | typealias CatalogDetailDispatcher = StateDispatcher 5 | 6 | let catalogDetailDispatcher = CatalogDetailDispatcher.pure { action in 7 | switch action { 8 | case .edit(item: let item): 9 | return edit(item: item) 10 | case .searchDependency: 11 | return searchDependency() 12 | case .remove(let dependency): 13 | return remove(dependency: dependency) 14 | case .dismissDetail: 15 | return clearSelection() 16 | case .generatePlayground(for: let item): 17 | return generatePlayground(for: item) 18 | } 19 | } 20 | 21 | func edit(item: CatalogItem) -> State { 22 | if case let .regular(recipe) = item { 23 | return .modify { state in 24 | state.copy(modalState: .edit(.editRecipe(recipe))) 25 | }^ 26 | } else { 27 | return .modify(id)^ 28 | } 29 | } 30 | 31 | func searchDependency() -> State { 32 | .modify { state in 33 | state.copy(panelState: .search) 34 | }^ 35 | } 36 | 37 | func remove(dependency: Dependency) -> State { 38 | .modify { state in 39 | if let selected = state.selectedItem { 40 | let newSelected = selected.removing(dependency: dependency) 41 | let newCatalog = state.catalog.replacing(selected, by: newSelected) 42 | return state.copy(catalog: newCatalog, selectedItem: newSelected) 43 | } else { 44 | return state 45 | } 46 | }^ 47 | } 48 | 49 | func clearSelection() -> State { 50 | .modify { state in 51 | if state.panelState == .search { 52 | return state.copy(panelState: .catalog) 53 | } else { 54 | return state.copy(selectedItem: .some(nil)) 55 | } 56 | }^ 57 | } 58 | 59 | func generatePlayground(for item: CatalogItem) -> State { 60 | .modify { state in 61 | state.copy(modalState: .generation(.initial(state.authenticationState, item))) 62 | }^ 63 | } 64 | -------------------------------------------------------------------------------- /NefEditorClient/Detail/View/CatalogItemDetailView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CatalogItemDetailView: View { 4 | let item: CatalogItem 5 | let handle: (CatalogDetailAction) -> Void 6 | 7 | var body: some View { 8 | CardView { 9 | VStack(alignment: .leading, spacing: 8) { 10 | HStack { 11 | Text(self.item.title) 12 | .largeTitleStyle() 13 | 14 | Spacer() 15 | 16 | if self.item.isEditable{ 17 | Button(action: { 18 | self.handle(.edit(item: self.item)) 19 | }) { 20 | Image.pencil 21 | } 22 | .buttonStyle(ActionButtonStyle()) 23 | .offset(y: 4) 24 | } 25 | 26 | Button(action: { 27 | self.handle(.dismissDetail) 28 | }) { 29 | Image.close 30 | }.buttonStyle(ActionButtonStyle()) 31 | .offset(y: 4) 32 | } 33 | 34 | Text(self.item.description) 35 | .foregroundColor(.gray) 36 | 37 | Rectangle.separator 38 | .padding(.top, 8) 39 | 40 | HStack(alignment: .center) { 41 | Text("Dependencies") 42 | .titleStyle() 43 | 44 | Spacer() 45 | 46 | if self.item.isEditable { 47 | Button(action: { 48 | self.handle(.searchDependency) 49 | }) { 50 | Image.plus 51 | } 52 | .buttonStyle(ActionButtonStyle()) 53 | .alignmentGuide(.firstTextBaseline) { d in d[.bottom] * 0.82 } 54 | } 55 | }.padding(.top, 24) 56 | 57 | DependencyListView( 58 | dependencies: self.item.dependencies, 59 | isEditable: self.item.isEditable 60 | ) { dependency in 61 | self.handle(.remove(dependency)) 62 | } 63 | 64 | Button(action: { self.handle(.generatePlayground(for: self.item)) }) { 65 | HStack(spacing: 12) { 66 | Image.nefClear 67 | .resizable() 68 | .frame(width: 32, height: 32) 69 | Text("Create Swift Playground") 70 | } 71 | }.frame(maxWidth: .infinity) 72 | .buttonStyle(TextButtonStyle()) 73 | }.padding() 74 | } 75 | } 76 | } 77 | 78 | #if DEBUG 79 | struct CatalogItemDetailView_Previews: PreviewProvider { 80 | static var previews: some View { 81 | CatalogItemDetailView(item: .regular(sampleRecipe)) { _ in } 82 | .padding() 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /NefEditorClient/Detail/View/DependencyListView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DependencyListView: View { 4 | let dependencies: [Dependency] 5 | let isEditable: Bool 6 | let onRemoveDependency: (Dependency) -> Void 7 | 8 | var body: some View { 9 | List { 10 | ForEach(Array(dependencies.enumerated()), id: \.element.id) { item in 11 | DependencyView(dependency: item.element) 12 | .if(self.isEditable) { view in 13 | view.contextMenu { 14 | Button(action: { self.onRemoveDependency(item.element) }) { 15 | Text("Delete dependency") 16 | Image.trash 17 | } 18 | } 19 | } 20 | }.if(isEditable) { view in 21 | view.onDelete { indexSet in 22 | if let index = indexSet.first { 23 | self.onRemoveDependency(self.dependencies[index]) 24 | } 25 | } 26 | }.listRowBackground(Color.card) 27 | }.background(Color.clear) 28 | } 29 | } 30 | 31 | #if DEBUG 32 | struct DependencyListView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | DependencyListView(dependencies: Array(repeating: bowDependency, count: 15), 35 | isEditable: false) { _ in } 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /NefEditorClient/Detail/View/DependencyView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DependencyView: View { 4 | let dependency: Dependency 5 | 6 | var body: some View { 7 | HStack(alignment: .center) { 8 | VStack(alignment: .leading, spacing: 8) { 9 | Text(dependency.repository) 10 | .font(.body) 11 | 12 | HStack { 13 | AvatarView(avatar: dependency.avatar) 14 | .frame(width: 24, height: 24) 15 | .mask(Circle()) 16 | 17 | Text(dependency.owner) 18 | .font(.caption) 19 | .foregroundColor(.gray) 20 | } 21 | } 22 | Spacer() 23 | TagView(tag: TagViewModel(text: dependency.requirement.title)) 24 | }.padding(.vertical, 4) 25 | } 26 | } 27 | 28 | #if DEBUG 29 | struct DependencyView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | Group { 32 | DependencyView(dependency: bowDependency) 33 | DependencyView(dependency: bowArchDependency) 34 | }.previewLayout(.sizeThatFits) 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /NefEditorClient/Edit/Component/EditComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias EditComponent = StoreComponent 4 | 5 | func editComponent(state: EditState) -> EditComponent { 6 | EditComponent( 7 | initialState: state, 8 | render: EditRecipeMetadataView.init) 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Edit/Logic/EditAction.swift: -------------------------------------------------------------------------------- 1 | enum EditAction { 2 | case saveRecipe(title: String, description: String) 3 | case dismissEdition 4 | } 5 | -------------------------------------------------------------------------------- /NefEditorClient/Edit/Logic/EditDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowArch 3 | 4 | typealias EditDispatcher = StateDispatcher 5 | 6 | let editDispatcher = EditDispatcher.pure { input in 7 | switch input { 8 | case let .saveRecipe(title: title, description: description): 9 | return saveRecipe(title: title, description: description) 10 | 11 | case .dismissEdition: 12 | return dismissModal() 13 | } 14 | } 15 | 16 | func saveRecipe(title: String, description: String) -> State { 17 | .modify { state in 18 | guard let editState = state.modalState.editState else { return state } 19 | 20 | switch editState { 21 | case .newRecipe: 22 | let recipe = createRecipe(title: title, description: description) 23 | return state.addRecipe(recipe) 24 | 25 | case .editRecipe(let recipe): 26 | let editedRecipe = edit(recipe: recipe, title: title, description: description) 27 | let newCatalog = state.catalog.replacing(.regular(recipe), by: .regular(editedRecipe)) 28 | return state.copy(modalState: .noModal, catalog: newCatalog, selectedItem: .regular(editedRecipe)) 29 | } 30 | }^ 31 | } 32 | 33 | func createRecipe(title: String, description: String) -> Recipe { 34 | .init(title: title.isEmpty ? "Empty title" : title, 35 | description: description, 36 | dependencies: []) 37 | } 38 | 39 | func edit(recipe: Recipe, title: String, description: String) -> Recipe { 40 | recipe.copy(title: title.isEmpty ? recipe.title : title, 41 | description: description) 42 | } 43 | 44 | extension AppState { 45 | func addRecipe(_ recipe: Recipe) -> Self { 46 | let catalogItem: CatalogItem = .regular(recipe) 47 | let newCatalog = catalog.appending(catalogItem) 48 | return self.copy(modalState: .noModal, catalog: newCatalog, selectedItem: catalogItem) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NefEditorClient/Edit/State/EditState.swift: -------------------------------------------------------------------------------- 1 | enum EditState: Equatable { 2 | case newRecipe 3 | case editRecipe(Recipe) 4 | } 5 | 6 | -------------------------------------------------------------------------------- /NefEditorClient/Edit/View/EditRecipeMetadataView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EditRecipeMetadataView: View { 4 | let state: EditState 5 | @State var title: String 6 | @State var description: String 7 | let handle: (EditAction) -> Void 8 | 9 | init(state: EditState, handle: @escaping (EditAction) -> Void) { 10 | self.state = state 11 | 12 | switch state { 13 | case .editRecipe(let recipe): 14 | self._title = State(initialValue: recipe.title) 15 | self._description = State(initialValue: recipe.description) 16 | default: 17 | self._title = State(initialValue: "") 18 | self._description = State(initialValue: "") 19 | } 20 | 21 | self.handle = handle 22 | } 23 | 24 | var body: some View { 25 | ZStack { 26 | Color.form.edgesIgnoringSafeArea(.all) 27 | Form { 28 | Section(header: Text("Title")) { 29 | TextField("Enter a title for your nef recipe", text: $title) 30 | } 31 | 32 | Section(header: Text("Description")) { 33 | TextField("Enter a description for your nef recipe", text: $description) 34 | } 35 | } 36 | }.navigationBarItems( 37 | leading: 38 | Button("Cancel") { 39 | self.handle(.dismissEdition) 40 | }.navigationBarButtonStyle(), 41 | trailing: 42 | Button("Save") { 43 | self.handle(.saveRecipe(title: self.title, description: self.description)) 44 | }.navigationBarButtonStyle() 45 | ) 46 | .navigationBarTitle(self.navigationTitle) 47 | } 48 | 49 | private var navigationTitle: String { 50 | switch state { 51 | case .newRecipe: 52 | return "New recipe" 53 | case .editRecipe(_): 54 | return "Edit recipe" 55 | } 56 | } 57 | } 58 | 59 | #if DEBUG 60 | struct EditRecipeMetadataView_Preview: PreviewProvider { 61 | static var previews: some View { 62 | NavigationView { 63 | EditRecipeMetadataView( 64 | state: .newRecipe) { _ in } 65 | .navigationBarTitle("Edit recipe") 66 | } 67 | .navigationViewStyle(StackNavigationViewStyle()) 68 | } 69 | } 70 | #endif 71 | -------------------------------------------------------------------------------- /NefEditorClient/Extensions/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array { 4 | public subscript(safe index: Int) -> Element? { 5 | (index >= 0 && index < self.count) ? 6 | self[index] : 7 | nil 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Extensions/Bundle+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Bundle { 4 | var version: String { "\(appVersion) [\(buildVersion)]" } 5 | 6 | var appVersion: String { 7 | infoDictionary?["CFBundleShortVersionString"] as? String ?? "" 8 | } 9 | 10 | var buildVersion: String { 11 | infoDictionary?["CFBundleVersion"] as? String ?? "" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /NefEditorClient/Extensions/URLSession+IO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bow 3 | import BowEffects 4 | import NefEditorError 5 | 6 | enum DownloadTaskError: Error { 7 | case malformedURL 8 | case dataCorrupted 9 | case serverError 10 | case errorResponse(Reason) 11 | } 12 | 13 | 14 | extension URLSession { 15 | func downloadDataTaskIO(with request: URLRequest) -> IO, Data> { 16 | func downloadTaskHandler(data: Data, statusCode: Int) -> Either, Data> { 17 | switch statusCode { 18 | case 200 ..< 300: 19 | return .right(data) 20 | default: 21 | if let error = try? JSONDecoder().decode(ErrorResponse.self, from: data) { 22 | return .left(.errorResponse(error.reason)) 23 | } else { 24 | return .left(.serverError) 25 | } 26 | } 27 | } 28 | 29 | return IO.async { callback in 30 | self.downloadTask(with: request) { url, response, error in 31 | guard let httpResponse = response as? HTTPURLResponse else { 32 | callback(.left(.malformedURL)); return 33 | } 34 | 35 | guard let url = url, let data = try? Data(contentsOf: url) else { 36 | callback(.left(.dataCorrupted)); return 37 | } 38 | 39 | let either = downloadTaskHandler(data: data, statusCode: httpResponse.statusCode) 40 | return callback(either) 41 | }.resume() 42 | }^ 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NefEditorClient/FAQ/Component/FAQComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias FAQComponent = StoreComponent 4 | 5 | func faqComponent() -> FAQComponent { 6 | FAQComponent(initialState: ()) { _, handle in 7 | FAQView(handle: handle) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/FAQ/Logic/FAQAction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum FAQAction { 4 | case visitNef 5 | case visit47Degrees 6 | case visitBowSwift 7 | case followBowSwift 8 | case dismissFAQ 9 | 10 | var url: URL? { 11 | switch self { 12 | case .visitNef: 13 | return URL(string: "https://github.com/bow-swift/nef") 14 | case .visit47Degrees: 15 | return URL(string: "https://www.47deg.com") 16 | case .visitBowSwift: 17 | return URL(string: "https://github.com/bow-swift") 18 | case .followBowSwift: 19 | return URL(string: "https://www.twitter.com/bow_swift") 20 | default: 21 | return nil 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NefEditorClient/FAQ/Logic/FAQDispatcher.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Bow 3 | import BowEffects 4 | import BowArch 5 | import Foundation 6 | 7 | typealias FAQDispatcher = StateDispatcher 8 | 9 | let faqDispatcher = FAQDispatcher.effectful { input in 10 | switch input { 11 | case .dismissFAQ: 12 | return EnvIO.pure(dismissModal())^ 13 | default: 14 | return open(url: input.url) 15 | } 16 | } 17 | 18 | func open(url: URL?) -> EnvIO> { 19 | EnvIO.later(.main) { 20 | if let url = url { 21 | UIApplication.shared.open(url, completionHandler: nil) 22 | } 23 | }.as(.modify(id)^)^ 24 | } 25 | -------------------------------------------------------------------------------- /NefEditorClient/Generation/Component/GenerationComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias GenerationComponent = StoreComponent 4 | 5 | func generationComponent(state: GenerationState) -> GenerationComponent { 6 | GenerationComponent( 7 | initialState: state) { state, handle in 8 | GenerationView(state: state, handle: handle) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /NefEditorClient/Generation/Logic/GenerationAction.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import Foundation 3 | 4 | enum GenerationAction { 5 | case authenticationResult(Either, item: CatalogItem) 6 | case dismissGeneration 7 | case generate(item: CatalogItem, token: String) 8 | case sharePlayground 9 | case dismissShare 10 | } 11 | -------------------------------------------------------------------------------- /NefEditorClient/Generation/State/GenerationState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum GenerationState: Equatable { 4 | case notGenerating 5 | case authenticating 6 | case initial(AuthenticationState, CatalogItem) 7 | case generating(CatalogItem) 8 | case finished(CatalogItem, URL, SharingState) 9 | case error(GenerationError) 10 | 11 | var shouldShowShare: Bool { 12 | guard case let .finished(_, _, sharingState) = self else { 13 | return false 14 | } 15 | return sharingState == .sharing 16 | } 17 | } 18 | 19 | enum SharingState: Equatable { 20 | case notSharing 21 | case sharing 22 | } 23 | 24 | enum GenerationError: Error, Equatable, CustomStringConvertible { 25 | case invalidAuthentication 26 | case invalidBearer 27 | case networkFailure 28 | case dataCorrupted 29 | 30 | var description: String { 31 | switch self { 32 | case .invalidAuthentication: 33 | return "There was a problem with your authentication. Please, try again later." 34 | case .invalidBearer: 35 | return "Oops! Your session has expired, please login again." 36 | case .networkFailure: 37 | return "An error ocurred while obtaining your Swift Playground. Please, try again later." 38 | case .dataCorrupted: 39 | return "Data for this Playground is corrupted. Please, try to generate it again." 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /NefEditorClient/Generation/View/GenerationErrorView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GenerationErrorView: View { 4 | let message: String 5 | 6 | var body: some View { 7 | VStack { 8 | AnimationView(animation: .init(lottie: .generalError)) 9 | .aspectRatio(contentMode: .fit) 10 | .frame(width: 256, height: 256) 11 | 12 | Text(message).activityStyle() 13 | Spacer() 14 | }.padding(.top, 56) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /NefEditorClient/Generation/View/GenerationPlaygroundBookView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GenerationPlaygroundBookView: View { 4 | let playgroundName: String 5 | 6 | var body: some View { 7 | VStack { 8 | AnimationView(animation: AnimationView.Animation(lottie: .playgroundLoading, isLoop: true)) 9 | .aspectRatio(contentMode: .fill) 10 | .frame(height: 448) 11 | 12 | Text( 13 | """ 14 | Generating Swift Playground '\(playgroundName)'... 15 | 16 | 17 | Please wait, this may take several minutes. 18 | """ 19 | ).activityStyle() 20 | .multilineTextAlignment(.center) 21 | 22 | Spacer() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NefEditorClient/Generation/View/GenerationSigninView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GenerationSigninView: View { 4 | var body: some View { 5 | VStack { 6 | AnimationView(animation: AnimationView.Animation(lottie: .generalLoading, isLoop: true)) 7 | .aspectRatio(contentMode: .fill) 8 | .frame(width: 400, height: 200) 9 | 10 | Text("Signing in...") 11 | .activityStyle() 12 | Spacer() 13 | }.padding(.top, 56) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /NefEditorClient/Generation/View/GenerationSuccessView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GenerationSuccessView: View { 4 | let animation: LottieAnimation 5 | 6 | var body: some View { 7 | AnimationView(animation: .init(lottie: animation)) 8 | .aspectRatio(contentMode: .fit) 9 | .frame(width: 256, height: 256) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NefEditorClient/GitHubExtensions/Data+Equatable.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | 3 | extension Repository: Equatable { 4 | public static func ==(lhs: Repository, rhs: Repository) -> Bool { 5 | lhs.fullName == rhs.fullName 6 | } 7 | } 8 | 9 | extension Tag: Equatable { 10 | public static func ==(lhs: Tag, rhs: Tag) -> Bool { 11 | lhs.name == rhs.name 12 | } 13 | } 14 | 15 | extension Branch: Equatable { 16 | public static func ==(lhs: Branch, rhs: Branch) -> Bool { 17 | lhs.name == rhs.name 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /NefEditorClient/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Viewer 24 | CFBundleURLName 25 | com.47deg.nef-playgrounds 26 | CFBundleURLSchemes 27 | 28 | nef-playgrounds 29 | 30 | 31 | 32 | CFBundleVersion 33 | $(CURRENT_PROJECT_VERSION) 34 | ITSAppUsesNonExemptEncryption 35 | 36 | LSApplicationCategoryType 37 | public.app-category.developer-tools 38 | LSRequiresIPhoneOS 39 | 40 | UIApplicationSceneManifest 41 | 42 | UIApplicationSupportsMultipleScenes 43 | 44 | UISceneConfigurations 45 | 46 | UIWindowSceneSessionRoleApplication 47 | 48 | 49 | UISceneConfigurationName 50 | Default Configuration 51 | UISceneDelegateClassName 52 | $(PRODUCT_MODULE_NAME).SceneDelegate 53 | 54 | 55 | 56 | 57 | UILaunchStoryboardName 58 | LaunchScreen 59 | UIRequiredDeviceCapabilities 60 | 61 | armv7 62 | 63 | UIRequiresFullScreen 64 | 65 | UIStatusBarHidden~ipad 66 | 67 | UISupportedInterfaceOrientations 68 | 69 | UIInterfaceOrientationPortrait 70 | UIInterfaceOrientationLandscapeLeft 71 | UIInterfaceOrientationLandscapeRight 72 | 73 | UISupportedInterfaceOrientations~ipad 74 | 75 | UIInterfaceOrientationPortrait 76 | UIInterfaceOrientationPortraitUpsideDown 77 | UIInterfaceOrientationLandscapeLeft 78 | UIInterfaceOrientationLandscapeRight 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /NefEditorClient/Main/Component/AppComponent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | import NefAPI 4 | import Bow 5 | import BowArch 6 | import BowOptics 7 | import BowEffects 8 | 9 | typealias AppComponent = 10 | StoreComponent< 11 | AppDependencies, 12 | AppState, 13 | AppAction, 14 | AppView 15 | > 16 | 17 | typealias AppComponentView = AppComponent 18 | 19 | func appComponent(urlContexts: Set) -> AppComponentView { 20 | let deepLinkState = urlContexts.first.map(\.url).flatMap(schemeRecipe).flatMap(DeepLinkState.recipe) ?? .none 21 | return appComponent(deepLinkState: deepLinkState) 22 | } 23 | 24 | func appComponent(userActivity: NSUserActivity) -> AppComponentView { 25 | let deepLinkState = userActivity.webpageURL.flatMap(schemeRecipe).flatMap(DeepLinkState.recipe) ?? .none 26 | return appComponent(deepLinkState: deepLinkState) 27 | } 28 | 29 | fileprivate func appComponent(deepLinkState: DeepLinkState) -> AppComponentView { 30 | let ref = IORef.unsafe(nil) 31 | let gitHubConfig = makeGitHubConfig() 32 | let nefConfig = makeNefConfig() 33 | let persistence = ICloudPersistence() 34 | let dependencies = AppDependencies(persistence: persistence, 35 | gitHubConfig: gitHubConfig, 36 | nefConfig: nefConfig) 37 | 38 | let initialState = AppState( 39 | panelState: .catalog, 40 | modalState: .noModal, 41 | searchState: SearchState(loadingState: .initial, modalState: .noModal), 42 | deepLinkState: deepLinkState, 43 | catalog: Catalog.initial, 44 | selectedItem: nil, 45 | iCloudStatus: .enabled, 46 | iCloudAlert: .hidden, 47 | authenticationState: .unauthenticated) 48 | 49 | return AppComponent ( 50 | initialState: initialState, 51 | environment: dependencies, 52 | dispatcher: appDispatcher 53 | ) { state, handle in 54 | AppView( 55 | state: state, 56 | 57 | catalog: catalogComponent(catalog: state.catalog, selectedItem: state.selectedItem) 58 | .using(handle, transformInput: AppAction.prism(for: AppAction.catalogAction)), 59 | 60 | search: searchComponent(config: gitHubConfig, state: state.searchState) 61 | .using(handle, transformInput: AppAction.prism(for: AppAction.searchAction)), 62 | 63 | detail: { item in 64 | catalogDetailComponent(state: item) 65 | .using(handle, transformInput: AppAction.prism(for: AppAction.catalogDetailAction)) 66 | }, 67 | 68 | modal: { state in 69 | appModalComponent(state: state) 70 | .using(handle, transformInput: Prism.identity) 71 | }, 72 | 73 | handle: handle) 74 | }.onEffect { component in 75 | let oldRecipes = IO.var() 76 | let newRecipes = component.store().state.catalog.userCreated.items.map(\.recipe) 77 | 78 | return binding( 79 | oldRecipes <- ref.get(), 80 | |<-((oldRecipes.get != newRecipes) ? 81 | persist(state: component.store().state).provide(persistence) : 82 | IO.lazy()), 83 | |<-ref.set(newRecipes), 84 | yield: ()) 85 | } 86 | } 87 | 88 | func makeGitHubConfig() -> GitHub.API.Config { 89 | GitHub.API.Config(basePath: "https://api.github.com") 90 | } 91 | 92 | func makeNefConfig() -> NefAPI.API.Config { 93 | let configuration = URLSessionConfiguration.default 94 | configuration.timeoutIntervalForRequest = 420 95 | configuration.timeoutIntervalForResource = 420 96 | 97 | let session = URLSession(configuration: configuration) 98 | 99 | return NefAPI.API.Config( 100 | basePath: "https://nef.47deg.com", 101 | session: session) 102 | .appending(contentType: .json) 103 | } 104 | -------------------------------------------------------------------------------- /NefEditorClient/Main/Component/AppModalComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias AppModalComponent = StoreComponent> 4 | 5 | func appModalComponent(state: AppModalState) -> AppModalComponent { 6 | AppModalComponent(initialState: state) { state, handle in 7 | AppModalView( 8 | state: state, 9 | 10 | editView: { editState in 11 | editComponent(state: editState) 12 | .using(handle, transformInput: AppAction.prism(for: AppAction.editAction)) 13 | }, 14 | 15 | generationView: { generationState in 16 | generationComponent(state: generationState) 17 | .using(handle, transformInput: AppAction.prism(for: AppAction.generationAction)) 18 | }, 19 | 20 | creditsView: creditsComponent() 21 | .using(handle, transformInput: AppAction.prism(for: AppAction.creditsAction)), 22 | 23 | faqView: faqComponent() 24 | .using(handle, transformInput: AppAction.prism(for: AppAction.faqAction)), 25 | 26 | whatsNewView: whatsNewComponent() 27 | .using(handle, transformInput: AppAction.prism(for: AppAction.whatsNewAction)) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NefEditorClient/Main/Dependencies/AppDependencies.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import NefAPI 3 | 4 | struct AppDependencies { 5 | let persistence: Persistence 6 | let gitHubConfig: GitHub.API.Config 7 | let nefConfig: NefAPI.API.Config 8 | } 9 | -------------------------------------------------------------------------------- /NefEditorClient/Main/Logic/AppAction.swift: -------------------------------------------------------------------------------- 1 | import BowOptics 2 | 3 | enum AppAction: AutoPrism { 4 | case catalogAction(CatalogAction) 5 | case searchAction(SearchAction) 6 | case editAction(EditAction) 7 | case catalogDetailAction(CatalogDetailAction) 8 | case generationAction(GenerationAction) 9 | case creditsAction(CreditsAction) 10 | case faqAction(FAQAction) 11 | case whatsNewAction(WhatsNewAction) 12 | case initialLoad(DeepLinkAction) 13 | case dismissModal 14 | case showICloudAlert 15 | case dismissICloudAlert 16 | case showSettings 17 | case showCredits 18 | case showFAQ 19 | case showWhatsNew 20 | } 21 | -------------------------------------------------------------------------------- /NefEditorClient/Main/Logic/AppDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import BowEffects 3 | import BowOptics 4 | import BowArch 5 | 6 | typealias AppDispatcher = StateDispatcher 7 | 8 | let appDispatcher: AppDispatcher = mainDispatcher 9 | .widen(transformEnvironment: \.persistence) 10 | 11 | .combine(catalogDispatcher.widen( 12 | transformEnvironment: id, 13 | transformInput: AppAction.prism(for: AppAction.catalogAction))) 14 | 15 | .combine(editDispatcher.widen( 16 | transformEnvironment: id, 17 | transformInput: AppAction.prism(for: AppAction.editAction))) 18 | 19 | .combine(catalogDetailDispatcher.widen( 20 | transformEnvironment: id, 21 | transformInput: AppAction.prism(for: AppAction.catalogDetailAction))) 22 | 23 | .combine(addDependencyDispatcher.widen( 24 | transformEnvironment: id, 25 | transformInput: AppAction.prism(for: AppAction.searchAction) + 26 | SearchAction.prism(for: SearchAction.repositoryDetailAction))) 27 | 28 | .combine(searchDispatcher.widen( 29 | transformEnvironment: \.gitHubConfig, 30 | transformState: AppState.searchStateLens, 31 | transformInput: AppAction.prism(for: AppAction.searchAction))) 32 | 33 | .combine(generationDispatcher.widen( 34 | transformEnvironment: \.nefConfig, 35 | transformInput: AppAction.prism(for: AppAction.generationAction))) 36 | 37 | .combine(creditsDispatcher.widen( 38 | transformEnvironment: id, 39 | transformInput: AppAction.prism(for: AppAction.creditsAction))) 40 | 41 | .combine(faqDispatcher.widen( 42 | transformEnvironment: id, 43 | transformInput: AppAction.prism(for: AppAction.faqAction))) 44 | 45 | .combine(whatsNewDispatcher.widen( 46 | transformEnvironment: \.persistence, 47 | transformInput: AppAction.prism(for: AppAction.whatsNewAction))) 48 | 49 | .combine(deepLinkDispatcher.widen( 50 | transformEnvironment: \.persistence, 51 | transformInput: AppAction.prism(for: AppAction.initialLoad))) 52 | -------------------------------------------------------------------------------- /NefEditorClient/Main/State/AppModalState.swift: -------------------------------------------------------------------------------- 1 | enum AppModalState: Equatable { 2 | case noModal 3 | case edit(EditState) 4 | case credits 5 | case generation(GenerationState) 6 | case faq 7 | case whatsNew 8 | 9 | var editState: EditState? { 10 | guard case let .edit(state) = self else { 11 | return nil 12 | } 13 | return state 14 | } 15 | 16 | var generationState: GenerationState? { 17 | guard case let .generation(state) = self else { 18 | return nil 19 | } 20 | return state 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NefEditorClient/Main/State/AppState.swift: -------------------------------------------------------------------------------- 1 | import BowOptics 2 | 3 | struct AppState: Equatable { 4 | let panelState: PanelState 5 | let modalState: AppModalState 6 | let searchState: SearchState 7 | let deepLinkState: DeepLinkState 8 | let catalog: Catalog 9 | let selectedItem: CatalogItem? 10 | let iCloudStatus: ICloudStatus 11 | let iCloudAlert: ICloudAlert 12 | let authenticationState: AuthenticationState 13 | 14 | func copy( 15 | panelState: PanelState? = nil, 16 | modalState: AppModalState? = nil, 17 | searchState: SearchState? = nil, 18 | deepLinkState: DeepLinkState? = nil, 19 | catalog: Catalog? = nil, 20 | selectedItem: CatalogItem?? = nil, 21 | iCloudStatus: ICloudStatus? = nil, 22 | iCloudAlert: ICloudAlert? = nil, 23 | authenticationState: AuthenticationState? = nil 24 | ) -> AppState { 25 | AppState( 26 | panelState: panelState ?? self.panelState, 27 | modalState: modalState ?? self.modalState, 28 | searchState: searchState ?? self.searchState, 29 | deepLinkState: deepLinkState ?? self.deepLinkState, 30 | catalog: catalog ?? self.catalog, 31 | selectedItem: selectedItem ?? self.selectedItem, 32 | iCloudStatus: iCloudStatus ?? self.iCloudStatus, 33 | iCloudAlert: iCloudAlert ?? self.iCloudAlert, 34 | authenticationState: authenticationState ?? self.authenticationState 35 | ) 36 | } 37 | 38 | static var searchStateLens: Lens { 39 | Lens( 40 | get: { app in app.searchState }, 41 | set: { app, search in app.copy(searchState: search) } 42 | ) 43 | } 44 | 45 | static var catalogLens: Lens { 46 | Lens( 47 | get: { app in (app.catalog, app.selectedItem) }, 48 | set: { app, new in app.copy(catalog: new.0, selectedItem: new.1) } 49 | ) 50 | } 51 | 52 | static var selectedItemLens: Lens { 53 | Lens( 54 | get: { app in app.selectedItem }, 55 | set: { app, item in app.copy(selectedItem: item) }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /NefEditorClient/Main/State/ICloudStatus.swift: -------------------------------------------------------------------------------- 1 | enum ICloudStatus { 2 | case enabled 3 | case disabled 4 | } 5 | 6 | enum ICloudAlert { 7 | case shown 8 | case hidden 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Main/State/PanelState.swift: -------------------------------------------------------------------------------- 1 | enum PanelState: Equatable { 2 | case catalog 3 | case search 4 | } 5 | -------------------------------------------------------------------------------- /NefEditorClient/Main/View/AppModalView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AppModalView: View { 4 | let state: AppModalState 5 | let editView: (EditState) -> EditView 6 | let generationView: (GenerationState) -> GenerationView 7 | let creditsView: CreditsView 8 | let faqView: FAQView 9 | let whatsNewView: WhatsNewView 10 | 11 | var body: some View { 12 | switch state { 13 | case .noModal: 14 | return AnyView(EmptyView()) 15 | case .edit(let editState): 16 | return AnyView(editView(editState)) 17 | case .credits: 18 | return AnyView(creditsView) 19 | case .generation(let generationState): 20 | return AnyView(generationView(generationState)) 21 | case .faq: 22 | return AnyView(faqView) 23 | case .whatsNew: 24 | return AnyView(whatsNewView) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NefEditorClient/NefEditorClient.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | com.apple.developer.associated-domains 10 | 11 | applinks:badge.bow-swift.io 12 | 13 | com.apple.developer.icloud-container-identifiers 14 | 15 | iCloud.com.47deg.nef-editor-recipes 16 | 17 | com.apple.developer.icloud-services 18 | 19 | CloudDocuments 20 | 21 | com.apple.developer.ubiquity-container-identifiers 22 | 23 | iCloud.com.47deg.nef-editor-recipes 24 | 25 | com.apple.developer.ubiquity-kvstore-identifier 26 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 27 | com.apple.security.app-sandbox 28 | 29 | com.apple.security.network.client 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /NefEditorClient/NefEditorClientRelease.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | com.apple.developer.associated-domains 10 | 11 | applinks:badge.bow-swift.io 12 | 13 | com.apple.developer.icloud-container-identifiers 14 | 15 | iCloud.com.47deg.nef-editor-recipes 16 | 17 | com.apple.developer.icloud-services 18 | 19 | CloudDocuments 20 | 21 | com.apple.developer.ubiquity-container-identifiers 22 | 23 | iCloud.com.47deg.nef-editor-recipes 24 | 25 | com.apple.developer.ubiquity-kvstore-identifier 26 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 27 | com.apple.security.app-sandbox 28 | 29 | com.apple.security.network.client 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /NefEditorClient/Persistence/ICloudPersistence.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bow 3 | import BowEffects 4 | 5 | class ICloudPersistence: Persistence { 6 | private var documents: URL? { 7 | FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents") 8 | } 9 | 10 | private var preferencesStore: NSUbiquitousKeyValueStore { .default } 11 | 12 | private var recipes: URL? { 13 | documents? 14 | .appendingPathComponent("nefRecipes") 15 | .appendingPathExtension("json") 16 | } 17 | 18 | var isPersistenceAvailable: Bool { 19 | self.recipes != nil 20 | } 21 | 22 | // MARK: recipes 23 | func loadUserRecipes() -> EnvIO { 24 | EnvIO.invoke { _ in 25 | guard let recipes = self.recipes else { return [] } 26 | let decoder = JSONDecoder() 27 | let data = try Data.init(contentsOf: recipes) 28 | return try decoder.decode([Recipe].self, from: data) 29 | } 30 | } 31 | 32 | func saveUserRecipes(_ recipes: [Recipe]) -> EnvIO { 33 | EnvIO.invoke { _ in 34 | guard let recipesFile = self.recipes else { return } 35 | let encoder = JSONEncoder() 36 | let data = try encoder.encode(recipes) 37 | try data.write(to: recipesFile) 38 | } 39 | } 40 | 41 | // MARK: user preferences 42 | func loadUserPreferences() -> EnvIO { 43 | EnvIO.invoke { _ in 44 | guard let data = self.preferencesStore.data(forKey: StoreKey.userPreferences), 45 | let preferences = try? JSONDecoder().decode(UserPreferences.self, from: data) else { 46 | throw UserPreferencesError.invalidData 47 | } 48 | 49 | return preferences 50 | } 51 | } 52 | 53 | func updateUserPreferences(bundleVersion: String) -> EnvIO { 54 | EnvIO.invoke { _ in 55 | let userPreferences = UserPreferences(lastVersionShown: bundleVersion) 56 | let data = try JSONEncoder().encode(userPreferences) 57 | self.preferencesStore.set(data, forKey: StoreKey.userPreferences) 58 | self.preferencesStore.synchronize() 59 | } 60 | } 61 | 62 | // MARK: - Constants 63 | private enum StoreKey { 64 | static let userPreferences = "user-preferences" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /NefEditorClient/Persistence/Persistence.swift: -------------------------------------------------------------------------------- 1 | import BowEffects 2 | 3 | protocol Persistence { 4 | var isPersistenceAvailable: Bool { get } 5 | func loadUserRecipes() -> EnvIO 6 | func saveUserRecipes(_ recipes: [Recipe]) -> EnvIO 7 | func loadUserPreferences() -> EnvIO 8 | func updateUserPreferences(bundleVersion: String) -> EnvIO 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Persistence/UserPreferences.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct UserPreferences: Codable { 4 | let lastVersionShown: String 5 | } 6 | 7 | enum UserPreferencesError: Error { 8 | case invalidData 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NefEditorClient/Preview Content/PreviewData.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import SwiftUI 3 | 4 | #if DEBUG 5 | 6 | let bow = Repository( 7 | name: "bow", 8 | fullName: "bow-swift/bow", 9 | _description: "🏹 Bow is a library for Typed Functional Programming in Swift", 10 | _private: false, 11 | htmlUrl: "https://github.com/bow-swift/bow", 12 | stargazersCount: 407, 13 | owner: Owner(login: "bow-swift", avatarUrl: "https://avatars2.githubusercontent.com/u/44965417?s=200&v=4")) 14 | 15 | let nef = Repository( 16 | name: "nef", 17 | fullName: "bow-swift/nef", 18 | _description: "💊 steroids for Xcode Playgrounds", 19 | _private: false, 20 | htmlUrl: "https://github.com/bow-swift/nef", 21 | stargazersCount: 134, 22 | owner: Owner(login: "bow-swift", avatarUrl: "https://avatars2.githubusercontent.com/u/44965417?s=200&v=4")) 23 | 24 | let version = Requirement.version(Tag(name: "0.7.5")) 25 | let branch = Requirement.branch(Branch(name: "master")) 26 | 27 | let sampleRepo = bow 28 | let sampleRepos = Array(repeating: bow, count: 17) 29 | let sampleSearchResults = [bow, nef] 30 | 31 | let sampleBowTag = TagViewModel(text: "bow") 32 | let sampleNefTag = TagViewModel(text: "nef", foregroundColor: .yellow, backgroundColor: .purple) 33 | 34 | let sampleRequirements = Array(repeating: version, count: 5) + Array(repeating: branch, count: 3) 35 | 36 | let bowDependency = Dependency( 37 | repository: "bow", 38 | owner: "bow-swift", 39 | url: "https://github.com/bow-swift/bow", 40 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4", 41 | requirement: .version(Tag(name: "0.8.0")), 42 | products: .all) 43 | 44 | let bowArchDependency = Dependency( 45 | repository: "bow-arch", 46 | owner: "bow-swift", 47 | url: "https://github.com/bow-swift/bow-arch", 48 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4", 49 | requirement: .branch(Branch(name: "0.1.0")), 50 | products: .all) 51 | 52 | let sampleRecipe = Recipe( 53 | title: "FP Basics", 54 | description: "Learn Functional Programming", 55 | dependencies: [ 56 | bowDependency, 57 | bowArchDependency 58 | ]) 59 | 60 | let sampleRecipes = Array(repeating: sampleRecipe, count: 13).map(CatalogItem.regular) 61 | 62 | let sampleFeaturedRecipe = FeaturedRecipe( 63 | recipe: sampleRecipe, 64 | backgroundImage: "bow-background", 65 | textColor: .white) 66 | 67 | let sampleFeaturedRecipes = Array(repeating: sampleFeaturedRecipe, count: 2).map(CatalogItem.featured) 68 | 69 | let sampleRecipesSection = CatalogSection( 70 | title: "My recipes", 71 | action: CatalogSectionAction(icon: "plus", action: .addRecipe), 72 | items: sampleRecipes) 73 | let sampleFeaturedSection = CatalogSection( 74 | title: "Featured", 75 | items: sampleFeaturedRecipes) 76 | 77 | let sampleCatalog = Catalog(featured: sampleFeaturedSection, userCreated: sampleRecipesSection) 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/Component/RepositoryDetailComponent.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import BowArch 3 | 4 | typealias RepositoryDetailComponent = StoreComponent 5 | 6 | func repositoryDetail( 7 | config: API.Config, 8 | state: SearchModalState 9 | ) -> RepositoryDetailComponent { 10 | 11 | RepositoryDetailComponent( 12 | initialState: state, 13 | environment: config, 14 | dispatcher: repositoryDetailDispatcher, 15 | render: RepositoryDetailView.init) 16 | } 17 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/Logic/RepositoryDetailAction.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | 3 | enum RepositoryDetailAction { 4 | case loadRequirements(Repository) 5 | case dependencySelected(Requirement, from: Repository) 6 | case dismissDetails 7 | } 8 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/Logic/RepositoryDetailDispatcher.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import Bow 3 | import BowEffects 4 | import BowArch 5 | 6 | typealias RepositoryDetailDispatcher = StateDispatcher 7 | 8 | let repositoryDetailDispatcher = RepositoryDetailDispatcher.effectful { action in 9 | switch action { 10 | case .loadRequirements(let repository): 11 | return loadRequirements(repository: repository).handleError { _ in 12 | onError(repository: repository) 13 | }^ 14 | case .dependencySelected(_, from: _): 15 | return EnvIO.pure(.modify(id)^)^ 16 | case .dismissDetails: 17 | return EnvIO.pure(.set(.noModal)^)^ 18 | } 19 | } 20 | 21 | func loadRequirements( 22 | repository: Repository 23 | ) -> EnvIO> { 24 | 25 | let tags = EnvIO.var() 26 | let branches = EnvIO.var() 27 | let requirements = EnvIO.var() 28 | 29 | return binding( 30 | (tags, branches) <- parallel(loadTags(repository: repository), 31 | loadBranches(repository: repository)), 32 | requirements <- EnvIO.pure(merge(tags: tags.get, branches: branches.get)), 33 | yield: requirementsLoaded(requirements.get, for: repository))^ 34 | } 35 | 36 | func onError( 37 | repository: Repository 38 | ) -> State { 39 | .set(.repositoryDetail( 40 | .error(repository, 41 | message: "An error ocurred trying to fetch tags and branches for the repository '\(repository.fullName)'")) 42 | )^ 43 | } 44 | 45 | func loadTags( 46 | repository: Repository 47 | ) -> EnvIO { 48 | API.repository.getVersions(fullName: repository.fullName) 49 | .mapError(id) 50 | } 51 | 52 | func loadBranches( 53 | repository: Repository 54 | ) -> EnvIO { 55 | API.repository.getBranches(fullName: repository.fullName) 56 | .mapError(id) 57 | } 58 | 59 | func merge(tags: Tags, branches: Branches) -> [Requirement] { 60 | tags.map(Requirement.version) + 61 | branches.map(Requirement.branch) 62 | } 63 | 64 | func requirementsLoaded( 65 | _ requirements: [Requirement], 66 | for repository: Repository 67 | ) -> State { 68 | if requirements.isEmpty { 69 | return .set(.repositoryDetail(.empty(repository)))^ 70 | } else { 71 | return .set(.repositoryDetail(.loaded(repository, requirements: requirements)))^ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/State/RepositoryDetailState.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | 3 | enum RepositoryDetailState: Equatable { 4 | case loading(Repository) 5 | case empty(Repository) 6 | case loaded(Repository, requirements: [Requirement]) 7 | case error(Repository, message: String) 8 | 9 | var repository: Repository { 10 | switch self { 11 | case .loading(let repo), 12 | .empty(let repo), 13 | .loaded(let repo, requirements: _), 14 | .error(let repo, message: _): return repo 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/State/Requirement.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | 3 | enum Requirement: Equatable, Codable { 4 | case version(Tag) 5 | case branch(Branch) 6 | 7 | var title: String { 8 | switch self { 9 | case .version(let tag): return tag.name 10 | case .branch(let branch): return branch.name 11 | } 12 | } 13 | 14 | enum Keys: CodingKey { 15 | case version 16 | case branch 17 | } 18 | 19 | init(from decoder: Decoder) throws { 20 | let container = try decoder.container(keyedBy: Keys.self) 21 | if let tag = try? container.decode(Tag.self, forKey: .version) { 22 | self = .version(tag) 23 | } else if let branch = try? container.decode(Branch.self, forKey: .branch) { 24 | self = .branch(branch) 25 | } else { 26 | throw Swift.DecodingError.dataCorrupted( 27 | Swift.DecodingError.Context( 28 | codingPath: container.codingPath, 29 | debugDescription: "Error decoding Requirement") 30 | ) 31 | } 32 | } 33 | 34 | func encode(to encoder: Encoder) throws { 35 | var container = encoder.container(keyedBy: Keys.self) 36 | switch self { 37 | case let .version(tag): 38 | try container.encode(tag, forKey: .version) 39 | case let .branch(branch): 40 | try container.encode(branch, forKey: .branch) 41 | } 42 | } 43 | } 44 | 45 | extension Requirement: Identifiable { 46 | var id: String { 47 | title 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/View/EmptyRequirementsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | 4 | struct EmptyRequirementsView: View { 5 | let repository: Repository 6 | 7 | var body: some View { 8 | ActivityTextView(message: "'\(repository.fullName)' does not contain any tag or branch.") 9 | } 10 | } 11 | 12 | #if DEBUG 13 | struct EmptyRequirementsView_Previews: PreviewProvider { 14 | static var previews: some View { 15 | EmptyRequirementsView(repository: sampleRepo) 16 | .previewLayout(.sizeThatFits) 17 | } 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/View/ErrorRequirementsView.swift: -------------------------------------------------------------------------------- 1 | typealias ErrorRequirementsView = ActivityTextView 2 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/View/LoadingRequirementsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | 4 | struct LoadingRequirementsView: View { 5 | let repository: Repository 6 | 7 | var body: some View { 8 | LoadingView(message: "Loading tags and branches for repository '\(repository.fullName)'...") 9 | } 10 | } 11 | 12 | #if DEBUG 13 | struct LoadingRequirementsView_Previews: PreviewProvider { 14 | static var previews: some View { 15 | LoadingRequirementsView(repository: sampleRepo) 16 | .previewLayout(.sizeThatFits) 17 | } 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/View/RepositoryDetailView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | 4 | struct RepositoryDetailView: View { 5 | let state: SearchModalState 6 | let handle: (RepositoryDetailAction) -> Void 7 | 8 | var body: some View { 9 | self.contentView 10 | .navigationBarTitle(self.title) 11 | .navigationBarItems(leading: 12 | Button("Cancel") { 13 | self.handle(.dismissDetails) 14 | }.navigationBarButtonStyle()) 15 | .onAppear { 16 | self.loadRequirements() 17 | } 18 | } 19 | 20 | private var title: String { 21 | switch state { 22 | case .noModal: return "" 23 | case .repositoryDetail(let detail): return detail.repository.name 24 | } 25 | } 26 | 27 | private var contentView: some View { 28 | switch state { 29 | case .noModal: return AnyView(EmptyView()) 30 | case .repositoryDetail(let detail): 31 | switch detail { 32 | case .empty(let repo): 33 | return AnyView(EmptyRequirementsView(repository: repo)) 34 | case .loading(let repo): 35 | return AnyView(LoadingRequirementsView(repository: repo)) 36 | case .loaded(let repository, requirements: let requirements): 37 | return AnyView(RequirementListView(requirements: requirements) { selected in 38 | self.handle(.dependencySelected(selected, from: repository)) 39 | }) 40 | case .error(_, message: let message): 41 | return AnyView(ErrorRequirementsView(message: message)) 42 | } 43 | } 44 | } 45 | 46 | private func loadRequirements() { 47 | switch state { 48 | case .repositoryDetail(let detail): 49 | self.handle(.loadRequirements(detail.repository)) 50 | case .noModal: return 51 | } 52 | } 53 | } 54 | 55 | #if DEBUG 56 | struct RepositoryDetailView_Previews: PreviewProvider { 57 | static var previews: some View { 58 | Group { 59 | NavigationView { 60 | RepositoryDetailView(state: .repositoryDetail(.empty(bow))) { _ in } 61 | }.navigationViewStyle(StackNavigationViewStyle()) 62 | 63 | NavigationView { 64 | RepositoryDetailView(state: .repositoryDetail(.loading(bow))) { _ in } 65 | }.navigationViewStyle(StackNavigationViewStyle()) 66 | 67 | NavigationView { 68 | RepositoryDetailView(state: .repositoryDetail(.loaded(bow, requirements: [version, branch]))) { _ in } 69 | }.navigationViewStyle(StackNavigationViewStyle()) 70 | 71 | NavigationView { 72 | RepositoryDetailView(state: .repositoryDetail(.error(bow, message: "Could not load tags or branches."))) { _ in } 73 | }.navigationViewStyle(StackNavigationViewStyle()) 74 | }.previewLayout(.fixed(width: 500, height: 500)) 75 | } 76 | } 77 | #endif 78 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/View/RequirementListView.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import SwiftUI 3 | 4 | struct RequirementListView: View { 5 | let requirements: [Requirement] 6 | let onRequirementSelected: (Requirement) -> () 7 | 8 | var body: some View { 9 | List { 10 | ForEach(requirements, id: \.title) { requirement in 11 | Button(action: { self.onRequirementSelected(requirement) }) { 12 | RequirementView(requirement: requirement) 13 | } 14 | } 15 | } 16 | } 17 | } 18 | 19 | #if DEBUG 20 | struct RequirementListView_Previews: PreviewProvider { 21 | static var previews: some View { 22 | RequirementListView(requirements: sampleRequirements) { _ in } 23 | .previewLayout(.sizeThatFits) 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /NefEditorClient/Requirements/View/RequirementView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct RequirementView: View { 4 | let requirement: Requirement 5 | 6 | var body: some View { 7 | HStack { 8 | Image(systemName: self.requirement.iconName) 9 | Text(self.requirement.title) 10 | Spacer() 11 | }.padding() 12 | } 13 | } 14 | 15 | private extension Requirement { 16 | var iconName: String { 17 | switch self { 18 | case .version: return "tag" 19 | case .branch: return "arrow.branch" 20 | } 21 | } 22 | } 23 | 24 | #if DEBUG 25 | struct RequirementView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | Group { 28 | RequirementView(requirement: version) 29 | 30 | RequirementView(requirement: branch) 31 | }.frame(width: 400) 32 | .previewLayout(.sizeThatFits) 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /NefEditorClient/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | import BowArch 4 | 5 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 6 | var window: UIWindow? 7 | 8 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 9 | // For some reason, I cannot set the background color of a SwiftUI List 🤷🏻‍♂️ 10 | UITableView.appearance().backgroundColor = .clear 11 | 12 | // Remove any extra separator in the Lists 13 | UITableView.appearance().tableFooterView = UIView() 14 | 15 | if let userActivity = connectionOptions.userActivities.first { 16 | loadScene(scene, contentView: appComponent(userActivity: userActivity)) 17 | } else { 18 | loadScene(scene, contentView: appComponent(urlContexts: connectionOptions.urlContexts)) 19 | } 20 | } 21 | 22 | func scene(_ scene: UIScene, openURLContexts urlContexts: Set) { 23 | loadScene(scene, contentView: appComponent(urlContexts: urlContexts)) 24 | } 25 | 26 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { 27 | loadScene(scene, contentView: appComponent(userActivity: userActivity)) 28 | } 29 | 30 | private func loadScene(_ scene: UIScene, contentView: V) { 31 | guard let windowScene = scene as? UIWindowScene else { return } 32 | 33 | window = UIWindow(windowScene: windowScene) 34 | window?.rootViewController = UIHostingController(rootView: contentView) 35 | window?.makeKeyAndVisible() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NefEditorClient/Search/Component/SearchComponent.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import Bow 3 | import BowArch 4 | 5 | typealias SearchComponent = StoreComponent> 6 | 7 | func searchComponent( 8 | config: API.Config, 9 | state: SearchState 10 | ) -> SearchComponent { 11 | SearchComponent( 12 | initialState: state, 13 | environment: config, 14 | dispatcher: searchDispatcher) { state, handle in 15 | SearchView( 16 | state: state, 17 | detail: repositoryDetail(config: config, state: state.modalState) 18 | .using(handle, transformInput: SearchAction.prism(for: SearchAction.repositoryDetailAction)), 19 | handle: handle 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NefEditorClient/Search/Logic/SearchAction.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import BowOptics 3 | 4 | enum SearchAction: AutoPrism { 5 | case search(query: String) 6 | case showDetails(Repository) 7 | case dismissDetails 8 | case cancelSearch 9 | case repositoryDetailAction(RepositoryDetailAction) 10 | } 11 | -------------------------------------------------------------------------------- /NefEditorClient/Search/Logic/SearchDispatcher.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import Bow 3 | import BowArch 4 | import BowEffects 5 | 6 | typealias SearchDispatcher = StateDispatcher 7 | 8 | let searchDispatcher = SearchDispatcher.workflow { action in 9 | switch action { 10 | 11 | case .search(query: let query): 12 | return search(query: query) 13 | 14 | case .showDetails(let repository): 15 | return [EnvIO.pure(showDetails(repository))^] 16 | 17 | case .dismissDetails: 18 | return [EnvIO.pure(dismissDetails())^] 19 | 20 | case .repositoryDetailAction(let detailAction): 21 | switch detailAction { 22 | case .dependencySelected(_, from: _): 23 | return [EnvIO.pure(dismissDetails())^] 24 | default: 25 | return [] 26 | } 27 | 28 | case .cancelSearch: 29 | return [] 30 | } 31 | }.combine(repositoryDetailDispatcher.widen( 32 | transformState: SearchState.modalStateLens, 33 | transformInput: SearchAction.prism(for: SearchAction.repositoryDetailAction) 34 | )) 35 | 36 | func search( 37 | query: String 38 | ) -> [EnvIO>] { 39 | [ 40 | EnvIO.pure(setLoading(query: query))^, 41 | backgroundSearch(query: query) 42 | .handleError { _ in onError(query: query) }^ 43 | ] 44 | } 45 | 46 | func setLoading(query: String) -> State { 47 | .modify { state in 48 | state.copy(loadingState: .loading(query: query)) 49 | }^ 50 | } 51 | 52 | func backgroundSearch(query: String) -> EnvIO> { 53 | let repositories = EnvIO.var() 54 | 55 | return binding( 56 | continueOn(.global(qos: .background)), 57 | repositories <- gitHubSearch(query: query), 58 | yield: showResults(repositories: repositories.get, for: query) 59 | )^ 60 | } 61 | 62 | func gitHubSearch( 63 | query: String 64 | ) -> EnvIO { 65 | API.search.searchRepositories(q: "\(query)+language:Swift" ) 66 | .bimap(id, { result in result.items }) 67 | } 68 | 69 | func showResults(repositories: Repositories, for query: String) -> State { 70 | .modify { state in 71 | let newState: SearchLoadingState = (repositories.isEmpty) 72 | ? .empty(query: query) 73 | : .loaded(repositories) 74 | return state.copy(loadingState: newState) 75 | }^ 76 | } 77 | 78 | func onError( 79 | query: String 80 | ) -> State { 81 | .modify { state in 82 | state.copy(loadingState: .error(message: "An error happened while performing your query '\(query)'")) 83 | }^ 84 | } 85 | 86 | func showDetails( 87 | _ repository: Repository 88 | ) -> State { 89 | .modify { state in 90 | state.copy(modalState: .repositoryDetail(.loading(repository))) 91 | }^ 92 | } 93 | 94 | func dismissDetails() -> State { 95 | .modify { state in 96 | state.copy(modalState: .noModal) 97 | }^ 98 | } 99 | -------------------------------------------------------------------------------- /NefEditorClient/Search/State/SearchState.swift: -------------------------------------------------------------------------------- 1 | import GitHub 2 | import BowOptics 3 | 4 | struct SearchState: Equatable { 5 | let loadingState: SearchLoadingState 6 | let modalState: SearchModalState 7 | 8 | func copy( 9 | loadingState: SearchLoadingState? = nil, 10 | modalState: SearchModalState? = nil 11 | ) -> SearchState { 12 | SearchState( 13 | loadingState: loadingState ?? self.loadingState, 14 | modalState: modalState ?? self.modalState) 15 | } 16 | 17 | static var modalStateLens: Lens { 18 | Lens( 19 | get: { search in search.modalState }, 20 | set: { search, modal in search.copy(modalState: modal) }) 21 | } 22 | } 23 | 24 | enum SearchLoadingState: Equatable { 25 | case initial 26 | case loading(query: String) 27 | case empty(query: String) 28 | case loaded(Repositories) 29 | case error(message: String) 30 | } 31 | 32 | enum SearchModalState: Equatable { 33 | case noModal 34 | case repositoryDetail(RepositoryDetailState) 35 | } 36 | -------------------------------------------------------------------------------- /NefEditorClient/Search/View/EmptySearchView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EmptySearchView: View { 4 | let query: String 5 | 6 | var body: some View { 7 | ActivityTextView(message: "Your query '\(query)' did not produce any results.") 8 | } 9 | } 10 | 11 | #if DEBUG 12 | struct EmptySearchView_Previews: PreviewProvider { 13 | static var previews: some View { 14 | EmptySearchView(query: "Bow") 15 | .previewLayout(.sizeThatFits) 16 | } 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /NefEditorClient/Search/View/ErrorSearchView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | typealias ErrorSearchView = ActivityTextView 4 | -------------------------------------------------------------------------------- /NefEditorClient/Search/View/InitialSearchView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct InitialSearchView: View { 4 | var body: some View { 5 | ActivityTextView(message: "Search GitHub for your favorite Swift repositories and add them to your nef recipe.") 6 | } 7 | } 8 | 9 | #if DEBUG 10 | struct InitialSearchView_Previews: PreviewProvider { 11 | static var previews: some View { 12 | InitialSearchView() 13 | .previewLayout(.sizeThatFits) 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /NefEditorClient/Search/View/LoadingSearchView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LoadingSearchView: View { 4 | let query: String 5 | 6 | var body: some View { 7 | LoadingView(message: "Searching Swift repositories with query '\(query)'...") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Search/View/RepositoryGridView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | 4 | struct RepositoryGridView: View { 5 | let repositories: [Repository] 6 | let columns: Int 7 | let onRepositorySelected: (Repository) -> () 8 | 9 | var body: some View { 10 | GridView(rows: self.rows, columns: self.columns) { row, column in 11 | self.viewForItemAt(row, column) 12 | .aspectRatio(16/9, contentMode: .fit) 13 | .onTapGesture { 14 | if let repository = self.itemAt(row, column) { 15 | self.onRepositorySelected(repository) 16 | } 17 | } 18 | .safeHoverEffect() 19 | } 20 | } 21 | 22 | private var rows: Int { 23 | Int(ceil(Double(repositories.count) / Double(columns))) 24 | } 25 | 26 | private func itemAt(_ row: Int, _ column: Int) -> Repository? { 27 | repositories[safe: indexAt(row, column)] 28 | } 29 | 30 | private func indexAt(_ row: Int, _ column: Int) -> Int { 31 | row * self.columns + column 32 | } 33 | 34 | private func viewForItemAt(_ row: Int, _ column: Int) -> some View { 35 | if let repository = itemAt(row, column) { 36 | return AnyView(RepositoryView(repository: repository)) 37 | } else { 38 | return AnyView(Color.clear) 39 | } 40 | } 41 | } 42 | 43 | #if DEBUG 44 | struct RepositoryGridView_Previews: PreviewProvider { 45 | 46 | static var previews: some View { 47 | Group { 48 | RepositoryGridView(repositories: sampleRepos, columns: 3) { _ in } 49 | 50 | ScrollView { 51 | RepositoryGridView(repositories: sampleRepos, columns: 3) { _ in } 52 | } 53 | } 54 | } 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /NefEditorClient/Search/View/RepositoryView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | 4 | struct RepositoryView: View { 5 | let repository: Repository 6 | 7 | var body: some View { 8 | CardView { 9 | VStack(alignment: .leading, spacing: 8) { 10 | Text(self.repository.name) 11 | .titleStyle() 12 | .padding(.bottom, 4) 13 | 14 | Text(self.repository._description ?? "No description") 15 | 16 | Spacer() 17 | 18 | HStack(alignment: .lastTextBaseline) { 19 | HStack { 20 | AvatarView(avatar: self.repository.owner.avatarUrl) 21 | .frame(width: 24, height: 24) 22 | .mask(Circle()) 23 | Text(self.repository.owner.login) 24 | } 25 | 26 | Spacer() 27 | 28 | self.labeledImage( 29 | "star.fill", 30 | tint: .yellow, 31 | text: "\(self.repository.stargazersCount)") 32 | } 33 | }.padding() 34 | } 35 | } 36 | 37 | private func labeledImage( 38 | _ image: String, 39 | tint: Color? = nil, 40 | text: String) -> some View { 41 | 42 | HStack { 43 | Image(systemName: image).foregroundColor(tint) 44 | Text(text) 45 | } 46 | } 47 | } 48 | 49 | #if DEBUG 50 | struct RepositoryView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | Group { 53 | RepositoryView(repository: sampleRepo) 54 | .frame(maxWidth: 300, maxHeight: 200) 55 | .aspectRatio(4/3, contentMode: .fit) 56 | .previewLayout(.sizeThatFits) 57 | .padding() 58 | } 59 | } 60 | } 61 | #endif 62 | -------------------------------------------------------------------------------- /NefEditorClient/Search/View/SearchView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import GitHub 3 | 4 | struct SearchView: View { 5 | let state: SearchState 6 | let detail: Detail 7 | @State var query: String = "" 8 | let handle: (SearchAction) -> Void 9 | 10 | let isDetailPresented: Binding 11 | 12 | init( 13 | state: SearchState, 14 | detail: Detail, 15 | handle: @escaping (SearchAction) -> Void) { 16 | self.state = state 17 | self.detail = detail 18 | self.handle = handle 19 | self.isDetailPresented = Binding( 20 | get: { state.modalState != .noModal }, 21 | set: { newState in 22 | if !newState { 23 | handle(.dismissDetails) 24 | } 25 | }) 26 | } 27 | 28 | var body: some View { 29 | VStack { 30 | self.searchView 31 | self.contentView.fill.layoutPriority(1) 32 | }.modal(isPresented: isDetailPresented) { 33 | self.detail 34 | }.modifier(KeyboardPadding()) 35 | } 36 | 37 | private var searchView: some View { 38 | CardView { 39 | HStack { 40 | SearchBar(placeholder: "Search repositories...", query: self.$query) { query in 41 | self.handle(.search(query: query)) 42 | } 43 | 44 | Button(action: { self.handle(.cancelSearch) }) { 45 | Text("Cancel").foregroundColor(.nef) 46 | }.padding(.trailing) 47 | } 48 | }.padding(.top).padding(.trailing) 49 | } 50 | 51 | private var contentView: some View { 52 | switch state.loadingState { 53 | case .initial: 54 | return AnyView(InitialSearchView()) 55 | case .loading(let query): 56 | return AnyView(LoadingSearchView(query: query)) 57 | case .empty(let query): 58 | return AnyView(EmptySearchView(query: query)) 59 | case .loaded(let repositories): 60 | return AnyView(loadedView(repositories: repositories)) 61 | case .error(let message): 62 | return AnyView(ErrorSearchView(message: message)) 63 | } 64 | } 65 | 66 | private func loadedView(repositories: Repositories) -> some View { 67 | GeometryReader { geometry in 68 | ScrollView { 69 | RepositoryGridView( 70 | repositories: repositories, 71 | columns: self.columns(for: geometry.size.width)) { repository in 72 | self.handle(.showDetails(repository)) 73 | } 74 | .padding(.trailing) 75 | .padding(.bottom) 76 | .fill 77 | } 78 | } 79 | } 80 | 81 | private func columns(for width: CGFloat) -> Int { 82 | let minimumCardWidth: CGFloat = Card.minimumWidth 83 | return max(Int(floor(width / minimumCardWidth)), 1) 84 | } 85 | } 86 | 87 | #if DEBUG 88 | struct SearchView_Previews: PreviewProvider { 89 | static func state(_ loadingState: SearchLoadingState) -> SearchState { 90 | SearchState(loadingState: loadingState, modalState: .noModal) 91 | } 92 | 93 | static var previews: some View { 94 | Group { 95 | SearchView(state: state(.initial), detail: EmptyView()) { _ in } 96 | 97 | SearchView(state: state(.empty(query: "Bow")), detail: EmptyView()) { _ in } 98 | 99 | SearchView(state: state(.loading(query: "Bow")), detail: EmptyView()) { _ in } 100 | 101 | SearchView(state: state(.loaded(sampleSearchResults)), detail: EmptyView()) { _ in } 102 | 103 | SearchView(state: state(.loaded(sampleRepos)), detail: EmptyView()) { _ in } 104 | 105 | SearchView(state: state(.error(message: "Unexpected error happened.")), detail: EmptyView()) { _ in } 106 | } 107 | .previewLayout(.fixed(width: 910, height: 1024)) 108 | } 109 | } 110 | #endif 111 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/ActionButtonStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ActionButtonStyle: ButtonStyle { 4 | func makeBody(configuration: Self.Configuration) -> some View { 5 | configuration.label 6 | .padding(8) 7 | .foregroundColor(.white) 8 | .contentShape(Circle()) 9 | .background( 10 | Circle().fill( 11 | configuration.isPressed 12 | ? Color.nef.opacity(0.7) 13 | : Color.nef)) 14 | .safeHoverEffect() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ActivityIndicator: UIViewRepresentable { 4 | @Binding var isAnimating: Bool 5 | let style: UIActivityIndicatorView.Style 6 | 7 | init(isAnimating: Binding, style: UIActivityIndicatorView.Style = .medium) { 8 | self._isAnimating = isAnimating 9 | self.style = style 10 | } 11 | 12 | func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { 13 | UIActivityIndicatorView(style: style) 14 | } 15 | 16 | func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { 17 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating() 18 | } 19 | } 20 | 21 | #if DEBUG 22 | struct ActivityIndicator_Previews: PreviewProvider { 23 | static var previews: some View { 24 | Group { 25 | ActivityIndicator(isAnimating: .constant(true)) 26 | .previewLayout(.sizeThatFits) 27 | 28 | ActivityIndicator(isAnimating: .constant(true), style: .large) 29 | .previewLayout(.sizeThatFits) 30 | 31 | ActivityIndicator(isAnimating: .constant(false), style: .large) 32 | .previewLayout(.sizeThatFits) 33 | } 34 | } 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/ActivityTextView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ActivityTextView: View { 4 | let message: String 5 | 6 | var body: some View { 7 | Text(message) 8 | .activityStyle() 9 | .multilineTextAlignment(.center) 10 | } 11 | } 12 | 13 | #if DEBUG 14 | struct ActivityTextView_Previews: PreviewProvider { 15 | static var previews: some View { 16 | ActivityTextView(message: "This is a sample message.") 17 | .previewLayout(.sizeThatFits) 18 | } 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/ActivityViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | struct ActivityViewController: UIViewControllerRepresentable { 5 | let activityItems: [Any] 6 | let applicationActivities: [UIActivity]? 7 | 8 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { 9 | let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) 10 | return controller 11 | } 12 | 13 | func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {} 14 | 15 | } 16 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/AnimationView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AnimationView: View { 4 | struct Animation: Identifiable { 5 | var id: LottieAnimation { lottie } 6 | 7 | let lottie: LottieAnimation 8 | let isLoop: Bool 9 | 10 | init(lottie: LottieAnimation, isLoop: Bool = false) { 11 | self.lottie = lottie 12 | self.isLoop = isLoop 13 | } 14 | } 15 | 16 | let animation: Animation 17 | 18 | init(animation: Animation) { 19 | self.animation = animation 20 | } 21 | 22 | var body: some View { 23 | self.animationView() 24 | } 25 | 26 | private func animationView() -> some View { 27 | guard let lottieView = animation.lottie.view(isLoop: animation.isLoop) else { 28 | return AnyView(ActivityIndicator(isAnimating: .constant(true), style: .large)) 29 | } 30 | 31 | return AnyView( 32 | GeometryReader { geometry in 33 | lottieView 34 | .offset(self.animation.lottie.fixOffset(size: geometry.size)) 35 | .aspectRatio(contentMode: .fill) 36 | .frame(width: geometry.size.width, height: geometry.size.height) 37 | } 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/AvatarView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AvatarView: View { 4 | let avatar: String 5 | 6 | var body: some View { 7 | // GitHub API has a parameter 's' that lets us control the size of the received image 8 | if let url = URL(string: avatar + "&s=24") { 9 | return AnyView( 10 | URLImage(url: url, placeholder: Image.person) 11 | ) 12 | } else { 13 | return AnyView( 14 | Image.person 15 | ) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/CardView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum Card { 4 | static var minimumWidth: CGFloat { 5 | let screen = UIScreen.main.bounds 6 | let width = max(screen.size.width, screen.size.height) 7 | if width > 1200 { 8 | return 420 9 | } else { 10 | return 340 11 | } 12 | } 13 | } 14 | 15 | struct CardView: View { 16 | @Environment(\.colorScheme) var colorScheme 17 | 18 | let isSelected: Bool 19 | let content: () -> Content 20 | 21 | init(isSelected: Bool = false, 22 | @ViewBuilder content: @escaping () -> Content) { 23 | self.isSelected = isSelected 24 | self.content = content 25 | } 26 | 27 | var body: some View { 28 | ZStack { 29 | RoundedRectangle(cornerRadius: 8) 30 | .fill(Color.card) 31 | .if(colorScheme == .light, 32 | then: { 33 | $0.shadow( 34 | color: self.isSelected ? 35 | Color.shadow.opacity(0.3) : 36 | Color.shadow.opacity(0.1), 37 | radius: self.isSelected ? 6 : 2, 38 | x: 1, 39 | y: 1) 40 | }) 41 | .if(isSelected && colorScheme == .dark, 42 | then: { 43 | $0.overlay( 44 | RoundedRectangle(cornerRadius: 8) 45 | .stroke(Color.shadow, lineWidth: 2) 46 | ) 47 | }) 48 | self.content() 49 | } 50 | } 51 | } 52 | 53 | #if DEBUG 54 | struct CardView_Preview: PreviewProvider { 55 | static var previews: some View { 56 | Group { 57 | cardSized(width: 200, height: 200) 58 | cardSized(width: 400, height: 100) 59 | cardSized(width: 100, height: 400) 60 | } 61 | } 62 | 63 | static func cardSized(width: CGFloat, height: CGFloat) -> some View { 64 | CardView { 65 | EmptyView() 66 | }.frame(width: width, height: height) 67 | .previewLayout(.sizeThatFits) 68 | .padding() 69 | } 70 | } 71 | #endif 72 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/ColorPalette.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Color { 4 | static var nef: Color { 5 | Color("nefColor") 6 | } 7 | 8 | static var card: Color { 9 | Color("card") 10 | } 11 | 12 | static var shadow: Color { 13 | Color("shadow") 14 | } 15 | 16 | static var form: Color { 17 | Color("form") 18 | } 19 | 20 | static var mainBackground: Color { 21 | Color("mainBackground") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/GridView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GridView: View { 4 | let rows: Int 5 | let columns: Int 6 | let horizontalSpacing: CGFloat 7 | let verticalSpacing: CGFloat 8 | let horizontalAlignment: HorizontalAlignment 9 | let verticalAlignment: VerticalAlignment 10 | let cellAt: (Int, Int) -> Cell 11 | 12 | init( 13 | rows: Int, 14 | columns: Int, 15 | horizontalSpacing: CGFloat = 8, 16 | verticalSpacing: CGFloat = 8, 17 | horizontalAlignment: HorizontalAlignment = .center, 18 | verticalAlignment: VerticalAlignment = .center, 19 | @ViewBuilder cellAt: @escaping (Int, Int) -> Cell) { 20 | self.rows = rows 21 | self.columns = columns 22 | self.horizontalSpacing = horizontalSpacing 23 | self.verticalSpacing = verticalSpacing 24 | self.horizontalAlignment = horizontalAlignment 25 | self.verticalAlignment = verticalAlignment 26 | self.cellAt = cellAt 27 | } 28 | 29 | var body: some View { 30 | VStack(alignment: self.horizontalAlignment, spacing: self.horizontalSpacing) { 31 | ForEach(0 ..< self.rows, id: \.self) { row in 32 | HStack(alignment: self.verticalAlignment, spacing: self.verticalSpacing) { 33 | ForEach(0 ..< self.columns, id: \.self) { column in 34 | Group { 35 | if column != 0 { 36 | Spacer(minLength: 0) 37 | } 38 | self.cellAt(row, column) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | #if DEBUG 48 | struct GridView_Previews: PreviewProvider { 49 | static var previews: some View { 50 | Group { 51 | GridView(rows: 4, columns: 3) { row, column in 52 | ZStack { 53 | Circle().fill(Color.gray) 54 | Text("\(row), \(column)") 55 | } 56 | } 57 | 58 | ScrollView { 59 | GridView(rows: 20, columns: 6, horizontalSpacing: 24, verticalSpacing: 16) { row, column in 60 | ZStack { 61 | RoundedRectangle(cornerRadius: 8).fill(Color.blue) 62 | Text("\(row), \(column)") 63 | }.aspectRatio(4/3, contentMode: .fit) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | #endif 70 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/Image+System.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Image { 4 | static var pencil: Image { 5 | Image(systemName: "pencil") 6 | } 7 | 8 | static var plus: Image { 9 | Image(systemName: "plus") 10 | } 11 | 12 | static var duplicate: Image { 13 | Image(systemName: "plus.square.on.square") 14 | } 15 | 16 | static var trash: Image { 17 | Image(systemName: "trash") 18 | } 19 | 20 | static var person: Image { 21 | Image(systemName: "person.crop.circle") 22 | } 23 | 24 | static var close: Image { 25 | Image(systemName: "xmark") 26 | } 27 | 28 | static var warning: Image { 29 | Image(systemName: "exclamationmark.triangle.fill") 30 | } 31 | 32 | static var info: Image { 33 | Image(systemName: "info.circle.fill") 34 | } 35 | 36 | static var faq: Image { 37 | Image(systemName: "questionmark.circle.fill") 38 | } 39 | 40 | static var whatsNew: Image { 41 | Image(systemName: "sparkles") 42 | } 43 | 44 | static var heart: Image { 45 | Image(systemName: "suit.heart.fill") 46 | } 47 | 48 | static var wand: Image { 49 | Image(systemName: "wand.and.stars") 50 | } 51 | 52 | 53 | static var nefClear: Image { 54 | Image("nef") 55 | } 56 | 57 | static var appIcon: Image { 58 | Image("nef-icon") 59 | } 60 | 61 | static var githubIcon: Image { 62 | Image("github-icon") 63 | } 64 | 65 | static var bow: Image { 66 | Image("bow-brand") 67 | } 68 | 69 | static var bowArch: Image { 70 | Image("bow-arch-brand") 71 | } 72 | 73 | static var bowOpenAPI: Image { 74 | Image("bow-openapi-brand") 75 | } 76 | 77 | static var bowLite: Image { 78 | Image("bow-lite-brand") 79 | } 80 | 81 | static var fortySeven: Image { 82 | Image("fortyseven") 83 | } 84 | 85 | static var badgePlatform: Image { 86 | Image("bow-platform-badge") 87 | } 88 | 89 | static var badgeActions: Image { 90 | Image("bow-actions-badge") 91 | } 92 | 93 | static var badgeNef: Image { 94 | Image("nef-playgrounds-badge") 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/KeyboardPadding.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | 4 | struct KeyboardPadding: ViewModifier { 5 | @State var currentHeight: CGFloat = 0 6 | let maxPadding: CGFloat 7 | 8 | init(maxPadding: CGFloat = .infinity) { 9 | self.maxPadding = maxPadding 10 | } 11 | 12 | func body(content: Content) -> some View { 13 | content 14 | .padding(.bottom, min(currentHeight, maxPadding)) 15 | .edgesIgnoringSafeArea(currentHeight == 0 ? [] : .bottom) 16 | .onAppear(perform: subscribeToKeyboardEvents) 17 | } 18 | 19 | private func subscribeToKeyboardEvents() { 20 | NotificationCenter.Publisher( 21 | center: NotificationCenter.default, 22 | name: UIResponder.keyboardWillShowNotification 23 | ).compactMap { notification in 24 | notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect 25 | }.map { rect in 26 | rect.height 27 | }.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight)) 28 | 29 | NotificationCenter.Publisher( 30 | center: NotificationCenter.default, 31 | name: UIResponder.keyboardWillHideNotification 32 | ).compactMap { _ in 33 | CGFloat.zero 34 | }.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/LibraryButtonStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LibraryButtonStyle: ButtonStyle { 4 | func makeBody(configuration: Self.Configuration) -> some View { 5 | configuration.label 6 | .padding(4) 7 | .contentShape(RoundedRectangle(cornerRadius: 8)) 8 | .background(RoundedRectangle(cornerRadius: 8) 9 | .fill(configuration.isPressed 10 | ? Color.nef.opacity(0.1) 11 | : Color.clear)) 12 | .safeHoverEffect() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/LoadingView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LoadingView: View { 4 | let message: String 5 | 6 | var body: some View { 7 | VStack { 8 | ActivityIndicator(isAnimating: .constant(true), style: .large) 9 | Text(message).activityStyle() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/LottieAnimation.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Lottie 3 | 4 | enum LottieAnimation: String { 5 | case generalError = "general-error" 6 | case generalLoading = "general-loading" 7 | case githubSearch = "github-search" 8 | case playgroundLoading = "playgroundbook-loading" 9 | case playgroundSuccess = "playgroundbook-success" 10 | 11 | func fixOffset(size: CGSize) -> CGSize { 12 | switch self { 13 | case .playgroundLoading: 14 | return .init(width: -size.width*0.2, height: 0) 15 | default: 16 | return .zero 17 | } 18 | } 19 | } 20 | 21 | extension LottieAnimation { 22 | func view(isLoop: Bool) -> AnimationLottieView? { 23 | guard let animation = Lottie.Animation.named(rawValue, subdirectory: "Animations") else { return nil } 24 | return AnimationLottieView(id: rawValue, animation: animation, isLoop: isLoop) 25 | } 26 | } 27 | 28 | struct AnimationLottieView: UIViewRepresentable, Identifiable { 29 | let id: String 30 | let animation: Lottie.Animation 31 | let isLoop: Bool 32 | 33 | func makeCoordinator() -> Coordinator { 34 | Coordinator(self) 35 | } 36 | 37 | func makeUIView(context: UIViewRepresentableContext) -> UIView { 38 | UIView() 39 | } 40 | 41 | func updateUIView(_ view: UIView, context: UIViewRepresentableContext) { 42 | let animationView = Lottie.AnimationView() 43 | animationView.contentMode = .scaleAspectFit 44 | animationView.translatesAutoresizingMaskIntoConstraints = false 45 | 46 | view.subviews.first?.removeFromSuperview() 47 | view.addSubview(animationView) 48 | 49 | NSLayoutConstraint.activate([ 50 | animationView.widthAnchor.constraint(equalTo: view.widthAnchor), 51 | animationView.heightAnchor.constraint(equalTo: view.heightAnchor) 52 | ]) 53 | 54 | animationView.animation = animation 55 | animationView.loopMode = isLoop ? .loop : .playOnce 56 | animationView.play() 57 | } 58 | 59 | class Coordinator: NSObject { 60 | let parent: AnimationLottieView 61 | 62 | init(_ animationView: AnimationLottieView) { 63 | self.parent = animationView 64 | super.init() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/NavigationBarButtonStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Button { 4 | func navigationBarButtonStyle() -> some View { 5 | self.buttonStyle(NavigationBarButtonStyle()) 6 | } 7 | } 8 | 9 | struct NavigationBarButtonStyle: ButtonStyle { 10 | func makeBody(configuration: Self.Configuration) -> some View { 11 | configuration.label 12 | .foregroundColor(.nef) 13 | .padding(8) 14 | .contentShape(RoundedRectangle(cornerRadius: 4)) 15 | .safeHoverEffect() 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/Rectangle+Separator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Rectangle { 4 | static var separator: some View { 5 | Rectangle() 6 | .fill(Color.gray) 7 | .frame(height: 0.5) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/SearchBar.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SearchBar: UIViewRepresentable { 4 | let placeholder: String 5 | @Binding var query: String 6 | let barStyle: UIBarStyle 7 | let onSearch: (String) -> Void 8 | 9 | init(placeholder: String, 10 | query: Binding, 11 | barStyle: UIBarStyle = .default, 12 | onSearch: @escaping (String) -> Void) { 13 | self.placeholder = placeholder 14 | self._query = query 15 | self.barStyle = barStyle 16 | self.onSearch = onSearch 17 | } 18 | 19 | class SearchCoordinator: NSObject, UISearchBarDelegate { 20 | @Binding var query: String 21 | let onSearch: (String) -> Void 22 | 23 | init(query: Binding, onSearch: @escaping (String) -> Void) { 24 | self._query = query 25 | self.onSearch = onSearch 26 | } 27 | 28 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 29 | self.query = searchText 30 | } 31 | 32 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 33 | self.onSearch(self.query) 34 | } 35 | } 36 | 37 | func makeUIView(context: UIViewRepresentableContext) -> UISearchBar { 38 | let searchBar = UISearchBar() 39 | searchBar.delegate = context.coordinator 40 | searchBar.placeholder = self.placeholder 41 | searchBar.barStyle = self.barStyle 42 | searchBar.searchBarStyle = .minimal 43 | return searchBar 44 | } 45 | 46 | func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) { 47 | uiView.text = query 48 | } 49 | 50 | func makeCoordinator() -> SearchCoordinator { 51 | SearchCoordinator(query: $query, onSearch: self.onSearch) 52 | } 53 | } 54 | 55 | #if DEBUG 56 | struct SearchBar_Previews: PreviewProvider { 57 | static var previews: some View { 58 | Group { 59 | SearchBar(placeholder: "Search repository...", 60 | query: .constant(""), 61 | onSearch: { _ in }) 62 | .previewLayout(.sizeThatFits) 63 | 64 | SearchBar(placeholder: "Search repository...", 65 | query: .constant("Bow"), 66 | barStyle: .black, 67 | onSearch: { _ in }) 68 | .previewLayout(.sizeThatFits) 69 | } 70 | } 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/SectionTitle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SectionTitle: View { 4 | struct Action { 5 | let icon: Image 6 | let handle: () -> Void 7 | 8 | init(icon: Image, handle: @autoclosure @escaping () -> Void) { 9 | self.icon = icon 10 | self.handle = handle 11 | } 12 | } 13 | 14 | let title: String 15 | let action: Action? 16 | 17 | init(title: String, action: Action? = nil) { 18 | self.title = title 19 | self.action = action 20 | } 21 | 22 | var body: some View { 23 | HStack(alignment: .firstTextBaseline) { 24 | Text(title) 25 | .largeTitleStyle() 26 | self.actionView() 27 | .alignmentGuide(.firstTextBaseline) { d in d[.bottom] * 0.82 } 28 | .padding(.leading, 16) 29 | Spacer() 30 | } 31 | } 32 | 33 | private func actionView() -> some View { 34 | guard let action = action else { 35 | return AnyView(EmptyView()) 36 | } 37 | 38 | return AnyView( 39 | Button(action: action.handle) { action.icon } 40 | .buttonStyle(ActionButtonStyle()) 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/TextButtonStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct TextButtonStyle: ButtonStyle { 4 | func makeBody(configuration: Self.Configuration) -> some View { 5 | configuration.label 6 | .padding() 7 | .font(Font.system(.body).bold()) 8 | .foregroundColor(.white) 9 | .frame(maxWidth: .infinity) 10 | .contentShape(RoundedRectangle(cornerRadius: 8)) 11 | .background(RoundedRectangle(cornerRadius: 8) 12 | .fill(configuration.isPressed 13 | ? Color.nef.opacity(0.7) 14 | : Color.nef)) 15 | .safeHoverEffect() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/TextStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Text { 4 | func largeTitleStyle() -> Text { 5 | self.bold().font(.largeTitle) 6 | } 7 | 8 | func titleStyle() -> Text { 9 | self.bold().font(.title) 10 | } 11 | 12 | func activityStyle() -> Text { 13 | self.font(.callout) 14 | .foregroundColor(Color.gray) 15 | } 16 | 17 | func cardTitleStyle() -> some View { 18 | self.scaledFont(.system(desiredSize: 20, weight: .thin)) 19 | .foregroundColor(.gray) 20 | } 21 | 22 | func cardBodyStyle() -> some View { 23 | self.scaledFont(.system(desiredSize: 20, weight: .thin)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/URLImage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Bow 3 | import BowEffects 4 | 5 | struct URLImage: View { 6 | @ObservedObject private var loader: ImageLoader 7 | private let placeholder: Placeholder? 8 | 9 | init(url: URL, placeholder: Placeholder? = nil) { 10 | loader = ImageLoader(url: url) 11 | self.placeholder = placeholder 12 | } 13 | 14 | var body: some View { 15 | image.onAppear(perform: loader.load) 16 | } 17 | 18 | private var image: some View { 19 | Group { 20 | if loader.image != nil { 21 | Image(uiImage: loader.image!) 22 | .resizable() 23 | } else { 24 | placeholder 25 | } 26 | } 27 | } 28 | } 29 | 30 | private class ImageLoader: ObservableObject { 31 | @Published var image: UIImage? 32 | private let url: URL 33 | private static let downloadQueue = DispatchQueue(label: "download-images") 34 | 35 | init(url: URL) { 36 | self.url = url 37 | } 38 | 39 | func load() { 40 | URLSession.shared.downloadTaskIO(with: self.url) 41 | .flatMap { result in 42 | IO.invoke { 43 | try Data.init(contentsOf: result.url) 44 | } 45 | }.map(UIImage.init(data:)) 46 | .handleError { _ in nil }^ 47 | .unsafeRunAsync(on: ImageLoader.downloadQueue) { either in 48 | DispatchQueue.main.async { [weak self] in 49 | self?.image = either.getOrElse(nil) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/View+Conditional.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import SwiftUI 3 | 4 | extension View { 5 | func `if`( 6 | _ condition: Bool, 7 | then f: @escaping (Self) -> Then, 8 | else g: @escaping (Self) -> Else 9 | ) -> some View { 10 | Group { 11 | if condition { 12 | f(self) 13 | } else { 14 | g(self) 15 | } 16 | } 17 | } 18 | 19 | func `if`( 20 | _ condition: Bool, 21 | then f: @escaping (Self) -> Then 22 | ) -> some View { 23 | self.if(condition, 24 | then: f, 25 | else: { $0 }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/View+Hover.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func safeHoverEffect() -> some View { 5 | if #available(iOS 13.4, *) { 6 | return AnyView(self.hoverEffect()) 7 | } else { 8 | return AnyView(self) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/View+Modal.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func modal( 5 | isPresented: Binding, 6 | withNavigation: Bool = true, 7 | @ViewBuilder content: @escaping () -> V 8 | ) -> some View { 9 | background( 10 | EmptyView().sheet(isPresented: isPresented) { 11 | Group { 12 | if withNavigation { 13 | NavigationView { 14 | content() 15 | }.navigationViewStyle(StackNavigationViewStyle()) 16 | } else { 17 | content() 18 | } 19 | } 20 | } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NefEditorClient/Theme/ViewStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | var fill: some View { 5 | self.frame(maxWidth: .infinity, maxHeight: .infinity) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /NefEditorClient/ViewModifiers/ScaledFont.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // MARK: - ViewModifier 4 | struct ScaledFont: ViewModifier { 5 | @Environment(\.sizeCategory) var sizeCategory 6 | var font: ScaledFontType 7 | 8 | func body(content: Content) -> some View { 9 | content.font(font.dynamic) 10 | } 11 | } 12 | 13 | // MARK: - Models 14 | enum ScaledFontType { 15 | case system(desiredSize: CGFloat, weight: Font.Weight) 16 | case custom(desiredSize: CGFloat, name: String) 17 | 18 | var dynamic: Font { 19 | let scaledSize = UIFontMetrics.default.scaledValue(for: desiredSize) 20 | 21 | switch self { 22 | case let .custom(_, name): 23 | return .custom(name, size: scaledSize) 24 | case let .system(_, weight): 25 | return .system(size: scaledSize, weight: weight) 26 | } 27 | } 28 | 29 | var desiredSize: CGFloat { 30 | switch self { 31 | case let .system(desiredSize, _): 32 | return desiredSize 33 | case let .custom(desiredSize, _): 34 | return desiredSize 35 | } 36 | } 37 | } 38 | 39 | // MARK: - Helpers 40 | extension View { 41 | func scaledFont(_ font: ScaledFontType) -> some View { 42 | modifier(ScaledFont(font: font)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NefEditorClient/WhatsNew/Component/WhatsNewComponent.swift: -------------------------------------------------------------------------------- 1 | import BowArch 2 | 3 | typealias WhatsNewComponent = StoreComponent 4 | 5 | func whatsNewComponent() -> WhatsNewComponent { 6 | WhatsNewComponent(initialState: ()) { _, handle in 7 | WhatsNewView(handle: handle) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /NefEditorClient/WhatsNew/Logic/WhatsNewAction.swift: -------------------------------------------------------------------------------- 1 | import Bow 2 | import Foundation 3 | 4 | enum WhatsNewAction { 5 | case openGenerator 6 | case dismiss 7 | } 8 | -------------------------------------------------------------------------------- /NefEditorClient/WhatsNew/Logic/WhatsNewDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bow 3 | import BowEffects 4 | import BowArch 5 | 6 | typealias WhatsNewDispatcher = StateDispatcher 7 | 8 | let whatsNewDispatcher = WhatsNewDispatcher.effectful { action in 9 | switch action { 10 | case .openGenerator: 11 | return open(url: URL(string: "https://badge.bow-swift.io")) 12 | case .dismiss: 13 | return updateUserPreferences().as(dismissModal())^ 14 | } 15 | } 16 | 17 | 18 | func updateUserPreferences() -> EnvIO { 19 | EnvIO.accessM { persistence in 20 | let bundleVersion = Bundle.main.version 21 | return persistence.updateUserPreferences(bundleVersion: bundleVersion)^ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NefEditorClient/WhatsNew/View/BadgeGeneratorCard.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BadgeGeneratorCard: View { 4 | let handle: (WhatsNewAction) -> Void 5 | 6 | var body: some View { 7 | VStack { 8 | Text("Badge generator") 9 | .cardTitleStyle() 10 | .frame(maxWidth: .infinity, alignment: .leading) 11 | .padding(.init(top: 24, leading: 24, bottom: 0, trailing: 24)) 12 | 13 | HStack { 14 | Image.appIcon.resizable().frame(width: 120, height: 120) 15 | .clipShape(Circle()) 16 | .overlay(Circle().stroke(lineWidth: 4).foregroundColor(.nef)) 17 | Image.heart 18 | .font(.body).foregroundColor(.red) 19 | .padding() 20 | Image.githubIcon.resizable().frame(width: 120, height: 120) 21 | .clipShape(Circle()) 22 | }.padding(24) 23 | 24 | Divider().padding(.horizontal, 24) 25 | 26 | HStack { 27 | Spacer() 28 | Image.badgePlatform.opacity(0.5) 29 | Image.badgeActions.opacity(0.5) 30 | Image.badgeNef 31 | Spacer() 32 | } 33 | 34 | Text("You can attach a nef badge to your GitHub Swift repo, to let users try your project directly in their iPads.") 35 | .cardBodyStyle() 36 | .multilineTextAlignment(.center) 37 | .padding(.init(top: 18, leading: 44, bottom: 0, trailing: 44)) 38 | 39 | Button(action: { self.handle(.openGenerator) }) { 40 | HStack { 41 | Image.wand.resizable().frame(width: 30, height: 30) 42 | Text("Open badge generator") 43 | .font(.body) 44 | .foregroundColor(Color.blue) 45 | } 46 | }.padding(44) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NefEditorClient/WhatsNew/View/WhatsNewView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct WhatsNewView: View { 4 | @Environment(\.colorScheme) var colorScheme 5 | let handle: (WhatsNewAction) -> Void 6 | 7 | var body: some View { 8 | VStack { 9 | whatsNewCard() 10 | Spacer() 11 | Button("Got it!", action: { self.handle(.dismiss) }) 12 | .frame(maxWidth: .infinity) 13 | .buttonStyle(TextButtonStyle()) 14 | .padding() 15 | 16 | }.navigationBarTitle("What's New", displayMode: .inline) 17 | } 18 | 19 | private func whatsNewCard() -> some View { 20 | BadgeGeneratorCard(handle: handle) 21 | } 22 | } 23 | 24 | #if DEBUG 25 | struct WhatsNewView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | Group { 28 | WhatsNewView() { _ in }.environment(\.colorScheme, .light) 29 | WhatsNewView() { _ in }.preferredColorScheme(.dark) 30 | }.previewLayout(.fixed(width: 800, height: 800)) 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /NefEditorClientTests/AppDispatcherTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Bow 3 | import BowEffects 4 | import BowArch 5 | @testable import NefEditorClient 6 | 7 | class AppDispatcherTests: XCTestCase { 8 | 9 | func testAddDependency() { 10 | let id = UUID() 11 | let recipe = Recipe(id: id, title: "A", description: "B", dependencies: []) 12 | let updatedRecipe = Recipe(id: id, title: "A", description: "B", dependencies: [Dependency(repository: bow.name, url: bow.htmlUrl, requirement: sampleRequirements[0])]) 13 | 14 | let appState = AppState( 15 | panelState: .catalog, 16 | editState: .notEditing, 17 | searchState: SearchState(loadingState: .initial, modalState: .noModal), 18 | catalog: Catalog(featured: CatalogSection(title: "1", items: []), 19 | userCreated: CatalogSection(title: "2", items: [.regular(recipe)])), 20 | selectedItem: .regular(recipe)) 21 | 22 | let expected = AppState( 23 | panelState: .catalog, 24 | editState: .notEditing, 25 | searchState: SearchState(loadingState: .initial, modalState: .noModal), 26 | catalog: Catalog(featured: CatalogSection(title: "1", items: []), 27 | userCreated: CatalogSection(title: "2", items: [.regular(updatedRecipe)])), 28 | selectedItem: .regular(updatedRecipe)) 29 | 30 | assert(dispatcher: addDependencyDispatcher, 31 | on: .dependencySelected(sampleRequirements[0], from: bow), 32 | initialState: appState, 33 | expectedState: expected) 34 | } 35 | 36 | func testAddDependencyAppDispatcher() { 37 | let id = UUID() 38 | let recipe = Recipe(id: id, title: "A", description: "B", dependencies: []) 39 | let updatedRecipe = Recipe(id: id, title: "A", description: "B", dependencies: [Dependency(repository: bow.name, url: bow.htmlUrl, requirement: sampleRequirements[0])]) 40 | 41 | let appState = AppState( 42 | panelState: .catalog, 43 | editState: .notEditing, 44 | searchState: SearchState(loadingState: .initial, modalState: .noModal), 45 | catalog: Catalog(featured: CatalogSection(title: "1", items: []), 46 | userCreated: CatalogSection(title: "2", items: [.regular(recipe)])), 47 | selectedItem: .regular(recipe)) 48 | 49 | let expected = AppState( 50 | panelState: .catalog, 51 | editState: .notEditing, 52 | searchState: SearchState(loadingState: .initial, modalState: .noModal), 53 | catalog: Catalog(featured: CatalogSection(title: "1", items: []), 54 | userCreated: CatalogSection(title: "2", items: [.regular(updatedRecipe)])), 55 | selectedItem: .regular(updatedRecipe)) 56 | 57 | assert(dispatcher: appDispatcher, 58 | on: .searchAction( 59 | .repositoryDetailAction( 60 | .dependencySelected(sampleRequirements[0], from: bow))), 61 | initialState: appState, 62 | expectedState: expected) 63 | } 64 | 65 | func assert( 66 | dispatcher: StateDispatcher, 67 | on input: I, 68 | with environment: E, 69 | initialState: S, 70 | expectedState: S) { 71 | 72 | let actions = dispatcher.on(input) 73 | let finalState = actions.reduce(initialState) { state, next in 74 | try! next.map { action in 75 | action^.runS(state)^.value 76 | }^ 77 | .provide(environment) 78 | .unsafeRunSync() 79 | } 80 | 81 | XCTAssertEqual(finalState, expectedState) 82 | } 83 | 84 | func assert( 85 | dispatcher: StateDispatcher, 86 | on input: I, 87 | initialState: S, 88 | expectedState: S) { 89 | 90 | assert( 91 | dispatcher: dispatcher, 92 | on: input, 93 | with: (), 94 | initialState: initialState, 95 | expectedState: expectedState) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /NefEditorClientTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NefEditorClientTests/NefEditorClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NefEditorClientTests.swift 3 | // NefEditorClientTests 4 | // 5 | // Created by Tomás Ruiz López on 30/03/2020. 6 | // Copyright © 2020 The Bow Authors. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NefEditorClient 11 | 12 | class NefEditorClientTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nef Playgrounds 2 | 3 | Welcome to the client-side code of [nef Playgrounds for iPad](https://apps.apple.com/us/app/nef-playgrounds/id1511012848)! 4 | 5 | nef Playgrounds lets you: 6 | 7 | 👨‍🍳 Create a nef recipe... 8 | 9 | 📦 ... add your favorite Swift dependencies ... 10 | 11 | 📲 ... and create a Swift Playground that you can use on your iPad! 12 | 13 | ## How does it work? 14 | 15 | nef Playgrounds uses GitHub API to search for Swift repositories and select a branch or tag that can be used as a dependency in Swift Package Manager. Then, it communicates with [its backend](https://github.com/bow-swift/nef-editor-server), which is also open source, to generate a Swift Playground that contains the selected dependencies. This Playground is sent back to the client and users can open it in the Playgrounds app. It will let users write Swift code using the Swift Packages of their choice. 16 | 17 | > Unfortunately, this may not always work; your repository must contain only Swift code, have a `Package.swift` manifest file, and be prepared to run on the iPad (the runtime in the iPad is slightly different and there may be parts of your library that do not work properly). 18 | 19 | ## How do I run this project? 20 | 21 | - Open the project on Xcode. 22 | - Run the schemes `GenerateGitHubAPI` and `GenerateNefAPI`. You will need to have [Bow OpenAPI](https://openapi.bow-swift.io) installed on your Mac. These tasks will generate two folders named `GitHub` and `NefAPI` respectively. 23 | - Add the folders to the root of the project. 24 | - You may need to configure your app ID and create the necessary entitlements to handle Apple Sign In and iCloud storage. 25 | 26 | ## License 27 | 28 | Copyright (C) 2020-2021 The nef Authors 29 | 30 | Licensed under the Apache License, Version 2.0 (the "License"); 31 | you may not use this file except in compliance with the License. 32 | You may obtain a copy of the License at 33 | 34 | http://www.apache.org/licenses/LICENSE-2.0 35 | 36 | Unless required by applicable law or agreed to in writing, software 37 | distributed under the License is distributed on an "AS IS" BASIS, 38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 | See the License for the specific language governing permissions and 40 | limitations under the License. 41 | -------------------------------------------------------------------------------- /github.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: '0.1.0' 4 | title: GitHub API 5 | paths: 6 | '/search/repositories': 7 | get: 8 | operationId: searchRepositories 9 | tags: 10 | - Search 11 | description: Searches for repositories matching the provided query. 12 | parameters: 13 | - in: query 14 | name: q 15 | description: Query for the repository search. 16 | required: true 17 | schema: 18 | type: string 19 | responses: 20 | '200': 21 | description: Returns a list of repositories matching the provided query. 22 | content: 23 | application/json: 24 | schema: 25 | $ref: '#/components/schemas/SearchRepositoriesResult' 26 | '/repos/{full_name}/branches': 27 | get: 28 | operationId: getBranches 29 | tags: 30 | - Repository 31 | description: Obtains all branches of a repository. 32 | parameters: 33 | - in: path 34 | name: full_name 35 | description: Full name (owner + repository) of a repository. 36 | required: true 37 | schema: 38 | type: string 39 | responses: 40 | '200': 41 | description: Returns a list of branches for a given repository. 42 | content: 43 | application/json: 44 | schema: 45 | $ref: '#/components/schemas/Branches' 46 | '/repos/{full_name}/tags': 47 | get: 48 | operationId: getVersions 49 | tags: 50 | - Repository 51 | description: Obtains all tags of a repository. 52 | parameters: 53 | - in: path 54 | name: full_name 55 | description: Full name (owner + repository) of a repository. 56 | required: true 57 | schema: 58 | type: string 59 | responses: 60 | '200': 61 | description: Returns a list of tags for a given repository. 62 | content: 63 | application/json: 64 | schema: 65 | $ref: '#/components/schemas/Tags' 66 | components: 67 | schemas: 68 | Branches: 69 | description: An array of branches. 70 | type: array 71 | items: 72 | $ref: '#/components/schemas/Branch' 73 | Branch: 74 | description: A representation of the metadata of a branch in a GitHub repository. 75 | type: object 76 | properties: 77 | name: 78 | type: string 79 | required: 80 | - name 81 | Owner: 82 | description: A representation of the metadata of the owner of a GitHub repository. 83 | type: object 84 | properties: 85 | login: 86 | type: string 87 | avatar_url: 88 | type: string 89 | required: 90 | - login 91 | - avatar_url 92 | Repositories: 93 | description: An array of repositories. 94 | type: array 95 | items: 96 | $ref: '#/components/schemas/Repository' 97 | Repository: 98 | description: A representation of the metadata of a GitHub repository. 99 | type: object 100 | properties: 101 | name: 102 | type: string 103 | full_name: 104 | type: string 105 | description: 106 | type: string 107 | private: 108 | type: boolean 109 | html_url: 110 | type: string 111 | stargazers_count: 112 | type: integer 113 | owner: 114 | $ref: '#/components/schemas/Owner' 115 | required: 116 | - name 117 | - full_name 118 | - private 119 | - html_url 120 | - stargazers_count 121 | - owner 122 | SearchRepositoriesResult: 123 | description: Describes the results of a repository search. 124 | type: object 125 | properties: 126 | total_count: 127 | type: integer 128 | items: 129 | $ref: '#/components/schemas/Repositories' 130 | required: 131 | - total_count 132 | - items 133 | Tags: 134 | description: An array of tags. 135 | type: array 136 | items: 137 | $ref: '#/components/schemas/Tag' 138 | Tag: 139 | description: A representation of the metadata of a tag in a GitHub repository. 140 | type: object 141 | properties: 142 | name: 143 | type: string 144 | required: 145 | - name 146 | -------------------------------------------------------------------------------- /nef.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: nef Playgrounds - Server 4 | version: "1.0.0" 5 | 6 | paths: 7 | /signin: 8 | post: 9 | summary: Sign in with Apple. 10 | operationId: signin 11 | requestBody: 12 | required: true 13 | content: 14 | application/json: 15 | schema: 16 | $ref: "#/components/schemas/AppleSignInRequest" 17 | responses: 18 | '200': 19 | description: The request was successful. 20 | content: 21 | application/json: 22 | schema: 23 | $ref: '#/components/schemas/AppleSignInResponse' 24 | '500': 25 | description: The request was aborted. 26 | content: 27 | application/json: 28 | schema: 29 | $ref: '#/components/schemas/NefEditorError' 30 | 31 | components: 32 | schemas: 33 | 34 | NefEditorError: 35 | type: object 36 | required: 37 | - error 38 | - reason 39 | properties: 40 | error: 41 | type: boolean 42 | reason: 43 | type: string 44 | 45 | AppleSignInRequest: 46 | type: object 47 | required: 48 | - identityToken 49 | - authorizationCode 50 | properties: 51 | identityToken: 52 | type: string 53 | authorizationCode: 54 | type: string 55 | 56 | AppleSignInResponse: 57 | type: object 58 | required: 59 | - token 60 | properties: 61 | token: 62 | type: string 63 | --------------------------------------------------------------------------------