├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── scripts │ ├── import-certificate.sh │ ├── import-profile.sh │ └── set-env-from-xcodeproj.sh └── workflows │ ├── deploy.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── SECURITY.md ├── SnippetsLibrary.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── SnippetsLibrary.xcscheme └── xcuserdata │ └── krzysztoflowiec.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── SnippetsLibrary ├── Application │ ├── ActiveAppView.swift │ ├── AppAlert.swift │ ├── AppDelegate.swift │ ├── AppMenu.swift │ ├── AppSheet.swift │ ├── AppView.swift │ └── SnippetsLibraryApp.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256-1.png │ │ ├── 256.png │ │ ├── 32-1.png │ │ ├── 32.png │ │ ├── 512-1.png │ │ ├── 512.png │ │ ├── 64.png │ │ └── Contents.json │ ├── Colors │ │ ├── Contents.json │ │ ├── accent.colorset │ │ │ └── Contents.json │ │ ├── background.colorset │ │ │ └── Contents.json │ │ ├── darkGrey.colorset │ │ │ └── Contents.json │ │ ├── defaultBackground.colorset │ │ │ └── Contents.json │ │ ├── secondaryBackground.colorset │ │ │ └── Contents.json │ │ ├── skeletonLight.colorset │ │ │ └── Contents.json │ │ ├── skeletonRegular.colorset │ │ │ └── Contents.json │ │ └── skeletonThin.colorset │ │ │ └── Contents.json │ ├── Contents.json │ ├── icLogo.imageset │ │ ├── Contents.json │ │ └── icLogo.pdf │ ├── icLogoStatusBar.imageset │ │ ├── Books_front_matte.pdf │ │ └── Contents.json │ ├── icSnippetFileBlank.imageset │ │ ├── Contents.json │ │ └── icSnippetFileBlank.png │ └── icSnippetFileWhite.imageset │ │ ├── Contents.json │ │ └── icSnippetFile.png ├── Dependencies │ └── DependencyContainer.swift ├── Extensions │ ├── AnyPublisher+SinkWithoutValue.swift │ ├── AppDelegate+NSMenuDelegate.swift │ ├── AppMenu+HideWindow.swift │ ├── DatabaseReference+Timeout.swift │ ├── NSApplication+AppVersion.swift │ ├── NSNotification+Name.swift │ ├── NSTableView+BackgroundColor.swift │ ├── SnippetPlist+Dictonary.swift │ ├── SnippetsLibraryView+Equatable.swift │ ├── StartView+Equatable.swift │ ├── String+Extensions.swift │ ├── View+Displayed.swift │ ├── View+Skeletonable.swift │ └── View+Visibility.swift ├── Info.plist ├── Models │ ├── Snippet.swift │ └── SnippetPlist.swift ├── Modules │ ├── SnippetDetails │ │ ├── SnippetDetailsView.swift │ │ └── SnippetDetailsViewModel.swift │ ├── SnippetImport │ │ ├── SnippetImportView.swift │ │ ├── SnippetImportViewModel+DropDelegate.swift │ │ └── SnippetImportViewModel.swift │ ├── SnippetsLibrary │ │ ├── SnippetLibraryList │ │ │ ├── SnippetsLibraryListFilterView.swift │ │ │ ├── SnippetsLibraryListSectionView.swift │ │ │ ├── SnippetsLibraryListView.swift │ │ │ └── SnippetsLibraryListViewModel.swift │ │ ├── SnippetsLibraryPreview │ │ │ └── SnippetsLibraryPreviewView.swift │ │ ├── SnippetsLibraryView.swift │ │ └── SnippetsLibraryViewModel.swift │ ├── SnippetsUpload │ │ ├── SnippetsUploadView.swift │ │ └── SnippetsUploadViewModel.swift │ ├── Start │ │ ├── StartView.swift │ │ ├── StartViewMenuItem │ │ │ └── StartViewMenuItem.swift │ │ ├── StartViewModel.swift │ │ └── StartViewRecentSnippets │ │ │ └── StartViewRecentSnippetsView.swift │ └── Status │ │ ├── StatusView.swift │ │ └── StatusViewModel.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Services │ ├── Firebase │ │ ├── Crashlytics │ │ │ └── CrashlyticsService.swift │ │ ├── DatabaseService │ │ │ └── DatabaseService.swift │ │ └── Logs │ │ │ └── LogsService.swift │ ├── Network │ │ └── NetworkService.swift │ ├── SnippetParser │ │ └── SnippetsParserService.swift │ ├── URLFactory │ │ └── URLFactory.swift │ └── UserDefaults │ │ └── UserDefaultsService.swift ├── SnippetsLibrary.entitlements ├── SnippetsLibraryDebug.entitlements ├── Supporting Files │ ├── CustomCodeTheme.swift │ ├── Enums │ │ ├── DisabledCommandGroupButtonType.swift │ │ ├── Errors │ │ │ ├── CrashlyticsError.swift │ │ │ ├── DatabaseError.swift │ │ │ ├── SnippetsParserServiceError.swift │ │ │ └── UserDefaultsServiceError.swift │ │ ├── FileStatusCardType.swift │ │ ├── PlistCodingKeys.swift │ │ ├── SnippetAvailability.swift │ │ ├── SnippetCategory.swift │ │ ├── SnippetDetailsViewType.swift │ │ ├── SnippetFileCardViewState.swift │ │ ├── SnippetPlatform.swift │ │ ├── SnippetType.swift │ │ ├── SnippetWriteType.swift │ │ ├── SnippetsFetchingType.swift │ │ ├── StartViewMenuItemType.swift │ │ ├── URLType.swift │ │ ├── UploadingStatus.swift │ │ ├── UserActivityLevel.swift │ │ └── UserActivityLogType.swift │ ├── Layout.swift │ ├── Mocks.swift │ ├── NetworkObserver.swift │ ├── System.swift │ └── TextFieldStyleModifier.swift └── Views │ ├── DisabledCommandGroupButton.swift │ ├── EmptySnippetsListView.swift │ ├── FileStatusCard │ ├── FileStatusCard.swift │ └── FileStatusCardViewModel.swift │ ├── RecentSnippetCardView.swift │ ├── SearchBar.swift │ ├── SnippetCreationManualView.swift │ ├── SnippetDropCellView.swift │ ├── SnippetFileCard │ ├── SnippetFileCardView.swift │ └── SnippetFileCardViewModel.swift │ ├── SnippetListItem │ └── SnippetListItemView.swift │ ├── ToastView.swift │ └── VisualEffectView.swift ├── SnippetsLibraryUITests ├── Info.plist └── SnippetsLibraryUITests.swift ├── SnippetsLibraryUnitTests ├── Info.plist └── SnippetsLibraryUnitTests.swift ├── SnippetsLibraryWidget ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── WidgetBackground.colorset │ │ └── Contents.json ├── Info.plist ├── Models │ └── WidgetEntry.swift ├── Provider.swift ├── SnippetsLibraryWidget.entitlements ├── SnippetsLibraryWidget.swift └── Views │ ├── MediumWidget │ ├── MediumWidgetListItemView.swift │ └── MediumWidgetView.swift │ ├── SmallWidget │ └── SmallWidgetView.swift │ └── SnippetsLibraryWidgetView.swift ├── Sources └── SnippetsLibrary │ └── SnippetsLibrary.swift ├── Tests └── SnippetsLibraryTests │ └── SnippetsLibraryTests.swift └── public ├── docs ├── privacy_policy.md └── terms_and_conditions.md └── images └── app_preview.png /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # More details are here: https://help.github.com/articles/about-codeowners/ 5 | 6 | # The '*' pattern is global owners. 7 | 8 | # Order is important. The last matching pattern has the most precedence. 9 | # The folders are ordered as follows: 10 | 11 | # In each subsection folders are ordered first by depth, then alphabetically. 12 | # This should make it easy to add new rules without breaking existing ones. 13 | 14 | # Global rule: 15 | * @tryboxx 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment:** 27 | - device: [e.g. MacBook Pro (13-inch, M1, 2020)] 28 | - macOS: [e.g. Big Sur, version 11.3.1] 29 | - appVersion: [e.g. 1.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/scripts/import-certificate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | security create-keychain -p "" build.keychain 6 | security list-keychains -s build.keychain 7 | security default-keychain -s build.keychain 8 | security unlock-keychain -p "" build.keychain 9 | security set-keychain-settings 10 | security import <(echo $SIGNING_CERTIFICATE_P12_DATA | base64 --decode) \ 11 | -f pkcs12 \ 12 | -k build.keychain \ 13 | -P $SIGNING_CERTIFICATE_PASSWORD \ 14 | -T /usr/bin/codesign 15 | security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain 16 | -------------------------------------------------------------------------------- /.github/scripts/import-profile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles 6 | echo "$PROVISIONING_PROFILE_DATA" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/SnippetsLibrary_Distribution.provisionprofile 7 | -------------------------------------------------------------------------------- /.github/scripts/set-env-from-xcodeproj.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | SCHEME="$(xcodebuild -list -json | jq -r '.project.schemes[0]')" 6 | PRODUCT_NAME="$(xcodebuild -scheme "$SCHEME" -showBuildSettings | grep " PRODUCT_NAME " | sed "s/[ ]*PRODUCT_NAME = //")" 7 | echo "::set-env name=PRODUCT_NAME::$PRODUCT_NAME" 8 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # name: deploy 2 | 3 | # on: 4 | # push: 5 | # branches: [ main ] 6 | # pull_request: 7 | # branches: [ main ] 8 | 9 | # workflow_dispatch: 10 | 11 | # jobs: 12 | # deploy: 13 | # name: Deploying to Testflight 14 | # runs-on: macOS-latest 15 | 16 | # steps: 17 | # - name: Checkout repository 18 | # uses: actions/checkout@v1 19 | 20 | # - name: Deploy iOS Beta to TestFlight via Fastlane 21 | # uses: maierj/fastlane-action@v1.4.0 22 | # with: 23 | # lane: closed_beta 24 | # env: 25 | # APP_STORE_CONNECT_TEAM_ID: '${{ secrets.APP_STORE_CONNECT_TEAM_ID }}' 26 | # DEVELOPER_APP_ID: '${{ secrets.DEVELOPER_APP_ID }}' 27 | # DEVELOPER_APP_IDENTIFIER: '${{ secrets.DEVELOPER_APP_IDENTIFIER }}' 28 | # DEVELOPER_PORTAL_TEAM_ID: '${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}' 29 | # FASTLANE_APPLE_ID: '${{ secrets.FASTLANE_APPLE_ID }}' 30 | # FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: '${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}' 31 | # MATCH_PASSWORD: '${{ secrets.MATCH_PASSWORD }}' 32 | # GIT_AUTHORIZATION: '${{ secrets.GIT_AUTHORIZATION }}' 33 | # PROVISIONING_PROFILE_SPECIFIER: '${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}' 34 | # TEMP_KEYCHAIN_PASSWORD: '${{ secrets.TEMP_KEYCHAIN_PASSWORD }}' 35 | # TEMP_KEYCHAIN_USER: '${{ secrets.TEMP_KEYCHAIN_USER }}' 36 | # APPLE_KEY_ID: '${{ secrets.APPLE_KEY_ID }}' 37 | # APPLE_ISSUER_ID: '${{ secrets.APPLE_ISSUER_ID }}' 38 | # APPLE_KEY_CONTENT: '${{ secrets.APPLE_KEY_CONTENT }}' 39 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | GoogleService-Info.plist -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SnippetsLibrary", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "SnippetsLibrary", 12 | targets: ["SnippetsLibrary"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "SnippetsLibrary", 23 | dependencies: []), 24 | .testTarget( 25 | name: "SnippetsLibraryTests", 26 | dependencies: ["SnippetsLibrary"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Snippets Library Logo 4 |

Snippets Library for SwiftUI

5 |

6 | 7 | [![Main Branch Actions Status](https://github.com/tryboxx/SnippetsLibrary/actions/workflows/tests.yml/badge.svg)](https://github.com/tryboxx/SnippetsLibrary/actions) 8 | [![Twitter Account](https://img.shields.io/twitter/follow/swift_devtools?color=00acee&label=twitter&style=flat-square)](https://twitter.com/swift_devtools) 9 | 10 |
11 | 12 | [**SnippetsLibrary 1.0.2 is now available!**](https://apps.apple.com/pl/app/snippets-library/id1585687204?mt=12) 13 | 14 | SnippetsLibrary is a helpful tool for SwiftUI developers to help with their daily coding life. SnippetsLibrary contains all the needed code snippets for you to view, edit, or add more and more. This will make your daily work easier and faster. 15 | 16 | ![SnippetsLibrary](public/images/app_preview.png) 17 | 18 | Table of Contents: 19 | 20 | - [Installation](#installation) 21 | - [App Store](#app-store) 22 | - [Test Flight](#test-flight) 23 | - [Getting Started](#getting-started) 24 | - [Availabe Functionalities](#availabe-functionalities) 25 | - [Language support](#language-support) 26 | - [Contributing](#contributing) 27 | - [Security](#security) 28 | - [Follow Us](#follow-us) 29 | - [License](#license) 30 | 31 | ## Installation 32 | 33 | The SnippetsLibrary app can be downloaded directly from the App Store or you can join the Developer Beta Testing Program via Test Flight: 34 | 35 | ### App Store 36 | 37 | Check the newest version of the Snippets Library app: 38 | 39 | ``` 40 | https://apps.apple.com/pl/app/snippets-library/id1585687204?mt=12 41 | ``` 42 | 43 | ### Test Flight 44 | 45 | You can join the SnippetsLibrary app beta testing by clicking on the invite link below (macOS Monterey required): 46 | 47 | ``` 48 | https://testflight.apple.com/join/P1tNGffH 49 | ``` 50 | 51 | ## Getting Started 52 | 53 | Getting started with the SnippetsLibrary is as simple as cloning this project with your favorite Git client, you can choose it from the recommended clients listed below: 54 | 55 | * [Sourcetree App](https://www.sourcetreeapp.com) 56 | * [GitHub Desktop](https://desktop.github.com) 57 | 58 | ### Availabe Functionalities 59 | 60 | * [**Search for snippets**](https://swiftdevtools/snippetslibrary/docs/snippet/searching) - Open and search in the snippets library. 61 | * [**Create new snippet**](https://swiftdevtools/snippetslibrary/docs/snippet/creating) - Create a new snippet file. 62 | * [**Upload existing snippet**](https://swiftdevtools/snippetslibrary/docs/snippet/uploading) - Import an existing code snippet. 63 | * [**Upload all snippets to Xcode**](https://swiftdevtools/snippetslibrary/docs/snippet/xcode-uploading) - Export all code snippets from the Snippets Library directly to the Xcode. 64 | * [**Download the code snippet**](https://swiftdevtools/snippetslibrary/docs/snippet/xcode-uploading) - Save a code snippet locally on your Mac. 65 | * [**Use status bar app quick menu**](https://swiftdevtools/snippetslibrary/docs/snippet/quick-menu) - Quick access to the featured code snippets, developer documentation and actions. 66 | * [**Add widgets to the notification center**](https://swiftdevtools/snippetslibrary/docs/snippet/widgets) - Open a snippet from the macOS widget. 67 | 68 | For the complete documentation, visit [https://swiftdevtools/snippetslibrary/docs](https://swiftdevtools/snippetslibrary/docs). For more tutorials, news and announcements check out our [blog](https://medium.com/swiftdevtools) or social media accounts. 69 | 70 | ### Language support 71 | * ✅   [SwiftUI](https://developer.apple.com/documentation/swiftui/) 72 | * ❌   Swift 73 | * ❌   Objective-C 74 | 75 | Looking for more language support? Help us by contributing a pull request with the code snippets! 76 | 77 | ## Contributing 78 | 79 | All code contributions must go through a pull request and be approved by a core developer before being merged. This is to ensure a proper review of all the code. 80 | 81 | We truly ❤️ pull requests! If you wish to help, you can email us at [contribution@swiftdevtools.com](mailto:contribution@swiftdevtools.com) or DM us on [Twitter](https://twitter.com/swift_devtools). 82 | 83 | ## Security 84 | 85 | For security issues, kindly email us at [security@swiftdevtools.com](mailto:security@swiftdevtools.com) instead of posting a public issue on GitHub. 86 | 87 | ## Follow Us 88 | 89 | Join our growing community around the world! See our official [Blog](https://medium.com/swiftdevtools). Follow us on [Twitter](https://twitter.com/swift_devtools) for more help, ideas, and discussions. 90 | 91 | ## License 92 | 93 | This repository is available under the [Apache 2.0](./LICENSE). 94 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.x | :white_check_mark: | 8 | 9 | ## Reports 10 | 11 | For security issues, kindly email us at security@swiftdevtools.com instead of posting a public issue on GitHub. 12 | -------------------------------------------------------------------------------- /SnippetsLibrary.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SnippetsLibrary.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SnippetsLibrary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "abseil", 6 | "repositoryURL": "https://github.com/firebase/abseil-cpp-SwiftPM.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "fffc3c2729be5747390ad02d5100291a0d9ad26a", 10 | "version": "0.20200225.4" 11 | } 12 | }, 13 | { 14 | "package": "BoringSSL-GRPC", 15 | "repositoryURL": "https://github.com/firebase/boringssl-SwiftPM.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "734a8247442fde37df4364c21f6a0085b6a36728", 19 | "version": "0.7.2" 20 | } 21 | }, 22 | { 23 | "package": "Firebase", 24 | "repositoryURL": "https://github.com/firebase/firebase-ios-sdk.git", 25 | "state": { 26 | "branch": "master", 27 | "revision": "08686f04881483d2bc098b2696e674c0ba135e47", 28 | "version": null 29 | } 30 | }, 31 | { 32 | "package": "GoogleAppMeasurement", 33 | "repositoryURL": "https://github.com/google/GoogleAppMeasurement.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "9b2f6aca5b4685c45f9f5481f19bee8e7982c538", 37 | "version": "8.9.1" 38 | } 39 | }, 40 | { 41 | "package": "GoogleDataTransport", 42 | "repositoryURL": "https://github.com/google/GoogleDataTransport.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "15ccdfd25ac55b9239b82809531ff26605e7556e", 46 | "version": "9.1.2" 47 | } 48 | }, 49 | { 50 | "package": "GoogleUtilities", 51 | "repositoryURL": "https://github.com/google/GoogleUtilities.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "b3bb0c5551fb3f80ca939829639ab5b093edd14f", 55 | "version": "7.7.0" 56 | } 57 | }, 58 | { 59 | "package": "gRPC", 60 | "repositoryURL": "https://github.com/firebase/grpc-SwiftPM.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "fb405dd2c7901485f7e158b24e3a0a47e4efd8b5", 64 | "version": "1.28.4" 65 | } 66 | }, 67 | { 68 | "package": "GTMSessionFetcher", 69 | "repositoryURL": "https://github.com/google/gtm-session-fetcher.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "bc6a19702ac76ac4e488b68148710eb815f9bc56", 73 | "version": "1.7.0" 74 | } 75 | }, 76 | { 77 | "package": "leveldb", 78 | "repositoryURL": "https://github.com/firebase/leveldb.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", 82 | "version": "1.22.2" 83 | } 84 | }, 85 | { 86 | "package": "nanopb", 87 | "repositoryURL": "https://github.com/firebase/nanopb.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "7ee9ef9f627d85cbe1b8c4f49a3ed26eed216c77", 91 | "version": "2.30908.0" 92 | } 93 | }, 94 | { 95 | "package": "PopupView", 96 | "repositoryURL": "https://github.com/exyte/PopupView.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "0d37d6f94e0ec9b38ab43300213ad72d28af921f", 100 | "version": "1.0.2" 101 | } 102 | }, 103 | { 104 | "package": "Promises", 105 | "repositoryURL": "https://github.com/google/promises.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "611337c330350c9c1823ad6d671e7f936af5ee13", 109 | "version": "2.0.0" 110 | } 111 | }, 112 | { 113 | "package": "Sourceful", 114 | "repositoryURL": "https://github.com/twostraws/Sourceful.git", 115 | "state": { 116 | "branch": "main", 117 | "revision": "04f7e85900451ebdc34345ee19bb0139f05f3a01", 118 | "version": null 119 | } 120 | }, 121 | { 122 | "package": "SwiftProtobuf", 123 | "repositoryURL": "https://github.com/apple/swift-protobuf.git", 124 | "state": { 125 | "branch": null, 126 | "revision": "7e2c5f3cbbeea68e004915e3a8961e20bd11d824", 127 | "version": "1.18.0" 128 | } 129 | }, 130 | { 131 | "package": "SwiftUISkeleton", 132 | "repositoryURL": "https://github.com/serbats/SwiftUISkeleton.git", 133 | "state": { 134 | "branch": "master", 135 | "revision": "fb41f67b39e9e23b9714ae41473462ca8bed4373", 136 | "version": null 137 | } 138 | } 139 | ] 140 | }, 141 | "version": 1 142 | } 143 | -------------------------------------------------------------------------------- /SnippetsLibrary.xcodeproj/xcshareddata/xcschemes/SnippetsLibrary.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /SnippetsLibrary.xcodeproj/xcuserdata/krzysztoflowiec.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 57 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /SnippetsLibrary.xcodeproj/xcuserdata/krzysztoflowiec.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Promises (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 24 13 | 14 | Promises (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 25 20 | 21 | Promises (Playground) 3.xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 1 27 | 28 | Promises (Playground) 4.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 5 34 | 35 | Promises (Playground) 5.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 6 41 | 42 | Promises (Playground).xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 23 48 | 49 | SnippetsLibrary.xcscheme_^#shared#^_ 50 | 51 | orderHint 52 | 0 53 | 54 | SnippetsLibraryWidgetExtension.xcscheme_^#shared#^_ 55 | 56 | orderHint 57 | 1 58 | 59 | 60 | SuppressBuildableAutocreation 61 | 62 | B82561AF26E81D570040A67E 63 | 64 | primary 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /SnippetsLibrary/Application/ActiveAppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActiveAppView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ActiveAppView: Identifiable, Equatable { 11 | case create 12 | case importSnippet 13 | case snippetsLibrary(_ directWithSnippetId: SnippetId?) 14 | 15 | var id: Int { 16 | switch self { 17 | case .create: return 0 18 | case .importSnippet: return 1 19 | case .snippetsLibrary: return 2 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SnippetsLibrary/Application/AppAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppAlert.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/10/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AppAlert: Identifiable { 11 | case snippetDownload 12 | 13 | var id: Int { 14 | switch self { 15 | case .snippetDownload: return 0 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SnippetsLibrary/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Firebase 10 | 11 | final class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Environment(\.openURL) var openURL 16 | 17 | internal var statusBarItem: NSStatusItem? 18 | internal var statusView: NSView? 19 | internal var menu: AppMenu? 20 | 21 | // MARK: - Methods 22 | 23 | func applicationDidFinishLaunching(_ notification: Notification) { 24 | FirebaseApp.configure() 25 | setupMenuItems() 26 | return 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /SnippetsLibrary/Application/AppMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppMenu.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class AppMenu: NSMenu { 11 | 12 | // MARK: - Stored Properties 13 | 14 | private lazy var applicationName = ProcessInfo.processInfo.processName 15 | 16 | private var statusItem: NSMenuItem { 17 | NSMenuItem( 18 | title: "Status", 19 | action: nil, 20 | keyEquivalent: "" 21 | ) 22 | } 23 | 24 | private var aboutItem: NSMenuItem { 25 | let item = NSMenuItem( 26 | title: "", 27 | action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), 28 | keyEquivalent: "" 29 | ) 30 | item.attributedTitle = NSAttributedString( 31 | string: "About \(applicationName)", 32 | attributes: [NSAttributedString.Key.font : NSFont.systemFont(ofSize: 13.0)] 33 | ) 34 | return item 35 | } 36 | 37 | private var docsItem: NSMenuItem { 38 | let item = NSMenuItem( 39 | title: "", 40 | action: nil, 41 | keyEquivalent: "" 42 | ) 43 | item.attributedTitle = NSAttributedString( 44 | string: "Developer Documentation...", 45 | attributes: [NSAttributedString.Key.font : NSFont.systemFont(ofSize: 13.0)] 46 | ) 47 | return item 48 | } 49 | 50 | private var quitItem: NSMenuItem { 51 | let item = NSMenuItem( 52 | title: "", 53 | action: #selector(NSApplication.terminate(_:)), 54 | keyEquivalent: "q" 55 | ) 56 | item.attributedTitle = NSAttributedString( 57 | string: "Quit \(applicationName)", 58 | attributes: [NSAttributedString.Key.font : NSFont.systemFont(ofSize: 13.0)] 59 | ) 60 | return item 61 | } 62 | 63 | // MARK: - Life Cycle 64 | 65 | override init(title: String) { 66 | super.init(title: title) 67 | 68 | setupMenuItems() 69 | NSApplication.shared.windows.first?.delegate = self 70 | } 71 | 72 | required init(coder: NSCoder) { 73 | super.init(coder: coder) 74 | } 75 | 76 | // MARK: - Methods 77 | 78 | private func setupMenuItems() { 79 | items = [ 80 | statusItem, 81 | NSMenuItem.separator(), 82 | aboutItem, 83 | docsItem, 84 | NSMenuItem.separator(), 85 | quitItem 86 | ] 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /SnippetsLibrary/Application/AppSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSheet.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum AppSheet: Identifiable { 11 | case snippetDetails(_ snippet: Snippet, _ type: SnippetDetailsViewType) 12 | case snippetsUpload 13 | 14 | var id: Int { 15 | switch self { 16 | case .snippetDetails: return 0 17 | case .snippetsUpload: return 1 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Application/AppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppView: View { 11 | 12 | private enum Constants { 13 | static let statusWindowClassName = "NSStatusBarWindow" 14 | static let delay = DispatchTime.now() + 0.01 15 | } 16 | 17 | // MARK: - Stored Properties 18 | 19 | @Environment(\.scenePhase) var scenePhase 20 | 21 | @Binding internal var activeAppView: ActiveAppView? 22 | @Binding internal var activeAppSheet: AppSheet? 23 | @Binding internal var shouldBeDisabled: Bool 24 | 25 | @ObservedObject private var networkObserver = NetworkObserver() 26 | @State private var shouldShowNetworkAlert = false 27 | 28 | // MARK: - Views 29 | 30 | var body: some View { 31 | showActiveAppView() 32 | .onChange(of: networkObserver.isConnected) { _ in 33 | withAnimation { 34 | shouldShowNetworkAlert.toggle() 35 | } 36 | } 37 | .onOpenURL { 38 | openSnippet(fromURL: $0) 39 | } 40 | .makeDisplayed( 41 | with: $shouldShowNetworkAlert, 42 | imageName: "network", 43 | title: networkObserver.isConnected ? "Connected" : "No connection", 44 | state: networkObserver.isConnected ? .success : .failure 45 | ) 46 | } 47 | 48 | // MARK: - Methods 49 | 50 | @ViewBuilder 51 | private func showActiveAppView() -> some View { 52 | switch activeAppView { 53 | case .create: 54 | SnippetsLibraryView( 55 | viewModel: SnippetsLibraryViewModel(hasConnection: $networkObserver.isConnected), 56 | activeSheet: $activeAppSheet 57 | ) 58 | .onAppear { 59 | updateSystemButtons(hidden: false) 60 | } 61 | case .importSnippet: 62 | SnippetImportView(viewModel: SnippetImportViewModel(activeAppView: $activeAppView)) 63 | .onAppear { 64 | updateSystemButtons(hidden: false) 65 | } 66 | case let .snippetsLibrary(snippetId): 67 | SnippetsLibraryView( 68 | viewModel: SnippetsLibraryViewModel( 69 | activeSnippetId: snippetId, 70 | hasConnection: $networkObserver.isConnected 71 | ), 72 | activeSheet: $activeAppSheet 73 | ) 74 | .onAppear { 75 | updateSystemButtons(hidden: false) 76 | } 77 | case .none: 78 | StartView( 79 | viewModel: StartViewModel( 80 | activeAppView: $activeAppView, 81 | activeAppSheet: $activeAppSheet 82 | ) 83 | ) 84 | .onChange(of: scenePhase) { 85 | guard $0 == .active else { return } 86 | 87 | updateSystemButtons(hidden: true) 88 | } 89 | } 90 | } 91 | 92 | private func updateSystemButtons(hidden: Bool) { 93 | for window in NSApplication.shared.windows { 94 | window.standardWindowButton(.zoomButton)?.isHidden = hidden 95 | window.standardWindowButton(.miniaturizeButton)?.isHidden = hidden 96 | window.standardWindowButton(.closeButton)?.isHidden = hidden 97 | } 98 | } 99 | 100 | private func openSnippet(fromURL url: URL) { 101 | closeWindowsIfVisible() 102 | let snippetId = url.absoluteString.replacingOccurrences(of: "widget://", with: "") 103 | 104 | DispatchQueue.main.asyncAfter(deadline: Constants.delay) { 105 | NSApplication.shared.windows.first?.orderFront(nil) 106 | activeAppView = .snippetsLibrary(snippetId) 107 | } 108 | } 109 | 110 | private func closeWindowsIfVisible() { 111 | for window in NSApplication.shared.windows where window.className != Constants.statusWindowClassName { 112 | DispatchQueue.main.async { 113 | window.close() 114 | } 115 | } 116 | } 117 | 118 | } 119 | 120 | 121 | struct AppView_Previews: PreviewProvider { 122 | static var previews: some View { 123 | AppView(activeAppView: .constant(nil), activeAppSheet: .constant(nil), shouldBeDisabled: .constant(false)) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /SnippetsLibrary/Application/SnippetsLibraryApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryApp.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SnippetsLibraryApp: App { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Environment(\.openURL) var openURL 16 | 17 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 18 | 19 | @State private var activeAppView: ActiveAppView? = nil 20 | @State private var activeAppSheet: AppSheet? = nil 21 | @State private var shouldBeDisabled = false 22 | 23 | // MARK: - Views 24 | 25 | var body: some Scene { 26 | WindowGroup { 27 | AppView( 28 | activeAppView: $activeAppView, 29 | activeAppSheet: $activeAppSheet, 30 | shouldBeDisabled: $shouldBeDisabled 31 | ) 32 | .onReceive(NotificationCenter.default.publisher(for: NSNotification.statusBarSnippetTapped)) { 33 | openSnippet(from: $0) 34 | } 35 | } 36 | .windowStyle(HiddenTitleBarWindowStyle()) 37 | .onChange(of: activeAppView) { 38 | shouldBeDisabled = ($0 == .snippetsLibrary(nil)) 39 | } 40 | .commands { 41 | CommandGroup(replacing: .newItem) { 42 | Button("Add New Code Snippet...") { 43 | if activeAppView != .snippetsLibrary(nil) { 44 | activeAppView = .create 45 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 46 | activeAppSheet = .snippetDetails(Snippet(), .create) 47 | } 48 | } else { 49 | activeAppSheet = .snippetDetails(Snippet(), .create) 50 | } 51 | } 52 | 53 | Button("Import Local Snippet...") { 54 | activeAppView = .importSnippet 55 | } 56 | 57 | DisabledCommandGroupButton( 58 | text: "Open Snippets Library...", 59 | shouldBeDisabled: $shouldBeDisabled, 60 | type: .openLibrary 61 | ) { 62 | activeAppView = .snippetsLibrary(nil) 63 | } 64 | 65 | Divider() 66 | 67 | DisabledCommandGroupButton( 68 | text: "Upload All Snippets To Xcode...", 69 | shouldBeDisabled: $shouldBeDisabled, 70 | type: .uploadSnippets 71 | ) { 72 | activeAppSheet = .snippetsUpload 73 | } 74 | .keyboardShortcut( 75 | "u", 76 | modifiers: [.command, .control, .option] 77 | ) 78 | } 79 | 80 | CommandGroup(replacing: .help) { 81 | Button("Report issue...") { 82 | composeEmail() 83 | } 84 | 85 | Button("Show User Guides") { 86 | openURL(url: DIContainer.urlFactory.getURL(withType: .userGuides)) 87 | } 88 | } 89 | 90 | CommandGroup(replacing: .windowList) { 91 | Button("Developer Documentation") { 92 | openURL(url: DIContainer.urlFactory.getURL(withType: .docs)) 93 | } 94 | .keyboardShortcut( 95 | "0", 96 | modifiers: [.command, .shift]) 97 | } 98 | } 99 | } 100 | 101 | // MARK: - Methods 102 | 103 | private func composeEmail() { 104 | let service = NSSharingService(named: NSSharingService.Name.composeEmail) 105 | service?.recipients = ["support@swiftdevtools.com"] 106 | service?.subject = "Snippets Library Issue" 107 | service?.perform(withItems: ["Please describe your problem here..."]) 108 | } 109 | 110 | private func openURL(url: URL?) { 111 | guard let safeURL = url else { return } 112 | openURL(safeURL) 113 | } 114 | 115 | private func openSnippet(from output: NotificationCenter.Publisher.Output) { 116 | if let userInfo = output.userInfo, let snippetId = userInfo["snippetId"] as? String { 117 | NSApplication.shared.windows.first?.center() 118 | NSApplication.shared.windows.first?.makeKeyAndOrderFront(nil) 119 | NSApp.activate(ignoringOtherApps: true) 120 | activeAppView = .snippetsLibrary(snippetId) 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.478", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/256-1.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/32-1.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/512-1.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32-1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256-1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512-1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/accent.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.478", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.125", 9 | "green" : "0.125", 10 | "red" : "0.125" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/darkGrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.180", 9 | "green" : "0.176", 10 | "red" : "0.176" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/defaultBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.971", 9 | "green" : "0.962", 10 | "red" : "0.966" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.102", 27 | "green" : "0.102", 28 | "red" : "0.102" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/secondaryBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.188", 9 | "green" : "0.165", 10 | "red" : "0.161" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/skeletonLight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.837", 9 | "green" : "0.837", 10 | "red" : "0.837" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.261", 27 | "green" : "0.261", 28 | "red" : "0.261" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/skeletonRegular.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.754", 9 | "green" : "0.754", 10 | "red" : "0.754" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.371", 27 | "green" : "0.371", 28 | "red" : "0.371" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Colors/skeletonThin.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.894", 9 | "green" : "0.895", 10 | "red" : "0.894" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.130", 27 | "green" : "0.130", 28 | "red" : "0.130" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icLogo.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icLogo.imageset/icLogo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/icLogo.imageset/icLogo.pdf -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icLogoStatusBar.imageset/Books_front_matte.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/icLogoStatusBar.imageset/Books_front_matte.pdf -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icLogoStatusBar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Books_front_matte.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icSnippetFileBlank.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icSnippetFileBlank.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icSnippetFileBlank.imageset/icSnippetFileBlank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/icSnippetFileBlank.imageset/icSnippetFileBlank.png -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icSnippetFileWhite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icSnippetFile.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SnippetsLibrary/Assets.xcassets/icSnippetFileWhite.imageset/icSnippetFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/SnippetsLibrary/Assets.xcassets/icSnippetFileWhite.imageset/icSnippetFile.png -------------------------------------------------------------------------------- /SnippetsLibrary/Dependencies/DependencyContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyContainer.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | let DIContainer = DependencyContainer.shared 11 | 12 | final class DependencyContainer { 13 | 14 | // MARK: - Services 15 | 16 | lazy var snippetsParserService: SnippetsParserService = SnippetsParserServiceImpl() 17 | lazy var urlFactory: URLFactory = URLFactoryImpl() 18 | lazy var networkServcie: NetworkService = NetworkServiceImpl() 19 | 20 | lazy var databaseService: DatabaseService = DatabaseServiceImpl( 21 | logsService: logsService, 22 | crashlyticsService: crashlyticsService 23 | ) 24 | lazy var logsService: LogsService = LogsServiceImpl() 25 | lazy var crashlyticsService: CrashlyticsService = CrashlyticsServiceImpl() 26 | lazy var userDefaultsService: UserDefaultsService = UserDefaultsServiceImpl() 27 | 28 | // MARK: - Shared 29 | 30 | static let shared = DependencyContainer() 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/AnyPublisher+SinkWithoutValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyPublisher+SinkWithoutValue.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/09/2021. 6 | // 7 | 8 | import Combine 9 | 10 | extension AnyPublisher where Self.Output == Void { 11 | 12 | func sink(receiveCompletion: @escaping (Subscribers.Completion) -> Void) -> AnyCancellable { 13 | return sink(receiveCompletion: receiveCompletion) { _ in } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/AppDelegate+NSMenuDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+NSMenuDelegate.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension AppDelegate: NSMenuDelegate { 11 | 12 | func menuWillOpen(_ menu: NSMenu) { 13 | setupStatusView(for: menu) 14 | } 15 | 16 | func menuDidClose(_ menu: NSMenu) {} 17 | 18 | internal func setupMenuItems() { 19 | menu = AppMenu() 20 | 21 | self.statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 22 | self.statusBarItem?.menu = menu 23 | menu?.delegate = self 24 | 25 | if let item = menu?.items.first(where: { $0.title == "Developer Documentation..." }) { 26 | item.action = #selector(openDocsURL(_:)) 27 | } 28 | 29 | if let button = self.statusBarItem?.button { 30 | button.image = NSImage(named: "icLogoStatusBar") 31 | } 32 | } 33 | 34 | private func setupStatusView(for menu: NSMenu) { 35 | if let item = menu.items.first { 36 | statusView = NSHostingView(rootView: StatusView()) 37 | setupItemView( 38 | menu: menu, 39 | item: item 40 | ) 41 | } 42 | } 43 | 44 | private func setupItemView( 45 | menu: NSMenu, 46 | item: NSMenuItem 47 | ) { 48 | statusView?.frame = CGRect( 49 | x: .zero, 50 | y: .zero, 51 | width: 250.0, 52 | height: 170.0 53 | ) 54 | item.view = statusView 55 | } 56 | 57 | @objc private func openDocsURL(_ sender: Any) { 58 | let urlFactory = DIContainer.urlFactory 59 | let docsURL = urlFactory.getURL(withType: .docs) 60 | guard let safeURL = docsURL else { return } 61 | openURL(safeURL) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/AppMenu+HideWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppMenu+HideWindow.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension AppMenu: NSWindowDelegate { 11 | 12 | func windowShouldClose(_ sender: NSWindow) -> Bool { 13 | NSApp.hide(nil) 14 | return false 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/DatabaseReference+Timeout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseReference+Timeout.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import FirebaseDatabase 9 | 10 | extension DatabaseReference { 11 | 12 | func observe( 13 | _ eventType: DataEventType, 14 | timeout: TimeInterval, 15 | with block: @escaping (DataSnapshot?) -> Void 16 | ) -> UInt { 17 | var handle: UInt! 18 | 19 | let timer = Timer.scheduledTimer( 20 | withTimeInterval: timeout, 21 | repeats: false 22 | ) { _ in 23 | self.removeObserver(withHandle: handle) 24 | block(nil) 25 | } 26 | 27 | handle = observe(eventType) { snapshot in 28 | timer.invalidate() 29 | block(snapshot) 30 | } 31 | 32 | return handle 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/NSApplication+AppVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSApplication+AppVersion.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension NSApplication { 11 | 12 | static var appVersion: String { 13 | let versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String 14 | let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String 15 | 16 | let formattedBuildNumber = buildNumber.map { 17 | return "(\($0))" 18 | } 19 | 20 | return [versionNumber,formattedBuildNumber].compactMap { $0 }.joined(separator: " ") 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/NSNotification+Name.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSNotification+Name.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSNotification { 11 | 12 | static let statusBarSnippetTapped = Notification.Name("statusBarSnippetTapped") 13 | 14 | } 15 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/NSTableView+BackgroundColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTableView+BackgroundColor.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension NSTableView { 11 | 12 | open override func viewDidMoveToWindow() { 13 | super.viewDidMoveToWindow() 14 | 15 | backgroundColor = NSColor.clear 16 | enclosingScrollView?.drawsBackground = false 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/SnippetPlist+Dictonary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetPlist+Dictonary.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 19/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension SnippetPlist { 11 | 12 | func convertedToDictonary(id: String? = nil) -> [String: Any] { 13 | return [ 14 | "IDECodeSnippetIdentifier": id ?? self.id, 15 | "IDECodeSnippetTitle": title, 16 | "IDECodeSnippetSummary": summary, 17 | "IDECodeSnippetCompletionPrefix": completion, 18 | "IDECodeSnippetCompletionScopes": availability, 19 | "IDECodeSnippetContents": contents, 20 | "IDECodeSnippetLanguage": language, 21 | "IDECodeSnippetPlatformFamily": platform, 22 | "IDECodeSnippetUserSnippet": userSnippet, 23 | "IDECodeSnippetVersion": version, 24 | "IDECodeSnippetAuthor": author ?? "Christopher Lowiec" 25 | ] as [String: Any] 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/SnippetsLibraryView+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryView+Equatable.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension SnippetsLibraryView: Equatable { 11 | 12 | static func == (lhs: SnippetsLibraryView, rhs: SnippetsLibraryView) -> Bool { 13 | return lhs.viewModel.snippets != rhs.viewModel.snippets 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/StartView+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartView+Equatable.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension StartView: Equatable { 11 | 12 | static func == (lhs: StartView, rhs: StartView) -> Bool { 13 | return lhs.viewModel.activeAppView == rhs.viewModel.activeAppView 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/10/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | 12 | func numberOfLines() -> Int { 13 | return numberOfOccurrencesOf(string: "\n") + 1 14 | } 15 | 16 | func numberOfOccurrencesOf(string: String) -> Int { 17 | return components(separatedBy: string).count - 1 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/View+Displayed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Displayed.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | 12 | func makeDisplayed( 13 | with visible: Binding, 14 | imageName: String? = nil, 15 | title: String, 16 | subtitle: String? = nil, 17 | displayingTime: Double = 2, 18 | state: ToastViewState = .none 19 | ) -> some View { 20 | self.overlay( 21 | ToastView( 22 | imageName: imageName, 23 | title: title, 24 | subtitle: subtitle, 25 | state: state 26 | ) 27 | .offset(y: visible.wrappedValue ? -((Layout.defaultWindowSize.height / 2) - Layout.smallPadding) : 0) 28 | .makeVisible( 29 | visible.wrappedValue, 30 | removed: true 31 | ) 32 | .animation(.default) 33 | .onChange(of: visible.wrappedValue) { _ in 34 | DispatchQueue.main.asyncAfter(deadline: .now() + displayingTime) { 35 | withAnimation { 36 | visible.wrappedValue = false 37 | } 38 | } 39 | } 40 | .onTapGesture { 41 | withAnimation { 42 | visible.wrappedValue = false 43 | } 44 | } 45 | ) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/View+Skeletonable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Skeletonable.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUISkeleton 10 | 11 | extension View { 12 | 13 | func makeSkeletonable( 14 | animating: Bool, 15 | isBottomView: Bool = false 16 | ) -> some View { 17 | self.skeleton( 18 | with: animating, 19 | shape: RoundedRectangle(cornerRadius: 4.0), 20 | animation: .linear(duration: 1) 21 | .repeatForever(autoreverses: true), 22 | gradient: 23 | Gradient( 24 | colors: [ 25 | Color(isBottomView ? "skeletonLight" : "skeletonRegular"), 26 | Color(isBottomView ? "skeletonThin": "skeletonLight") 27 | ] 28 | ) 29 | ) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SnippetsLibrary/Extensions/View+Visibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Visibility.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | 12 | @ViewBuilder func makeVisible( 13 | _ visible: Bool, 14 | removed: Bool = false 15 | ) -> some View { 16 | if !visible { 17 | if !removed { 18 | self.hidden() 19 | } 20 | } else { 21 | self 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /SnippetsLibrary/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSApplicationCategoryType 22 | public.app-category.developer-tools 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Apache 2.0 License. All trademarks are protected. 27 | NSSystemAdministrationUsageDescription 28 | I want to read all your files 29 | 30 | 31 | -------------------------------------------------------------------------------- /SnippetsLibrary/Models/Snippet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Snippet.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias SnippetId = String 11 | 12 | struct Snippet: Codable, Identifiable, Hashable { 13 | 14 | // MARK: - Stored Properties 15 | 16 | let id: SnippetId 17 | var title: String 18 | var summary: String 19 | var content: String 20 | var author: String 21 | var completion: String 22 | var platform: SnippetPlatform 23 | var availability: SnippetAvailability 24 | var tags: [String]? 25 | 26 | // MARK: - Computed Properties 27 | 28 | var type: SnippetType { 29 | title.contains("+") ? .extending : .snippets 30 | } 31 | 32 | var category: SnippetCategory { 33 | return SnippetCategory.allCases.first(where: { title.lowercased().contains($0.rawValue) }) ?? .helper 34 | } 35 | 36 | // MARK: - CodingKeys 37 | 38 | enum CodingKeys: String, CodingKey { 39 | case id, title, summary, content, author, completion, platform, availability, tags 40 | } 41 | 42 | // MARK: - Initialization 43 | 44 | init() { 45 | id = UUID().uuidString 46 | title = "" 47 | summary = "" 48 | content = "" 49 | author = "" 50 | completion = "" 51 | platform = .all 52 | availability = .allScopes 53 | } 54 | 55 | init(mockedForSkeleton: Bool) { 56 | id = UUID().uuidString 57 | title = "Default snippet title" 58 | summary = "Default snippet summary" 59 | content = "" 60 | author = "" 61 | completion = "" 62 | platform = .all 63 | availability = .allScopes 64 | } 65 | 66 | init( 67 | id: SnippetId, 68 | title: String, 69 | summary: String, 70 | content: String, 71 | author: String, 72 | completion: String, 73 | platform: SnippetPlatform, 74 | availability: SnippetAvailability, 75 | tags: [String] = [] 76 | ) { 77 | self.id = id 78 | self.title = title 79 | self.summary = summary 80 | self.content = content 81 | self.author = author 82 | self.completion = completion 83 | self.platform = platform 84 | self.availability = availability 85 | self.tags = tags 86 | } 87 | 88 | init(from snippetPlist: SnippetPlist) { 89 | id = snippetPlist.id 90 | title = snippetPlist.title 91 | summary = snippetPlist.summary 92 | content = snippetPlist.contents 93 | author = snippetPlist.author ?? "" 94 | completion = snippetPlist.completion 95 | platform = SnippetPlatform.allCases.first(where: { $0.rawValue == snippetPlist.platform }) ?? SnippetPlatform.all 96 | availability = SnippetAvailability.allCases.first(where: { $0.string == snippetPlist.availability.first }) ?? SnippetAvailability.allScopes 97 | tags = [] 98 | } 99 | 100 | init(from decoder: Decoder) throws { 101 | let values = try decoder.container(keyedBy: PlistCodingKeys.self) 102 | id = try values.decode(String.self, forKey: .id) 103 | title = try values.decode(String.self, forKey: .title) 104 | summary = try values.decode(String.self, forKey: .summary) 105 | content = try values.decode(String.self, forKey: .contents) 106 | author = try values.decode(String.self, forKey: .author) 107 | completion = try values.decode(String.self, forKey: .completion) 108 | 109 | let platformName = try values.decode(String.self, forKey: .platform) 110 | platform = SnippetPlatform.allCases.first { $0.rawValue == platformName } ?? .all 111 | 112 | let availabilityName = try values.decode([String].self, forKey: .availability) 113 | availability = SnippetAvailability.allCases.first { $0.string == availabilityName.first } ?? .allScopes 114 | tags = try? values.decode([String].self, forKey: .tags) 115 | } 116 | 117 | // MARK: - Methods 118 | 119 | func encode(to encoder: Encoder) throws { 120 | var container = encoder.container(keyedBy: CodingKeys.self) 121 | try container.encode(id, forKey: .id) 122 | try container.encode(title, forKey: .title) 123 | try container.encode(summary, forKey: .summary) 124 | try container.encode(content, forKey: .content) 125 | try container.encode(author, forKey: .author) 126 | try container.encode(completion, forKey: .completion) 127 | try container.encode(platform.title, forKey: .platform) 128 | try container.encode(availability.title, forKey: .availability) 129 | try? container.encode(tags, forKey: .tags) 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /SnippetsLibrary/Models/SnippetPlist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetPlist.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SnippetPlist: Codable { 11 | 12 | var id: String 13 | var title: String 14 | var summary: String 15 | var contents: String 16 | var author: String? 17 | var language: String 18 | var completion: String 19 | var platform: String 20 | var availability: [String] 21 | var userSnippet: Bool 22 | var version: Int 23 | 24 | init(from snippet: Snippet, withId id: String? = nil) { 25 | self.id = id ?? snippet.id 26 | title = snippet.title 27 | summary = snippet.summary 28 | contents = snippet.content 29 | author = snippet.author 30 | completion = snippet.completion 31 | platform = snippet.platform.rawValue 32 | availability = [snippet.availability.string] 33 | language = "Xcode.SourceCodeLanguage.Swift" 34 | userSnippet = true 35 | version = 2 36 | } 37 | 38 | init(from decoder: Decoder) throws { 39 | let values = try decoder.container(keyedBy: PlistCodingKeys.self) 40 | id = try values.decode(String.self, forKey: .id) 41 | title = try values.decode(String.self, forKey: .title) 42 | summary = try values.decode(String.self, forKey: .summary) 43 | contents = try values.decode(String.self, forKey: .contents) 44 | author = try values.decode(String.self, forKey: .author) 45 | completion = try values.decode(String.self, forKey: .completion) 46 | language = try values.decode(String.self, forKey: .language) 47 | userSnippet = try values.decode(Bool.self, forKey: .userSnippet) 48 | version = try values.decode(Int.self, forKey: .version) 49 | 50 | let platformName = try values.decode(String.self, forKey: .platform) 51 | platform = SnippetPlatform.allCases.first { $0.rawValue == platformName }?.rawValue ?? SnippetPlatform.all.rawValue 52 | 53 | let availabilityKey = try values.decode([String].self, forKey: .availability) 54 | availability = [SnippetAvailability.allCases.first { $0.string == availabilityKey.first }?.string ?? SnippetAvailability.allScopes.string] 55 | } 56 | 57 | func encode(to encoder: Encoder) throws { 58 | var container = encoder.container(keyedBy: PlistCodingKeys.self) 59 | try container.encode(id, forKey: .id) 60 | try container.encode(title, forKey: .title) 61 | try container.encode(summary, forKey: .summary) 62 | try container.encode(contents, forKey: .contents) 63 | try container.encode(author, forKey: .author) 64 | try container.encode(completion, forKey: .completion) 65 | try container.encode(platform, forKey: .platform) 66 | try container.encode(availability, forKey: .availability) 67 | try container.encode(language, forKey: .language) 68 | try container.encode(userSnippet, forKey: .userSnippet) 69 | try container.encode(version, forKey: .version) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetDetails/SnippetDetailsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetDetailsViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 11/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class SnippetDetailsViewModel: ObservableObject { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Published var snippet: Snippet 16 | @Published var text: String = "" 17 | @Binding private(set) var activeAppView: ActiveAppView? 18 | 19 | let type: SnippetDetailsViewType 20 | private var lastSavedSnippet: Snippet? = nil 21 | 22 | @Published var platformSelectionIndex = 0 23 | let platforms = SnippetPlatform.allCases 24 | 25 | @Published var availabilitySelectionIndex = 0 26 | let availabilities = SnippetAvailability.allCases 27 | 28 | @Published private(set) var shouldDismissView = false 29 | @Published var shouldShowErrorAlert = false 30 | 31 | private let snippetsParserService: SnippetsParserService 32 | private let databaseService: DatabaseService 33 | 34 | private var cancellables = Set() 35 | 36 | // MARK: - Computed Properties 37 | 38 | var hasChanges: Bool { 39 | lastSavedSnippet != snippet && !snippet.title.isEmpty && 40 | !snippet.summary.isEmpty && !snippet.content.isEmpty && 41 | !snippet.completion.isEmpty 42 | } 43 | 44 | // MARK: - Initialization 45 | 46 | init( 47 | snippet: Snippet, 48 | type: SnippetDetailsViewType, 49 | activeAppView: Binding, 50 | snippetsParserService: SnippetsParserService = DIContainer.snippetsParserService, 51 | databaseService: DatabaseService = DIContainer.databaseService 52 | ) { 53 | self.snippet = snippet 54 | self.type = type 55 | self._activeAppView = activeAppView 56 | self.snippetsParserService = snippetsParserService 57 | self.databaseService = databaseService 58 | 59 | setup() 60 | text = snippet.content 61 | } 62 | 63 | // MARK: - Methods 64 | 65 | func performChanges() { 66 | guard hasChanges else { return } 67 | 68 | if type == .create { 69 | createSnippet() 70 | } else { 71 | updateSnippet() 72 | } 73 | } 74 | 75 | private func createSnippet() { 76 | snippetsParserService.createSnippet(snippet) 77 | .sink { [weak self] completion in 78 | switch completion { 79 | case .finished: return 80 | case .failure: 81 | self?.shouldShowErrorAlert.toggle() 82 | } 83 | } receiveValue: { [weak self] in 84 | self?.saveSnippetsInDatabase($0) 85 | } 86 | .store(in: &cancellables) 87 | } 88 | 89 | private func updateSnippet() { 90 | databaseService.updateSnippet(snippet) 91 | .sink { [weak self] completion in 92 | switch completion { 93 | case .finished: 94 | DispatchQueue.main.async { 95 | self?.shouldDismissView.toggle() 96 | } 97 | case .failure: 98 | self?.shouldShowErrorAlert.toggle() 99 | } 100 | } 101 | .store(in: &cancellables) 102 | } 103 | 104 | private func saveSnippetsInDatabase(_ snippet: SnippetPlist) { 105 | databaseService.saveSnippet(snippet) 106 | .sink { [weak self] completion in 107 | switch completion { 108 | case .finished: 109 | DispatchQueue.main.async { 110 | self?.shouldDismissView.toggle() 111 | } 112 | case .failure: 113 | self?.shouldShowErrorAlert.toggle() 114 | } 115 | } 116 | .store(in: &cancellables) 117 | } 118 | 119 | private func setup() { 120 | lastSavedSnippet = snippet 121 | platformSelectionIndex = SnippetPlatform.allCases.firstIndex(of: snippet.platform) ?? 0 122 | availabilitySelectionIndex = SnippetAvailability.allCases.firstIndex(of: snippet.availability) ?? 0 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetImport/SnippetImportView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetImportView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetImportView: View { 11 | 12 | private enum Constants { 13 | static let dropCellSize = CGSize(width: 250, height: 170) 14 | static let cornerRadius: CGFloat = 12.0 15 | static let lineWidth: CGFloat = 1 16 | static let dashCount: CGFloat = 10 17 | } 18 | 19 | // MARK: - Stored Properties 20 | 21 | @ObservedObject private(set) var viewModel: SnippetImportViewModel 22 | 23 | // MARK: - Views 24 | 25 | var body: some View { 26 | ZStack { 27 | VisualEffectView( 28 | material: .windowBackground, 29 | blendingMode: .behindWindow 30 | ) 31 | 32 | ZStack { 33 | VStack { 34 | SnippetDropCellView(snippet: viewModel.snippet) 35 | .frame( 36 | width: Constants.dropCellSize.width, 37 | height: Constants.dropCellSize.height 38 | ) 39 | .overlay( 40 | RoundedRectangle(cornerRadius: Constants.cornerRadius) 41 | .stroke( 42 | style: StrokeStyle( 43 | lineWidth: Constants.lineWidth, 44 | dash: [Constants.dashCount] 45 | ) 46 | ) 47 | .foregroundColor(Color.primary) 48 | ) 49 | .padding([.horizontal, .bottom]) 50 | } 51 | } 52 | .onDrop( 53 | of: [.fileURL], 54 | delegate: viewModel.self 55 | ) 56 | } 57 | .makeDisplayed( 58 | with: $viewModel.shouldShowErrorAlert, 59 | imageName: "network", 60 | title: "Network error", 61 | subtitle: "Unable to import the snippet", 62 | state: .failure 63 | ) 64 | } 65 | 66 | } 67 | 68 | struct SnippetImportView_Previews: PreviewProvider { 69 | static var previews: some View { 70 | SnippetImportView(viewModel: SnippetImportViewModel(activeAppView: .constant(nil))) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetImport/SnippetImportViewModel+DropDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetImportViewModel+DropDelegate.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SnippetImportViewModel: DropDelegate { 11 | 12 | func validateDrop(info: DropInfo) -> Bool { 13 | debugPrint("DropInfo: \(info)") 14 | return true 15 | } 16 | 17 | func performDrop(info: DropInfo) -> Bool { 18 | receiveDrop( 19 | dropIndex: .zero, 20 | itemProviders: info.itemProviders(for: [.fileURL]) 21 | ) 22 | } 23 | 24 | @discardableResult 25 | func receiveDrop( 26 | dropIndex: Int, 27 | itemProviders: [NSItemProvider], 28 | create: Bool = true 29 | ) -> Bool { 30 | var result = false 31 | for provider in itemProviders { 32 | if provider.canLoadObject(ofClass: String.self) { 33 | result = true 34 | loadSnippet( 35 | from: provider, 36 | at: dropIndex 37 | ) 38 | } 39 | } 40 | return result 41 | } 42 | 43 | private func loadSnippet( 44 | from provider: NSItemProvider, 45 | at dropIndex: Int 46 | ) { 47 | provider.loadItem(forTypeIdentifier: kUTTypeFileURL as String) { [weak self] (data, error) in 48 | guard 49 | let safeData = data as? Data, 50 | let url = URL(dataRepresentation: safeData, relativeTo: nil) 51 | else { 52 | // Unable to load 53 | return 54 | } 55 | 56 | let decoder = PropertyListDecoder() 57 | 58 | guard 59 | let dataFromURL = try? Data(contentsOf: url), 60 | let snippet = try? decoder.decode(Snippet.self, from: dataFromURL) 61 | else { 62 | // Unable to load 63 | return 64 | } 65 | 66 | self?.createSnippet(snippet) 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetImport/SnippetImportViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetImportViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class SnippetImportViewModel: ObservableObject { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Published internal var snippet: Snippet? 16 | @Binding internal var activeAppView: ActiveAppView? 17 | 18 | @Published internal var shouldShowErrorAlert = false 19 | 20 | private let snippetsParserService: SnippetsParserService 21 | private let databaseService: DatabaseService 22 | private let crashlyticsService: CrashlyticsService 23 | 24 | internal var cancellables = Set() 25 | 26 | // MARK: - Initialization 27 | 28 | init( 29 | snippetsParserService: SnippetsParserService = DIContainer.snippetsParserService, 30 | databaseService: DatabaseService = DIContainer.databaseService, 31 | crashlyticsService: CrashlyticsService = DIContainer.crashlyticsService, 32 | activeAppView: Binding 33 | ) { 34 | self.snippetsParserService = snippetsParserService 35 | self._activeAppView = activeAppView 36 | self.databaseService = databaseService 37 | self.crashlyticsService = crashlyticsService 38 | } 39 | 40 | // MARK: - Methods 41 | 42 | internal func createSnippet(_ snippet: Snippet) { 43 | snippetsParserService.createSnippet(snippet) 44 | .sink { [weak self] completion in 45 | switch completion { 46 | case .finished: return 47 | case .failure: 48 | self?.shouldShowErrorAlert.toggle() 49 | self?.crashlyticsService.logNonFatalError(.unableToCreateSnippetFromDroppedFile) 50 | } 51 | } receiveValue: { [weak self] in 52 | self?.saveSnippetsInDatabase($0) 53 | } 54 | .store(in: &self.cancellables) 55 | } 56 | 57 | internal func saveSnippetsInDatabase(_ snippet: SnippetPlist) { 58 | databaseService.saveSnippet(snippet) 59 | .sink { [weak self] completion in 60 | switch completion { 61 | case .finished: 62 | self?.activeAppView = .snippetsLibrary(snippet.id) 63 | case .failure: 64 | self?.shouldShowErrorAlert.toggle() 65 | } 66 | } 67 | .store(in: &cancellables) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsLibrary/SnippetLibraryList/SnippetsLibraryListFilterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryListFilterView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 01/10/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetsLibraryListFilterView: View { 11 | 12 | private enum Constants { 13 | static let dividerRotationDegree = 90.0 14 | static let viewHeight: CGFloat = 24.0 15 | } 16 | 17 | // MARK: - Stored Properites 18 | 19 | @Binding internal var selectedCategory: SnippetCategory? 20 | private let categories = SnippetCategory.allCases 21 | 22 | // MARK: - Views 23 | 24 | var body: some View { 25 | ScrollView( 26 | .horizontal, 27 | showsIndicators: false 28 | ) { 29 | LazyHStack(spacing: .zero) { 30 | ForEach(categories, id: \.self) { category in 31 | Button { 32 | selectedCategory = category 33 | } label: { 34 | Text(category.rawValue.capitalized) 35 | .font(.system(size: 11.0)) 36 | .foregroundColor(selectedCategory == category ? Color.white : Color.primary) 37 | } 38 | .buttonStyle(PlainButtonStyle()) 39 | .padding(.horizontal, Layout.smallPadding) 40 | .padding(.vertical, Layout.smallPadding / 4) 41 | .background( 42 | Capsule() 43 | .foregroundColor(Color.accentColor) 44 | .makeVisible(selectedCategory == category) 45 | ) 46 | .padding( 47 | .leading, 48 | category == .view ? Layout.standardPadding : .zero 49 | ) 50 | 51 | Divider() 52 | .rotationEffect(.degrees(Constants.dividerRotationDegree)) 53 | } 54 | 55 | Button { 56 | selectedCategory = nil 57 | } label: { 58 | Text("Remove all filters") 59 | .font(.system(size: 11.0)) 60 | .foregroundColor(Color.red) 61 | } 62 | .buttonStyle(PlainButtonStyle()) 63 | .padding(.leading, Layout.smallPadding) 64 | } 65 | } 66 | .frame(height: Constants.viewHeight) 67 | } 68 | 69 | } 70 | 71 | struct SnippetsLibraryListFilterView_Previews: PreviewProvider { 72 | static var previews: some View { 73 | SnippetsLibraryListFilterView(selectedCategory: .constant(nil)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsLibrary/SnippetLibraryList/SnippetsLibraryListSectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryListSectionView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 21/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetsLibraryListSectionView: View { 11 | 12 | private enum Constants { 13 | static let defaultSectionName = "Section" 14 | } 15 | 16 | // MARK: - Stored Properties 17 | 18 | private(set) var snippets: [Snippet] 19 | @Binding internal var shouldShowRemoveAlert: Bool 20 | 21 | // MARK: - Views 22 | 23 | var body: some View { 24 | Section(header: Text(snippets.first?.type.title ?? Constants.defaultSectionName)) { 25 | ForEach(snippets, id: \.id) { 26 | SnippetListItemView(snippet: $0) 27 | #if DEBUG 28 | .contextMenu { 29 | Button("Delete") { 30 | shouldShowRemoveAlert.toggle() 31 | } 32 | } 33 | #endif 34 | } 35 | } 36 | .tag(snippets.first?.type.rawValue ?? .zero) 37 | } 38 | 39 | } 40 | 41 | struct SnippetsLibraryListSectionView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | SnippetsLibraryListSectionView(snippets: [], shouldShowRemoveAlert: .constant(false)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsLibrary/SnippetLibraryList/SnippetsLibraryListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryListView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetsLibraryListView: View { 11 | 12 | private enum Constants { 13 | static let minWidth: CGFloat = Layout.defaultWindowSize.width * 0.35 14 | } 15 | 16 | // MARK: - Stored Properties 17 | 18 | @ObservedObject private(set) var viewModel: SnippetsLibraryListViewModel 19 | 20 | // MARK: - Views 21 | 22 | var body: some View { 23 | VStack { 24 | HStack { 25 | SearchBar { 26 | viewModel.searchedText = $0 27 | } 28 | .padding(.leading) 29 | 30 | Button { 31 | withAnimation { 32 | viewModel.shouldShowFilterView.toggle() 33 | } 34 | } label: { 35 | Image(systemName: "line.horizontal.3.decrease.circle") 36 | .font(.system(size: 15, weight: .light)) 37 | .foregroundColor( 38 | Color.primary 39 | .opacity(Layout.mediumOpacity) 40 | ) 41 | } 42 | .buttonStyle(PlainButtonStyle()) 43 | .help("Choose the filters") 44 | 45 | Button { 46 | viewModel.onReload() 47 | } label: { 48 | Image(systemName: "arrow.counterclockwise.circle") 49 | .font(.system(size: 15, weight: .light)) 50 | .foregroundColor( 51 | Color.primary 52 | .opacity(Layout.mediumOpacity) 53 | ) 54 | } 55 | .buttonStyle(PlainButtonStyle()) 56 | .help("Pull all changes from remote") 57 | .padding(.trailing, Layout.smallPadding) 58 | } 59 | .padding(.top, Layout.largePadding) 60 | 61 | SnippetsLibraryListFilterView(selectedCategory: $viewModel.selectedCategory) 62 | .makeVisible( 63 | viewModel.shouldShowFilterView, 64 | removed: true 65 | ) 66 | .animation(.default) 67 | 68 | List(selection: $viewModel.selectedSnippetId) { 69 | ForEach( 70 | viewModel.snippetGroups, 71 | id: \.self 72 | ) { 73 | SnippetsLibraryListSectionView( 74 | snippets: $0, 75 | shouldShowRemoveAlert: $viewModel.shouldShowRemoveAlert 76 | ) 77 | .makeVisible( 78 | !$0.isEmpty, 79 | removed: true 80 | ) 81 | } 82 | } 83 | .overlay( 84 | EmptySnippetsListView(isListEmpty: .constant(viewModel.snippets.isEmpty || viewModel.hasAnyResults )) 85 | ) 86 | } 87 | .frame(minWidth: Constants.minWidth) 88 | .background( 89 | VisualEffectView( 90 | material: .menu, 91 | blendingMode: .behindWindow 92 | ) 93 | ) 94 | .edgesIgnoringSafeArea(.top) 95 | .alert(isPresented: $viewModel.shouldShowRemoveAlert) { 96 | Alert( 97 | title: Text("Confirm removing"), 98 | message: Text("You sure want to remove this snippet?"), 99 | primaryButton: 100 | .destructive( 101 | Text("Yes, remove"), 102 | action: { viewModel.onRemove(viewModel.selectedSnippetId) } 103 | ), 104 | secondaryButton: .cancel() 105 | ) 106 | } 107 | } 108 | 109 | } 110 | 111 | struct SnippetsLibraryListView_Previews: PreviewProvider { 112 | static var previews: some View { 113 | SnippetsLibraryListView(viewModel: SnippetsLibraryListViewModel(snippets: .constant([]), selectedSnippetId: .constant(nil), onReload: {}, onRemove: { _ in })) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsLibrary/SnippetLibraryList/SnippetsLibraryListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryListViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 01/10/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class SnippetsLibraryListViewModel: ObservableObject { 11 | 12 | // MARK: - Stored Properties 13 | 14 | @Binding internal var snippets: [Snippet] 15 | @Binding internal var selectedSnippetId: SnippetId? 16 | 17 | @Published internal var searchedText = "" 18 | @Published internal var shouldShowRemoveAlert = false 19 | @Published internal var shouldShowFilterView = false 20 | @Published internal var selectedCategory: SnippetCategory? = nil 21 | 22 | private(set) var onReload: () -> Void 23 | private(set) var onRemove: (_ snippetId: SnippetId?) -> Void 24 | 25 | // MARK: - Computed Properties 26 | 27 | internal var snippetGroups: [[Snippet]] { 28 | return [ 29 | snippets.filter({ $0.type == .snippets }).filter({ matchingSnippet($0) }).filter({ $0.category == selectedCategory || selectedCategory == nil }), 30 | snippets.filter({ $0.type == .extending }).filter { matchingSnippet($0) } 31 | ] 32 | } 33 | 34 | internal var hasAnyResults: Bool { 35 | (snippetGroups.first?.isEmpty == true) && (snippetGroups.last?.isEmpty == true) 36 | } 37 | 38 | // MARK: - Initialization 39 | 40 | init( 41 | snippets: Binding<[Snippet]>, 42 | selectedSnippetId: Binding, 43 | onReload: @escaping () -> Void, 44 | onRemove: @escaping (_ snippetId: SnippetId?) -> Void 45 | ) { 46 | self._snippets = snippets 47 | self._selectedSnippetId = selectedSnippetId 48 | self.onReload = onReload 49 | self.onRemove = onRemove 50 | } 51 | 52 | // MARK: - Methods 53 | 54 | private func matchingSnippet(_ snippet: Snippet) -> Bool { 55 | snippet.title.lowercased().contains(searchedText.lowercased()) || 56 | snippet.summary.lowercased().contains(searchedText.lowercased()) || 57 | searchedText.isEmpty 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsLibrary/SnippetsLibraryPreview/SnippetsLibraryPreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryPreviewView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetsLibraryPreviewView: View { 11 | 12 | // MARK: - Stored Properties 13 | 14 | @Binding internal var snippets: [Snippet] 15 | @Binding internal var selectedSnippetId: SnippetId? 16 | @Binding internal var activeSheet: AppSheet? 17 | @Binding internal var appAlert: AppAlert? 18 | 19 | private(set) var onTap: (_ snippet: Snippet) -> Void 20 | 21 | // MARK: - Views 22 | 23 | var body: some View { 24 | VStack(spacing: .zero) { 25 | Spacer() 26 | .makeVisible( 27 | selectedSnippetId == nil, 28 | removed: true 29 | ) 30 | 31 | Text("Tap the snippet on the left to preview") 32 | .font(.system(size: 17.0)) 33 | .foregroundColor( 34 | Color.primary 35 | .opacity(Layout.mediumOpacity) 36 | ) 37 | .makeVisible( 38 | selectedSnippetId == nil, 39 | removed: true 40 | ) 41 | 42 | SnippetFileCardView( 43 | viewModel: SnippetFileCardViewModel( 44 | snippet: snippets.first(where: { $0.id == selectedSnippetId }), 45 | state: .preview, 46 | activeSheet: $activeSheet, 47 | appAlert: $appAlert 48 | ) 49 | ) 50 | .padding() 51 | .onTapGesture { 52 | guard let snippet = snippets.first(where: { $0.id == selectedSnippetId }) else { return } 53 | activeSheet = .snippetDetails(snippet, .edit) 54 | onTap(snippet) 55 | } 56 | .makeVisible( 57 | selectedSnippetId != nil, 58 | removed: true 59 | ) 60 | 61 | Spacer() 62 | } 63 | .frame(minWidth: Layout.defaultWindowSize.width * 0.65) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsLibrary/SnippetsLibraryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import PopupView 10 | 11 | struct SnippetsLibraryView: View { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @ObservedObject private(set) var viewModel: SnippetsLibraryViewModel 16 | 17 | @Binding internal var activeSheet: AppSheet? 18 | @State private var appAlert: AppAlert? = nil 19 | @State private var shouldShowSnippetManual = false 20 | 21 | // MARK: - Views 22 | 23 | var body: some View { 24 | ZStack { 25 | HSplitView { 26 | SnippetsLibraryListView( 27 | viewModel: SnippetsLibraryListViewModel( 28 | snippets: $viewModel.snippets, 29 | selectedSnippetId: $viewModel.selectedSnippetId 30 | ) { 31 | viewModel.fetchSnippets() 32 | } onRemove: { 33 | viewModel.onRemove($0) 34 | } 35 | ) 36 | 37 | SnippetsLibraryPreviewView( 38 | snippets: $viewModel.snippets, 39 | selectedSnippetId: $viewModel.selectedSnippetId, 40 | activeSheet: $activeSheet, 41 | appAlert: $appAlert 42 | ) { 43 | viewModel.saveSnippetToRecentSnippets($0) 44 | } 45 | } 46 | } 47 | .frame( 48 | minWidth: Layout.defaultWindowSize.width, 49 | maxWidth: .infinity, 50 | minHeight: Layout.defaultWindowSize.height, 51 | maxHeight: .infinity 52 | ) 53 | .onChange(of: viewModel.snippets) { _ in 54 | viewModel.showSnippetPreview() 55 | } 56 | .onChange(of: viewModel.hasConnection) { 57 | viewModel.onChangeConnectionState(hasConnection: $0) 58 | } 59 | .sheet(item: $activeSheet) { 60 | switch $0 { 61 | case let .snippetDetails(snippet, type): 62 | SnippetDetailsView( 63 | viewModel: SnippetDetailsViewModel( 64 | snippet: snippet, 65 | type: type, 66 | activeAppView: .constant(nil) 67 | ) 68 | ) 69 | case .snippetsUpload: 70 | SnippetsUploadView(viewModel: SnippetsUploadViewModel(snippets: viewModel.snippets)) 71 | } 72 | } 73 | .alert(item: $appAlert) { 74 | switch $0 { 75 | case .snippetDownload: 76 | return Alert( 77 | title: Text("Unexpected error"), 78 | message: Text("Unable to download the snippet. Please try again later."), 79 | dismissButton: .cancel() 80 | ) 81 | } 82 | } 83 | .makeDisplayed( 84 | with: $viewModel.shouldShowErrorAlert, 85 | imageName: "network", 86 | title: "Network error", 87 | subtitle: "Requested operation couldn't be completed", 88 | state: .failure 89 | ) 90 | .popup( 91 | isPresented: $shouldShowSnippetManual, 92 | type: .floater(verticalPadding: Layout.defaultWindowSize.height), 93 | position: .bottom, 94 | animation: .easeInOut, 95 | closeOnTapOutside: true, 96 | backgroundColor: Color.black.opacity(Layout.mediumOpacity) 97 | ) { 98 | viewModel.markSnippetManualAsReaded() 99 | } view: { 100 | SnippetCreationManualView() 101 | } 102 | .onAppear { 103 | showSnippetManualIfNeeded() 104 | } 105 | } 106 | 107 | // MARK: - Methods 108 | 109 | private func showSnippetManualIfNeeded() { 110 | let userDefaultsService = DIContainer.userDefaultsService 111 | shouldShowSnippetManual = userDefaultsService.shouldShowSnippetManual() 112 | } 113 | 114 | } 115 | 116 | struct SnippetsLibraryView_Previews: PreviewProvider { 117 | static var previews: some View { 118 | SnippetsLibraryView(viewModel: SnippetsLibraryViewModel(hasConnection: .constant(true)), activeSheet: .constant(nil)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsLibrary/SnippetsLibraryViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class SnippetsLibraryViewModel: ObservableObject { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Published internal var snippets: [Snippet] = skeletonSnippets 16 | @Published internal var selectedSnippetId: SnippetId? 17 | @Published internal var shouldShowErrorAlert = false 18 | @Published internal var hasConnection: Bool 19 | 20 | private let activeSnippetId: SnippetId? 21 | 22 | private let databaseService: DatabaseService 23 | private let userDefaultsService: UserDefaultsService 24 | 25 | private var cancellables = Set() 26 | 27 | // MARK: - Initialization 28 | 29 | init( 30 | activeSnippetId: SnippetId? = nil, 31 | hasConnection: Binding, 32 | databaseService: DatabaseService = DIContainer.databaseService, 33 | userDefaultsService: UserDefaultsService = DIContainer.userDefaultsService 34 | ) { 35 | self.activeSnippetId = activeSnippetId 36 | self.hasConnection = hasConnection.wrappedValue 37 | self.databaseService = databaseService 38 | self.userDefaultsService = userDefaultsService 39 | 40 | fetchSnippets() 41 | } 42 | 43 | // MARK: - Methods 44 | 45 | internal func onRemove(_ snippetId: SnippetId?) { 46 | guard 47 | let snippetId = snippetId, 48 | let snippet = snippets.first(where: { $0.id == snippetId }) 49 | else { 50 | shouldShowErrorAlert.toggle() 51 | return 52 | } 53 | 54 | removeSnippet(snippet) 55 | } 56 | 57 | internal func fetchSnippets() { 58 | databaseService.snippets 59 | .receive(on: DispatchQueue.main) 60 | .sink { [weak self] completion in 61 | switch completion { 62 | case .finished: return 63 | case .failure: 64 | self?.shouldShowErrorAlert.toggle() 65 | self?.snippets = [] 66 | self?.onChangeConnectionState(hasConnection: false) 67 | } 68 | } receiveValue: { [weak self] in 69 | self?.snippets = $0 70 | } 71 | .store(in: &cancellables) 72 | } 73 | 74 | internal func saveSnippetToRecentSnippets(_ snippet: Snippet) { 75 | userDefaultsService.saveRecentSnippet(snippet) 76 | } 77 | 78 | internal func showSnippetPreview() { 79 | guard let snippetId = activeSnippetId else { return } 80 | selectedSnippetId = snippetId 81 | } 82 | 83 | internal func onChangeConnectionState(hasConnection: Bool) { 84 | if !hasConnection && !snippets.isEmpty && snippets != skeletonSnippets { 85 | userDefaultsService.saveSnippetsLocally(snippets) 86 | } else if !hasConnection && snippets.isEmpty { 87 | fetchLocalSnippets() 88 | } 89 | } 90 | 91 | internal func markSnippetManualAsReaded() { 92 | userDefaultsService.markSnippetManualAsReaded() 93 | } 94 | 95 | private func fetchLocalSnippets() { 96 | userDefaultsService.fetchSnippets(fetchingType: .local) 97 | .receive(on: DispatchQueue.main) 98 | .sink { [weak self] completion in 99 | switch completion { 100 | case .finished: return 101 | case .failure: 102 | self?.shouldShowErrorAlert.toggle() 103 | self?.snippets = [] 104 | } 105 | } receiveValue: { [weak self] in 106 | self?.snippets = $0 107 | } 108 | .store(in: &cancellables) 109 | } 110 | 111 | private func removeSnippet(_ snippet: Snippet) { 112 | databaseService.removeSnippet(snippet) 113 | .sink { [weak self] completion in 114 | switch completion { 115 | case .finished: 116 | self?.snippets.removeAll { $0.id == snippet.id } 117 | case .failure: 118 | self?.shouldShowErrorAlert.toggle() 119 | } 120 | } 121 | .store(in: &cancellables) 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsUpload/SnippetsUploadView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsUploadView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 26/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetsUploadView: View { 11 | 12 | private enum Constants { 13 | static let totalProgressValue: CGFloat = 1.0 14 | } 15 | 16 | // MARK: - Stored Properties 17 | 18 | @Environment (\.presentationMode) var presentationMode 19 | 20 | @StateObject internal var viewModel: SnippetsUploadViewModel 21 | 22 | // MARK: - Views 23 | 24 | var body: some View { 25 | VStack(spacing: Layout.standardPadding) { 26 | Spacer() 27 | 28 | Image(systemName: viewModel.uploadingStatus == .done ? "checkmark.circle.fill" : "xmark.octagon.fill") 29 | .font(.system(size: 56, weight: .regular)) 30 | .foregroundColor(viewModel.uploadingStatus == .done ? Color.green : Color.red) 31 | .makeVisible( 32 | viewModel.uploadingStatus != .uploading && viewModel.uploadingStatus != .initializing, 33 | removed: true 34 | ) 35 | 36 | Text(viewModel.uploadingStatus.title) 37 | .font(.system(size: 17, weight: .semibold)) 38 | .foregroundColor(Color.primary) 39 | 40 | Text(viewModel.uploadingStatus.message) 41 | .font(.system(size: 13, weight: .regular)) 42 | .foregroundColor( 43 | Color.primary 44 | .opacity(Layout.largeOpacity) 45 | ) 46 | .multilineTextAlignment(.center) 47 | .padding(.top, -Layout.smallPadding) 48 | .makeVisible( 49 | viewModel.uploadingStatus != .uploading && viewModel.uploadingStatus != .initializing, 50 | removed: true 51 | ) 52 | 53 | ProgressView( 54 | value: viewModel.progressValue, 55 | total: Constants.totalProgressValue, 56 | label: { EmptyView() }, 57 | currentValueLabel: { 58 | HStack { 59 | Spacer() 60 | 61 | Text(viewModel.uploadingStatus.currentDescription) 62 | 63 | Spacer() 64 | } 65 | .padding(.top, Layout.smallPadding) 66 | } 67 | ) 68 | .progressViewStyle(LinearProgressViewStyle(tint: Color.accentColor)) 69 | .makeVisible( 70 | viewModel.uploadingStatus == .uploading || viewModel.uploadingStatus == .initializing, 71 | removed: true 72 | ) 73 | 74 | Spacer() 75 | 76 | HStack { 77 | Spacer() 78 | 79 | Button("Done") { presentationMode.wrappedValue.dismiss() } 80 | } 81 | .makeVisible( 82 | viewModel.uploadingStatus != .uploading && viewModel.uploadingStatus != .initializing, 83 | removed: true 84 | ) 85 | .padding(.bottom) 86 | } 87 | .padding(.horizontal, Layout.largePadding) 88 | .frame( 89 | width: Layout.defaultWindowSize.width * 0.6, 90 | height: Layout.defaultWindowSize.height * 0.7 91 | ) 92 | .onChange(of: viewModel.shouldDismissView) { _ in 93 | presentationMode.wrappedValue.dismiss() 94 | } 95 | .onAppear { 96 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 97 | viewModel.uploadSnippetsToXcode() 98 | } 99 | } 100 | } 101 | 102 | } 103 | 104 | struct SnippetsUploadView_Previews: PreviewProvider { 105 | static var previews: some View { 106 | SnippetsUploadView(viewModel: SnippetsUploadViewModel(snippets: [])) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/SnippetsUpload/SnippetsUploadViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsUploadViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 26/09/2021. 6 | // 7 | 8 | import AppKit 9 | import Combine 10 | 11 | final class SnippetsUploadViewModel: ObservableObject { 12 | 13 | private enum Constants { 14 | static let midValue: CGFloat = 0.5 15 | static let fullValue: CGFloat = 1.0 16 | } 17 | 18 | // MARK: - Stored Properties 19 | 20 | private(set) var snippets: [Snippet] 21 | 22 | @Published internal var progressValue: CGFloat = .zero 23 | @Published private(set) var shouldDismissView = false 24 | @Published internal var uploadingStatus: UploadingStatus = .initializing 25 | 26 | private let snippetsParserService: SnippetsParserService 27 | 28 | private var cancellables = Set() 29 | 30 | // MARK: - Initialization 31 | 32 | init( 33 | snippets: [Snippet], 34 | snippetsParserService: SnippetsParserService = DIContainer.snippetsParserService 35 | ) { 36 | self.snippets = snippets 37 | self.snippetsParserService = snippetsParserService 38 | } 39 | 40 | // MARK: - Methods 41 | 42 | internal func uploadSnippetsToXcode() { 43 | snippetsParserService.writeToPath( 44 | type: .uploadToXcode, 45 | snippets: snippets 46 | ) { 47 | self.uploadingStatus = .uploading 48 | self.progressValue = Constants.midValue 49 | DispatchQueue.main.asyncAfter(deadline: .now() + Constants.midValue) { 50 | self.progressValue = Constants.fullValue 51 | self.finishUpload() 52 | } 53 | } onError: { 54 | self.uploadingStatus = .error 55 | } 56 | } 57 | 58 | private func finishUpload() { 59 | DispatchQueue.main.asyncAfter(deadline: .now() + Constants.fullValue) { 60 | self.uploadingStatus = .done 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/Start/StartView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StartView: View { 11 | 12 | private enum Constants { 13 | static let logoImageHeight: CGFloat = 130 14 | static let menuItemsSpacing: CGFloat = 12.0 15 | } 16 | 17 | // MARK: - Stored Properties 18 | 19 | @ObservedObject private(set) var viewModel: StartViewModel 20 | 21 | // MARK: - Views 22 | 23 | var body: some View { 24 | HSplitView { 25 | VStack(spacing: .zero) { 26 | HStack { 27 | Button { 28 | viewModel.closeView() 29 | } label: { 30 | Image(systemName: "xmark") 31 | .font(.system(size: 10.0, weight: .medium)) 32 | } 33 | 34 | Spacer() 35 | } 36 | .padding() 37 | .buttonStyle(PlainButtonStyle()) 38 | 39 | Image("icLogo") 40 | .resizable() 41 | .aspectRatio(contentMode: .fit) 42 | .frame(height: Constants.logoImageHeight) 43 | .padding(.top, Layout.smallPadding) 44 | 45 | Text("Snippets Library") 46 | .font(.system(size: 40.0, weight: .regular)) 47 | 48 | Text("Version \(NSApplication.appVersion)") 49 | .font(.system(size: 13.0, weight: .light)) 50 | .foregroundColor(Color.primary.opacity(Layout.mediumOpacity)) 51 | .padding(.top, Layout.smallPadding / 2) 52 | 53 | Spacer() 54 | 55 | VStack( 56 | alignment: .leading, 57 | spacing: Constants.menuItemsSpacing 58 | ) { 59 | StartViewMenuItem(type: .create) { 60 | if viewModel.activeAppView != .snippetsLibrary(nil) { 61 | viewModel.activeAppView = .create 62 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 63 | viewModel.activeAppSheet = .snippetDetails(Snippet(), .create) 64 | } 65 | } else { 66 | viewModel.activeAppSheet = .snippetDetails(Snippet(), .create) 67 | } 68 | } 69 | 70 | StartViewMenuItem(type: .importSnippet) { 71 | viewModel.activeAppView = .importSnippet 72 | } 73 | 74 | StartViewMenuItem(type: .open) { 75 | viewModel.activeAppView = .snippetsLibrary(nil) 76 | } 77 | } 78 | .padding(.leading, Layout.largePadding * 2) 79 | .padding(.trailing, Layout.largePadding) 80 | 81 | Spacer() 82 | } 83 | .frame(width: Layout.defaultWindowSize.width * 0.62) 84 | .ignoresSafeArea() 85 | 86 | StartViewRecentSnippetsView( 87 | recentSnippets: $viewModel.recentSnippets, 88 | activeAppView: $viewModel.activeAppView 89 | ) 90 | } 91 | .frame( 92 | width: Layout.defaultWindowSize.width, 93 | height: Layout.defaultWindowSize.height 94 | ) 95 | } 96 | 97 | } 98 | 99 | struct StartView_Previews: PreviewProvider { 100 | static var previews: some View { 101 | StartView(viewModel: StartViewModel(activeAppView: .constant(nil), activeAppSheet: .constant(nil))) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/Start/StartViewMenuItem/StartViewMenuItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartViewMenuItem.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StartViewMenuItem: View { 11 | 12 | // MARK: - Stored Properties 13 | 14 | let type: StartViewMenuItemType 15 | private(set) var onTap: () -> Void 16 | 17 | // MARK: - Views 18 | 19 | var body: some View { 20 | Button { 21 | onTap() 22 | } label: { 23 | HStack(spacing: Layout.standardPadding) { 24 | Image(systemName: type.systemImageName) 25 | .font(.system(size: type.imageFontSize, weight: .light)) 26 | .foregroundColor(Color("accent")) 27 | 28 | VStack( 29 | alignment: .leading, 30 | spacing: .zero 31 | ) { 32 | Text(type.title) 33 | .font(.system(size: 13.0, weight: .semibold)) 34 | 35 | Text(type.subtitle) 36 | .font(.system(size: 13.0)) 37 | .foregroundColor( 38 | Color.primary 39 | .opacity(Layout.mediumOpacity) 40 | ) 41 | } 42 | .padding(.leading, type.leadingPadding) 43 | 44 | Spacer() 45 | } 46 | } 47 | .buttonStyle(PlainButtonStyle()) 48 | } 49 | 50 | } 51 | 52 | struct StartViewMenuItem_Previews: PreviewProvider { 53 | static var previews: some View { 54 | StartViewMenuItem(type: .create) {} 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/Start/StartViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class StartViewModel: ObservableObject { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Published internal var recentSnippets: [Snippet] = [] 16 | 17 | @Binding internal var activeAppView: ActiveAppView? 18 | @Binding internal var activeAppSheet: AppSheet? 19 | 20 | private let userDefaultsService: UserDefaultsService 21 | 22 | private var cancellables = Set() 23 | 24 | // MARK: - Initialization 25 | 26 | init( 27 | activeAppView: Binding, 28 | activeAppSheet: Binding, 29 | userDefaultsService: UserDefaultsService = DIContainer.userDefaultsService 30 | ) { 31 | self._activeAppView = activeAppView 32 | self._activeAppSheet = activeAppSheet 33 | self.userDefaultsService = userDefaultsService 34 | 35 | fetchRecentSnippets() 36 | } 37 | 38 | // MARK: - Methods 39 | 40 | internal func closeView() { 41 | NSApplication.shared.keyWindow?.close() 42 | } 43 | 44 | internal func fetchRecentSnippets() { 45 | userDefaultsService.fetchSnippets(fetchingType: .recent) 46 | .receive(on: DispatchQueue.main) 47 | .sink { [weak self] in 48 | self?.recentSnippets = $0 49 | } 50 | .store(in: &cancellables) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/Start/StartViewRecentSnippets/StartViewRecentSnippetsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartViewRecentSnippetsView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StartViewRecentSnippetsView: View { 11 | 12 | private enum Constants { 13 | static let viewSize = CGSize(width: 850, height: 460) 14 | static let spacerHeight: CGFloat = 16.0 15 | static let scrollViewHorizontalPadding: CGFloat = 10.0 16 | } 17 | 18 | // MARK: - Stored Properties 19 | 20 | @Binding internal var recentSnippets: [Snippet] 21 | @Binding internal var activeAppView: ActiveAppView? 22 | 23 | @State private var selectedSnippetIndex: Int? 24 | 25 | // MARK: - Views 26 | 27 | var body: some View { 28 | VStack(spacing: .zero) { 29 | Text("No Recent Files") 30 | .font(.system(size: 17.0)) 31 | .foregroundColor( 32 | Color.primary 33 | .opacity(Layout.mediumOpacity) 34 | ) 35 | .frame(height: Constants.viewSize.height) 36 | .makeVisible( 37 | recentSnippets.isEmpty, 38 | removed: true 39 | ) 40 | 41 | ScrollView(showsIndicators: false) { 42 | Rectangle() 43 | .foregroundColor(Color.clear) 44 | .frame(height: Constants.spacerHeight) 45 | 46 | ForEach( 47 | Array(recentSnippets.enumerated()), 48 | id: \.offset 49 | ) { (index, snippet) in 50 | RecentSnippetCardView( 51 | snippet: snippet, 52 | tag: index, 53 | selectedFileIndex: $selectedSnippetIndex 54 | ) { 55 | activeAppView = .snippetsLibrary(snippet.id) 56 | } 57 | } 58 | } 59 | .background(Color.clear) 60 | .makeVisible(!recentSnippets.isEmpty) 61 | .padding(.horizontal, Constants.scrollViewHorizontalPadding) 62 | } 63 | .frame(width: Constants.viewSize.width * 0.38) 64 | .background( 65 | VisualEffectView( 66 | material: .menu, 67 | blendingMode: .behindWindow 68 | ) 69 | ) 70 | .ignoresSafeArea() 71 | } 72 | 73 | } 74 | 75 | struct StartViewRecentSnippetsView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | StartViewRecentSnippetsView(recentSnippets: .constant([]), activeAppView: .constant(nil)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/Status/StatusView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StatusView: View { 11 | 12 | private enum Constants { 13 | static let emptySnippetsViewSize = CGSize(width: 230, height: 150) 14 | static let cornerRadius: CGFloat = 12.0 15 | static let strokeLineWidth: CGFloat = 1 16 | static let strokeDashSpacing: CGFloat = 10 17 | static let viewWidth: CGFloat = 230 18 | static let viewMinHeight: CGFloat = 80 19 | static let viewMaxHeight: CGFloat = 150 20 | } 21 | 22 | // MARK: - Stored Properties 23 | 24 | @StateObject var viewModel = StatusViewModel() 25 | 26 | // MARK: - Views 27 | 28 | var body: some View { 29 | VStack { 30 | VStack { 31 | Text("No recent snippets") 32 | .font(.system(size: 17.0)) 33 | } 34 | .frame( 35 | width: Constants.emptySnippetsViewSize.width, 36 | height: Constants.emptySnippetsViewSize.height 37 | ) 38 | .overlay( 39 | RoundedRectangle(cornerRadius: Constants.cornerRadius) 40 | .stroke( 41 | style: StrokeStyle( 42 | lineWidth: Constants.strokeLineWidth, 43 | dash: [Constants.strokeDashSpacing], 44 | dashPhase: .zero 45 | ) 46 | ) 47 | .foregroundColor(.primary) 48 | ) 49 | .padding() 50 | .makeVisible( 51 | viewModel.snippets.isEmpty, 52 | removed: true 53 | ) 54 | 55 | Group { 56 | FileStatusCard( 57 | viewModel: FileStatusCardViewModel( 58 | snippet: viewModel.snippets.first, 59 | type: .large 60 | ) 61 | ) 62 | 63 | HStack { 64 | HStack { 65 | FileStatusCard( 66 | viewModel: FileStatusCardViewModel( 67 | snippet: viewModel.snippets.last 68 | ) 69 | ) 70 | 71 | Spacer() 72 | } 73 | .makeVisible( 74 | viewModel.snippets.count > 1, 75 | removed: true 76 | ) 77 | 78 | FileStatusCard( 79 | viewModel: FileStatusCardViewModel( 80 | snippet: viewModel.snippets.randomElement() 81 | ) 82 | ) 83 | .makeVisible( 84 | viewModel.snippets.count > 2, 85 | removed: true 86 | ) 87 | } 88 | } 89 | .makeVisible( 90 | !viewModel.snippets.isEmpty, 91 | removed: true 92 | ) 93 | 94 | Spacer() 95 | } 96 | .accentColor(Color.accentColor) 97 | .frame( 98 | width: Constants.viewWidth, 99 | height: viewModel.snippets.count == 1 ? Constants.viewMinHeight : Constants.viewMaxHeight 100 | ) 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /SnippetsLibrary/Modules/Status/StatusViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class StatusViewModel: ObservableObject { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Published internal var snippets: [Snippet] = [] 16 | @Published internal var shouldShowRemoveAlert: Bool = false 17 | 18 | private let userDefaultsService: UserDefaultsService 19 | 20 | private var cancellables = Set() 21 | 22 | // MARK: - Initialization 23 | 24 | init(userDefaultsService: UserDefaultsService = DIContainer.userDefaultsService) { 25 | self.userDefaultsService = userDefaultsService 26 | 27 | fetchSnippets() 28 | } 29 | 30 | // MARK: - Methods 31 | 32 | private func fetchSnippets() { 33 | userDefaultsService.fetchSnippets(fetchingType: .recent) 34 | .receive(on: DispatchQueue.main) 35 | .sink { completion in 36 | switch completion { 37 | case let .failure(error): 38 | debugPrint(error.localizedDescription) 39 | case .finished: 40 | return 41 | } 42 | } receiveValue: { [weak self] in 43 | self?.snippets = $0 44 | } 45 | .store(in: &cancellables) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /SnippetsLibrary/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SnippetsLibrary/Services/Firebase/Crashlytics/CrashlyticsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashlyticsService.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 17/09/2021. 6 | // 7 | 8 | import Firebase 9 | 10 | protocol CrashlyticsService { 11 | func logNonFatalError(_ type: CrashlyticsErrorType) 12 | func logError(withMessage message: String) 13 | } 14 | 15 | final class CrashlyticsServiceImpl: CrashlyticsService { 16 | 17 | // MARK: - Initialization 18 | 19 | init() { 20 | Crashlytics.crashlytics().setUserID(System.getHardwareUUID()) 21 | } 22 | 23 | // MARK: - Methods 24 | 25 | internal func logNonFatalError(_ type: CrashlyticsErrorType) { 26 | let userInfo = [ 27 | NSLocalizedDescriptionKey: NSLocalizedString("Non-fatal error reported.", comment: ""), 28 | NSLocalizedFailureReasonErrorKey: NSLocalizedString("\(type.rawValue)", comment: "") 29 | ] 30 | 31 | let error = NSError( 32 | domain: NSCocoaErrorDomain, 33 | code: -1001, 34 | userInfo: userInfo 35 | ) 36 | Crashlytics.crashlytics().record(error: error) 37 | } 38 | 39 | internal func logError(withMessage message: String) { 40 | Crashlytics.crashlytics().log(message) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /SnippetsLibrary/Services/Firebase/Logs/LogsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogsService.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 17/09/2021. 6 | // 7 | 8 | import Foundation 9 | import FirebaseDatabase 10 | 11 | protocol LogsService { 12 | func logUserActivity(type: UserActivityLogType) 13 | } 14 | 15 | final class LogsServiceImpl: LogsService { 16 | 17 | // MARK: - Stored Properties 18 | 19 | private let ref = Database.database().reference() 20 | 21 | // MARK: - Methods 22 | 23 | internal func logUserActivity(type: UserActivityLogType) { 24 | let values: [String: Any] = [ 25 | "userDeviceUUID": System.getHardwareUUID(), 26 | "actionDescription": type.title, 27 | "date": Date().description(with: Locale.current), 28 | "level": type.level.rawValue 29 | ] 30 | #if RELEASE 31 | ref.child("usersActivityLog").childByAutoId().setValue(values) 32 | #else 33 | ref.child("developersActivityLog").childByAutoId().setValue(values) 34 | #endif 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /SnippetsLibrary/Services/Network/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import Network 9 | import Combine 10 | 11 | protocol NetworkService { 12 | var isConnected: Published.Publisher { get } 13 | } 14 | 15 | final class NetworkServiceImpl: NetworkService { 16 | 17 | // MARK: - Stored Properties 18 | 19 | private let monitor = NWPathMonitor() 20 | 21 | @Published var isConnectedValue: Bool = false 22 | var isConnected: Published.Publisher { $isConnectedValue } 23 | 24 | // MARK: - Initialization 25 | 26 | init() { 27 | setup() 28 | observeState() 29 | } 30 | 31 | // MARK: - Methods 32 | 33 | internal func observeState() { 34 | self.monitor.pathUpdateHandler = { 35 | self.isConnectedValue = $0.status == .satisfied 36 | } 37 | } 38 | 39 | private func setup() { 40 | let queue = DispatchQueue(label: "NWPathMonitor") 41 | monitor.start(queue: queue) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /SnippetsLibrary/Services/URLFactory/URLFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLFactory.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 23/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol URLFactory { 11 | func getURL(withType type: URLType) -> URL? 12 | } 13 | 14 | final class URLFactoryImpl: URLFactory { 15 | 16 | private enum Constants { 17 | static let userGuidesUrlString = "https://github.com/tryboxx/SnippetsLibrary/blob/main/README.md" 18 | static let developerDocumentationUrlString = "https://github.com/tryboxx/SnippetsLibrary/wiki" 19 | } 20 | 21 | // MARK: - Methods 22 | 23 | internal func getURL(withType type: URLType) -> URL? { 24 | switch type { 25 | case .userGuides: 26 | return URL(string: Constants.userGuidesUrlString) 27 | case .docs: 28 | return URL(string: Constants.developerDocumentationUrlString) 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SnippetsLibrary/Services/UserDefaults/UserDefaultsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsService.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 19/09/2021. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | protocol UserDefaultsService { 12 | func saveRecentSnippet(_ snippet: Snippet) 13 | func fetchSnippets(fetchingType: SnippetsFetchingType) -> AnyPublisher<[Snippet], Never> 14 | func saveSnippetsLocally(_ snippets: [Snippet]) 15 | 16 | func fetchRecentSnippetsFromAppGroup() -> [Snippet] 17 | 18 | func shouldShowSnippetManual() -> Bool 19 | func markSnippetManualAsReaded() 20 | } 21 | 22 | final class UserDefaultsServiceImpl: UserDefaultsService { 23 | 24 | private enum Constants { 25 | static let recentSnippetKey = "RecentSnippet" 26 | static let localSnippetKey = "LocalSnippet" 27 | static let appGroupName = "group.com.cphlowiec.SnippetsLibrary" 28 | static let snippetManualKey = "wasSnippetManualReaded" 29 | } 30 | 31 | // MARK: - Stored Properties 32 | 33 | private let userDefaults = UserDefaults.standard 34 | 35 | // MARK: - Methods 36 | 37 | // MARK: - Recent Snippets - 38 | 39 | internal func saveRecentSnippet(_ snippet: Snippet) { 40 | let snippet = SnippetPlist(from: snippet) 41 | let snippetDictonary = snippet.convertedToDictonary() 42 | let snippetKey = Constants.recentSnippetKey + " \(snippet.id)" 43 | 44 | userDefaults.set( 45 | snippetDictonary, 46 | forKey: snippetKey 47 | ) 48 | 49 | saveSnippetDictonaryIntoAppGroup( 50 | snippetDictonary, 51 | key: snippetKey 52 | ) 53 | } 54 | 55 | internal func fetchSnippets(fetchingType: SnippetsFetchingType) -> AnyPublisher<[Snippet], Never> { 56 | let fetchingKey = fetchingType == .recent ? Constants.recentSnippetKey : Constants.localSnippetKey 57 | let keys = userDefaults.dictionaryRepresentation().keys.filter { $0.contains(fetchingKey) } 58 | 59 | return Future<[Snippet], Never> { [weak self] promise in 60 | var snippets = [Snippet]() 61 | 62 | for key in keys { 63 | guard 64 | let snippetDictonary = self?.userDefaults.object(forKey: key), 65 | let data = try? JSONSerialization.data(withJSONObject: snippetDictonary, options: []), 66 | let snippet = try? JSONDecoder().decode(SnippetPlist.self, from: data) 67 | else { continue } 68 | 69 | snippets.append(Snippet(from: snippet)) 70 | } 71 | 72 | promise(.success(snippets)) 73 | } 74 | .eraseToAnyPublisher() 75 | } 76 | 77 | // MARK: - Local snippets - 78 | 79 | internal func saveSnippetsLocally(_ snippets: [Snippet]) { 80 | for snippet in snippets { 81 | saveSnippetLocally(snippet) 82 | } 83 | } 84 | 85 | private func saveSnippetLocally(_ snippet: Snippet) { 86 | let snippet = SnippetPlist(from: snippet) 87 | let snippetDictonary = snippet.convertedToDictonary() 88 | let snippetKey = Constants.localSnippetKey + " \(snippet.id)" 89 | 90 | userDefaults.set( 91 | snippetDictonary, 92 | forKey: snippetKey 93 | ) 94 | } 95 | 96 | // MARK: - App Group - 97 | 98 | internal func fetchRecentSnippetsFromAppGroup() -> [Snippet] { 99 | guard let userDefaults = UserDefaults(suiteName: Constants.appGroupName) else { return [] } 100 | 101 | let keys = userDefaults.dictionaryRepresentation().keys.filter { $0.contains(Constants.recentSnippetKey) } 102 | var snippets = [Snippet]() 103 | 104 | for key in keys { 105 | guard 106 | let snippetDictonary = userDefaults.object(forKey: key), 107 | let data = try? JSONSerialization.data(withJSONObject: snippetDictonary, options: []), 108 | let snippet = try? JSONDecoder().decode(SnippetPlist.self, from: data) 109 | else { continue } 110 | 111 | snippets.append(Snippet(from: snippet)) 112 | } 113 | 114 | return snippets 115 | } 116 | 117 | private func saveSnippetDictonaryIntoAppGroup( 118 | _ snippetDictonary: [String: Any], 119 | key: String 120 | ) { 121 | guard let userDefaults = UserDefaults(suiteName: Constants.appGroupName) else { return } 122 | 123 | userDefaults.set( 124 | snippetDictonary, 125 | forKey: key 126 | ) 127 | } 128 | 129 | // MARK: - Snippet Manual 130 | 131 | internal func shouldShowSnippetManual() -> Bool { 132 | let wasSnippetsManualReaded = userDefaults.bool(forKey: Constants.snippetManualKey) 133 | return !wasSnippetsManualReaded 134 | } 135 | 136 | internal func markSnippetManualAsReaded() { 137 | userDefaults.set( 138 | true, 139 | forKey: Constants.snippetManualKey 140 | ) 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /SnippetsLibrary/SnippetsLibrary.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.cphlowiec.SnippetsLibrary 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SnippetsLibrary/SnippetsLibraryDebug.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.cphlowiec.SnippetsLibrary 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/CustomCodeTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCodeTheme.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Sourceful 10 | 11 | struct CustomCodeTheme: SourceCodeTheme { 12 | 13 | private enum Constants { 14 | static let backgroundDarkColor = NSColor(red: 41/255, green: 42/255, blue: 48/255, alpha: 1.0) 15 | static let plainTypeLightColor = NSColor(red: 2/255, green: 2/255, blue: 2/255, alpha: 1.0) 16 | static let plainTypeDarkColor = NSColor(red: 223/255, green: 223/255, blue: 224/255, alpha: 1.0) 17 | static let numberTypeLightColor = NSColor(red: 40/255, green: 41/255, blue: 208/255, alpha: 1.0) 18 | static let numberTypeDarkColor = NSColor(red: 214/255, green: 202/255, blue: 134/255, alpha: 1.0) 19 | static let stringTypeLightColor = NSColor(red: 192/255, green: 62/255, blue: 41/255, alpha: 1.0) 20 | static let stringTypeDarkColor = NSColor(red: 239/255, green: 136/255, blue: 118/255, alpha: 1.0) 21 | static let identifierTypeLightColor = NSColor(red: 121/255, green: 82/255, blue: 178/255, alpha: 1.0) 22 | static let identifierTypeDarkColor = NSColor(red: 171/255, green: 131/255, blue: 228/255, alpha: 1.0) 23 | static let keywordTypeLightColor = NSColor(red: 160/255, green: 69/255, blue: 160/255, alpha: 1.0) 24 | static let keywordTypeDarkColor = NSColor(red: 238/255, green: 130/255, blue: 176/255, alpha: 1.0) 25 | static let commentColor = NSColor(red: 129/255.0, green: 140/255.0, blue: 150/255.0, alpha: 1.0) 26 | 27 | static let font = NSFont.monospacedSystemFont(ofSize: 15, weight: .regular) 28 | static let gutterStyle = GutterStyle(backgroundColor: NSColor.clear, minimumWidth: 0) 29 | } 30 | 31 | // MARK: - Stored Properties 32 | 33 | private var colorScheme: ColorScheme = .dark 34 | 35 | private static var lineNumbersColor: NSColor = .clear 36 | 37 | internal let lineNumbersStyle: LineNumbersStyle? = nil 38 | internal let gutterStyle: GutterStyle = Constants.gutterStyle 39 | internal let font = Constants.font 40 | 41 | // MARK: - Computed Properties 42 | 43 | internal var backgroundColor: NSColor { 44 | return colorScheme == .dark ? Constants.backgroundDarkColor : NSColor.white 45 | } 46 | 47 | // MARK: - Initialization 48 | 49 | init(colorScheme: ColorScheme) { 50 | self.colorScheme = colorScheme 51 | } 52 | 53 | // MARK: - Methods 54 | 55 | func color(for syntaxColorType: SourceCodeTokenType) -> NSColor { 56 | switch syntaxColorType { 57 | case .plain: 58 | return colorScheme == .dark ? Constants.plainTypeDarkColor : Constants.plainTypeLightColor 59 | case .number: 60 | return colorScheme == .dark ? Constants.numberTypeDarkColor : Constants.numberTypeLightColor 61 | case .string: 62 | return colorScheme == .dark ? Constants.stringTypeDarkColor : Constants.stringTypeLightColor 63 | case .identifier: 64 | return colorScheme == .dark ? Constants.identifierTypeDarkColor : Constants.identifierTypeLightColor 65 | case .keyword: 66 | return colorScheme == .dark ? Constants.keywordTypeDarkColor : Constants.keywordTypeLightColor 67 | case .comment: 68 | return Constants.commentColor 69 | case .editorPlaceholder: 70 | return colorScheme == .dark ? .gray : .lightGray 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/DisabledCommandGroupButtonType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisabledCommandGroupButtonType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 26/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DisabledCommandGroupButtonType { 11 | case openLibrary 12 | case uploadSnippets 13 | } 14 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/Errors/CrashlyticsError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashlyticsError.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 17/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CrashlyticsErrorType: String { 11 | case unableToCreateSnippet 12 | case unableToUpdateSnippet 13 | case unableToRemoveSnippet 14 | case unableToCreateSnippetFromDroppedFile 15 | } 16 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/Errors/DatabaseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseError.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 17/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DatabaseError: Error { 11 | case unableToRetrieveKeyForChild 12 | case unableToSaveSnippet 13 | case unableToFetchData 14 | case unableToDecodeSnippet 15 | case unableToRemoveSnippet 16 | case unableToUpdateSnippet 17 | } 18 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/Errors/SnippetsParserServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsParserServiceError.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetsParserServiceError: Error { 11 | case unableToFindSnippetsDirectory 12 | case unableToSaveSnippet 13 | case unableToDecodeSnippet 14 | case unableToCreateDirectory 15 | case unableToRemoveSnippet 16 | } 17 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/Errors/UserDefaultsServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsServiceError.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 02/10/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserDefaultsServiceError: Error { 11 | case unableToGetData 12 | } 13 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/FileStatusCardType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileStatusCardType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FileStatusCardType { 11 | case large 12 | case normal 13 | } 14 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/PlistCodingKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlistCodingKeys.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum PlistCodingKeys: String, CodingKey { 11 | case id = "IDECodeSnippetIdentifier" 12 | case title = "IDECodeSnippetTitle" 13 | case summary = "IDECodeSnippetSummary" 14 | case completion = "IDECodeSnippetCompletionPrefix" 15 | case availability = "IDECodeSnippetCompletionScopes" 16 | case contents = "IDECodeSnippetContents" 17 | case language = "IDECodeSnippetLanguage" 18 | case platform = "IDECodeSnippetPlatformFamily" 19 | case userSnippet = "IDECodeSnippetUserSnippet" 20 | case version = "IDECodeSnippetVersion" 21 | case author = "IDECodeSnippetAuthor" 22 | case tags = "IDECodeSnippetTags" 23 | } 24 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetAvailability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetAvailability.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetAvailability: CaseIterable { 11 | case allScopes 12 | case implementation 13 | case expression 14 | case function 15 | case string 16 | case topLevel 17 | 18 | var title: String { 19 | switch self { 20 | case .allScopes: return "All Scopes" 21 | case .implementation: return "Class Implementation" 22 | case .expression: return "Code Expression" 23 | case .function: return "Function or Method" 24 | case .string: return "String or Comment" 25 | case .topLevel: return "Top Level" 26 | } 27 | } 28 | 29 | var string: String { 30 | switch self { 31 | case .allScopes: return "All" 32 | case .implementation: return "ClassImplementation" 33 | case .expression: return "CodeExpression" 34 | case .function: return "CodeBlock" 35 | case .string: return "StringOrComment" 36 | case .topLevel: return "TopLevel" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetCategory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetCategory.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 01/10/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetCategory: String, CaseIterable { 11 | case view 12 | case helper 13 | case service 14 | } 15 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetDetailsViewType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetDetailsViewType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetDetailsViewType { 11 | case create 12 | case edit 13 | } 14 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetFileCardViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetFileCardViewState.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetFileCardViewState { 11 | case preview 12 | case edit 13 | } 14 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetPlatform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetPlatform: String, CaseIterable { 11 | case all 12 | case iphoneos 13 | case macos 14 | case tvos 15 | case watchos 16 | 17 | var title: String { 18 | switch self { 19 | case .all: return "All platforms" 20 | case .iphoneos: return "iOS" 21 | case .macos: return "macOS" 22 | case .tvos: return "tvOS" 23 | case .watchos: return "watchOS" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 21/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetType: Int, CaseIterable { 11 | case snippets 12 | case extending 13 | 14 | var title: String { 15 | switch self { 16 | case .snippets: return "Snippets" 17 | case .extending: return "Extensions" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetWriteType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetWriteType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 14/10/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetWriteType { 11 | case download 12 | case uploadToXcode 13 | } 14 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/SnippetsFetchingType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsFetchingType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/10/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SnippetsFetchingType { 11 | case recent 12 | case local 13 | } 14 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/StartViewMenuItemType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartViewMenuItemType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 13/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum StartViewMenuItemType { 11 | case create 12 | case importSnippet 13 | case open 14 | 15 | var systemImageName: String { 16 | switch self { 17 | case .create: return "plus.square" 18 | case .importSnippet: return "square.and.arrow.down" 19 | case .open: return "folder" 20 | } 21 | } 22 | 23 | var imageFontSize: CGFloat { 24 | switch self { 25 | case .create: return 30.0 26 | case .importSnippet: return 31.0 27 | case .open: return 25.0 28 | } 29 | } 30 | 31 | var title: String { 32 | switch self { 33 | case .create: return "Create a new snippet file" 34 | case .importSnippet: return "Import an existing code snippet" 35 | case .open: return "Open snippets library" 36 | } 37 | } 38 | 39 | var subtitle: String { 40 | switch self { 41 | case .create: return "Create and upload the own code snippets." 42 | case .importSnippet: return "Import local code snippets into the Snippets Library." 43 | case .open: return "Open and browse an existing Snippets Library." 44 | } 45 | } 46 | 47 | var leadingPadding: CGFloat { 48 | switch self { 49 | case .importSnippet: return 1 50 | case .open: return -1 51 | default: return 0 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/URLType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 23/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum URLType { 11 | case userGuides 12 | case docs 13 | } 14 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/UploadingStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UploadingStatus.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 26/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UploadingStatus { 11 | case initializing 12 | case uploading 13 | case done 14 | case error 15 | 16 | var currentDescription: String { 17 | switch self { 18 | case .initializing: 19 | return "Initializing..." 20 | case .uploading: 21 | return "Uploading code snippets into the Xcode..." 22 | default: 23 | return "" 24 | } 25 | } 26 | 27 | internal var title: String { 28 | switch self { 29 | case .uploading, 30 | .initializing: 31 | return "Uploading" 32 | case .done: 33 | return "Upload Successful" 34 | case .error: 35 | return "Upload Failed" 36 | } 37 | } 38 | 39 | internal var message: String { 40 | switch self { 41 | case .done: return "Please relaunch Xcode to see all uploaded code snippets." 42 | case .error: return "Unable to upload code snippets. Please try again later." 43 | default: return "" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/UserActivityLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserActivityLevel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 17/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserActivityLevel: Int { 11 | case low 12 | case medium 13 | case high 14 | } 15 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Enums/UserActivityLogType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserActivityLogType.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 17/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserActivityLogType { 11 | case userSavedSnippet(_ snippetId: String) 12 | case userUpdatedSnippet(_ snippetId: String) 13 | case userRemovedSnippet(_ snippetId: String) 14 | 15 | var title: String { 16 | switch self { 17 | case let .userSavedSnippet(snippetId): 18 | return "User saved snippet with id: \(snippetId)" 19 | case let .userUpdatedSnippet(snippetId): 20 | return "User updated snippet with id: \(snippetId)" 21 | case let .userRemovedSnippet(snippetId): 22 | return "User removed snippet with id: \(snippetId)" 23 | } 24 | } 25 | 26 | var level: UserActivityLevel { 27 | switch self { 28 | case .userSavedSnippet: return .low 29 | case .userUpdatedSnippet: return .medium 30 | case .userRemovedSnippet: return .high 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Layout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Layout.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Layout { 11 | static let screenWidth = NSScreen.main?.visibleFrame.width ?? 0 12 | static let screenHeight = NSScreen.main?.visibleFrame.height ?? 0 13 | 14 | static let defaultWindowSize = CGSize(width: 850, height: 460) 15 | 16 | static let zeroPadding: CGFloat = 0.0 17 | static let smallPadding: CGFloat = 8.0 18 | static let standardPadding: CGFloat = 16.0 19 | static let mediumPadding: CGFloat = 24.0 20 | static let largePadding: CGFloat = 32.0 21 | 22 | static let fullOpacity: Double = 1.0 23 | static let largeOpacity: Double = 0.75 24 | static let mediumOpacity: Double = 0.5 25 | static let tinyOpacity: Double = 0.25 26 | static let minimalOpacity: Double = 0.1 27 | static let zeroOpacity: Double = 0.0 28 | } 29 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/Mocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mocks.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | let mockedSnippets = [ 11 | Snippet( 12 | id: UUID().uuidString, 13 | title: "AnyPublisher+Method", 14 | summary: "Easly create AnyPublisher method", 15 | content: 16 | """ 17 | func <#method name#>() -> AnyPublisher { 18 | return Future { promise in 19 | do { 20 | try <#throwing action#> 21 | promise(.success(())) 22 | } catch { 23 | promise(.failure(<#Error#>)) 24 | } 25 | } 26 | .eraseToAnyPublisher() 27 | } 28 | """, 29 | author: "Christopher Lowiec", 30 | completion: "anypubmeth", 31 | platform: .iphoneos, 32 | availability: .topLevel 33 | ) 34 | ] 35 | 36 | let skeletonSnippets = [ 37 | Snippet(mockedForSkeleton: true), 38 | Snippet(mockedForSkeleton: true), 39 | Snippet(mockedForSkeleton: true), 40 | Snippet(mockedForSkeleton: true), 41 | Snippet(mockedForSkeleton: true), 42 | Snippet(mockedForSkeleton: true) 43 | ] 44 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/NetworkObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkObserver.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | final class NetworkObserver: ObservableObject { 12 | 13 | // MARK: - Stored Porperties 14 | 15 | @Published var isConnected = true 16 | 17 | private let networkService: NetworkService 18 | 19 | private var cancellables = Set() 20 | 21 | // MARK: - Initialization 22 | 23 | init(networkService: NetworkService = DIContainer.networkServcie) { 24 | self.networkService = networkService 25 | 26 | observeNetworkState() 27 | } 28 | 29 | // MARK: - Methods 30 | 31 | private func observeNetworkState() { 32 | networkService.isConnected 33 | .receive(on: DispatchQueue.main) 34 | .sink { [weak self] in 35 | self?.isConnected = $0 36 | } 37 | .store(in: &cancellables) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/System.swift: -------------------------------------------------------------------------------- 1 | // 2 | // System.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 17/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct System { 11 | 12 | static func getHardwareUUID() -> String { 13 | let matchingDict = IOServiceMatching("IOPlatformExpertDevice") 14 | let platformExpert = IOServiceGetMatchingService( 15 | kIOMasterPortDefault, 16 | matchingDict 17 | ) 18 | 19 | defer { IOObjectRelease(platformExpert) } 20 | 21 | guard platformExpert != .zero else { return "" } 22 | 23 | return IORegistryEntryCreateCFProperty( 24 | platformExpert, kIOPlatformUUIDKey as CFString, 25 | kCFAllocatorDefault, 26 | .zero 27 | ).takeRetainedValue() as? String ?? "" 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /SnippetsLibrary/Supporting Files/TextFieldStyleModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldStyleModifier.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 11/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TextFieldStyleModifier: ViewModifier { 11 | 12 | // MARK: - Stored Properties 13 | 14 | let type: SnippetDetailsViewType 15 | 16 | // MARK: - Views 17 | 18 | @ViewBuilder 19 | func body(content: Content) -> some View { 20 | if type == .create { 21 | content 22 | .textFieldStyle(DefaultTextFieldStyle()) 23 | } else { 24 | content 25 | .textFieldStyle(PlainTextFieldStyle()) 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/DisabledCommandGroupButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisabledCommandGroupButton.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 23/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DisabledCommandGroupButton: View { 11 | 12 | // MARK: - Stored Properties 13 | 14 | let text: String 15 | @Binding private(set) var shouldBeDisabled: Bool 16 | let type: DisabledCommandGroupButtonType 17 | let onTap: () -> Void 18 | 19 | // MARK: - Views 20 | 21 | var body: some View { 22 | Button( 23 | text, 24 | action: onTap 25 | ) 26 | .disabled(type == .openLibrary ? shouldBeDisabled : !shouldBeDisabled) 27 | } 28 | 29 | } 30 | 31 | struct DisabledCommandGroupButton_Previews: PreviewProvider { 32 | static var previews: some View { 33 | DisabledCommandGroupButton(text: "", shouldBeDisabled: .constant(false), type: .openLibrary) {} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/EmptySnippetsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptySnippetsListView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EmptySnippetsListView: View { 11 | 12 | // MARK: - Stored Properties 13 | 14 | @Binding private(set) var isListEmpty: Bool 15 | 16 | // MARK: - Views 17 | 18 | var body: some View { 19 | VStack(spacing: Layout.smallPadding) { 20 | Image(systemName: "cloud") 21 | .font(.system(size: 24, weight: .light)) 22 | .foregroundColor( 23 | Color.primary 24 | .opacity(Layout.mediumOpacity) 25 | ) 26 | 27 | Text("No code snippets found \nin a database") 28 | .foregroundColor( 29 | Color.primary 30 | .opacity(Layout.mediumOpacity) 31 | ) 32 | .multilineTextAlignment(.center) 33 | } 34 | .offset(y: -Layout.largePadding) 35 | .makeVisible( 36 | isListEmpty, 37 | removed: true 38 | ) 39 | } 40 | 41 | } 42 | 43 | struct EmptySnippetsListView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | EmptySnippetsListView(isListEmpty: .constant(true)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/FileStatusCard/FileStatusCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileStatusCard.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FileStatusCard: View { 11 | 12 | private enum Constants { 13 | static let cornerRadius: CGFloat = 10.0 14 | static let strokeWidth: CGFloat = 0.2 15 | static let mediumOpacity = 0.5 16 | static let tinyOpacity = 0.2 17 | static let imageLeadingPadding: CGFloat = 10.0 18 | static let imageRegularHeight: CGFloat = 38.0 19 | static let imageSmallHeight: CGFloat = 30.0 20 | static let lineLimit = 1 21 | static let smallPadding: CGFloat = 6.0 22 | static let shadowRadius: CGFloat = 4.0 23 | static let shadowPosition = CGPoint(x: 0, y: 2) 24 | static let windowSize = CGSize(width: 230, height: 65) 25 | static let smallWindowWidth: CGFloat = 110 26 | } 27 | 28 | // MARK: - Stored Properties 29 | 30 | @ObservedObject internal var viewModel: FileStatusCardViewModel 31 | 32 | // MARK: - Views 33 | 34 | var body: some View { 35 | Button { 36 | viewModel.sendOpenSnippetNotification() 37 | } label: { 38 | ZStack { 39 | VisualEffectView( 40 | material: .menu, 41 | blendingMode: .behindWindow 42 | ) 43 | 44 | Rectangle() 45 | .fill(Color.clear) 46 | .overlay( 47 | RoundedRectangle(cornerRadius: Constants.cornerRadius) 48 | .stroke(style: StrokeStyle(lineWidth: Constants.strokeWidth)) 49 | .foregroundColor(.gray) 50 | .cornerRadius(Constants.cornerRadius) 51 | ) 52 | 53 | HStack { 54 | Image("icSnippetFileWhite") 55 | .resizable() 56 | .aspectRatio(contentMode: .fit) 57 | .frame(height: Constants.imageRegularHeight) 58 | .padding(.leading, Constants.imageLeadingPadding) 59 | 60 | VStack( 61 | alignment: .leading, 62 | spacing: .zero 63 | ) { 64 | Text(viewModel.snippet.title) 65 | .font(.system(size: 13)) 66 | .lineLimit(Constants.lineLimit) 67 | .padding(.bottom, Constants.smallPadding / 4) 68 | 69 | Text(viewModel.snippet.summary) 70 | .font(.system(size: 11)) 71 | .opacity(Layout.mediumOpacity) 72 | .lineLimit(Constants.lineLimit) 73 | .padding(.bottom, Constants.smallPadding) 74 | } 75 | .padding(.leading, Constants.smallPadding) 76 | 77 | Spacer() 78 | } 79 | .makeVisible( 80 | viewModel.type == .large, 81 | removed: true 82 | ) 83 | 84 | VStack( 85 | alignment: .center, 86 | spacing: .zero 87 | ) { 88 | Image("icSnippetFileWhite") 89 | .resizable() 90 | .aspectRatio(contentMode: .fit) 91 | .frame(height: Constants.imageSmallHeight) 92 | 93 | Text(viewModel.snippet.title) 94 | .font(.system(size: 11)) 95 | .lineLimit(Constants.lineLimit) 96 | .padding(Constants.smallPadding) 97 | } 98 | .makeVisible( 99 | viewModel.type == .normal, 100 | removed: true 101 | ) 102 | } 103 | } 104 | .buttonStyle(PlainButtonStyle()) 105 | .frame( 106 | width: viewModel.type == .large ? Constants.windowSize.width : Constants.smallWindowWidth, 107 | height: Constants.windowSize.height, 108 | alignment: .leading 109 | ) 110 | .cornerRadius(Constants.cornerRadius) 111 | .shadow( 112 | color: Color.black.opacity(Constants.tinyOpacity), 113 | radius: Constants.shadowRadius, 114 | x: Constants.shadowPosition.x, 115 | y: Constants.shadowPosition.y 116 | ) 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/FileStatusCard/FileStatusCardViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileStatusCardViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 27/09/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | final class FileStatusCardViewModel: ObservableObject { 11 | 12 | // MARK: - Stored Properties 13 | 14 | private(set) var snippet: Snippet 15 | private(set) var type: FileStatusCardType 16 | 17 | // MARK: - Initialization 18 | 19 | init( 20 | snippet: Snippet?, 21 | type: FileStatusCardType = .normal 22 | ) { 23 | self.snippet = snippet ?? Snippet() 24 | self.type = type 25 | } 26 | 27 | // MARK: - Methods 28 | 29 | internal func sendOpenSnippetNotification() { 30 | NotificationCenter.default.post( 31 | name: NSNotification.statusBarSnippetTapped, 32 | object: nil, 33 | userInfo: ["snippetId": snippet.id] 34 | ) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/RecentSnippetCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecentSnippetCardView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RecentSnippetCardView: View { 11 | 12 | private enum Constants { 13 | static let imageHeight: CGFloat = 38.0 14 | static let spacerHeight: CGFloat = 16.0 15 | static let cornerRadius: CGFloat = 6.0 16 | static let lineLimit = 1 17 | static let tapGestureCount = 2 18 | } 19 | 20 | // MARK: - Stored Properties 21 | 22 | let snippet: Snippet 23 | let tag: Int 24 | 25 | @Binding internal var selectedFileIndex: Int? 26 | 27 | private(set) var onTap: () -> Void 28 | 29 | // MARK: - Views 30 | 31 | var body: some View { 32 | Button { 33 | selectedFileIndex = tag 34 | } label: { 35 | HStack(spacing: .zero) { 36 | Image("icSnippetFileWhite") 37 | .resizable() 38 | .aspectRatio(contentMode: .fit) 39 | .frame(height: Constants.imageHeight) 40 | 41 | VStack( 42 | alignment: .leading, 43 | spacing: Layout.smallPadding / 4 44 | ) { 45 | Text(snippet.title) 46 | .font(.system(size: 13)) 47 | .foregroundColor(Color.primary) 48 | .lineLimit(Constants.lineLimit) 49 | 50 | Text(snippet.summary) 51 | .font(.system(size: 11)) 52 | .foregroundColor( 53 | Color.primary 54 | .opacity(Layout.mediumOpacity) 55 | ) 56 | .lineLimit(Constants.lineLimit) 57 | } 58 | .padding(.leading, Layout.smallPadding) 59 | 60 | Spacer() 61 | } 62 | } 63 | .buttonStyle(PlainButtonStyle()) 64 | .tag(tag) 65 | .padding(.horizontal, Layout.smallPadding) 66 | .padding(.vertical, Layout.smallPadding / 2) 67 | .background(selectedFileIndex == tag ? Color("accent") : nil) 68 | .cornerRadius(Constants.cornerRadius) 69 | .simultaneousGesture( 70 | TapGesture(count: Constants.tapGestureCount) 71 | .onEnded(onTap) 72 | ) 73 | } 74 | 75 | } 76 | 77 | struct RecentSnippetCardView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | RecentSnippetCardView(snippet: Snippet(), tag: 0, selectedFileIndex: .constant(nil)) {} 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchBar: View { 11 | 12 | private enum Constants { 13 | static let animationDuration: Double = 0.2 14 | } 15 | 16 | // MARK: - Stored Properties 17 | 18 | @State private var searchedText = "" 19 | @State private var isEditing = false 20 | 21 | private(set) var onChange: (String) -> Void 22 | 23 | // MARK: - Views 24 | 25 | var body: some View { 26 | HStack { 27 | Image(systemName: "magnifyingglass") 28 | .font(.system(size: 13.0)) 29 | .foregroundColor( 30 | Color.primary 31 | .opacity(Layout.mediumOpacity) 32 | ) 33 | 34 | TextField( 35 | "Search for snippets...", 36 | text: $searchedText 37 | ) { isEditing in 38 | onChange(searchedText) 39 | withAnimation { 40 | self.isEditing = isEditing 41 | } 42 | } onCommit: { 43 | onChange(searchedText) 44 | isEditing = false 45 | } 46 | .textFieldStyle(PlainTextFieldStyle()) 47 | .padding(.vertical, Layout.smallPadding) 48 | .onChange(of: searchedText) { text in 49 | onChange(text) 50 | } 51 | 52 | Button { 53 | searchedText = "" 54 | isEditing = false 55 | } label: { 56 | Image(systemName: "multiply.circle.fill") 57 | .foregroundColor(Color.gray) 58 | .padding(.leading, Layout.smallPadding) 59 | } 60 | .buttonStyle(PlainButtonStyle()) 61 | .makeVisible(isEditing) 62 | .animation(.easeIn(duration: Constants.animationDuration)) 63 | } 64 | } 65 | 66 | } 67 | 68 | struct SearchBar_Previews: PreviewProvider { 69 | static var previews: some View { 70 | SearchBar() { _ in } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/SnippetCreationManualView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetCreationManualView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 03/11/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetCreationManualView: View { 11 | 12 | private enum Constants { 13 | static let imageHeight: CGFloat = 38.0 14 | static let lineSpacing: CGFloat = 5.0 15 | static let viewSize = CGSize(width: 350.0, height: 400.0) 16 | static let cornerRadius: CGFloat = 12.0 17 | } 18 | 19 | // MARK: - Views 20 | 21 | var body: some View { 22 | VStack { 23 | Image("icSnippetFileWhite") 24 | .resizable() 25 | .aspectRatio(contentMode: .fit) 26 | .frame(height: Constants.imageHeight) 27 | 28 | Text("How to create a snippet?") 29 | .font(.system(size: 17, weight: .bold)) 30 | .padding(.bottom, Layout.smallPadding) 31 | 32 | Text("To maintain consistency and unify all code snippets in the Snippets Library, follow this two main rules:") 33 | .font(.system(size: 11, weight: .medium)) 34 | .padding(.bottom) 35 | .opacity(Layout.mediumOpacity) 36 | .multilineTextAlignment(.center) 37 | 38 | Text( 39 | """ 40 | ✅ Make sure, that your snippet is helpful, complete and contains only the necessary lines required for use. 41 | 42 | ✅ Enter the title, summary, supported platforms and scopes and remember, that the completion should be self-explaining. 43 | """ 44 | ) 45 | .lineSpacing(Constants.lineSpacing) 46 | .font(.system(size: 13)) 47 | .multilineTextAlignment(.leading) 48 | } 49 | .padding(Layout.largePadding) 50 | .frame( 51 | width: Constants.viewSize.width, 52 | height: Constants.viewSize.height 53 | ) 54 | .background(Color("defaultBackground")) 55 | .cornerRadius(Constants.cornerRadius) 56 | } 57 | 58 | } 59 | 60 | struct SnippetCreationManualView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | SnippetCreationManualView() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/SnippetDropCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetDropCellView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 12/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetDropCellView: View { 11 | 12 | // MARK: - Stored Properties 13 | 14 | let snippet: Snippet? 15 | 16 | // MARK: - Views 17 | 18 | var body: some View { 19 | VStack(spacing: .zero) { 20 | Image(systemName: "doc.text.fill") 21 | .font(.system(size: 36.0)) 22 | .padding(.bottom, Layout.smallPadding) 23 | 24 | Text("Drop the snippet file here") 25 | .font(.system(size: 15.0)) 26 | } 27 | } 28 | 29 | } 30 | 31 | struct SnippetDropCellView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | SnippetDropCellView(snippet: Snippet()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/SnippetFileCard/SnippetFileCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetFileCardView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Sourceful 10 | 11 | struct SnippetFileCardView: View { 12 | 13 | private enum Constants { 14 | static let cornerRadius: CGFloat = 4.0 15 | static let codeEditorHorizontalPadding: CGFloat = 1.0 16 | static let codeEditorVerticlaPadding: CGFloat = 0.0 17 | static let codeEditorHeight: CGFloat = Layout.defaultWindowSize.height - (Layout.largePadding * 4) 18 | static let strokeWidth: CGFloat = 1.0 19 | static let minimalOpacity = 0.0001 20 | static let bottomBarHeight: CGFloat = 20.0 21 | static let bottomBarCornerRadius: CGFloat = 2.0 22 | } 23 | 24 | // MARK: - Stored Properties 25 | 26 | @Environment(\.colorScheme) var colorScheme 27 | 28 | @ObservedObject private(set) var viewModel: SnippetFileCardViewModel 29 | 30 | // MARK: - Views 31 | 32 | var body: some View { 33 | VStack( 34 | alignment: .leading, 35 | spacing: .zero 36 | ) { 37 | HStack { 38 | Text(viewModel.titleText.isEmpty ? viewModel.snippet.title : viewModel.titleText) 39 | .font(.headline) 40 | 41 | Spacer() 42 | 43 | Group { 44 | Button { 45 | viewModel.downloadSnippet() 46 | } label: { 47 | Image(systemName: "arrow.down.circle") 48 | .font(.system(size: 15, weight: .light)) 49 | .opacity(Layout.largeOpacity) 50 | } 51 | .buttonStyle(PlainButtonStyle()) 52 | .help("Download selected snippet") 53 | 54 | Button { 55 | viewModel.openSnippetDetails() 56 | } label: { 57 | Image(systemName: "info.circle") 58 | .font(.system(size: 15, weight: .light)) 59 | .opacity(Layout.largeOpacity) 60 | } 61 | .buttonStyle(PlainButtonStyle()) 62 | .help("Show snippet details") 63 | } 64 | } 65 | 66 | HStack { 67 | ForEach( 68 | viewModel.snippet.tags ?? [], 69 | id: \.self 70 | ) { 71 | Text($0) 72 | .font(.footnote) 73 | .foregroundColor(Color.white) 74 | .padding(.horizontal, Layout.smallPadding / 2) 75 | .padding(.vertical, Layout.smallPadding / 4) 76 | .background( 77 | RoundedRectangle(cornerRadius: Constants.cornerRadius) 78 | .foregroundColor( 79 | Color.accentColor 80 | .opacity(Layout.mediumOpacity) 81 | ) 82 | ) 83 | } 84 | } 85 | .padding(.vertical, Layout.smallPadding) 86 | 87 | ZStack { 88 | SourceCodeTextEditor( 89 | text: .constant(viewModel.snippet.content), 90 | customization: SourceCodeTextEditor.Customization( 91 | didChangeText: { _ in }, 92 | insertionPointColor: { colorScheme == .dark ? .white : .black }, 93 | lexerForSource: { _ in SwiftLexer() }, 94 | textViewDidBeginEditing: { _ in }, 95 | theme: { CustomCodeTheme(colorScheme: colorScheme) } 96 | ), 97 | shouldBecomeFirstResponder: false 98 | ) 99 | .frame( 100 | minHeight: Constants.codeEditorHeight, 101 | alignment: .leading 102 | ) 103 | .padding(.horizontal, Constants.codeEditorHorizontalPadding) 104 | .padding(.vertical, Constants.codeEditorVerticlaPadding) 105 | .cornerRadius(Constants.cornerRadius) 106 | .background( 107 | RoundedRectangle(cornerRadius: Constants.cornerRadius) 108 | .stroke(style: StrokeStyle(lineWidth: Constants.strokeWidth)) 109 | .foregroundColor(Color.black) 110 | ) 111 | 112 | RoundedRectangle(cornerRadius: Constants.cornerRadius) 113 | .foregroundColor( 114 | Color.black 115 | .opacity(Constants.minimalOpacity) 116 | ) 117 | .makeVisible(viewModel.state == .preview) 118 | } 119 | 120 | HStack { 121 | Text("SwiftUI") 122 | .font(.footnote) 123 | .opacity(Layout.mediumOpacity) 124 | .padding(.leading, Layout.smallPadding) 125 | 126 | Divider() 127 | 128 | Spacer() 129 | 130 | Divider() 131 | 132 | Text("\(viewModel.linesCount()) lines") 133 | .font(.footnote) 134 | .opacity(Layout.mediumOpacity) 135 | .padding(.trailing, Layout.smallPadding) 136 | } 137 | .frame(height: Constants.bottomBarHeight) 138 | .background( 139 | RoundedRectangle(cornerRadius: Constants.bottomBarCornerRadius) 140 | .foregroundColor(Color("skeletonLight")) 141 | ) 142 | .padding(.vertical, Layout.smallPadding) 143 | } 144 | } 145 | 146 | } 147 | 148 | struct SnippetFileCardView_Previews: PreviewProvider { 149 | static var previews: some View { 150 | SnippetFileCardView(viewModel: SnippetFileCardViewModel(snippet: Snippet(), state: .preview, activeSheet: .constant(nil), appAlert: .constant(nil))) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/SnippetFileCard/SnippetFileCardViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetFileCardViewModel.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class SnippetFileCardViewModel: ObservableObject { 11 | 12 | // MARK: - Stored Properties 13 | 14 | private(set) var snippet: Snippet 15 | private(set) var state: SnippetFileCardViewState 16 | 17 | @Published internal var titleText = "" 18 | @Published internal var contentText = "" 19 | @Binding internal var appAlert: AppAlert? 20 | @Binding internal var activeSheet: AppSheet? 21 | 22 | private let snippetParserService: SnippetsParserService 23 | 24 | private(set) var onDelete: (() -> Void)? = nil 25 | 26 | // MARK: - Initialization 27 | 28 | init( 29 | snippet: Snippet?, 30 | state: SnippetFileCardViewState, 31 | activeSheet: Binding, 32 | appAlert: Binding, 33 | snippetParserService: SnippetsParserService = DIContainer.snippetsParserService, 34 | onDelete: (() -> Void)? = nil 35 | ) { 36 | self.snippet = snippet ?? Snippet() 37 | self.state = state 38 | _activeSheet = activeSheet 39 | _appAlert = appAlert 40 | self.snippetParserService = snippetParserService 41 | self.onDelete = onDelete 42 | 43 | setup() 44 | } 45 | 46 | // MARK: - Methods 47 | 48 | internal func downloadSnippet() { 49 | snippetParserService.writeToPath( 50 | type: .download, 51 | snippets: [snippet] 52 | ) { 53 | self.appAlert = .snippetDownload 54 | } 55 | } 56 | 57 | internal func linesCount() -> Int { 58 | return contentText.numberOfLines() 59 | } 60 | 61 | internal func openSnippetDetails() { 62 | activeSheet = .snippetDetails(snippet, .edit) 63 | } 64 | 65 | private func setup() { 66 | titleText = snippet.title 67 | contentText = snippet.content 68 | snippet.tags = snippet.tags?.isEmpty == true ? ["SwiftUI"] : [] 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/SnippetListItem/SnippetListItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetListItemView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 08/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnippetListItemView: View { 11 | 12 | private enum Constants { 13 | static let imageHeight: CGFloat = 38.0 14 | static let lineLimit = 1 15 | static let spacing = Layout.smallPadding / 2 16 | static let smallSpacing = Layout.smallPadding / 4 17 | static let cornerRadius: CGFloat = 8.0 18 | } 19 | 20 | // MARK: - Stored Properties 21 | 22 | let snippet: Snippet 23 | 24 | // MARK: - Computed Properties 25 | 26 | private var shouldAnimate: Bool { 27 | snippet.content.isEmpty 28 | } 29 | 30 | // MARK: - Views 31 | 32 | var body: some View { 33 | HStack(spacing: .zero) { 34 | Image("icSnippetFileWhite") 35 | .resizable() 36 | .aspectRatio(contentMode: .fit) 37 | .frame(height: Constants.imageHeight) 38 | .makeSkeletonable(animating: shouldAnimate) 39 | 40 | VStack( 41 | alignment: .leading, 42 | spacing: Constants.smallSpacing 43 | ) { 44 | Text("\(snippet.title)") 45 | .font(.system(size: 13)) 46 | .foregroundColor(Color.primary) 47 | .lineLimit(Constants.lineLimit) 48 | .makeSkeletonable(animating: shouldAnimate) 49 | 50 | Text(snippet.summary) 51 | .font(.system(size: 11)) 52 | .foregroundColor( 53 | Color.primary 54 | .opacity(Layout.mediumOpacity) 55 | ) 56 | .lineLimit(Constants.lineLimit) 57 | .makeSkeletonable(animating: shouldAnimate) 58 | } 59 | .padding(.leading, Layout.smallPadding) 60 | 61 | Spacer() 62 | } 63 | .padding( 64 | .horizontal, 65 | shouldAnimate ? Constants.spacing : .zero 66 | ) 67 | .padding( 68 | .vertical, 69 | shouldAnimate ? Constants.smallSpacing : .zero 70 | ) 71 | .background( 72 | RoundedRectangle(cornerRadius: Constants.cornerRadius) 73 | .makeSkeletonable( 74 | animating: shouldAnimate, 75 | isBottomView: true 76 | ) 77 | .makeVisible( 78 | shouldAnimate, 79 | removed: true 80 | ) 81 | ) 82 | } 83 | 84 | } 85 | 86 | struct SnippetListItemView_Previews: PreviewProvider { 87 | static var previews: some View { 88 | SnippetListItemView(snippet: Snippet()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/ToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 25/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum ToastViewState { 11 | case success 12 | case failure 13 | case none 14 | 15 | var color: Color { 16 | switch self { 17 | case .success: return Color.green 18 | case .failure: return Color.red 19 | case .none: return Color.primary 20 | } 21 | } 22 | } 23 | 24 | struct ToastView: View { 25 | 26 | private enum Constans { 27 | static let spacing: CGFloat = 16.0 28 | static let imageSize: CGFloat = 22.0 29 | static let lineLimit = 1 30 | static let height: CGFloat = 44.0 31 | static let shadowOpacity = 0.08 32 | static let shadowRadius: CGFloat = 8.0 33 | static let shadowOffset: CGFloat = 4.0 34 | } 35 | 36 | // MARK: - Stored Properties 37 | 38 | @Environment(\.colorScheme) var colorScheme 39 | 40 | let imageName: String? 41 | let title: String 42 | let subtitle: String? 43 | let state: ToastViewState? 44 | 45 | // MARK: - Views 46 | 47 | var body: some View { 48 | HStack(spacing: Constans.spacing) { 49 | Image(systemName: imageName ?? "") 50 | .renderingMode(.template) 51 | .resizable() 52 | .scaledToFit() 53 | .frame( 54 | width: Constans.imageSize, 55 | height: Constans.imageSize 56 | ) 57 | .foregroundColor(state?.color) 58 | .makeVisible( 59 | imageName != nil, 60 | removed: true 61 | ) 62 | 63 | VStack { 64 | Text(title) 65 | .lineLimit(Constans.lineLimit) 66 | .font(.headline) 67 | .foregroundColor(Color.primary) 68 | 69 | Text(subtitle ?? "") 70 | .lineLimit(Constans.lineLimit) 71 | .font(.subheadline) 72 | .foregroundColor(Color.secondary) 73 | .makeVisible( 74 | subtitle != nil, 75 | removed: true 76 | ) 77 | } 78 | .padding(imageName == nil ? .horizontal : .trailing) 79 | } 80 | .padding(.horizontal) 81 | .frame(height: Constans.height) 82 | .background(colorScheme == .dark ? Color("darkGrey") : Color.white) 83 | .cornerRadius(Constans.height / 2) 84 | .shadow( 85 | color: Color.black.opacity(Constans.shadowOpacity), 86 | radius: Constans.shadowRadius, 87 | x: .zero, 88 | y: Constans.shadowOffset 89 | ) 90 | } 91 | 92 | } 93 | 94 | struct ToastView_Previews: PreviewProvider { 95 | static var previews: some View { 96 | ToastView(imageName: nil, title: "", subtitle: nil, state: ToastViewState.none) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SnippetsLibrary/Views/VisualEffectView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisualEffectView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 07/09/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct VisualEffectView: NSViewRepresentable { 11 | 12 | // MARK: - Stored Properties 13 | 14 | let material: NSVisualEffectView.Material 15 | let blendingMode: NSVisualEffectView.BlendingMode 16 | 17 | // MARK: - Methods 18 | 19 | internal func makeNSView(context: Context) -> NSVisualEffectView { 20 | let visualEffectView = NSVisualEffectView() 21 | visualEffectView.material = material 22 | visualEffectView.blendingMode = blendingMode 23 | visualEffectView.state = NSVisualEffectView.State.active 24 | return visualEffectView 25 | } 26 | 27 | internal func updateNSView( 28 | _ visualEffectView: NSVisualEffectView, 29 | context: Context 30 | ) { 31 | visualEffectView.material = material 32 | visualEffectView.blendingMode = blendingMode 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /SnippetsLibraryUITests/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 | -------------------------------------------------------------------------------- /SnippetsLibraryUITests/SnippetsLibraryUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryUITests.swift 3 | // SnippetsLibraryUITests 4 | // 5 | // Created by Krzysztof Łowiec on 19/09/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class SnippetsLibraryUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SnippetsLibraryUnitTests/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 | -------------------------------------------------------------------------------- /SnippetsLibraryUnitTests/SnippetsLibraryUnitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryUnitTests.swift 3 | // SnippetsLibraryUnitTests 4 | // 5 | // Created by Krzysztof Łowiec on 19/09/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class SnippetsLibraryUnitTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | } 24 | 25 | func testPerformanceExample() throws { 26 | // This is an example of a performance test case. 27 | measure { 28 | // Put the code you want to measure the time of here. 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/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 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/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 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Models/WidgetEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetEntry.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 03/10/2021. 6 | // 7 | 8 | import WidgetKit 9 | 10 | struct WidgetEntry: TimelineEntry { 11 | 12 | var date: Date = Date() 13 | let snippet: Snippet 14 | let snippets: [Snippet] 15 | 16 | } 17 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Provider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Provider.swift 3 | // SnippetsLibraryWidgetExtension 4 | // 5 | // Created by Krzysztof Łowiec on 03/10/2021. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | struct Provider: TimelineProvider { 12 | 13 | private enum Constants { 14 | static let refreshInterval = 15 15 | } 16 | 17 | // MARK: - Computed Properties 18 | 19 | private var entries: [Entry] { 20 | var widgetEntries: [WidgetEntry] = [] 21 | let userDefaultsService: UserDefaultsService = UserDefaultsServiceImpl() 22 | let snippets = userDefaultsService.fetchRecentSnippetsFromAppGroup() 23 | 24 | for snippet in snippets { 25 | widgetEntries.append( 26 | WidgetEntry( 27 | snippet: snippet, 28 | snippets: snippets 29 | ) 30 | ) 31 | } 32 | 33 | return widgetEntries 34 | } 35 | 36 | private var defaultWidgetEntry: WidgetEntry { 37 | WidgetEntry( 38 | snippet: entries.first?.snippet ?? Snippet(mockedForSkeleton: true), 39 | snippets: entries.first?.snippets ?? [ 40 | Snippet(mockedForSkeleton: true), 41 | Snippet(mockedForSkeleton: true), 42 | Snippet(mockedForSkeleton: true) 43 | ] 44 | ) 45 | } 46 | 47 | // MARK: - Methods 48 | 49 | func getSnapshot( 50 | in context: Context, 51 | completion: @escaping (WidgetEntry) -> Void 52 | ) { 53 | completion(defaultWidgetEntry) 54 | } 55 | 56 | func getTimeline( 57 | in context: Context, 58 | completion: @escaping (Timeline) -> Void 59 | ) { 60 | var entries = entries 61 | 62 | let currentDate = Date() 63 | for index in .zero ..< entries.count { 64 | entries[index].date = Calendar.current.date( 65 | byAdding: .second, 66 | value: index * Constants.refreshInterval, 67 | to: currentDate 68 | )! 69 | } 70 | 71 | let timeline = Timeline( 72 | entries: entries, 73 | policy: .atEnd 74 | ) 75 | completion(timeline) 76 | } 77 | 78 | func placeholder(in context: Context) -> WidgetEntry { 79 | defaultWidgetEntry 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/SnippetsLibraryWidget.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.cphlowiec.SnippetsLibrary 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/SnippetsLibraryWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryWidget.swift 3 | // SnippetsLibraryWidget 4 | // 5 | // Created by Krzysztof Łowiec on 02/10/2021. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | @main 12 | struct SnippetsLibraryWidget: Widget { 13 | 14 | // MARK: - Stored Properties 15 | 16 | private let kind: String = "SnippetsLibraryWidget" 17 | 18 | // MARK: - Views 19 | 20 | var body: some WidgetConfiguration { 21 | StaticConfiguration( 22 | kind: kind, 23 | provider: Provider() 24 | ) { 25 | SnippetsLibraryWidgetView(entry: $0) 26 | } 27 | .configurationDisplayName("Snippets Library Widget") 28 | .description("General statistics for the Snippet Library.") 29 | .supportedFamilies([.systemSmall, .systemMedium]) 30 | } 31 | 32 | } 33 | 34 | struct SnippetsLibraryWidget_Previews: PreviewProvider { 35 | static var previews: some View { 36 | SnippetsLibraryWidgetView(entry: WidgetEntry(date: Date(), snippet: Snippet(), snippets: [])) 37 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Views/MediumWidget/MediumWidgetListItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediumWidgetListItemView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 03/10/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MediumWidgetListItemView: View { 11 | 12 | private enum Constants { 13 | static let imageHeight: CGFloat = 28.0 14 | static let lineLimit = 2 15 | } 16 | 17 | // MARK: - Stored Properties 18 | 19 | let snippet: Snippet 20 | 21 | // MARK: - Views 22 | 23 | var body: some View { 24 | HStack(spacing: .zero) { 25 | Image("icSnippetFileWhite") 26 | .resizable() 27 | .aspectRatio(contentMode: .fit) 28 | .frame(height: Constants.imageHeight) 29 | 30 | Text("\(snippet.title)") 31 | .font(.system(size: 13, weight: .medium)) 32 | .foregroundColor(Color.primary) 33 | .lineLimit(Constants.lineLimit) 34 | .padding(.leading, Layout.smallPadding) 35 | 36 | Spacer() 37 | } 38 | } 39 | 40 | } 41 | 42 | struct MediumWidgetListItemView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | MediumWidgetListItemView(snippet: Snippet()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Views/MediumWidget/MediumWidgetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediumWidgetView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 03/10/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MediumWidgetView: View { 11 | 12 | private enum Constants { 13 | static let imageHeight: CGFloat = 64.0 14 | static let maxSnippetsCount = 5 15 | } 16 | 17 | // MARK: - Stored Properties 18 | 19 | let snippets: [Snippet] 20 | 21 | // MARK: - Views 22 | 23 | var body: some View { 24 | GeometryReader { geometry in 25 | ZStack { 26 | HStack(spacing: .zero) { 27 | Spacer() 28 | 29 | Rectangle() 30 | .foregroundColor(Color("defaultBackground")) 31 | .frame(width: geometry.size.width / 2) 32 | } 33 | 34 | HStack { 35 | SmallWidgetView(snippet: snippets.first ?? Snippet()) 36 | .frame(width: (geometry.size.width / 2) - Layout.smallPadding) 37 | 38 | VStack(alignment: .leading) { 39 | ForEach(snippets.prefix(Constants.maxSnippetsCount).filter({ $0 != snippets.first }), id: \.self) { snippet in 40 | Link(destination: URL(string: "widget://\(snippet.id)")!) { 41 | MediumWidgetListItemView(snippet: snippet) 42 | } 43 | } 44 | 45 | Spacer() 46 | .makeVisible( 47 | snippets.count < Constants.maxSnippetsCount, 48 | removed: true 49 | ) 50 | } 51 | .frame( 52 | width: (geometry.size.width / 2) - Layout.smallPadding, 53 | height: geometry.size.height - Layout.largePadding 54 | ) 55 | } 56 | } 57 | } 58 | } 59 | 60 | } 61 | 62 | struct MediumWidgetView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | MediumWidgetView(snippets: []) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Views/SmallWidget/SmallWidgetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SmallWidgetView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 02/10/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SmallWidgetView: View { 11 | 12 | private enum Constants { 13 | static let imageHeight: CGFloat = 64.0 14 | static let shadowOpacity = 0.16 15 | static let shadowRadius: CGFloat = 6.0 16 | static let shadowYOffset: CGFloat = 3.0 17 | } 18 | 19 | // MARK: - Stored Properties 20 | 21 | let snippet: Snippet 22 | 23 | // MARK: - Views 24 | 25 | var body: some View { 26 | VStack( 27 | alignment: .leading, 28 | spacing: .zero 29 | ) { 30 | Image("icSnippetFileBlank") 31 | .resizable() 32 | .aspectRatio(contentMode: .fit) 33 | .frame(height: Constants.imageHeight) 34 | .shadow( 35 | color: Color.black 36 | .opacity(Constants.shadowOpacity), 37 | radius: Constants.shadowRadius, 38 | x: .zero, 39 | y: Constants.shadowYOffset 40 | ) 41 | 42 | Spacer() 43 | 44 | Text(snippet.title) 45 | .font(.headline) 46 | 47 | Text(snippet.summary) 48 | .font(.footnote) 49 | .foregroundColor( 50 | Color.primary 51 | .opacity(Layout.mediumOpacity) 52 | ) 53 | } 54 | .padding() 55 | .widgetURL(URL(string: "widget://\(snippet.id)")!) 56 | } 57 | 58 | } 59 | 60 | struct SmallWidgetView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | SmallWidgetView(snippet: Snippet()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SnippetsLibraryWidget/Views/SnippetsLibraryWidgetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnippetsLibraryWidgetView.swift 3 | // SnippetsLibrary 4 | // 5 | // Created by Krzysztof Łowiec on 03/10/2021. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | struct SnippetsLibraryWidgetView: View { 12 | 13 | // MARK: - Stored Properties 14 | 15 | @Environment(\.widgetFamily) var family: WidgetFamily 16 | 17 | private(set) var entry: Provider.Entry 18 | 19 | // MARK: - Views 20 | 21 | var body: some View { 22 | switch family { 23 | case .systemMedium: 24 | MediumWidgetView(snippets: entry.snippets) 25 | default: 26 | SmallWidgetView(snippet: entry.snippet) 27 | } 28 | } 29 | 30 | } 31 | 32 | struct SnippetsLibraryWidgetView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | SnippetsLibraryWidgetView(entry: .init(snippet: Snippet(), snippets: [])) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SnippetsLibrary/SnippetsLibrary.swift: -------------------------------------------------------------------------------- 1 | struct SnippetsLibrary { 2 | var text = "Hello, World!" 3 | } 4 | -------------------------------------------------------------------------------- /Tests/SnippetsLibraryTests/SnippetsLibraryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SnippetsLibrary 3 | 4 | final class SnippetsLibraryTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(SnippetsLibrary().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/images/app_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryboxx/SnippetsLibrary/d7a4c31ceb735b6cf92fcff8cfa38fcfa743ee0c/public/images/app_preview.png --------------------------------------------------------------------------------