├── .github ├── README.md └── workflows │ ├── build.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .swiftlint.yml ├── Brewfile ├── Horizon.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── Horizon.xcscheme └── xcuserdata │ └── tom.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Horizon ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024.png │ │ ├── Icon-128.png │ │ ├── Icon-256.png │ │ ├── Icon-32.png │ │ ├── Icon-512.png │ │ └── Icon-64.png │ ├── Background.colorset │ │ └── Contents.json │ ├── Contents.json │ └── MenuBarIcon.imageset │ │ ├── Contents.json │ │ └── menubaricon.png ├── Base.lproj │ └── Main.storyboard ├── Components │ ├── Dropdown.swift │ └── TextView.swift ├── Constants.swift ├── Horizon.entitlements ├── Info.plist ├── Models │ ├── AuthUser.swift │ ├── Entry.swift │ ├── File.swift │ ├── Journal.swift │ └── User.swift ├── Networking │ └── Futureland.swift ├── Prefs │ ├── AccountPrefsView.swift │ ├── GeneralPrefsView.swift │ └── PrefsViewModel.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Publish │ ├── PublishPanel.swift │ ├── PublishView.swift │ └── PublishViewModel.swift ├── StatusBar │ ├── StatusBarMenuItem.swift │ └── StatusItemMenu.swift ├── Store.swift └── Utilities.swift ├── HorizonTests ├── HorizonTests.swift └── Info.plist └── HorizonUITests ├── HorizonUITests.swift └── Info.plist /.github/README.md: -------------------------------------------------------------------------------- 1 | ## Horizon 2 | 3 | Horizon is a tiny macOS app for writing and publishing entries to [Futureland](https://futureland.tv). 4 | 5 | [Download the latest release](https://github.com/tmm/horizon/releases/latest/download/Horizon.app.zip) 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: macOS-latest 8 | steps: 9 | - name: Check out main 10 | uses: actions/checkout@v1 11 | 12 | - name: Make sure Xcode is installed 13 | run: ls /Applications | grep Xcode 14 | 15 | - name: Select Xcode 12 16 | run: sudo xcode-select --switch /Applications/Xcode_12.4.app 17 | 18 | - name: Build 19 | run: 20 | xcodebuild -scheme Horizon -sdk macosx clean build 21 | CODE_SIGNING_ALLOWED=NO 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | paths: 6 | - ".github/workflows/swiftlint.yml" 7 | - ".swiftlint.yml" 8 | - "**/*.swift" 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out main 15 | uses: actions/checkout@v1 16 | 17 | - name: Lint 18 | uses: norio-nomura/action-swiftlint@3.1.0 19 | 20 | - name: Lint --strict 21 | uses: norio-nomura/action-swiftlint@3.1.0 22 | with: 23 | args: --strict 24 | 25 | - name: Lint files changed in PR 26 | uses: norio-nomura/action-swiftlint@3.1.0 27 | env: 28 | DIFF_BASE: ${{ github.base_ref }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: macOS-latest 8 | steps: 9 | - name: Check out main 10 | uses: actions/checkout@v1 11 | 12 | - name: Make sure Xcode is installed 13 | run: ls /Applications | grep Xcode 14 | 15 | - name: Select Xcode 12 16 | run: sudo xcode-select --switch /Applications/Xcode_12.4.app 17 | 18 | - name: Test 19 | run: 20 | xcodebuild test -scheme Horizon \ 21 | FUTURELAND_EMAIL=$FUTURELAND_EMAIL \ 22 | FUTURELAND_PASSWORD=$FUTURELAND_PASSWORD 23 | env: 24 | FUTURELAND_EMAIL: ${{ secrets.FUTURELAND_EMAIL }} 25 | FUTURELAND_PASSWORD: ${{ secrets.FUTURELAND_PASSWORD }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Brewfile.lock.json 2 | 3 | ## User settings 4 | **/xcuserdata/ 5 | 6 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 7 | *.xcscmblueprint 8 | *.xccheckout 9 | 10 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 11 | build/ 12 | DerivedData/ 13 | *.moved-aside 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | 26 | ## App packaging 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | # *.xcodeproj 42 | # 43 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 44 | # hence it is not needed unless you have added a package configuration file to your project 45 | # .swiftpm 46 | 47 | .build/ 48 | 49 | # CocoaPods 50 | # 51 | # We recommend against adding the Pods directory to your .gitignore. However 52 | # you should judge for yourself, the pros and cons are mentioned at: 53 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 54 | # 55 | # Pods/ 56 | # 57 | # Add this line if you want to avoid checking in source code from the Xcode workspace 58 | # *.xcworkspace 59 | 60 | # Carthage 61 | # 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | Carthage/Checkouts 64 | 65 | Carthage/Build/ 66 | 67 | # Accio dependency management 68 | Dependencies/ 69 | .accio/ 70 | 71 | # fastlane 72 | # 73 | # It is recommended to not store the screenshots in the git repo. 74 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 75 | # For more information about the recommended setup visit: 76 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 77 | 78 | fastlane/report.xml 79 | fastlane/Preview.html 80 | fastlane/screenshots/**/*.png 81 | fastlane/test_output 82 | 83 | # Code Injection 84 | # 85 | # After new code Injection tools there's a generated folder /iOSInjectionProject 86 | # https://github.com/johnno1962/injectionforxcode 87 | 88 | iOSInjectionProject/ 89 | 90 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - identifier_name 4 | - todo 5 | 6 | opt_in_rules: 7 | - empty_count 8 | - empty_string 9 | 10 | excluded: 11 | - Carthage 12 | - Pods 13 | - SwiftLint/Common/3rdPartyLib 14 | 15 | line_length: 16 | warning: 150 17 | error: 200 18 | ignores_function_declarations: true 19 | ignores_comments: true 20 | ignores_urls: true 21 | 22 | function_body_length: 23 | warning: 300 24 | error: 500 25 | 26 | function_parameter_count: 27 | warning: 6 28 | error: 8 29 | 30 | type_body_length: 31 | warning: 300 32 | error: 500 33 | 34 | file_length: 35 | warning: 1000 36 | error: 1500 37 | ignore_comment_only_lines: true 38 | 39 | cyclomatic_complexity: 40 | warning: 15 41 | error: 25 42 | 43 | reporter: "xcode" 44 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew 'swiftlint' 2 | -------------------------------------------------------------------------------- /Horizon.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E233838025BE326200902F38 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = E233837F25BE326200902F38 /* Alamofire */; }; 11 | E275D2B325BFA63E00320CA5 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E275D2B225BFA63E00320CA5 /* TextView.swift */; }; 12 | E276483825C50824007ECF9B /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = E276483725C50824007ECF9B /* Store.swift */; }; 13 | E276484125C5CB74007ECF9B /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = E276484025C5CB74007ECF9B /* KeychainAccess */; }; 14 | E283259825C109980021BD90 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E283259725C109980021BD90 /* Constants.swift */; }; 15 | E2943D1725DB2EFE0012CDCB /* Dropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2943D1625DB2EFE0012CDCB /* Dropdown.swift */; }; 16 | E2AEB10B25BDF413003BD251 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB10A25BDF413003BD251 /* AppDelegate.swift */; }; 17 | E2AEB10F25BDF413003BD251 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2AEB10E25BDF413003BD251 /* Assets.xcassets */; }; 18 | E2AEB11225BDF413003BD251 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2AEB11125BDF413003BD251 /* Preview Assets.xcassets */; }; 19 | E2AEB11525BDF413003BD251 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2AEB11325BDF413003BD251 /* Main.storyboard */; }; 20 | E2AEB12125BDF413003BD251 /* HorizonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB12025BDF413003BD251 /* HorizonTests.swift */; }; 21 | E2AEB12C25BDF413003BD251 /* HorizonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB12B25BDF413003BD251 /* HorizonUITests.swift */; }; 22 | E2AEB13E25BDF438003BD251 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = E2AEB13D25BDF438003BD251 /* KeyboardShortcuts */; }; 23 | E2AEB14425BDF43F003BD251 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E2AEB14325BDF43F003BD251 /* LaunchAtLogin */; }; 24 | E2AEB14A25BDF446003BD251 /* Preferences in Frameworks */ = {isa = PBXBuildFile; productRef = E2AEB14925BDF446003BD251 /* Preferences */; }; 25 | E2AEB15725BDF4A7003BD251 /* Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB15225BDF4A7003BD251 /* Entry.swift */; }; 26 | E2AEB15825BDF4A7003BD251 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB15325BDF4A7003BD251 /* File.swift */; }; 27 | E2AEB15925BDF4A7003BD251 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB15425BDF4A7003BD251 /* User.swift */; }; 28 | E2AEB15A25BDF4A7003BD251 /* AuthUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB15525BDF4A7003BD251 /* AuthUser.swift */; }; 29 | E2AEB15B25BDF4A7003BD251 /* Journal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB15625BDF4A7003BD251 /* Journal.swift */; }; 30 | E2AEB16225BDF4D7003BD251 /* Futureland.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB16025BDF4D6003BD251 /* Futureland.swift */; }; 31 | E2AEB16B25BDF4F7003BD251 /* PrefsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB16825BDF4F7003BD251 /* PrefsViewModel.swift */; }; 32 | E2AEB16C25BDF4F7003BD251 /* AccountPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB16925BDF4F7003BD251 /* AccountPrefsView.swift */; }; 33 | E2AEB16D25BDF4F7003BD251 /* GeneralPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB16A25BDF4F7003BD251 /* GeneralPrefsView.swift */; }; 34 | E2AEB17525BDF52F003BD251 /* PublishView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB17225BDF52F003BD251 /* PublishView.swift */; }; 35 | E2AEB17625BDF52F003BD251 /* PublishViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB17325BDF52F003BD251 /* PublishViewModel.swift */; }; 36 | E2AEB17725BDF52F003BD251 /* PublishPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB17425BDF52F003BD251 /* PublishPanel.swift */; }; 37 | E2AEB17E25BDF537003BD251 /* StatusItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB17C25BDF537003BD251 /* StatusItemMenu.swift */; }; 38 | E2AEB17F25BDF537003BD251 /* StatusBarMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AEB17D25BDF537003BD251 /* StatusBarMenuItem.swift */; }; 39 | E2D9610725C0F87500E4210D /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D9610625C0F87500E4210D /* Utilities.swift */; }; 40 | E2DAAAF725C1B35200E9A847 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E2DAAAF625C1B35200E9A847 /* Sparkle */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXContainerItemProxy section */ 44 | E2AEB11D25BDF413003BD251 /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = E2AEB0FF25BDF412003BD251 /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = E2AEB10625BDF413003BD251; 49 | remoteInfo = Horizon; 50 | }; 51 | E2AEB12825BDF413003BD251 /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = E2AEB0FF25BDF412003BD251 /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = E2AEB10625BDF413003BD251; 56 | remoteInfo = Horizon; 57 | }; 58 | /* End PBXContainerItemProxy section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | E275D2B225BFA63E00320CA5 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; 62 | E276483725C50824007ECF9B /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 63 | E283259725C109980021BD90 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 64 | E2943D1625DB2EFE0012CDCB /* Dropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dropdown.swift; sourceTree = ""; }; 65 | E2AEB10725BDF413003BD251 /* Horizon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Horizon.app; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | E2AEB10A25BDF413003BD251 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 67 | E2AEB10E25BDF413003BD251 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 68 | E2AEB11125BDF413003BD251 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 69 | E2AEB11425BDF413003BD251 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 70 | E2AEB11625BDF413003BD251 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | E2AEB11725BDF413003BD251 /* Horizon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Horizon.entitlements; sourceTree = ""; }; 72 | E2AEB11C25BDF413003BD251 /* HorizonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HorizonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 73 | E2AEB12025BDF413003BD251 /* HorizonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizonTests.swift; sourceTree = ""; }; 74 | E2AEB12225BDF413003BD251 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 75 | E2AEB12725BDF413003BD251 /* HorizonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HorizonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | E2AEB12B25BDF413003BD251 /* HorizonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizonUITests.swift; sourceTree = ""; }; 77 | E2AEB12D25BDF413003BD251 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 78 | E2AEB15225BDF4A7003BD251 /* Entry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Entry.swift; sourceTree = ""; }; 79 | E2AEB15325BDF4A7003BD251 /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; 80 | E2AEB15425BDF4A7003BD251 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 81 | E2AEB15525BDF4A7003BD251 /* AuthUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthUser.swift; sourceTree = ""; }; 82 | E2AEB15625BDF4A7003BD251 /* Journal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Journal.swift; sourceTree = ""; }; 83 | E2AEB16025BDF4D6003BD251 /* Futureland.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Futureland.swift; sourceTree = ""; }; 84 | E2AEB16825BDF4F7003BD251 /* PrefsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefsViewModel.swift; sourceTree = ""; }; 85 | E2AEB16925BDF4F7003BD251 /* AccountPrefsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountPrefsView.swift; sourceTree = ""; }; 86 | E2AEB16A25BDF4F7003BD251 /* GeneralPrefsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralPrefsView.swift; sourceTree = ""; }; 87 | E2AEB17225BDF52F003BD251 /* PublishView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublishView.swift; sourceTree = ""; }; 88 | E2AEB17325BDF52F003BD251 /* PublishViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublishViewModel.swift; sourceTree = ""; }; 89 | E2AEB17425BDF52F003BD251 /* PublishPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublishPanel.swift; sourceTree = ""; }; 90 | E2AEB17C25BDF537003BD251 /* StatusItemMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItemMenu.swift; sourceTree = ""; }; 91 | E2AEB17D25BDF537003BD251 /* StatusBarMenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarMenuItem.swift; sourceTree = ""; }; 92 | E2D9610625C0F87500E4210D /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 93 | /* End PBXFileReference section */ 94 | 95 | /* Begin PBXFrameworksBuildPhase section */ 96 | E2AEB10425BDF413003BD251 /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | E2DAAAF725C1B35200E9A847 /* Sparkle in Frameworks */, 101 | E2AEB14425BDF43F003BD251 /* LaunchAtLogin in Frameworks */, 102 | E276484125C5CB74007ECF9B /* KeychainAccess in Frameworks */, 103 | E233838025BE326200902F38 /* Alamofire in Frameworks */, 104 | E2AEB14A25BDF446003BD251 /* Preferences in Frameworks */, 105 | E2AEB13E25BDF438003BD251 /* KeyboardShortcuts in Frameworks */, 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | E2AEB11925BDF413003BD251 /* Frameworks */ = { 110 | isa = PBXFrameworksBuildPhase; 111 | buildActionMask = 2147483647; 112 | files = ( 113 | ); 114 | runOnlyForDeploymentPostprocessing = 0; 115 | }; 116 | E2AEB12425BDF413003BD251 /* Frameworks */ = { 117 | isa = PBXFrameworksBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | ); 121 | runOnlyForDeploymentPostprocessing = 0; 122 | }; 123 | /* End PBXFrameworksBuildPhase section */ 124 | 125 | /* Begin PBXGroup section */ 126 | E2AEB0FE25BDF412003BD251 = { 127 | isa = PBXGroup; 128 | children = ( 129 | E2AEB10925BDF413003BD251 /* Horizon */, 130 | E2AEB11F25BDF413003BD251 /* HorizonTests */, 131 | E2AEB12A25BDF413003BD251 /* HorizonUITests */, 132 | E2AEB10825BDF413003BD251 /* Products */, 133 | ); 134 | sourceTree = ""; 135 | }; 136 | E2AEB10825BDF413003BD251 /* Products */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | E2AEB10725BDF413003BD251 /* Horizon.app */, 140 | E2AEB11C25BDF413003BD251 /* HorizonTests.xctest */, 141 | E2AEB12725BDF413003BD251 /* HorizonUITests.xctest */, 142 | ); 143 | name = Products; 144 | sourceTree = ""; 145 | }; 146 | E2AEB10925BDF413003BD251 /* Horizon */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | E2AEB10A25BDF413003BD251 /* AppDelegate.swift */, 150 | E283259725C109980021BD90 /* Constants.swift */, 151 | E276483725C50824007ECF9B /* Store.swift */, 152 | E2D9610625C0F87500E4210D /* Utilities.swift */, 153 | E2B8FFD425BF0BFF0090A6B6 /* Components */, 154 | E2AEB15125BDF491003BD251 /* Models */, 155 | E2AEB15F25BDF4C7003BD251 /* Networking */, 156 | E2AEB16725BDF4E5003BD251 /* Prefs */, 157 | E2AEB17125BDF503003BD251 /* Publish */, 158 | E2AEB17B25BDF537003BD251 /* StatusBar */, 159 | E2AEB10E25BDF413003BD251 /* Assets.xcassets */, 160 | E2AEB11325BDF413003BD251 /* Main.storyboard */, 161 | E2AEB11625BDF413003BD251 /* Info.plist */, 162 | E2AEB11725BDF413003BD251 /* Horizon.entitlements */, 163 | E2AEB11025BDF413003BD251 /* Preview Content */, 164 | ); 165 | path = Horizon; 166 | sourceTree = ""; 167 | }; 168 | E2AEB11025BDF413003BD251 /* Preview Content */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | E2AEB11125BDF413003BD251 /* Preview Assets.xcassets */, 172 | ); 173 | path = "Preview Content"; 174 | sourceTree = ""; 175 | }; 176 | E2AEB11F25BDF413003BD251 /* HorizonTests */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | E2AEB12025BDF413003BD251 /* HorizonTests.swift */, 180 | E2AEB12225BDF413003BD251 /* Info.plist */, 181 | ); 182 | path = HorizonTests; 183 | sourceTree = ""; 184 | }; 185 | E2AEB12A25BDF413003BD251 /* HorizonUITests */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | E2AEB12B25BDF413003BD251 /* HorizonUITests.swift */, 189 | E2AEB12D25BDF413003BD251 /* Info.plist */, 190 | ); 191 | path = HorizonUITests; 192 | sourceTree = ""; 193 | }; 194 | E2AEB15125BDF491003BD251 /* Models */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | E2AEB15525BDF4A7003BD251 /* AuthUser.swift */, 198 | E2AEB15225BDF4A7003BD251 /* Entry.swift */, 199 | E2AEB15325BDF4A7003BD251 /* File.swift */, 200 | E2AEB15625BDF4A7003BD251 /* Journal.swift */, 201 | E2AEB15425BDF4A7003BD251 /* User.swift */, 202 | ); 203 | path = Models; 204 | sourceTree = ""; 205 | }; 206 | E2AEB15F25BDF4C7003BD251 /* Networking */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | E2AEB16025BDF4D6003BD251 /* Futureland.swift */, 210 | ); 211 | path = Networking; 212 | sourceTree = ""; 213 | }; 214 | E2AEB16725BDF4E5003BD251 /* Prefs */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | E2AEB16925BDF4F7003BD251 /* AccountPrefsView.swift */, 218 | E2AEB16A25BDF4F7003BD251 /* GeneralPrefsView.swift */, 219 | E2AEB16825BDF4F7003BD251 /* PrefsViewModel.swift */, 220 | ); 221 | path = Prefs; 222 | sourceTree = ""; 223 | }; 224 | E2AEB17125BDF503003BD251 /* Publish */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | E2AEB17425BDF52F003BD251 /* PublishPanel.swift */, 228 | E2AEB17225BDF52F003BD251 /* PublishView.swift */, 229 | E2AEB17325BDF52F003BD251 /* PublishViewModel.swift */, 230 | ); 231 | path = Publish; 232 | sourceTree = ""; 233 | }; 234 | E2AEB17B25BDF537003BD251 /* StatusBar */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | E2AEB17C25BDF537003BD251 /* StatusItemMenu.swift */, 238 | E2AEB17D25BDF537003BD251 /* StatusBarMenuItem.swift */, 239 | ); 240 | path = StatusBar; 241 | sourceTree = ""; 242 | }; 243 | E2B8FFD425BF0BFF0090A6B6 /* Components */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | E275D2B225BFA63E00320CA5 /* TextView.swift */, 247 | E2943D1625DB2EFE0012CDCB /* Dropdown.swift */, 248 | ); 249 | path = Components; 250 | sourceTree = ""; 251 | }; 252 | /* End PBXGroup section */ 253 | 254 | /* Begin PBXNativeTarget section */ 255 | E2AEB10625BDF413003BD251 /* Horizon */ = { 256 | isa = PBXNativeTarget; 257 | buildConfigurationList = E2AEB13025BDF413003BD251 /* Build configuration list for PBXNativeTarget "Horizon" */; 258 | buildPhases = ( 259 | E28B966E25C3BD3A00FC303E /* ShellScript */, 260 | E2AEB10325BDF413003BD251 /* Sources */, 261 | E2AEB10425BDF413003BD251 /* Frameworks */, 262 | E2AEB10525BDF413003BD251 /* Resources */, 263 | E2AAC24025C23974003869BB /* ShellScript */, 264 | ); 265 | buildRules = ( 266 | ); 267 | dependencies = ( 268 | ); 269 | name = Horizon; 270 | packageProductDependencies = ( 271 | E2AEB13D25BDF438003BD251 /* KeyboardShortcuts */, 272 | E2AEB14325BDF43F003BD251 /* LaunchAtLogin */, 273 | E2AEB14925BDF446003BD251 /* Preferences */, 274 | E233837F25BE326200902F38 /* Alamofire */, 275 | E2DAAAF625C1B35200E9A847 /* Sparkle */, 276 | E276484025C5CB74007ECF9B /* KeychainAccess */, 277 | ); 278 | productName = Horizon; 279 | productReference = E2AEB10725BDF413003BD251 /* Horizon.app */; 280 | productType = "com.apple.product-type.application"; 281 | }; 282 | E2AEB11B25BDF413003BD251 /* HorizonTests */ = { 283 | isa = PBXNativeTarget; 284 | buildConfigurationList = E2AEB13325BDF413003BD251 /* Build configuration list for PBXNativeTarget "HorizonTests" */; 285 | buildPhases = ( 286 | E2AEB11825BDF413003BD251 /* Sources */, 287 | E2AEB11925BDF413003BD251 /* Frameworks */, 288 | E2AEB11A25BDF413003BD251 /* Resources */, 289 | ); 290 | buildRules = ( 291 | ); 292 | dependencies = ( 293 | E2AEB11E25BDF413003BD251 /* PBXTargetDependency */, 294 | ); 295 | name = HorizonTests; 296 | productName = HorizonTests; 297 | productReference = E2AEB11C25BDF413003BD251 /* HorizonTests.xctest */; 298 | productType = "com.apple.product-type.bundle.unit-test"; 299 | }; 300 | E2AEB12625BDF413003BD251 /* HorizonUITests */ = { 301 | isa = PBXNativeTarget; 302 | buildConfigurationList = E2AEB13625BDF413003BD251 /* Build configuration list for PBXNativeTarget "HorizonUITests" */; 303 | buildPhases = ( 304 | E2AEB12325BDF413003BD251 /* Sources */, 305 | E2AEB12425BDF413003BD251 /* Frameworks */, 306 | E2AEB12525BDF413003BD251 /* Resources */, 307 | ); 308 | buildRules = ( 309 | ); 310 | dependencies = ( 311 | E2AEB12925BDF413003BD251 /* PBXTargetDependency */, 312 | ); 313 | name = HorizonUITests; 314 | productName = HorizonUITests; 315 | productReference = E2AEB12725BDF413003BD251 /* HorizonUITests.xctest */; 316 | productType = "com.apple.product-type.bundle.ui-testing"; 317 | }; 318 | /* End PBXNativeTarget section */ 319 | 320 | /* Begin PBXProject section */ 321 | E2AEB0FF25BDF412003BD251 /* Project object */ = { 322 | isa = PBXProject; 323 | attributes = { 324 | LastSwiftUpdateCheck = 1230; 325 | LastUpgradeCheck = 1230; 326 | TargetAttributes = { 327 | E2AEB10625BDF413003BD251 = { 328 | CreatedOnToolsVersion = 12.3; 329 | }; 330 | E2AEB11B25BDF413003BD251 = { 331 | CreatedOnToolsVersion = 12.3; 332 | TestTargetID = E2AEB10625BDF413003BD251; 333 | }; 334 | E2AEB12625BDF413003BD251 = { 335 | CreatedOnToolsVersion = 12.3; 336 | TestTargetID = E2AEB10625BDF413003BD251; 337 | }; 338 | }; 339 | }; 340 | buildConfigurationList = E2AEB10225BDF412003BD251 /* Build configuration list for PBXProject "Horizon" */; 341 | compatibilityVersion = "Xcode 9.3"; 342 | developmentRegion = en; 343 | hasScannedForEncodings = 0; 344 | knownRegions = ( 345 | en, 346 | Base, 347 | ); 348 | mainGroup = E2AEB0FE25BDF412003BD251; 349 | packageReferences = ( 350 | E2AEB13C25BDF438003BD251 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 351 | E2AEB14225BDF43F003BD251 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */, 352 | E2AEB14825BDF446003BD251 /* XCRemoteSwiftPackageReference "Preferences" */, 353 | E233837E25BE326200902F38 /* XCRemoteSwiftPackageReference "Alamofire" */, 354 | E2DAAAF525C1B35200E9A847 /* XCRemoteSwiftPackageReference "Sparkle" */, 355 | E276483F25C5CB74007ECF9B /* XCRemoteSwiftPackageReference "KeychainAccess" */, 356 | ); 357 | productRefGroup = E2AEB10825BDF413003BD251 /* Products */; 358 | projectDirPath = ""; 359 | projectRoot = ""; 360 | targets = ( 361 | E2AEB10625BDF413003BD251 /* Horizon */, 362 | E2AEB11B25BDF413003BD251 /* HorizonTests */, 363 | E2AEB12625BDF413003BD251 /* HorizonUITests */, 364 | ); 365 | }; 366 | /* End PBXProject section */ 367 | 368 | /* Begin PBXResourcesBuildPhase section */ 369 | E2AEB10525BDF413003BD251 /* Resources */ = { 370 | isa = PBXResourcesBuildPhase; 371 | buildActionMask = 2147483647; 372 | files = ( 373 | E2AEB11525BDF413003BD251 /* Main.storyboard in Resources */, 374 | E2AEB11225BDF413003BD251 /* Preview Assets.xcassets in Resources */, 375 | E2AEB10F25BDF413003BD251 /* Assets.xcassets in Resources */, 376 | ); 377 | runOnlyForDeploymentPostprocessing = 0; 378 | }; 379 | E2AEB11A25BDF413003BD251 /* Resources */ = { 380 | isa = PBXResourcesBuildPhase; 381 | buildActionMask = 2147483647; 382 | files = ( 383 | ); 384 | runOnlyForDeploymentPostprocessing = 0; 385 | }; 386 | E2AEB12525BDF413003BD251 /* Resources */ = { 387 | isa = PBXResourcesBuildPhase; 388 | buildActionMask = 2147483647; 389 | files = ( 390 | ); 391 | runOnlyForDeploymentPostprocessing = 0; 392 | }; 393 | /* End PBXResourcesBuildPhase section */ 394 | 395 | /* Begin PBXShellScriptBuildPhase section */ 396 | E28B966E25C3BD3A00FC303E /* ShellScript */ = { 397 | isa = PBXShellScriptBuildPhase; 398 | buildActionMask = 2147483647; 399 | files = ( 400 | ); 401 | inputFileListPaths = ( 402 | ); 403 | inputPaths = ( 404 | ); 405 | outputFileListPaths = ( 406 | ); 407 | outputPaths = ( 408 | ); 409 | runOnlyForDeploymentPostprocessing = 0; 410 | shellPath = /bin/sh; 411 | shellScript = "if which /opt/homebrew/bin/swiftlint >/dev/null; then\n /opt/homebrew/bin/swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 412 | }; 413 | E2AAC24025C23974003869BB /* ShellScript */ = { 414 | isa = PBXShellScriptBuildPhase; 415 | buildActionMask = 2147483647; 416 | files = ( 417 | ); 418 | inputFileListPaths = ( 419 | ); 420 | inputPaths = ( 421 | ); 422 | outputFileListPaths = ( 423 | ); 424 | outputPaths = ( 425 | ); 426 | runOnlyForDeploymentPostprocessing = 0; 427 | shellPath = /bin/sh; 428 | shellScript = "\"${BUILT_PRODUCTS_DIR}/LaunchAtLogin_LaunchAtLogin.bundle/Contents/Resources/copy-helper-swiftpm.sh\"\n"; 429 | }; 430 | /* End PBXShellScriptBuildPhase section */ 431 | 432 | /* Begin PBXSourcesBuildPhase section */ 433 | E2AEB10325BDF413003BD251 /* Sources */ = { 434 | isa = PBXSourcesBuildPhase; 435 | buildActionMask = 2147483647; 436 | files = ( 437 | E275D2B325BFA63E00320CA5 /* TextView.swift in Sources */, 438 | E2AEB17E25BDF537003BD251 /* StatusItemMenu.swift in Sources */, 439 | E2AEB16B25BDF4F7003BD251 /* PrefsViewModel.swift in Sources */, 440 | E2AEB16D25BDF4F7003BD251 /* GeneralPrefsView.swift in Sources */, 441 | E2AEB17525BDF52F003BD251 /* PublishView.swift in Sources */, 442 | E2AEB16C25BDF4F7003BD251 /* AccountPrefsView.swift in Sources */, 443 | E2AEB15925BDF4A7003BD251 /* User.swift in Sources */, 444 | E2AEB16225BDF4D7003BD251 /* Futureland.swift in Sources */, 445 | E2AEB15725BDF4A7003BD251 /* Entry.swift in Sources */, 446 | E2AEB17625BDF52F003BD251 /* PublishViewModel.swift in Sources */, 447 | E2D9610725C0F87500E4210D /* Utilities.swift in Sources */, 448 | E2AEB15825BDF4A7003BD251 /* File.swift in Sources */, 449 | E2AEB15A25BDF4A7003BD251 /* AuthUser.swift in Sources */, 450 | E2AEB17725BDF52F003BD251 /* PublishPanel.swift in Sources */, 451 | E276483825C50824007ECF9B /* Store.swift in Sources */, 452 | E2AEB10B25BDF413003BD251 /* AppDelegate.swift in Sources */, 453 | E2AEB15B25BDF4A7003BD251 /* Journal.swift in Sources */, 454 | E2AEB17F25BDF537003BD251 /* StatusBarMenuItem.swift in Sources */, 455 | E2943D1725DB2EFE0012CDCB /* Dropdown.swift in Sources */, 456 | E283259825C109980021BD90 /* Constants.swift in Sources */, 457 | ); 458 | runOnlyForDeploymentPostprocessing = 0; 459 | }; 460 | E2AEB11825BDF413003BD251 /* Sources */ = { 461 | isa = PBXSourcesBuildPhase; 462 | buildActionMask = 2147483647; 463 | files = ( 464 | E2AEB12125BDF413003BD251 /* HorizonTests.swift in Sources */, 465 | ); 466 | runOnlyForDeploymentPostprocessing = 0; 467 | }; 468 | E2AEB12325BDF413003BD251 /* Sources */ = { 469 | isa = PBXSourcesBuildPhase; 470 | buildActionMask = 2147483647; 471 | files = ( 472 | E2AEB12C25BDF413003BD251 /* HorizonUITests.swift in Sources */, 473 | ); 474 | runOnlyForDeploymentPostprocessing = 0; 475 | }; 476 | /* End PBXSourcesBuildPhase section */ 477 | 478 | /* Begin PBXTargetDependency section */ 479 | E2AEB11E25BDF413003BD251 /* PBXTargetDependency */ = { 480 | isa = PBXTargetDependency; 481 | target = E2AEB10625BDF413003BD251 /* Horizon */; 482 | targetProxy = E2AEB11D25BDF413003BD251 /* PBXContainerItemProxy */; 483 | }; 484 | E2AEB12925BDF413003BD251 /* PBXTargetDependency */ = { 485 | isa = PBXTargetDependency; 486 | target = E2AEB10625BDF413003BD251 /* Horizon */; 487 | targetProxy = E2AEB12825BDF413003BD251 /* PBXContainerItemProxy */; 488 | }; 489 | /* End PBXTargetDependency section */ 490 | 491 | /* Begin PBXVariantGroup section */ 492 | E2AEB11325BDF413003BD251 /* Main.storyboard */ = { 493 | isa = PBXVariantGroup; 494 | children = ( 495 | E2AEB11425BDF413003BD251 /* Base */, 496 | ); 497 | name = Main.storyboard; 498 | sourceTree = ""; 499 | }; 500 | /* End PBXVariantGroup section */ 501 | 502 | /* Begin XCBuildConfiguration section */ 503 | E2AEB12E25BDF413003BD251 /* Debug */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | ALWAYS_SEARCH_USER_PATHS = NO; 507 | CLANG_ANALYZER_NONNULL = YES; 508 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 509 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 510 | CLANG_CXX_LIBRARY = "libc++"; 511 | CLANG_ENABLE_MODULES = YES; 512 | CLANG_ENABLE_OBJC_ARC = YES; 513 | CLANG_ENABLE_OBJC_WEAK = YES; 514 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 515 | CLANG_WARN_BOOL_CONVERSION = YES; 516 | CLANG_WARN_COMMA = YES; 517 | CLANG_WARN_CONSTANT_CONVERSION = YES; 518 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 519 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 520 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 521 | CLANG_WARN_EMPTY_BODY = YES; 522 | CLANG_WARN_ENUM_CONVERSION = YES; 523 | CLANG_WARN_INFINITE_RECURSION = YES; 524 | CLANG_WARN_INT_CONVERSION = YES; 525 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 526 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 527 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 528 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 529 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 530 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 531 | CLANG_WARN_STRICT_PROTOTYPES = YES; 532 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 533 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 534 | CLANG_WARN_UNREACHABLE_CODE = YES; 535 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 536 | COPY_PHASE_STRIP = NO; 537 | DEBUG_INFORMATION_FORMAT = dwarf; 538 | ENABLE_STRICT_OBJC_MSGSEND = YES; 539 | ENABLE_TESTABILITY = YES; 540 | GCC_C_LANGUAGE_STANDARD = gnu11; 541 | GCC_DYNAMIC_NO_PIC = NO; 542 | GCC_NO_COMMON_BLOCKS = YES; 543 | GCC_OPTIMIZATION_LEVEL = 0; 544 | GCC_PREPROCESSOR_DEFINITIONS = ( 545 | "DEBUG=1", 546 | "$(inherited)", 547 | ); 548 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 549 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 550 | GCC_WARN_UNDECLARED_SELECTOR = YES; 551 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 552 | GCC_WARN_UNUSED_FUNCTION = YES; 553 | GCC_WARN_UNUSED_VARIABLE = YES; 554 | MACOSX_DEPLOYMENT_TARGET = 11.0; 555 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 556 | MTL_FAST_MATH = YES; 557 | ONLY_ACTIVE_ARCH = YES; 558 | SDKROOT = macosx; 559 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 560 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 561 | }; 562 | name = Debug; 563 | }; 564 | E2AEB12F25BDF413003BD251 /* Release */ = { 565 | isa = XCBuildConfiguration; 566 | buildSettings = { 567 | ALWAYS_SEARCH_USER_PATHS = NO; 568 | CLANG_ANALYZER_NONNULL = YES; 569 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 570 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 571 | CLANG_CXX_LIBRARY = "libc++"; 572 | CLANG_ENABLE_MODULES = YES; 573 | CLANG_ENABLE_OBJC_ARC = YES; 574 | CLANG_ENABLE_OBJC_WEAK = YES; 575 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 576 | CLANG_WARN_BOOL_CONVERSION = YES; 577 | CLANG_WARN_COMMA = YES; 578 | CLANG_WARN_CONSTANT_CONVERSION = YES; 579 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 580 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 581 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 582 | CLANG_WARN_EMPTY_BODY = YES; 583 | CLANG_WARN_ENUM_CONVERSION = YES; 584 | CLANG_WARN_INFINITE_RECURSION = YES; 585 | CLANG_WARN_INT_CONVERSION = YES; 586 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 587 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 588 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 589 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 590 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 591 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 592 | CLANG_WARN_STRICT_PROTOTYPES = YES; 593 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 594 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 595 | CLANG_WARN_UNREACHABLE_CODE = YES; 596 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 597 | COPY_PHASE_STRIP = NO; 598 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 599 | ENABLE_NS_ASSERTIONS = NO; 600 | ENABLE_STRICT_OBJC_MSGSEND = YES; 601 | GCC_C_LANGUAGE_STANDARD = gnu11; 602 | GCC_NO_COMMON_BLOCKS = YES; 603 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 604 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 605 | GCC_WARN_UNDECLARED_SELECTOR = YES; 606 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 607 | GCC_WARN_UNUSED_FUNCTION = YES; 608 | GCC_WARN_UNUSED_VARIABLE = YES; 609 | MACOSX_DEPLOYMENT_TARGET = 11.0; 610 | MTL_ENABLE_DEBUG_INFO = NO; 611 | MTL_FAST_MATH = YES; 612 | SDKROOT = macosx; 613 | SWIFT_COMPILATION_MODE = wholemodule; 614 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 615 | }; 616 | name = Release; 617 | }; 618 | E2AEB13125BDF413003BD251 /* Debug */ = { 619 | isa = XCBuildConfiguration; 620 | buildSettings = { 621 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 622 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 623 | CODE_SIGN_ENTITLEMENTS = Horizon/Horizon.entitlements; 624 | CODE_SIGN_IDENTITY = "Apple Development"; 625 | CODE_SIGN_STYLE = Automatic; 626 | COMBINE_HIDPI_IMAGES = YES; 627 | CURRENT_PROJECT_VERSION = 113; 628 | DEVELOPMENT_ASSET_PATHS = "\"Horizon/Preview Content\""; 629 | DEVELOPMENT_TEAM = LCFAPK739L; 630 | ENABLE_HARDENED_RUNTIME = YES; 631 | ENABLE_PREVIEWS = YES; 632 | INFOPLIST_FILE = Horizon/Info.plist; 633 | LD_RUNPATH_SEARCH_PATHS = ( 634 | "$(inherited)", 635 | "@executable_path/../Frameworks", 636 | ); 637 | MACOSX_DEPLOYMENT_TARGET = 11.0; 638 | MARKETING_VERSION = 1.1.3; 639 | PRODUCT_BUNDLE_IDENTIFIER = co.meagher.Horizon; 640 | PRODUCT_NAME = "$(TARGET_NAME)"; 641 | SWIFT_VERSION = 5.0; 642 | }; 643 | name = Debug; 644 | }; 645 | E2AEB13225BDF413003BD251 /* Release */ = { 646 | isa = XCBuildConfiguration; 647 | buildSettings = { 648 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 649 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 650 | CODE_SIGN_ENTITLEMENTS = Horizon/Horizon.entitlements; 651 | CODE_SIGN_IDENTITY = "Apple Development"; 652 | CODE_SIGN_STYLE = Automatic; 653 | COMBINE_HIDPI_IMAGES = YES; 654 | CURRENT_PROJECT_VERSION = 113; 655 | DEVELOPMENT_ASSET_PATHS = "\"Horizon/Preview Content\""; 656 | DEVELOPMENT_TEAM = LCFAPK739L; 657 | ENABLE_HARDENED_RUNTIME = YES; 658 | ENABLE_PREVIEWS = YES; 659 | INFOPLIST_FILE = Horizon/Info.plist; 660 | LD_RUNPATH_SEARCH_PATHS = ( 661 | "$(inherited)", 662 | "@executable_path/../Frameworks", 663 | ); 664 | MACOSX_DEPLOYMENT_TARGET = 11.0; 665 | MARKETING_VERSION = 1.1.3; 666 | PRODUCT_BUNDLE_IDENTIFIER = co.meagher.Horizon; 667 | PRODUCT_NAME = "$(TARGET_NAME)"; 668 | SWIFT_VERSION = 5.0; 669 | }; 670 | name = Release; 671 | }; 672 | E2AEB13425BDF413003BD251 /* Debug */ = { 673 | isa = XCBuildConfiguration; 674 | buildSettings = { 675 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 676 | BUNDLE_LOADER = "$(TEST_HOST)"; 677 | CODE_SIGN_STYLE = Automatic; 678 | COMBINE_HIDPI_IMAGES = YES; 679 | DEVELOPMENT_TEAM = LCFAPK739L; 680 | INFOPLIST_FILE = HorizonTests/Info.plist; 681 | LD_RUNPATH_SEARCH_PATHS = ( 682 | "$(inherited)", 683 | "@executable_path/../Frameworks", 684 | "@loader_path/../Frameworks", 685 | ); 686 | MACOSX_DEPLOYMENT_TARGET = 11.0; 687 | PRODUCT_BUNDLE_IDENTIFIER = co.meagher.HorizonTests; 688 | PRODUCT_NAME = "$(TARGET_NAME)"; 689 | SWIFT_VERSION = 5.0; 690 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Horizon.app/Contents/MacOS/Horizon"; 691 | }; 692 | name = Debug; 693 | }; 694 | E2AEB13525BDF413003BD251 /* Release */ = { 695 | isa = XCBuildConfiguration; 696 | buildSettings = { 697 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 698 | BUNDLE_LOADER = "$(TEST_HOST)"; 699 | CODE_SIGN_STYLE = Automatic; 700 | COMBINE_HIDPI_IMAGES = YES; 701 | DEVELOPMENT_TEAM = LCFAPK739L; 702 | INFOPLIST_FILE = HorizonTests/Info.plist; 703 | LD_RUNPATH_SEARCH_PATHS = ( 704 | "$(inherited)", 705 | "@executable_path/../Frameworks", 706 | "@loader_path/../Frameworks", 707 | ); 708 | MACOSX_DEPLOYMENT_TARGET = 11.0; 709 | PRODUCT_BUNDLE_IDENTIFIER = co.meagher.HorizonTests; 710 | PRODUCT_NAME = "$(TARGET_NAME)"; 711 | SWIFT_VERSION = 5.0; 712 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Horizon.app/Contents/MacOS/Horizon"; 713 | }; 714 | name = Release; 715 | }; 716 | E2AEB13725BDF413003BD251 /* Debug */ = { 717 | isa = XCBuildConfiguration; 718 | buildSettings = { 719 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 720 | CODE_SIGN_STYLE = Automatic; 721 | COMBINE_HIDPI_IMAGES = YES; 722 | DEVELOPMENT_TEAM = LCFAPK739L; 723 | INFOPLIST_FILE = HorizonUITests/Info.plist; 724 | LD_RUNPATH_SEARCH_PATHS = ( 725 | "$(inherited)", 726 | "@executable_path/../Frameworks", 727 | "@loader_path/../Frameworks", 728 | ); 729 | PRODUCT_BUNDLE_IDENTIFIER = co.meagher.HorizonUITests; 730 | PRODUCT_NAME = "$(TARGET_NAME)"; 731 | SWIFT_VERSION = 5.0; 732 | TEST_TARGET_NAME = Horizon; 733 | }; 734 | name = Debug; 735 | }; 736 | E2AEB13825BDF413003BD251 /* Release */ = { 737 | isa = XCBuildConfiguration; 738 | buildSettings = { 739 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 740 | CODE_SIGN_STYLE = Automatic; 741 | COMBINE_HIDPI_IMAGES = YES; 742 | DEVELOPMENT_TEAM = LCFAPK739L; 743 | INFOPLIST_FILE = HorizonUITests/Info.plist; 744 | LD_RUNPATH_SEARCH_PATHS = ( 745 | "$(inherited)", 746 | "@executable_path/../Frameworks", 747 | "@loader_path/../Frameworks", 748 | ); 749 | PRODUCT_BUNDLE_IDENTIFIER = co.meagher.HorizonUITests; 750 | PRODUCT_NAME = "$(TARGET_NAME)"; 751 | SWIFT_VERSION = 5.0; 752 | TEST_TARGET_NAME = Horizon; 753 | }; 754 | name = Release; 755 | }; 756 | /* End XCBuildConfiguration section */ 757 | 758 | /* Begin XCConfigurationList section */ 759 | E2AEB10225BDF412003BD251 /* Build configuration list for PBXProject "Horizon" */ = { 760 | isa = XCConfigurationList; 761 | buildConfigurations = ( 762 | E2AEB12E25BDF413003BD251 /* Debug */, 763 | E2AEB12F25BDF413003BD251 /* Release */, 764 | ); 765 | defaultConfigurationIsVisible = 0; 766 | defaultConfigurationName = Release; 767 | }; 768 | E2AEB13025BDF413003BD251 /* Build configuration list for PBXNativeTarget "Horizon" */ = { 769 | isa = XCConfigurationList; 770 | buildConfigurations = ( 771 | E2AEB13125BDF413003BD251 /* Debug */, 772 | E2AEB13225BDF413003BD251 /* Release */, 773 | ); 774 | defaultConfigurationIsVisible = 0; 775 | defaultConfigurationName = Release; 776 | }; 777 | E2AEB13325BDF413003BD251 /* Build configuration list for PBXNativeTarget "HorizonTests" */ = { 778 | isa = XCConfigurationList; 779 | buildConfigurations = ( 780 | E2AEB13425BDF413003BD251 /* Debug */, 781 | E2AEB13525BDF413003BD251 /* Release */, 782 | ); 783 | defaultConfigurationIsVisible = 0; 784 | defaultConfigurationName = Release; 785 | }; 786 | E2AEB13625BDF413003BD251 /* Build configuration list for PBXNativeTarget "HorizonUITests" */ = { 787 | isa = XCConfigurationList; 788 | buildConfigurations = ( 789 | E2AEB13725BDF413003BD251 /* Debug */, 790 | E2AEB13825BDF413003BD251 /* Release */, 791 | ); 792 | defaultConfigurationIsVisible = 0; 793 | defaultConfigurationName = Release; 794 | }; 795 | /* End XCConfigurationList section */ 796 | 797 | /* Begin XCRemoteSwiftPackageReference section */ 798 | E233837E25BE326200902F38 /* XCRemoteSwiftPackageReference "Alamofire" */ = { 799 | isa = XCRemoteSwiftPackageReference; 800 | repositoryURL = "https://github.com/Alamofire/Alamofire"; 801 | requirement = { 802 | kind = upToNextMajorVersion; 803 | minimumVersion = 5.4.1; 804 | }; 805 | }; 806 | E276483F25C5CB74007ECF9B /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { 807 | isa = XCRemoteSwiftPackageReference; 808 | repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; 809 | requirement = { 810 | kind = upToNextMajorVersion; 811 | minimumVersion = 4.2.1; 812 | }; 813 | }; 814 | E2AEB13C25BDF438003BD251 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { 815 | isa = XCRemoteSwiftPackageReference; 816 | repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; 817 | requirement = { 818 | kind = upToNextMajorVersion; 819 | minimumVersion = 0.6.1; 820 | }; 821 | }; 822 | E2AEB14225BDF43F003BD251 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = { 823 | isa = XCRemoteSwiftPackageReference; 824 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin"; 825 | requirement = { 826 | kind = upToNextMajorVersion; 827 | minimumVersion = 4.0.0; 828 | }; 829 | }; 830 | E2AEB14825BDF446003BD251 /* XCRemoteSwiftPackageReference "Preferences" */ = { 831 | isa = XCRemoteSwiftPackageReference; 832 | repositoryURL = "https://github.com/sindresorhus/Preferences"; 833 | requirement = { 834 | kind = upToNextMajorVersion; 835 | minimumVersion = 2.2.0; 836 | }; 837 | }; 838 | E2DAAAF525C1B35200E9A847 /* XCRemoteSwiftPackageReference "Sparkle" */ = { 839 | isa = XCRemoteSwiftPackageReference; 840 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 841 | requirement = { 842 | branch = master; 843 | kind = branch; 844 | }; 845 | }; 846 | /* End XCRemoteSwiftPackageReference section */ 847 | 848 | /* Begin XCSwiftPackageProductDependency section */ 849 | E233837F25BE326200902F38 /* Alamofire */ = { 850 | isa = XCSwiftPackageProductDependency; 851 | package = E233837E25BE326200902F38 /* XCRemoteSwiftPackageReference "Alamofire" */; 852 | productName = Alamofire; 853 | }; 854 | E276484025C5CB74007ECF9B /* KeychainAccess */ = { 855 | isa = XCSwiftPackageProductDependency; 856 | package = E276483F25C5CB74007ECF9B /* XCRemoteSwiftPackageReference "KeychainAccess" */; 857 | productName = KeychainAccess; 858 | }; 859 | E2AEB13D25BDF438003BD251 /* KeyboardShortcuts */ = { 860 | isa = XCSwiftPackageProductDependency; 861 | package = E2AEB13C25BDF438003BD251 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; 862 | productName = KeyboardShortcuts; 863 | }; 864 | E2AEB14325BDF43F003BD251 /* LaunchAtLogin */ = { 865 | isa = XCSwiftPackageProductDependency; 866 | package = E2AEB14225BDF43F003BD251 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */; 867 | productName = LaunchAtLogin; 868 | }; 869 | E2AEB14925BDF446003BD251 /* Preferences */ = { 870 | isa = XCSwiftPackageProductDependency; 871 | package = E2AEB14825BDF446003BD251 /* XCRemoteSwiftPackageReference "Preferences" */; 872 | productName = Preferences; 873 | }; 874 | E2DAAAF625C1B35200E9A847 /* Sparkle */ = { 875 | isa = XCSwiftPackageProductDependency; 876 | package = E2DAAAF525C1B35200E9A847 /* XCRemoteSwiftPackageReference "Sparkle" */; 877 | productName = Sparkle; 878 | }; 879 | /* End XCSwiftPackageProductDependency section */ 880 | }; 881 | rootObject = E2AEB0FF25BDF412003BD251 /* Project object */; 882 | } 883 | -------------------------------------------------------------------------------- /Horizon.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Horizon.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Horizon.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Horizon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "repositoryURL": "https://github.com/Alamofire/Alamofire", 7 | "state": { 8 | "branch": null, 9 | "revision": "eaf6e622dd41b07b251d8f01752eab31bc811493", 10 | "version": "5.4.1" 11 | } 12 | }, 13 | { 14 | "package": "KeyboardShortcuts", 15 | "repositoryURL": "https://github.com/sindresorhus/KeyboardShortcuts", 16 | "state": { 17 | "branch": null, 18 | "revision": "6d0efcd1fbe00b15dfcb8a751c84ed291edf27aa", 19 | "version": "0.6.1" 20 | } 21 | }, 22 | { 23 | "package": "KeychainAccess", 24 | "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", 25 | "state": { 26 | "branch": null, 27 | "revision": "654d52d30f3dd4592e944c3e0bccb53178c992f6", 28 | "version": "4.2.1" 29 | } 30 | }, 31 | { 32 | "package": "LaunchAtLogin", 33 | "repositoryURL": "https://github.com/sindresorhus/LaunchAtLogin", 34 | "state": { 35 | "branch": null, 36 | "revision": "0f39982b9d6993eef253b81219d3c39ba1e680f3", 37 | "version": "4.0.0" 38 | } 39 | }, 40 | { 41 | "package": "Preferences", 42 | "repositoryURL": "https://github.com/sindresorhus/Preferences", 43 | "state": { 44 | "branch": null, 45 | "revision": "4802a493acef50c814e4eb63e9a44e0941ec8883", 46 | "version": "2.2.0" 47 | } 48 | }, 49 | { 50 | "package": "Sparkle", 51 | "repositoryURL": "https://github.com/sparkle-project/Sparkle", 52 | "state": { 53 | "branch": "master", 54 | "revision": "0b682d3349ddb51a4c42153eaae94e937be5935a", 55 | "version": null 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Horizon.xcodeproj/xcshareddata/xcschemes/Horizon.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 81 | 82 | 85 | 86 | 87 | 88 | 92 | 93 | 97 | 98 | 99 | 100 | 106 | 108 | 114 | 115 | 116 | 117 | 119 | 120 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /Horizon.xcodeproj/xcuserdata/tom.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Horizon.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E2AEB10625BDF413003BD251 16 | 17 | primary 18 | 19 | 20 | E2AEB11B25BDF413003BD251 21 | 22 | primary 23 | 24 | 25 | E2AEB12625BDF413003BD251 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Horizon/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 12:12 2 | 3 | import Cocoa 4 | import Combine 5 | import KeyboardShortcuts 6 | import Preferences 7 | import Sparkle 8 | import SwiftUI 9 | import UserNotifications 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, SUUpdaterDelegate { 13 | var store = Store() 14 | 15 | lazy var panel = PublishPanel( 16 | store: store, 17 | onClose: closePanel 18 | ) 19 | 20 | /// Set up preferences 21 | lazy var preferences: [PreferencePane] = [ 22 | GeneralPrefsViewController(store), 23 | AccountPrefsViewController(store) 24 | ] 25 | lazy var preferencesWindowController = PreferencesWindowController( 26 | preferencePanes: preferences, 27 | style: .segmentedControl, 28 | animated: false, 29 | hidesToolbarForSingleItem: true 30 | ) 31 | 32 | /// Set up menu bar 33 | lazy var menu = StatusItemMenu( 34 | openPanel: self.openPanel, 35 | openPrefs: self.openPrefs, 36 | checkForUpdates: self.checkForUpdates, 37 | quit: self.quit 38 | ) 39 | lazy var statusItem = with(NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)) { 40 | $0.menu = menu 41 | $0.button?.image = Constants.menuBarIcon 42 | $0.button?.image?.size = NSSize(width: 18.0, height: 18.0) 43 | $0.button?.image?.isTemplate = true 44 | } 45 | lazy var statusItemButton = statusItem.button 46 | 47 | func applicationDidFinishLaunching(_ notification: Notification) { 48 | _ = statusItemButton 49 | _ = panel 50 | 51 | if store.token == nil { 52 | preferencesWindowController.show(preferencePane: .account) 53 | } else { 54 | panel.makeKeyAndOrderFront(nil) 55 | } 56 | 57 | // Set up keyboard shortcuts 58 | KeyboardShortcuts.onKeyUp(for: .togglePanel) { [self] in togglePanel() } 59 | 60 | // Set up notifications 61 | setupNotifications() 62 | 63 | // Set up Sparkle for auto updates 64 | setupSparkle() 65 | } 66 | } 67 | 68 | // MARK: Menu bar 69 | extension AppDelegate { 70 | private func togglePanel() { 71 | if panel.isKeyWindow { 72 | closePanel() 73 | } else { 74 | openPanel() 75 | } 76 | } 77 | 78 | private func closePanel() { 79 | panel.close() 80 | } 81 | 82 | private func openPanel() { 83 | guard store.token != nil else { 84 | preferencesWindowController.show(preferencePane: .account) 85 | return 86 | } 87 | store.fetchJournals() 88 | panel.makeKeyAndOrderFront(nil) 89 | } 90 | 91 | private func openPrefs() { 92 | panel.close() 93 | preferencesWindowController.show() 94 | } 95 | 96 | private func quit() { 97 | NSApp.quit() 98 | } 99 | } 100 | 101 | // MARK: Notifications 102 | extension AppDelegate { 103 | private func setupNotifications() { 104 | UNUserNotificationCenter.current().delegate = self 105 | 106 | // Define custom actions 107 | let acceptAction = UNNotificationAction( 108 | identifier: Notifications.Actions.viewPublishedEntry, 109 | title: "View", 110 | options: UNNotificationActionOptions(rawValue: 0) 111 | ) 112 | 113 | // Define notification type 114 | let publishedEntryCategory = 115 | UNNotificationCategory( 116 | identifier: Notifications.Categories.publishedEntry, 117 | actions: [acceptAction], 118 | intentIdentifiers: [], 119 | hiddenPreviewsBodyPlaceholder: "" 120 | ) 121 | 122 | // Register notification type 123 | let notificationCenter = UNUserNotificationCenter.current() 124 | notificationCenter.setNotificationCategories([publishedEntryCategory]) 125 | 126 | // Request permissions 127 | UNUserNotificationCenter 128 | .current() 129 | .requestAuthorization(options: [.alert]) { success, error in 130 | if success { 131 | print("User accepted push notifications") 132 | } else if let error = error { 133 | print(error.localizedDescription) 134 | } 135 | } 136 | } 137 | 138 | func userNotificationCenter( 139 | _ center: UNUserNotificationCenter, 140 | didReceive response: UNNotificationResponse, 141 | withCompletionHandler completionHandler: @escaping () -> Void 142 | ) { 143 | let userInfo = response.notification.request.content.userInfo 144 | 145 | switch response.actionIdentifier { 146 | case Notifications.Actions.viewPublishedEntry: 147 | guard let entryUrl = userInfo["entryUrl"] as? String else { 148 | return 149 | } 150 | if let url = URL(string: entryUrl) { 151 | NSWorkspace.shared.open(url) 152 | } 153 | default: 154 | break 155 | } 156 | 157 | completionHandler() 158 | } 159 | } 160 | 161 | // MARK: Sparkle 162 | extension AppDelegate { 163 | private func setupSparkle() { 164 | guard let updater = SUUpdater.shared() else { return } 165 | updater.delegate = self 166 | updater.updateCheckInterval = TimeInterval(60 * 60 * 24) 167 | } 168 | 169 | private func checkForUpdates() { 170 | panel.close() 171 | guard let updater = SUUpdater.shared() else { return } 172 | updater.checkForUpdates(self) 173 | } 174 | 175 | func feedURLString(for updater: SUUpdater) -> String? { 176 | return "https://dl.dropbox.com/s/e22wt50uqlg7pu1/appcast.xml" 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "filename" : "Icon-32.png", 15 | "idiom" : "mac", 16 | "scale" : "1x", 17 | "size" : "32x32" 18 | }, 19 | { 20 | "filename" : "Icon-64.png", 21 | "idiom" : "mac", 22 | "scale" : "2x", 23 | "size" : "32x32" 24 | }, 25 | { 26 | "filename" : "Icon-128.png", 27 | "idiom" : "mac", 28 | "scale" : "1x", 29 | "size" : "128x128" 30 | }, 31 | { 32 | "idiom" : "mac", 33 | "scale" : "2x", 34 | "size" : "128x128" 35 | }, 36 | { 37 | "filename" : "Icon-256.png", 38 | "idiom" : "mac", 39 | "scale" : "1x", 40 | "size" : "256x256" 41 | }, 42 | { 43 | "idiom" : "mac", 44 | "scale" : "2x", 45 | "size" : "256x256" 46 | }, 47 | { 48 | "filename" : "Icon-512.png", 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "filename" : "Icon-1024.png", 55 | "idiom" : "mac", 56 | "scale" : "2x", 57 | "size" : "512x512" 58 | } 59 | ], 60 | "info" : { 61 | "author" : "xcode", 62 | "version" : 1 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/horizon/0328b5a2fefc927f52fb115f50a0981e6a3651aa/Horizon/Assets.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AppIcon.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/horizon/0328b5a2fefc927f52fb115f50a0981e6a3651aa/Horizon/Assets.xcassets/AppIcon.appiconset/Icon-128.png -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AppIcon.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/horizon/0328b5a2fefc927f52fb115f50a0981e6a3651aa/Horizon/Assets.xcassets/AppIcon.appiconset/Icon-256.png -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AppIcon.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/horizon/0328b5a2fefc927f52fb115f50a0981e6a3651aa/Horizon/Assets.xcassets/AppIcon.appiconset/Icon-32.png -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AppIcon.appiconset/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/horizon/0328b5a2fefc927f52fb115f50a0981e6a3651aa/Horizon/Assets.xcassets/AppIcon.appiconset/Icon-512.png -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/AppIcon.appiconset/Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/horizon/0328b5a2fefc927f52fb115f50a0981e6a3651aa/Horizon/Assets.xcassets/AppIcon.appiconset/Icon-64.png -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/Background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.129", 9 | "green" : "0.122", 10 | "red" : "0.137" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.929", 27 | "green" : "0.922", 28 | "red" : "0.933" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "display-p3", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0.129", 45 | "green" : "0.122", 46 | "red" : "0.137" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/MenuBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "MenuBarIcon.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 | -------------------------------------------------------------------------------- /Horizon/Assets.xcassets/MenuBarIcon.imageset/menubaricon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/horizon/0328b5a2fefc927f52fb115f50a0981e6a3651aa/Horizon/Assets.xcassets/MenuBarIcon.imageset/menubaricon.png -------------------------------------------------------------------------------- /Horizon/Base.lproj/Main.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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | Default 529 | 530 | 531 | 532 | 533 | 534 | 535 | Left to Right 536 | 537 | 538 | 539 | 540 | 541 | 542 | Right to Left 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | Default 554 | 555 | 556 | 557 | 558 | 559 | 560 | Left to Right 561 | 562 | 563 | 564 | 565 | 566 | 567 | Right to Left 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | -------------------------------------------------------------------------------- /Horizon/Components/Dropdown.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 2/15/21 at 17:36 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | struct Dropdown: NSViewRepresentable { 7 | @Binding 8 | var selectedValue: T? 9 | 10 | @Binding 11 | var items: [T] 12 | 13 | private var disabled: Bool 14 | 15 | private let getItemTitle: ((T) -> String) 16 | private let onChange: ((T) -> Void)? 17 | 18 | init( 19 | selectedValue: Binding, 20 | items: Binding<[T]>, 21 | disabled: Bool, 22 | getItemTitle: @escaping ((T) -> String), 23 | onChange: ((T) -> Void)? = nil 24 | ) { 25 | self._selectedValue = selectedValue 26 | self._items = items 27 | self.disabled = disabled 28 | self.getItemTitle = getItemTitle 29 | self.onChange = onChange 30 | } 31 | 32 | func makeCoordinator() -> Coordinator { 33 | return Coordinator(self) 34 | } 35 | 36 | func makeNSView(context: Context) -> NSPopUpButton { 37 | let button = NSPopUpButton(frame: .zero, pullsDown: false) 38 | 39 | // Add local shortcut `⌘ j` for opening dropdown 40 | NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { event in 41 | if event.keyCode == 38 && event.modifierFlags.contains(.command) { 42 | button.performClick(nil) 43 | } 44 | return event 45 | } 46 | 47 | return button 48 | } 49 | 50 | func updateNSView(_ view: NSPopUpButton, context: Context) { 51 | view.removeAllItems() 52 | 53 | for (index, element) in items.enumerated() { 54 | let menuItem = NSMenuItem( 55 | title: self.getItemTitle(element), 56 | action: #selector(Coordinator.valueChanged(_:)), 57 | keyEquivalent: index < 10 ? "\(index)" : "" 58 | ) 59 | menuItem.target = context.coordinator 60 | view.menu?.insertItem(menuItem, at: index) 61 | } 62 | 63 | if let selectedValue = self.selectedValue { 64 | let index = self.items.firstIndex(of: selectedValue) ?? 0 65 | view.selectItem(at: index) 66 | } 67 | 68 | view.isEnabled = !disabled 69 | } 70 | } 71 | 72 | extension Dropdown { 73 | final class Coordinator: NSObject { 74 | var parent: Dropdown 75 | 76 | init(_ parent: Dropdown) { 77 | self.parent = parent 78 | } 79 | 80 | @objc 81 | func valueChanged(_ sender: NSMenuItem) { 82 | guard let index = sender.menu?.index(of: sender) else { return } 83 | let item = self.parent.items[index] 84 | self.parent._selectedValue.wrappedValue = item 85 | self.parent.onChange?(item) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Horizon/Components/TextView.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/25/21 at 20:22 2 | 3 | // Based on: 4 | // 5 | // MacEditorTextView 6 | // Copyright (c) Thiago Holanda 2020 7 | // https://twitter.com/tholanda 8 | // 9 | // MIT license 10 | // 11 | // See: https://gist.github.com/unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0 12 | 13 | import Combine 14 | import SwiftUI 15 | 16 | struct HorizonTextView: View { 17 | var text: String 18 | 19 | var placeholder: String = "" 20 | var isEditable: Bool = true 21 | var isFirstResponder: Bool = false 22 | 23 | var onEditingChanged: () -> Void = {} 24 | var onCommit: () -> Void = {} 25 | var onTextChange: (String) -> Void = { _ in } 26 | 27 | var body: some View { 28 | ZStack(alignment: .topLeading) { 29 | if text.isEmpty { 30 | Text(placeholder) 31 | .foregroundColor(Color(NSColor.placeholderTextColor)) 32 | .font(.system(size: 14)) 33 | .padding(.horizontal, 5) 34 | .accessibility(hidden: true) 35 | } 36 | TextView( 37 | text: text, 38 | isFirstResponder: true, 39 | isEditable: isEditable, 40 | onEditingChanged: onEditingChanged, 41 | onCommit: onCommit, 42 | onTextChange: onTextChange 43 | ) 44 | } 45 | } 46 | } 47 | 48 | struct TextView: NSViewRepresentable { 49 | // TODO: Switch back to @Binding 50 | // Doesn't work with @Published in PublishViewModel, but works with @State fine 51 | var text: String 52 | 53 | var isFirstResponder: Bool = false 54 | var isEditable: Bool = true 55 | var font: NSFont? = .systemFont(ofSize: 14, weight: .regular) 56 | 57 | var onEditingChanged: () -> Void = {} 58 | var onCommit: () -> Void = {} 59 | var onTextChange: (String) -> Void = { _ in } 60 | 61 | func makeCoordinator() -> Coordinator { 62 | Coordinator(self) 63 | } 64 | 65 | func makeNSView(context: Context) -> CustomTextView { 66 | let textView = CustomTextView( 67 | text: text, 68 | isEditable: isEditable, 69 | isFirstResponder: isFirstResponder, 70 | font: font 71 | ) 72 | textView.delegate = context.coordinator 73 | 74 | return textView 75 | } 76 | 77 | func updateNSView(_ view: CustomTextView, context: Context) { 78 | // TODO: Check out https://www.markusbodner.com/til/2021/02/08/multi-line-text-field-with-swiftui-on-macos/ 79 | view.text = text 80 | 81 | view.isEditable = isEditable 82 | view.selectedRanges = context.coordinator.selectedRanges 83 | 84 | // TODO: When selecting emoji using character palette font changes to monospaced 85 | // for some reason so need to set font again 86 | view.font = .systemFont(ofSize: 14, weight: .regular) 87 | } 88 | } 89 | 90 | // MARK: - Coordinator 91 | extension TextView { 92 | class Coordinator: NSObject, NSTextViewDelegate { 93 | var parent: TextView 94 | var selectedRanges: [NSValue] = [] 95 | 96 | init(_ parent: TextView) { 97 | self.parent = parent 98 | } 99 | 100 | func textDidBeginEditing(_ notification: Notification) { 101 | guard let textView = notification.object as? NSTextView else { 102 | return 103 | } 104 | 105 | self.parent.text = textView.string 106 | self.parent.onEditingChanged() 107 | } 108 | 109 | func textDidChange(_ notification: Notification) { 110 | guard let textView = notification.object as? NSTextView else { 111 | return 112 | } 113 | 114 | self.parent.text = textView.string 115 | self.selectedRanges = textView.selectedRanges 116 | self.parent.onTextChange(textView.string) 117 | } 118 | 119 | func textDidEndEditing(_ notification: Notification) { 120 | guard let textView = notification.object as? NSTextView else { 121 | return 122 | } 123 | 124 | self.parent.text = textView.string 125 | self.parent.onCommit() 126 | } 127 | } 128 | } 129 | 130 | // MARK: - CustomTextView 131 | final class CustomTextView: NSView { 132 | private var isFirstResponder: Bool 133 | 134 | weak var delegate: NSTextViewDelegate? 135 | 136 | var font: NSFont? { 137 | didSet { 138 | textView.font = font 139 | } 140 | } 141 | 142 | var isEditable: Bool { 143 | didSet { 144 | textView.isEditable = isEditable 145 | } 146 | } 147 | 148 | var text: String { 149 | didSet { 150 | textView.string = text 151 | } 152 | } 153 | 154 | var selectedRanges: [NSValue] = [] { 155 | didSet { 156 | // swiftlint:disable empty_count 157 | guard selectedRanges.count > 0 else { 158 | return 159 | } 160 | // swiftlint:enable empty_count 161 | 162 | textView.selectedRanges = selectedRanges 163 | } 164 | } 165 | 166 | private lazy var scrollView: NSScrollView = { 167 | let scrollView = NSScrollView() 168 | scrollView.drawsBackground = false 169 | scrollView.borderType = .noBorder 170 | scrollView.hasVerticalScroller = true 171 | scrollView.hasHorizontalRuler = false 172 | scrollView.autoresizingMask = [.width, .height] 173 | scrollView.translatesAutoresizingMaskIntoConstraints = false 174 | scrollView.autohidesScrollers = true 175 | 176 | return scrollView 177 | }() 178 | 179 | private lazy var textView: NSTextView = { 180 | let contentSize = scrollView.contentSize 181 | let textStorage = NSTextStorage() 182 | 183 | let layoutManager = NSLayoutManager() 184 | textStorage.addLayoutManager(layoutManager) 185 | 186 | let textContainer = NSTextContainer(containerSize: scrollView.frame.size) 187 | textContainer.widthTracksTextView = true 188 | textContainer.containerSize = NSSize( 189 | width: contentSize.width, 190 | height: CGFloat.greatestFiniteMagnitude 191 | ) 192 | 193 | layoutManager.addTextContainer(textContainer) 194 | 195 | let textView = NSTextView(frame: .zero, textContainer: textContainer) 196 | textView.autoresizingMask = .width 197 | textView.delegate = self.delegate 198 | textView.drawsBackground = false 199 | textView.font = self.font 200 | textView.isEditable = self.isEditable 201 | textView.isRichText = false 202 | textView.isHorizontallyResizable = false 203 | textView.isVerticallyResizable = true 204 | textView.maxSize = NSSize( 205 | width: CGFloat.greatestFiniteMagnitude, 206 | height: CGFloat.greatestFiniteMagnitude 207 | ) 208 | textView.minSize = NSSize(width: 0, height: contentSize.height) 209 | textView.textColor = NSColor.labelColor 210 | textView.allowsUndo = true 211 | textView.isContinuousSpellCheckingEnabled = true 212 | 213 | return textView 214 | }() 215 | 216 | // MARK: - Init 217 | init(text: String, isEditable: Bool, isFirstResponder: Bool, font: NSFont?) { 218 | self.font = font 219 | self.isFirstResponder = isFirstResponder 220 | self.isEditable = isEditable 221 | self.text = text 222 | 223 | super.init(frame: .zero) 224 | } 225 | 226 | required init?(coder: NSCoder) { 227 | fatalError("init(coder:) has not been implemented") 228 | } 229 | 230 | // MARK: - Life cycle 231 | override func viewWillDraw() { 232 | super.viewWillDraw() 233 | 234 | setupScrollViewConstraints() 235 | setupTextView() 236 | 237 | if isFirstResponder { 238 | self.window?.makeFirstResponder(self.textView) 239 | } 240 | } 241 | 242 | func setupScrollViewConstraints() { 243 | scrollView.translatesAutoresizingMaskIntoConstraints = false 244 | 245 | addSubview(scrollView) 246 | 247 | NSLayoutConstraint.activate([ 248 | scrollView.topAnchor.constraint(equalTo: topAnchor), 249 | scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), 250 | scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), 251 | scrollView.leadingAnchor.constraint(equalTo: leadingAnchor) 252 | ]) 253 | } 254 | 255 | func setupTextView() { 256 | scrollView.documentView = textView 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /Horizon/Constants.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/26/21 at 21:38 2 | 3 | import KeyboardShortcuts 4 | import Preferences 5 | import SwiftUI 6 | import UniformTypeIdentifiers 7 | 8 | struct Constants { 9 | static let allowedContentTypes: [UTType] = [.image, .audiovisualContent] 10 | static let menuBarIcon = NSImage(named: "MenuBarIcon")! 11 | } 12 | 13 | enum Notifications { 14 | enum Actions { 15 | static let viewPublishedEntry = "VIEW_PUBLISHED_ENTRY_ACTION" 16 | } 17 | 18 | enum Categories { 19 | static let publishedEntry = "PUBLISHED_ENTRY" 20 | } 21 | } 22 | 23 | extension KeyboardShortcuts.Name { 24 | static let togglePanel = Self("togglePanel", default: .init(.d, modifiers: [.command])) 25 | } 26 | 27 | extension Preferences.PaneIdentifier { 28 | static let account = Self("account") 29 | static let general = Self("general") 30 | } 31 | 32 | extension Preferences { 33 | static let contentWidth: Double = 400.0 34 | } 35 | -------------------------------------------------------------------------------- /Horizon/Horizon.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Horizon/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSApplicationCategoryType 24 | public.app-category.productivity 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Horizon/Models/AuthUser.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:15 2 | 3 | import Foundation 4 | 5 | struct AuthUser: Codable { 6 | var token: String 7 | var user: User 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case token 11 | case user 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Horizon/Models/Entry.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:23 2 | 3 | import Foundation 4 | 5 | struct Entry: Codable, Identifiable { 6 | var id: Int 7 | var journalId: Int 8 | var notes: String 9 | 10 | enum CodingKeys: String, CodingKey { 11 | case id 12 | case journalId = "journal_id" 13 | case notes 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Horizon/Models/File.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:23 2 | 3 | import Foundation 4 | 5 | struct File { 6 | var name: String 7 | var data: Data 8 | var mimeType: String 9 | } 10 | -------------------------------------------------------------------------------- /Horizon/Models/Journal.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:23 2 | 3 | import Foundation 4 | 5 | struct Journal: Codable, Equatable, Hashable, Identifiable { 6 | var id: Int 7 | var entryTemplate: String? 8 | var entryTemplateActive: Bool 9 | var isPrivate: Bool 10 | var lastEntryAt: Date? 11 | var slug: String 12 | var title: String 13 | 14 | func hash(into hasher: inout Hasher) { 15 | hasher.combine(id) 16 | } 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case id 20 | case entryTemplate 21 | case entryTemplateActive 22 | case isPrivate = "private" 23 | case lastEntryAt = "last_entry_at" 24 | case slug 25 | case title 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Horizon/Models/User.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:16 2 | 3 | import Foundation 4 | 5 | struct User: Codable, Identifiable { 6 | var id: Int 7 | var username: String 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case id 11 | case username = "futureland_user" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Horizon/Networking/Futureland.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:22 2 | 3 | import Alamofire 4 | import Foundation 5 | import Combine 6 | 7 | enum Futureland { 8 | // TODO: Standardize token passing and headers 9 | // https://swiftwithmajid.com/2020/01/08/building-networking-layer-using-functions/ 10 | private static var baseURL: URL { 11 | guard let url = URL(string: "https://api.futureland.tv") else { 12 | fatalError("FAILED: https://api.futureland.tv") 13 | } 14 | return url 15 | } 16 | 17 | /// Gets journals for signed in user 18 | static func createEntry( 19 | token: String, 20 | notes: String, 21 | journalId: Int, 22 | file: File?, 23 | isPrivate: Bool 24 | ) -> UploadRequest { 25 | let url = baseURL.appendingPathComponent("/entries") 26 | var request = URLRequest(url: url) 27 | request.httpMethod = "POST" 28 | request.addValue("token=\(token)", forHTTPHeaderField: "Cookie") 29 | 30 | let now = Date() 31 | let formatter = DateFormatter() 32 | formatter.dateFormat = "yyyy-MM-dd" 33 | let streakDate = formatter.string(from: now) 34 | 35 | let fileData = file?.data ?? Data() 36 | let fileName = file?.name 37 | let mimeType = file?.mimeType 38 | 39 | return AF.upload(multipartFormData: { multipartFormData in 40 | multipartFormData.append(Data(notes.utf8), withName: "notes") 41 | multipartFormData.append(Data(streakDate.utf8), withName: "streakDate") 42 | multipartFormData.append(Data("\(journalId)".utf8), withName: "journal_id") 43 | multipartFormData.append(Data("\(isPrivate)".utf8), withName: "private") 44 | multipartFormData.append(fileData, withName: "file", fileName: fileName, mimeType: mimeType) 45 | }, with: request) 46 | } 47 | 48 | /// Gets journals for signed in user 49 | static func journals(token: String) -> AnyPublisher<[Journal], AFError> { 50 | let url = baseURL.appendingPathComponent("/users/log") 51 | let headers = HTTPHeaders([ 52 | HTTPHeader(name: "Content-Type", value: "application/json"), 53 | HTTPHeader(name: "Cookie", value: "token=\(token)") 54 | ]) 55 | 56 | let decoder = JSONDecoder() 57 | decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full) 58 | 59 | return AF 60 | .request(url, method: .get, headers: headers) 61 | .publishDecodable(type: [Journal].self, decoder: decoder) 62 | .value() 63 | } 64 | 65 | /// Sign in and get token 66 | static func login(email: String, password: String) -> AnyPublisher { 67 | let url = baseURL.appendingPathComponent("/auth/login") 68 | var request = URLRequest(url: url) 69 | request.httpMethod = "POST" 70 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 71 | 72 | let json: [String: Any] = ["email": email, "password": password] 73 | let body = try? JSONSerialization.data(withJSONObject: json) 74 | request.httpBody = body 75 | 76 | return AF 77 | .request(request) 78 | .publishDecodable(type: AuthUser.self) 79 | .value() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Horizon/Prefs/AccountPrefsView.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 13:56 2 | 3 | import SwiftUI 4 | import Preferences 5 | 6 | let AccountPrefsViewController: (Store) -> PreferencePane = { store in 7 | let toolbarIcon = NSImage( 8 | systemSymbolName: "person.crop.circle", 9 | accessibilityDescription: "Account preferences" 10 | ) 11 | let paneView = Preferences.Pane( 12 | identifier: .account, 13 | title: "Account", 14 | toolbarIcon: toolbarIcon! 15 | ) { 16 | AccountPrefsView( 17 | viewModel: PrefsViewModel(store: store) 18 | ).environmentObject(store) 19 | } 20 | 21 | return Preferences.PaneHostingController(pane: paneView) 22 | } 23 | 24 | struct AccountPrefsView: View { 25 | @EnvironmentObject 26 | var store: Store 27 | 28 | @ObservedObject 29 | var viewModel: PrefsViewModel 30 | 31 | init( 32 | viewModel: PrefsViewModel 33 | ) { 34 | self.viewModel = viewModel 35 | } 36 | 37 | var body: some View { 38 | VStack { 39 | if let username = viewModel.store.user?.username, 40 | viewModel.store.token != nil { 41 | VStack { 42 | Text("Signed in as @\(username)") 43 | Button("Log out", action: viewModel.logout) 44 | } 45 | } else { 46 | Form { 47 | Text("Log in to Futureland") 48 | 49 | TextField("Email", text: $viewModel.email) 50 | 51 | SecureField("Password", text: $viewModel.password) 52 | 53 | HStack { 54 | Button("Login", action: viewModel.login) 55 | .disabled(viewModel.networkActive) 56 | .keyboardShortcut(.return, modifiers: [.command]) 57 | 58 | Spacer() 59 | 60 | if let error = viewModel.error { 61 | Text(error) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | .frame(width: CGFloat(Preferences.contentWidth), alignment: .center) 68 | .padding(.vertical, 20.0) 69 | .padding(.horizontal, 30.0) 70 | } 71 | } 72 | 73 | struct AccountPrefsView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | let store = Store() 76 | 77 | AccountPrefsView( 78 | viewModel: PrefsViewModel(store: store) 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Horizon/Prefs/GeneralPrefsView.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 12:21 2 | 3 | import KeyboardShortcuts 4 | import LaunchAtLogin 5 | import Preferences 6 | import SwiftUI 7 | 8 | let GeneralPrefsViewController: (Store) -> PreferencePane = { store in 9 | let toolbarIcon = NSImage( 10 | systemSymbolName: "gearshape", 11 | accessibilityDescription: "General preferences" 12 | ) 13 | let paneView = Preferences.Pane( 14 | identifier: .general, 15 | title: "General", 16 | toolbarIcon: toolbarIcon! 17 | ) { 18 | GeneralPrefsView().environmentObject(store) 19 | } 20 | 21 | return Preferences.PaneHostingController(pane: paneView) 22 | } 23 | 24 | struct GeneralPrefsView: View { 25 | @EnvironmentObject 26 | var store: Store 27 | 28 | var body: some View { 29 | Preferences.Container(contentWidth: Preferences.contentWidth) { 30 | Preferences.Section(title: "Startup:") { 31 | LaunchAtLogin.Toggle { 32 | Text("Launch at login") 33 | } 34 | } 35 | Preferences.Section(title: "Keyboard Shortcuts:") { 36 | VStack(alignment: .leading) { 37 | Text("Toggle Horizon window") 38 | KeyboardShortcuts.Recorder(for: .togglePanel) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | struct GeneralPrefsView_Previews: PreviewProvider { 46 | static var previews: some View { 47 | GeneralPrefsView() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Horizon/Prefs/PrefsViewModel.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:14 2 | 3 | import SwiftUI 4 | import Combine 5 | 6 | class PrefsViewModel: ObservableObject, Identifiable { 7 | private(set) var store: Store 8 | private var disposables = Set() 9 | 10 | @Published 11 | var email = "" 12 | 13 | @Published 14 | var password = "" 15 | 16 | @Published 17 | var networkActive = false 18 | 19 | @Published 20 | var error: String? 21 | 22 | init( 23 | store: Store 24 | ) { 25 | self.store = store 26 | } 27 | 28 | func login() { 29 | self.networkActive = true 30 | Futureland 31 | .login(email: email, password: password) 32 | .sink(receiveCompletion: { completion in 33 | switch completion { 34 | case .finished: 35 | break 36 | case .failure(let error): 37 | print(error) 38 | self.error = "wrong credentials" 39 | } 40 | self.networkActive = false 41 | }, receiveValue: { authUser in 42 | self.store.token = authUser.token 43 | self.store.user = authUser.user 44 | 45 | self.email = "" 46 | self.password = "" 47 | }) 48 | .store(in: &disposables) 49 | } 50 | 51 | func logout() { 52 | self.store.token = nil 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Horizon/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Horizon/Publish/PublishPanel.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 12:14 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | class PublishPanel: NSPanel { 7 | override var canBecomeKey: Bool { true } 8 | override var canBecomeMain: Bool { true } 9 | override var acceptsFirstResponder: Bool { true } 10 | 11 | init( 12 | store: Store, 13 | onClose: @escaping () -> Void 14 | ) { 15 | super.init( 16 | contentRect: NSRect(x: 0, y: 0, width: 440, height: 300), 17 | styleMask: [.nonactivatingPanel, .titled, .fullSizeContentView], 18 | backing: .buffered, 19 | defer: false 20 | ) 21 | 22 | center() 23 | isMovableByWindowBackground = true 24 | hasShadow = true 25 | setFrameAutosaveName("main") 26 | 27 | collectionBehavior = [ 28 | .canJoinAllSpaces, 29 | .fullScreenAuxiliary 30 | ] 31 | hidesOnDeactivate = false 32 | isExcludedFromWindowsMenu = false 33 | 34 | isFloatingPanel = true 35 | level = .floating 36 | 37 | titleVisibility = .hidden 38 | titlebarAppearsTransparent = true 39 | 40 | let rootView = PublishView( 41 | viewModel: PublishViewModel( 42 | store: store, 43 | onClose: onClose 44 | ), 45 | parent: self 46 | ) 47 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) 48 | .edgesIgnoringSafeArea(.all) 49 | .environmentObject(store) 50 | 51 | contentView = NSHostingView(rootView: rootView) 52 | resize() 53 | } 54 | 55 | func resize() { 56 | setContentSize(contentView!.fittingSize) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Horizon/Publish/PublishView.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 12:12 2 | 3 | import Combine 4 | import SwiftUI 5 | 6 | struct PublishView: View { 7 | @EnvironmentObject 8 | private var store: Store 9 | 10 | @ObservedObject 11 | private var viewModel: PublishViewModel 12 | 13 | private var parent: PublishPanel 14 | 15 | init( 16 | viewModel: PublishViewModel, 17 | parent: PublishPanel 18 | ) { 19 | self.viewModel = viewModel 20 | self.parent = parent 21 | } 22 | 23 | var body: some View { 24 | VStack(spacing: 15) { 25 | if viewModel.progress > 0.0 { 26 | ProgressView(value: viewModel.progress) 27 | } 28 | 29 | HStack { 30 | Dropdown( 31 | selectedValue: $viewModel.selectedJournal, 32 | items: $store.journals, 33 | disabled: viewModel.networkActive, 34 | getItemTitle: { $0.title.count >= 30 ? "\(String($0.title.prefix(27)))…" : $0.title }, 35 | onChange: viewModel.onChangeJournal 36 | ) 37 | .frame(width: 220.0) 38 | .accessibility(value: Text("Selected journal: \(viewModel.selectedJournal?.title ?? "None")")) 39 | .onChange(of: store.token) { _ in store.fetchJournals() } 40 | .onAppear(perform: store.fetchJournals) 41 | 42 | Spacer() 43 | 44 | if let fileName = viewModel.file?.name { 45 | HStack { 46 | Text(fileName) 47 | .lineLimit(1) 48 | .frame(maxWidth: 150.0, alignment: .trailing) 49 | 50 | Button("x", action: viewModel.discardMedia) 51 | .disabled(viewModel.networkActive) 52 | .accessibility(label: Text("Discard attached media")) 53 | } 54 | } else { 55 | Button(action: viewModel.addMedia) { 56 | Text("Add media") 57 | Text("⌘ M") 58 | .font(.caption) 59 | .accessibility(hidden: true) 60 | } 61 | .disabled(viewModel.networkActive) 62 | .keyboardShortcut("m", modifiers: [.command]) 63 | .fileImporter( 64 | isPresented: $viewModel.isFileBrowserOpen, 65 | allowedContentTypes: Constants.allowedContentTypes, 66 | onCompletion: viewModel.attachMedia 67 | ) 68 | .accessibility(hint: Text("Attach media to entry")) 69 | } 70 | } 71 | 72 | HorizonTextView( 73 | text: viewModel.entryText, 74 | placeholder: "Write…", 75 | isEditable: !(viewModel.networkActive || viewModel.isDragAndDropActive), 76 | onTextChange: { val in 77 | self.viewModel.entryText = val 78 | } 79 | ) 80 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 85, maxHeight: 85) 81 | 82 | HStack { 83 | Button(action: viewModel.publish) { 84 | Text("Publish") 85 | Text("⌘ Enter") 86 | .font(.caption) 87 | .accessibility(hidden: true) 88 | } 89 | .disabled(viewModel.disabled) 90 | .keyboardShortcut(.return, modifiers: [.command]) 91 | .accessibility(hint: Text("Publish entry to Futureland")) 92 | 93 | Button(action: viewModel.reset) { 94 | Text("Cancel") 95 | Text("Esc") 96 | .font(.caption) 97 | .accessibility(hidden: true) 98 | } 99 | .disabled(viewModel.networkActive) 100 | .keyboardShortcut(.cancelAction) 101 | .accessibility(hint: Text("Discard changes and close window")) 102 | 103 | Spacer() 104 | 105 | if viewModel.wordCount > 1 { 106 | Text("\(viewModel.wordCount) words") 107 | .accessibility(value: Text("Word count")) 108 | } 109 | 110 | if !(viewModel.selectedJournal?.isPrivate ?? false) { 111 | Button("\(viewModel.isPrivate ? "🔒" : "🔓")") { 112 | viewModel.isPrivate = !viewModel.isPrivate 113 | } 114 | .disabled(viewModel.networkActive) 115 | .keyboardShortcut("p", modifiers: [.command]) 116 | .accessibility(label: Text("\(viewModel.isPrivate ? "Private" : "Public")")) 117 | .accessibility(hint: Text("Mark entry as public or private")) 118 | } 119 | 120 | Button("🙂") { 121 | NSApp.orderFrontCharacterPalette(nil) 122 | } 123 | .disabled(viewModel.networkActive) 124 | .keyboardShortcut("e", modifiers: [.command]) 125 | .accessibility(label: Text("Show emoji picker")) 126 | } 127 | } 128 | .padding(.top, 5) 129 | .padding(.horizontal) 130 | .frame(width: 440) 131 | .overlay( 132 | VStack { 133 | if viewModel.isDragAndDropActive { 134 | VStack { 135 | Text("Drop Media") 136 | } 137 | .frame(maxWidth: .infinity, maxHeight: .infinity) 138 | .background(Color("Background").opacity(0.85)) 139 | } 140 | } 141 | ) 142 | .onChange(of: viewModel.progress) { _ in self.parent.resize() } 143 | .onChange(of: store.token) { _ in 144 | if store.token == nil { viewModel.reset() } 145 | } 146 | .onDrop( 147 | of: Constants.allowedContentTypes, 148 | delegate: PublishDropDelegate( 149 | active: $viewModel.isDragAndDropActive, 150 | file: $viewModel.file 151 | ) 152 | ) 153 | } 154 | } 155 | 156 | struct PublishDropDelegate: DropDelegate { 157 | @Binding 158 | var active: Bool 159 | 160 | @Binding 161 | var file: File? 162 | 163 | func performDrop(info: DropInfo) -> Bool { 164 | guard let itemProvider = info.itemProviders(for: [(kUTTypeFileURL as String)]).first else { return false } 165 | 166 | itemProvider.loadItem(forTypeIdentifier: (kUTTypeFileURL as String), options: nil) { item, _ in 167 | if let data = item as? Data, 168 | let fileUrl = URL(dataRepresentation: data, relativeTo: nil), 169 | let mediaFile = getFileForUrl(url: fileUrl) { 170 | DispatchQueue.main.async { file = mediaFile } 171 | } 172 | } 173 | 174 | return true 175 | } 176 | 177 | func dropEntered(info: DropInfo) { 178 | // TODO: Swift bug `dropEntered` isn't called 179 | // - Check file type 180 | // - Set active to true if valid 181 | } 182 | 183 | func dropUpdated(info: DropInfo) -> DropProposal? { 184 | // TODO: Move this logic to `dropEntered` once supported 185 | // Determine if file type is audio, image, or video before showing active region 186 | guard 187 | info.hasItemsConforming(to: [(kUTTypeFileURL as String)]), 188 | let itemProvider = info.itemProviders(for: [(kUTTypeFileURL as String)]).first else { 189 | print("cancel") 190 | return DropProposal(operation: .cancel) 191 | } 192 | 193 | if !active { 194 | itemProvider.loadItem(forTypeIdentifier: (kUTTypeFileURL as String), options: nil) { item, _ in 195 | if let data = item as? Data, 196 | let fileUrl = URL(dataRepresentation: data, relativeTo: nil), 197 | let mediaFile = getFileForUrl(url: fileUrl), 198 | ["audio", "image", "video"].contains(where: mediaFile.mimeType.contains) { 199 | DispatchQueue.main.async { active = true } 200 | } 201 | } 202 | } 203 | 204 | return DropProposal(operation: .copy) 205 | } 206 | 207 | func dropExited(info: DropInfo) { 208 | active = false 209 | } 210 | } 211 | 212 | struct PublishView_Previews: PreviewProvider { 213 | static var previews: some View { 214 | let store = Store() 215 | 216 | PublishView( 217 | viewModel: PublishViewModel( 218 | store: store, 219 | onClose: { print("onClose") } 220 | ), 221 | parent: PublishPanel( 222 | store: store, 223 | onClose: { print("onClose") } 224 | ) 225 | ) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Horizon/Publish/PublishViewModel.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 14:28 2 | 3 | import SwiftUI 4 | import Combine 5 | import UserNotifications 6 | 7 | class PublishViewModel: ObservableObject, Identifiable { 8 | private(set) var store: Store 9 | private let onClose: () -> Void 10 | private var disposables = Set() 11 | 12 | @Published 13 | var networkActive = false 14 | 15 | @Published 16 | var progress = 0.0 17 | 18 | @Published 19 | var entryText = "" 20 | 21 | @Published 22 | var selectedJournal: Journal? { 23 | didSet { 24 | previousSelectedJournal = store.journals.first { $0.id == oldValue?.id } 25 | } 26 | } 27 | 28 | @Published 29 | var isDragAndDropActive = false 30 | 31 | @Published 32 | var isFileBrowserOpen = false 33 | 34 | @Published 35 | var isPrivate = false 36 | 37 | @Published 38 | var file: File? 39 | 40 | var disabled: Bool { networkActive || (entryText.isEmpty && file == nil ) } 41 | var wordCount: Int { entryText.split { $0 == " " || $0.isNewline }.count } 42 | 43 | var previousSelectedJournal: Journal? 44 | 45 | init( 46 | store: Store, 47 | onClose: @escaping () -> Void 48 | ) { 49 | self.store = store 50 | self.onClose = onClose 51 | } 52 | 53 | func publish() { 54 | guard let token = store.token else { return } 55 | 56 | networkActive = true 57 | 58 | Futureland 59 | .createEntry( 60 | token: token, 61 | notes: entryText, 62 | journalId: selectedJournal!.id, 63 | file: file, 64 | isPrivate: isPrivate 65 | ) 66 | .uploadProgress { progress in 67 | self.progress = progress.fractionCompleted 68 | } 69 | .publishDecodable(type: Entry.self) 70 | .value() 71 | .sink(receiveCompletion: { completion in 72 | switch completion { 73 | case .finished: 74 | break 75 | case .failure(let error): 76 | print(error) 77 | } 78 | self.networkActive = false 79 | }, receiveValue: { entry in 80 | guard let username = self.store.user?.username else { return } 81 | guard let slug = self.selectedJournal?.slug else { return } 82 | 83 | let entryUrl = "https://futureland.tv/\(username)/\(slug)/\(entry.id)?fullscreen=1" 84 | let content = UNMutableNotificationContent() 85 | content.title = "Published Entry" 86 | content.body = entry.notes 87 | content.userInfo = ["entryUrl": entryUrl] 88 | content.categoryIdentifier = Notifications.Categories.publishedEntry 89 | 90 | let uuidString = UUID().uuidString 91 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) 92 | let request = UNNotificationRequest( 93 | identifier: uuidString, 94 | content: content, 95 | trigger: trigger 96 | ) 97 | 98 | let notificationCenter = UNUserNotificationCenter.current() 99 | notificationCenter.add(request) { (error) in 100 | if let error = error { 101 | print(error) 102 | } 103 | } 104 | 105 | self.reset() 106 | }) 107 | .store(in: &disposables) 108 | } 109 | 110 | func reset() { 111 | progress = 0.0 112 | networkActive = false 113 | isPrivate = false 114 | file = nil 115 | entryText = "" 116 | 117 | if let journal = selectedJournal { 118 | setEntryToTemplate(journal: journal) 119 | } 120 | 121 | onClose() 122 | } 123 | 124 | func onChangeJournal(value: Journal?) { 125 | guard let journal = value else { return } 126 | 127 | setEntryToTemplate(journal: journal) 128 | isPrivate = journal.isPrivate 129 | } 130 | } 131 | 132 | // MARK: Media 133 | extension PublishViewModel { 134 | func addMedia() { 135 | isFileBrowserOpen = true 136 | } 137 | 138 | func discardMedia() { 139 | file = nil 140 | } 141 | 142 | func attachMedia(_ result: Result) { 143 | do { 144 | let fileUrl = try result.get() 145 | guard fileUrl.startAccessingSecurityScopedResource() else { return } 146 | 147 | guard let mediaFile = getFileForUrl(url: fileUrl) else { return } 148 | 149 | file = mediaFile 150 | 151 | fileUrl.stopAccessingSecurityScopedResource() 152 | } catch { 153 | print(error.localizedDescription) 154 | } 155 | } 156 | } 157 | 158 | // MARK: Entry template 159 | extension PublishViewModel { 160 | private func setEntryToTemplate(journal: Journal) { 161 | if entryText != previousSelectedJournal?.entryTemplate ?? "" { return } 162 | 163 | guard let template = journal.entryTemplate else { 164 | entryText = "" 165 | return 166 | } 167 | 168 | if journal.entryTemplateActive || template.isEmpty { 169 | entryText = template 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Horizon/StatusBar/StatusBarMenuItem.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 12:34 2 | 3 | import SwiftUI 4 | 5 | final class StatusBarMenuItem: NSMenuItem { 6 | private let callback: (NSMenuItem) -> Void 7 | 8 | init( 9 | _ title: String, 10 | key: String = "", 11 | callback: @escaping (NSMenuItem) -> Void 12 | ) { 13 | self.callback = callback 14 | super.init(title: title, action: #selector(action(_:)), keyEquivalent: key) 15 | self.target = self 16 | } 17 | 18 | required init(coder decoder: NSCoder) { 19 | fatalError("Not yet implemented.") 20 | } 21 | 22 | @objc 23 | func action(_ sender: NSMenuItem) { 24 | callback(sender) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Horizon/StatusBar/StatusItemMenu.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/23/21 at 12:34 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | final class StatusItemMenu: NSMenu, NSMenuDelegate { 7 | init( 8 | openPanel: @escaping () -> Void, 9 | openPrefs: @escaping () -> Void, 10 | checkForUpdates: @escaping () -> Void, 11 | quit: @escaping () -> Void 12 | ) { 13 | super.init(title: "Horizon Status Item Menu") 14 | delegate = self 15 | 16 | let openMenuItem = StatusBarMenuItem("Open Horizon") { _ in openPanel() } 17 | let preferencesMenuItem = StatusBarMenuItem("Preferences", key: ",") { _ in openPrefs() } 18 | let checkMenuItem = StatusBarMenuItem("Check for updates") { _ in checkForUpdates() } 19 | let quitMenuItem = StatusBarMenuItem("Quit Horizon", key: "q") { _ in quit() } 20 | 21 | addItem(openMenuItem) 22 | addItem(preferencesMenuItem) 23 | addItem(checkMenuItem) 24 | addItem(NSMenuItem.separator()) 25 | addItem(quitMenuItem) 26 | } 27 | 28 | required init(coder: NSCoder) { 29 | fatalError("Not yet implemented.") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Horizon/Store.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/29/21 at 22:21 2 | 3 | import Alamofire 4 | import Combine 5 | import Foundation 6 | import KeychainAccess 7 | import SwiftUI 8 | 9 | let keychain = Keychain() 10 | 11 | class Store: ObservableObject { 12 | private var disposables = Set() 13 | 14 | @Published 15 | var journals = [Journal]() 16 | 17 | @Published 18 | var token: String? { 19 | didSet { 20 | keychainSet(value: token, key: "Token") 21 | } 22 | } 23 | 24 | @Published 25 | var user: User? { 26 | didSet { 27 | keychainSet(value: user, key: "User") 28 | } 29 | } 30 | 31 | init() { 32 | guard let token: String = keychainGet(key: "Token") else { return } 33 | self.token = token 34 | 35 | guard let user: User = keychainGet(key: "User") else { return } 36 | self.user = user 37 | } 38 | 39 | func fetchJournals() { 40 | guard let token = token else { return } 41 | Futureland 42 | .journals(token: token) 43 | .sink( 44 | receiveCompletion: { completion in 45 | switch completion { 46 | case .finished: 47 | break 48 | case .failure(let error): 49 | print(error) 50 | } 51 | }, 52 | receiveValue: { journals in 53 | let sortedJournals = journals.sorted { (a, b) -> Bool in 54 | guard let lastEntryAtA = a.lastEntryAt else { return false } 55 | guard let lastEntryAtB = b.lastEntryAt else { return true } 56 | return lastEntryAtA > lastEntryAtB 57 | } 58 | self.journals = sortedJournals 59 | } 60 | ) 61 | .store(in: &disposables) 62 | } 63 | } 64 | 65 | extension Store { 66 | private func keychainSet(value: T, key: String) { 67 | do { 68 | let encoded = try JSONEncoder().encode(value) 69 | try keychain.set(encoded, key: key) 70 | } catch let error { 71 | print(error) 72 | } 73 | } 74 | 75 | private func keychainGet(key: String) -> T? { 76 | var value: T? 77 | do { 78 | try keychain.get(key) { attributes in 79 | if let attributes = attributes, 80 | let data = attributes.data { 81 | do { 82 | let decoded = try JSONDecoder().decode(T.self, from: data) 83 | value = decoded 84 | } catch let error { 85 | print(error) 86 | } 87 | } 88 | } 89 | } catch let error { 90 | print(error) 91 | } 92 | 93 | return value 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Horizon/Utilities.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/26/21 at 20:25 2 | 3 | import SwiftUI 4 | 5 | extension DateFormatter { 6 | static let iso8601Full: DateFormatter = { 7 | let formatter = DateFormatter() 8 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" 9 | formatter.calendar = Calendar(identifier: .iso8601) 10 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 11 | formatter.locale = Locale(identifier: "en_US_POSIX") 12 | return formatter 13 | }() 14 | } 15 | 16 | extension NSApplication { 17 | func quit() { 18 | NSApp.terminate(nil) 19 | } 20 | } 21 | 22 | /** 23 | Makes it easier to get mime type 24 | */ 25 | func getMimeTypeFor(fileUrl url: URL) -> String? { 26 | guard 27 | let extUTI = UTTypeCreatePreferredIdentifierForTag( 28 | kUTTagClassFilenameExtension, 29 | url.pathExtension as CFString, 30 | nil)?.takeUnretainedValue() 31 | else { return nil } 32 | 33 | guard 34 | let mimeUTI = UTTypeCopyPreferredTagWithClass(extUTI, kUTTagClassMIMEType) 35 | else { return nil } 36 | 37 | return mimeUTI.takeRetainedValue() as String 38 | } 39 | 40 | /** 41 | Convenience function for initializing an object and modifying its properties. 42 | ``` 43 | let label = with(NSTextField()) { 44 | $0.stringValue = "Foo" 45 | $0.textColor = .systemBlue 46 | view.addSubview($0) 47 | } 48 | ``` 49 | */ 50 | @discardableResult 51 | func with(_ item: T, update: (inout T) throws -> Void) rethrows -> T { 52 | var this = item 53 | try update(&this) 54 | return this 55 | } 56 | 57 | func getFileForUrl(url fileUrl: URL) -> File? { 58 | // Get file data 59 | guard let data = try? Data(contentsOf: fileUrl) else { return nil } 60 | 61 | // Get mime type 62 | guard let mimeType = getMimeTypeFor(fileUrl: fileUrl) else { return nil } 63 | 64 | return File(name: fileUrl.lastPathComponent, data: data, mimeType: mimeType) 65 | } 66 | -------------------------------------------------------------------------------- /HorizonTests/HorizonTests.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/24/21 at 13:29 2 | 3 | import XCTest 4 | @testable import Horizon 5 | 6 | class HorizonTests: XCTestCase { 7 | 8 | override func setUpWithError() throws { 9 | // Put setup code here. This method is called before the invocation of each test method in the class. 10 | } 11 | 12 | override func tearDownWithError() throws { 13 | // Put teardown code here. This method is called after the invocation of each test method in the class. 14 | } 15 | 16 | func testExample() throws { 17 | // This is an example of a functional test case. 18 | // Use XCTAssert and related functions to verify your tests produce the correct results. 19 | } 20 | 21 | func testPerformanceExample() throws { 22 | // This is an example of a performance test case. 23 | self.measure { 24 | // Put the code you want to measure the time of here. 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /HorizonTests/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 | -------------------------------------------------------------------------------- /HorizonUITests/HorizonUITests.swift: -------------------------------------------------------------------------------- 1 | // By Tom Meagher on 1/24/21 at 13:29 2 | 3 | import XCTest 4 | 5 | extension XCUIApplication { 6 | func statusItem() -> XCUIElement { 7 | menuBars.children(matching: .statusItem).firstMatch 8 | } 9 | 10 | func statusItemMenu() -> XCUIElement { 11 | statusItem().menus.firstMatch 12 | } 13 | 14 | func statusItemMenuItem(_ identifier: String) -> XCUIElement { 15 | statusItemMenu().menuItems[identifier].firstMatch 16 | } 17 | } 18 | 19 | extension HorizonUITests { 20 | func openPanel() { 21 | let statusItem = app.statusItem() 22 | statusItem.click() 23 | 24 | let statusItemMenuItem = app.statusItemMenuItem("Open Horizon") 25 | statusItemMenuItem.click() 26 | } 27 | 28 | func openPreferences() { 29 | let statusItem = app.statusItem() 30 | statusItem.click() 31 | 32 | let statusItemMenuItem = app.statusItemMenuItem("Preferences") 33 | statusItemMenuItem.click() 34 | } 35 | 36 | func openPreferencesWindow(_ identifier: String) { 37 | openPreferences() 38 | app.radioButtons[identifier].click() 39 | } 40 | 41 | func logIn(email: String, password: String) { 42 | openPreferencesWindow("Account") 43 | 44 | let emailTextField = app.textFields["Email"] 45 | XCTAssertTrue(emailTextField.exists) 46 | emailTextField.click() 47 | emailTextField.typeText(email) 48 | 49 | let passwordSecureTextField = app.secureTextFields["Password"] 50 | XCTAssertTrue(passwordSecureTextField.exists) 51 | passwordSecureTextField.click() 52 | passwordSecureTextField.typeText(password) 53 | 54 | let logInButton = app.buttons["Login"] 55 | XCTAssertTrue(logInButton.exists) 56 | logInButton.click() 57 | 58 | let logOutButton = app.buttons["Log out"] 59 | _ = logOutButton.waitForExistence(timeout: 5) 60 | XCTAssertTrue(logOutButton.exists) 61 | } 62 | 63 | func logOut() { 64 | openPreferencesWindow("Account") 65 | 66 | let logOutButton = app.buttons["Log out"] 67 | XCTAssertTrue(logOutButton.exists) 68 | logOutButton.click() 69 | 70 | let logInButton = app.buttons["Login"] 71 | XCTAssertTrue(logInButton.exists) 72 | } 73 | } 74 | 75 | class HorizonUITests: XCTestCase { 76 | var app: XCUIApplication! 77 | 78 | override func setUp() { 79 | app = XCUIApplication() 80 | app.launch() 81 | } 82 | 83 | override func setUpWithError() throws { 84 | continueAfterFailure = false 85 | } 86 | 87 | override func tearDown() { 88 | app.terminate() 89 | } 90 | 91 | func testLoginError() throws { 92 | logIn(email: "foo@example.com", password: "foobarbaz") 93 | 94 | let errorMessage = app.staticTexts["wrong credentials"] 95 | _ = errorMessage.waitForExistence(timeout: 5) 96 | 97 | XCTAssertTrue(errorMessage.exists) 98 | } 99 | 100 | func testLoginSuccess() throws { 101 | guard let email = ProcessInfo.processInfo.environment["FUTURELAND_EMAIL"] else { return } 102 | guard let password = ProcessInfo.processInfo.environment["FUTURELAND_PASSWORD"] else { return } 103 | 104 | logIn(email: email, password: password) 105 | logOut() 106 | } 107 | 108 | func testPublishEntry() throws { 109 | guard let email = ProcessInfo.processInfo.environment["FUTURELAND_EMAIL"] else { return } 110 | guard let password = ProcessInfo.processInfo.environment["FUTURELAND_PASSWORD"] else { return } 111 | 112 | logIn(email: email, password: password) 113 | openPanel() 114 | 115 | let journalPopUpButton = app.popUpButtons.firstMatch 116 | XCTAssertTrue(journalPopUpButton.exists) 117 | journalPopUpButton.click() 118 | 119 | let journalMenuItem = app.menuItems["Horizon Test"] 120 | XCTAssertTrue(journalMenuItem.exists) 121 | journalMenuItem.click() 122 | 123 | let entryTextView = app.textViews.firstMatch 124 | XCTAssertTrue(entryTextView.exists) 125 | entryTextView.click() 126 | entryTextView.typeText("testPublishEntry") 127 | 128 | let publishButton = app.buttons["Publish"] 129 | XCTAssertTrue(publishButton.exists) 130 | publishButton.click() 131 | 132 | logOut() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /HorizonUITests/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 | --------------------------------------------------------------------------------