├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── change-checker.yml ├── .gitignore ├── .slather.yml ├── AppScript ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── AppScript.xcscheme ├── Package.swift ├── README.md ├── Sources │ └── AppScript │ │ ├── Services.swift │ │ ├── Stores.swift │ │ └── Wrappers.swift └── Tests │ └── AppScriptTests │ └── AppScriptTests.swift ├── Application ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── GoogleService-Info.plist ├── Info.plist ├── StackOv.entitlements └── StackOv.xcdatamodeld │ ├── .xccurrentversion │ └── Shared.xcdatamodel │ └── contents ├── CHANGELOG.md ├── Common ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── Common.xcscheme ├── Package.swift ├── README.md ├── Sources │ └── Common │ │ ├── Cache.swift │ │ ├── Constants │ │ ├── PageConstants.swift │ │ ├── SidebarConstants.swift │ │ └── ThreadItemConstants.swift │ │ ├── Environments │ │ ├── MainContent+Environments.swift │ │ └── Window+Environments.swift │ │ ├── Extensions │ │ ├── Data+Extensions.swift │ │ ├── EdgeInsets+Extensions.swift │ │ ├── HTTPURLResponse+Extensions.swift │ │ ├── String+Extensions.swift │ │ ├── UIApplication+Extensions.swift │ │ ├── UIImage+Extensions.swift │ │ ├── UIUserInterfaceIdiom+Extensions.swift │ │ ├── URL+Extensions.swift │ │ ├── URLSession+Extensions.swift │ │ └── View+Extensions.swift │ │ ├── GlobalBanner │ │ └── GlobalBanner.swift │ │ ├── Models │ │ ├── AnswerModel.swift │ │ ├── CommentModel.swift │ │ ├── QuestionModel.swift │ │ ├── ShallowUserModel.swift │ │ └── UserModel.swift │ │ ├── Modifiers │ │ ├── ApplicationDidBecomeActiveViewModifier.swift │ │ └── DeviceRotationViewModifier.swift │ │ ├── NotificationBannerModel.swift │ │ ├── SidebarStyle.swift │ │ └── Wrappers │ │ └── Lazy.swift └── Tests │ └── CommonTests │ └── CommonTests.swift ├── Components ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── Components.xcscheme ├── Package.swift ├── README.md └── Sources │ └── Components │ ├── ActivityView.swift │ ├── BadgeView.swift │ ├── CheckmarkView.swift │ ├── EllipsisButton.swift │ ├── FilterButton.swift │ ├── LoaderView │ ├── LoaderCircle.swift │ └── LoaderView.swift │ ├── MarkdownPostView │ ├── Blocks │ │ ├── BlockQuoteView.swift │ │ ├── CodeBlockView.swift │ │ ├── DocumentView.swift │ │ ├── ImageView.swift │ │ ├── ListView.swift │ │ ├── RepetitiveView.swift │ │ ├── TableView.swift │ │ └── TextView.swift │ └── MarkdownPostView.swift │ ├── NotificationBanner │ └── NotificationBannerView.swift │ ├── PageButton │ ├── PageButton.swift │ └── PageModel.swift │ ├── PageDevMock.swift │ ├── PersonInfoView.swift │ ├── SafariView.swift │ ├── ShimmerView │ ├── ShimmerConfig.swift │ ├── ShimmerModifier.swift │ ├── ShimmerView.swift │ └── View+Extension.swift │ ├── SidebarLeftButton.swift │ ├── TagButton │ └── TagButton.swift │ ├── TagFilter.swift │ ├── TagsCollection │ ├── AdaptiveTagsCollectionView.swift │ └── TagsCollection.swift │ └── ThreadItemView │ ├── ThreadItemInfoView.swift │ ├── ThreadItemNavigationLinkStyle.swift │ └── ThreadItemView.swift ├── DEVPROCESS.md ├── DataTransferObjects ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── DataTransferObjects.xcscheme ├── Package.swift ├── README.md ├── Sources │ └── DataTransferObjects │ │ ├── AnswerEntry.swift │ │ ├── CommentEntry.swift │ │ ├── PostsEntry.swift │ │ ├── QuestionEntry.swift │ │ └── ShallowUserEntry.swift └── Tests │ └── DataTransferObjectsTests │ └── DataTransferObjectsTests.swift ├── Errors ├── .gitignore ├── Package.swift ├── README.md └── Sources │ └── Errors │ ├── DataError.swift │ ├── DisplaybleError.swift │ ├── HTTPError.swift │ ├── HTTPStatusCode.swift │ ├── OpenURLError.swift │ ├── PasteboardError.swift │ ├── ServiceError.swift │ └── URLSessionError.swift ├── Flows ├── FavoriteFlow │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── FavoriteFlow.xcscheme │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── FavoriteFlow │ │ ├── FavoriteFlow.swift │ │ └── PageView │ │ └── PageView.swift ├── HomeFlow │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── HomeFlow.xcscheme │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── HomeFlow │ │ ├── FilterView │ │ ├── FilterView.swift │ │ └── Rows │ │ │ ├── FilterOptionRow.swift │ │ │ └── SortOptionRow.swift │ │ ├── HomeFlow.swift │ │ ├── PageView │ │ └── PageView.swift │ │ ├── PagesView │ │ └── PagesView.swift │ │ └── PostItemLoadingView │ │ └── PostItemLoadingView.swift ├── MainFlow │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── MainFlow │ │ ├── GlobalBannerModifier.swift │ │ ├── MainBar │ │ └── MainBar.swift │ │ ├── MainFlow.swift │ │ ├── PadContentView.swift │ │ ├── PhoneContentView.swift │ │ └── Sidebar │ │ ├── Compact │ │ ├── CompactSidebarButton.swift │ │ ├── CompactSidebarView.swift │ │ └── CompactUserView.swift │ │ ├── Regular │ │ ├── RegularSidebarButton.swift │ │ ├── RegularSidebarView.swift │ │ └── RegularUserView.swift │ │ └── SidebarView.swift ├── MessagesFlow │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MessagesFlow.xcscheme │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── MessagesFlow │ │ └── MessagesFlow.swift ├── TagsFlow │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── TagsFlow.xcscheme │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── TagsFlow │ │ └── TagsFlow.swift ├── ThreadFlow │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── ThreadFlow │ │ ├── AnswerView │ │ ├── AnswerView.swift │ │ ├── PadAnswerView.swift │ │ └── PhoneAnswerView.swift │ │ ├── CommentsView │ │ └── CommentsView.swift │ │ ├── QuestionView │ │ ├── PadQuestionView.swift │ │ ├── PhoneQuestionView.swift │ │ └── QuestionView.swift │ │ ├── RetingView │ │ └── RetingView.swift │ │ ├── ThreadFlow.swift │ │ └── ThreadFlowScreenCofiguration.swift └── UsersFlow │ ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── UsersFlow.xcscheme │ ├── Package.swift │ ├── README.md │ └── Sources │ └── UsersFlow │ └── UsersFlow.swift ├── HTMLMarkdown ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── HTMLMarkdown.xcscheme ├── Package.swift ├── README.md ├── Sources │ └── HTMLMarkdown │ │ ├── HTMLMarkdown.swift │ │ └── Unit │ │ ├── UnitConfigurator.swift │ │ ├── UnitView.swift │ │ └── Units.swift └── Tests │ └── HTMLMarkdownTests │ └── HTMLMarkdownTests.swift ├── Icons ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── Icons.xcscheme ├── Package.swift ├── README.md ├── Scripts │ └── swiftgen.yml ├── Sources │ └── Icons │ │ ├── Icons.swift │ │ └── Icons.xcassets │ │ ├── Contents.json │ │ ├── arrow.left.imageset │ │ ├── Contents.json │ │ └── arrow.left.svg │ │ ├── bell.fill.imageset │ │ ├── Contents.json │ │ └── bell.fill.svg │ │ ├── bell.imageset │ │ ├── Contents.json │ │ └── bell.svg │ │ ├── bookmark.fill.imageset │ │ ├── Contents.json │ │ └── bookmark.fill.svg │ │ ├── bookmark.imageset │ │ ├── Contents.json │ │ └── bookmark.svg │ │ ├── checkmark.circle.fill.imageset │ │ ├── Contents.json │ │ └── checkmark.circle.fill.svg │ │ ├── checkmark.imageset │ │ ├── Contents.json │ │ └── checkmark.svg │ │ ├── crown.circle.bronze.fill.imageset │ │ ├── Contents.json │ │ └── crown.circle.bronze.fill.svg │ │ ├── crown.circle.gold.fill.imageset │ │ ├── Contents.json │ │ └── crown.circle.gold.fill.svg │ │ ├── crown.circle.silver.fill.imageset │ │ ├── Contents.json │ │ └── crown.circle.silver.fill.svg │ │ ├── eye.fill.imageset │ │ ├── Contents.json │ │ └── eye.fill.svg │ │ ├── hand.thumbsdown.fill.imageset │ │ ├── Contents.json │ │ └── hand.thumbsdown.fill.svg │ │ ├── hand.thumbsup.fill.imageset │ │ ├── Contents.json │ │ └── hand.thumbsup.fill.svg │ │ ├── magnifyingglass.imageset │ │ ├── Contents.json │ │ └── magnifyingglass.svg │ │ ├── paperclip.imageset │ │ ├── Contents.json │ │ └── paperclip.svg │ │ ├── pencil.imageset │ │ ├── Contents.json │ │ └── pencil.svg │ │ ├── person.2.fill.imageset │ │ ├── Contents.json │ │ └── person.2.fill.svg │ │ ├── planet.imageset │ │ ├── Contents.json │ │ └── planet.svg │ │ ├── square.and.arrow.up.imageset │ │ ├── Contents.json │ │ └── square.and.arrow.up.svg │ │ ├── star.fill.imageset │ │ ├── Contents.json │ │ └── star.fill.svg │ │ ├── star.imageset │ │ ├── Contents.json │ │ └── star.svg │ │ ├── star.rounded.fill.imageset │ │ ├── Contents.json │ │ └── star.rounded.fill.svg │ │ ├── tag.fill.imageset │ │ ├── Contents.json │ │ └── tag.fill.svg │ │ ├── tray.fill.imageset │ │ ├── Contents.json │ │ └── tray.fill.svg │ │ ├── xmark.imageset │ │ ├── Contents.json │ │ └── xmark.svg │ │ └── xmark.rounded.bold.imageset │ │ ├── Contents.json │ │ └── xmark.rounded.bold.svg └── Templates │ └── icons.stencil ├── LICENSE ├── Palette ├── .swiftpm │ └── xcode │ │ ├── package.xcworkspace │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── xcschemes │ │ └── Palette.xcscheme ├── Package.swift ├── README.md ├── Scripts │ └── swiftgen.yml ├── Sources │ └── Palette │ │ ├── Palette.swift │ │ ├── Palette.xcassets │ │ ├── Black.colorset │ │ │ └── Contents.json │ │ ├── Bluishblack.colorset │ │ │ └── Contents.json │ │ ├── Bluishwhite.colorset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── DarkDivider.colorset │ │ │ └── Contents.json │ │ ├── DarkGray.colorset │ │ │ └── Contents.json │ │ ├── DarkShadow.colorset │ │ │ └── Contents.json │ │ ├── DullGray.colorset │ │ │ └── Contents.json │ │ ├── Gainsboro.colorset │ │ │ └── Contents.json │ │ ├── Gray.colorset │ │ │ └── Contents.json │ │ ├── Grayblue.colorset │ │ │ └── Contents.json │ │ ├── LightBlack.colorset │ │ │ └── Contents.json │ │ ├── LightDeepGray.colorset │ │ │ └── Contents.json │ │ ├── LightDivider.colorset │ │ │ └── Contents.json │ │ ├── LightGray.colorset │ │ │ └── Contents.json │ │ ├── Main.colorset │ │ │ └── Contents.json │ │ ├── PaleSky.colorset │ │ │ └── Contents.json │ │ ├── PeriwinkleCrayola.colorset │ │ │ └── Contents.json │ │ ├── Red.colorset │ │ │ └── Contents.json │ │ ├── SlateGray.colorset │ │ │ └── Contents.json │ │ ├── SlateGrayLight.colorset │ │ │ └── Contents.json │ │ ├── SteelGray300.colorset │ │ │ └── Contents.json │ │ └── Telegrey.colorset │ │ │ └── Contents.json │ │ ├── RecursivePalette.swift │ │ └── UIColor+Extensions.swift └── Templates │ └── palette.stencil ├── README.md ├── Services └── StackexchangeNetworkService │ ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── StackexchangeNetworkService.xcscheme │ ├── Package.swift │ ├── README.md │ ├── Sources │ └── StackexchangeNetworkService │ │ └── StackexchangeNetworkService.swift │ └── Tests │ └── StackexchangeNetworkServiceTests │ └── StackexchangeNetworkServiceTests.swift ├── Shared └── StackOvApp.swift ├── StackOv.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDETemplateMacros.plist │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ └── StackOv (iOS).xcscheme ├── Stores ├── CommentsStore │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── CommentsStore │ │ │ └── CommentsStore.swift │ └── Tests │ │ └── CommentsStoreTests │ │ └── CommentsStoreTests.swift ├── FavoriteStore │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── FavoriteStore.xcscheme │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── FavoriteStore │ │ │ ├── FavoriteDataManager │ │ │ ├── FavoriteDataManager.swift │ │ │ └── FavoriteDataManagerProtocol.swift │ │ │ └── FavoriteStore.swift │ └── Tests │ │ └── FavoriteStoreTests │ │ └── FavoriteStoreTests.swift ├── FilterStore │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── FilterStore.xcscheme │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── FilterStore │ │ │ └── FilterStore.swift │ └── Tests │ │ └── FilterStoreTests │ │ └── FilterStoreTests.swift ├── GlobalBannerStore │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── GlobalBannerStore.xcscheme │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── GlobalBannerStore │ │ │ └── GlobalBannerStore.swift │ └── Tests │ │ └── GlobalBannerStoreTests │ │ └── GlobalBannerStoreTests.swift ├── PageStore │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── PageStore.xcscheme │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── PageStore │ │ │ ├── PageDataManager │ │ │ ├── PageDataManager.swift │ │ │ └── PageDataManagerProtocol.swift │ │ │ └── PageStore.swift │ └── Tests │ │ └── PageStoreTests │ │ └── PageStoreTests.swift ├── SidebarStore │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── SidebarStore.xcscheme │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── SidebarStore │ │ │ └── SidebarStore.swift │ └── Tests │ │ └── SidebarStoreTests │ │ └── SidebarStoreTests.swift └── ThreadStore │ ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata │ ├── Package.swift │ ├── README.md │ ├── Sources │ └── ThreadStore │ │ ├── ThreadDataManager │ │ ├── ThreadDataManager.swift │ │ └── ThreadDataManagerProtocol.swift │ │ └── ThreadStore.swift │ └── Tests │ └── ThreadStoreTests │ └── ThreadStoreTests.swift └── assets └── logo.png /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: Puasonych 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 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Big Sur] 28 | - Version [e.g. 1.0] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhoneXr] 32 | - OS: [e.g. iOS13] 33 | - Version [e.g. 1.0] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: Puasonych 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/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 10 | # Description 11 | 14 | 15 | # How has this been tested? 16 | 19 | 20 | # Checklist 21 | 22 | - [ ] My code follows the style guidelines of this project 23 | - [ ] I have performed a self-review of my own code 24 | - [ ] I have commented my code, particularly in hard-to-understand areas 25 | - [ ] I have made corresponding changes to the documentation 26 | - [ ] My changes generate no new warnings 27 | - [ ] I have added tests that prove my fix is effective or that my feature works 28 | - [ ] New and existing unit tests pass locally with my changes 29 | 30 | --- 31 | 32 | Closes #[issue number] 33 | -------------------------------------------------------------------------------- /.github/workflows/change-checker.yml: -------------------------------------------------------------------------------- 1 | name: Change checker 2 | 3 | on: 4 | pull_request: 5 | banches: 6 | - main 7 | - develop 8 | 9 | jobs: 10 | build-and-test: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Resolve external dependencies 21 | run: | 22 | brew update || brew update 23 | brew install swiftgen 24 | brew install xcbeautify 25 | gem install slather 26 | 27 | - name: Build and test 28 | run: | 29 | set -o pipefail 30 | xcodebuild -project 'StackOv.xcodeproj' \ 31 | -scheme 'StackOv (iOS)' \ 32 | -sdk iphonesimulator \ 33 | -destination platform='iOS Simulator,name=iPhone SE (2nd generation),OS=14.4' \ 34 | build test \ 35 | | xcbeautify 36 | env: 37 | FIREBASE_DISABLED: YES 38 | 39 | - name: Prepare .xccoverage file to Codecov 40 | run: slather 41 | 42 | - name: Upload coverage to Codecov 43 | run: bash <(curl -s https://codecov.io/bash) -f ./cobertura.xml 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | # OS X Finder stuff 6 | .DS_Store 7 | 8 | ## User settings 9 | xcuserdata/ 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | coverage_service: cobertura_xml 2 | xcodeproj: ./StackOv.xcodeproj 3 | scheme: StackOv (iOS) 4 | output_directory: ./ -------------------------------------------------------------------------------- /AppScript/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 | import Foundation 6 | 7 | let package = Package( 8 | name: "AppScript", 9 | platforms: [.iOS(.v14)], 10 | products: [ 11 | .library(name: "AppScript", targets: ["AppScript"]) 12 | ], 13 | dependencies: Package.dependencies, 14 | targets: [ 15 | .target(name: "AppScript", dependencies: Target.dependencies), 16 | .testTarget(name: "AppScriptTests", dependencies: ["AppScript"]) 17 | ] 18 | ) 19 | 20 | fileprivate extension Target { 21 | 22 | static var dependencies: [Dependency] { 23 | Package.dependencies.map { Dependency(stringLiteral: $0.name!) } 24 | } 25 | } 26 | 27 | fileprivate extension Package { 28 | 29 | static var dependencies: [Dependency] { 30 | externalDependencies + serviceDependencies + storeDependencies 31 | } 32 | 33 | static var externalDependencies: [Dependency] { 34 | [.package(name: "Swinject", url: "https://github.com/Swinject/Swinject.git", from: "2.7.1")] 35 | } 36 | 37 | static var serviceDependencies: [Dependency] { 38 | [.package(name: "StackexchangeNetworkService", path: "../Services/StackexchangeNetworkService")] 39 | } 40 | 41 | static var storeDependencies: [Dependency] { 42 | [.package(name: "GlobalBannerStore", path: "../Stores/GlobalBannerStore"), 43 | .package(name: "ThreadStore", path: "../Stores/ThreadStore"), 44 | .package(name: "PageStore", path: "../Stores/PageStore"), 45 | .package(name: "FilterStore", path: "../Stores/FilterStore"), 46 | .package(name: "FavoriteStore", path: "../Stores/FavoriteStore"), 47 | .package(name: "SidebarStore", path: "../Stores/SidebarStore"), 48 | .package(name: "CommentsStore", path: "../Stores/CommentsStore")] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /AppScript/README.md: -------------------------------------------------------------------------------- 1 | # AppScript 2 | 3 | This package contains Swinject containers to manage all dependencies of the app. 4 | 5 | For now, we use [the modularizing pattern](https://github.com/Swinject/Swinject/blob/master/Documentation/Assembler.md) provided by Swinject framework that makes work with DI containers much more flexible. 6 | 7 | ## Structure 8 | 9 | - Any services we keep in the `ServicesAssembly` 10 | - Any stores we keep in the `StoresAssembly` 11 | - `ServicesAssembly` is a parent of `StoresAssembly` 12 | 13 | To manage the behaviour of each object we use [the ability to change object scope](https://github.com/Swinject/Swinject/blob/master/Documentation/ObjectScopes.md) which gives us a lot of variants to organise our global dependencies script, that is why the package is called `AppScript`. 14 | 15 | We have public access for all dependencies through next assemblers: `ServicesAssembler` and `StoresAssembler`. For example, we can resolve some registered object with commands: 16 | ```swift 17 | StoresAssembler.shared.resolve(YourServiceOrStoreType.self) 18 | ServicesAssembly.shared.resolve(YourServiceType.self) 19 | ``` 20 | 21 | ## Features 22 | 23 | We have `@Store` wrapper to make our life much more comfortable with injecting stores like `@StateObject` to the view layer. Consider the next code: 24 | 25 | ```swift 26 | import AppScript 27 | 28 | struct PageView: View { 29 | @Store var store: PageStore 30 | var body: some View { EmptyView() } 31 | } 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /AppScript/Sources/AppScript/Services.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Services.swift 3 | // StackOv (AppScript module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Swinject 10 | import StackexchangeNetworkService 11 | import class PageStore.PageDataManager 12 | import class FavoriteStore.FavoriteDataManager 13 | import class ThreadStore.ThreadDataManager 14 | 15 | // MARK: - Services Assembly 16 | 17 | final class ServicesAssembly: Assembly { 18 | 19 | func assemble(container: Container) { 20 | container.register(StackexchangeNetworkService.self) { _ in 21 | StackexchangeNetworkService() 22 | }.inObjectScope(.weak) 23 | 24 | container.register(PageDataManager.self) { resolver in 25 | PageDataManager(service: resolver.resolve(StackexchangeNetworkService.self)!) 26 | }.inObjectScope(.transient) 27 | 28 | container.register(FavoriteDataManager.self) { resolver in 29 | FavoriteDataManager(service: resolver.resolve(StackexchangeNetworkService.self)!) 30 | }.inObjectScope(.transient) 31 | 32 | container.register(ThreadDataManager.self) { reslover in 33 | ThreadDataManager(service: reslover.resolve(StackexchangeNetworkService.self)!) 34 | }.inObjectScope(.transient) 35 | } 36 | } 37 | 38 | // MARK: - Services Assembler 39 | 40 | public struct ServicesAssembler { 41 | 42 | public static var shared: Resolver { 43 | assembler.resolver 44 | } 45 | 46 | public static let assembler: Assembler = { 47 | Assembler([ 48 | ServicesAssembly() 49 | ]) 50 | }() 51 | } 52 | -------------------------------------------------------------------------------- /AppScript/Sources/AppScript/Stores.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stores.swift 3 | // StackOv (AppScript module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Swinject 10 | import StackexchangeNetworkService 11 | 12 | @_exported import GlobalBannerStore 13 | @_exported import PageStore 14 | @_exported import FilterStore 15 | @_exported import ThreadStore 16 | @_exported import FavoriteStore 17 | @_exported import SidebarStore 18 | @_exported import CommentsStore 19 | 20 | // MARK: - Stores Assembly 21 | 22 | final class StoresAssembly: Assembly { 23 | 24 | func assemble(container: Container) { 25 | container.register(GlobalBannerStore.self) { resolver in 26 | GlobalBannerStore() 27 | }.inObjectScope(.weak) 28 | 29 | container.register(PageStore.self) { resolver in 30 | PageStore(dataManager: resolver.resolve(PageDataManager.self)!, 31 | filterStore: resolver.resolve(FilterStore.self)!) 32 | }.inObjectScope(.transient) 33 | 34 | container.register(ThreadStore.self) { resolver, model in 35 | ThreadStore(model: model, dataManager: resolver.resolve(ThreadDataManager.self)!) 36 | }.inObjectScope(.transient) 37 | 38 | container.register(FilterStore.self) { resolver in 39 | FilterStore() 40 | }.inObjectScope(.transient) 41 | 42 | container.register(SidebarStore.self) { resolver in 43 | SidebarStore() 44 | }.inObjectScope(.weak) 45 | 46 | container.register(FavoriteStore.self) { resolver in 47 | FavoriteStore(dataManager: resolver.resolve(FavoriteDataManager.self)!) 48 | }.inObjectScope(.transient) 49 | 50 | container.register(CommentsStore.self) { resolver, model in 51 | CommentsStore(model: model) 52 | }.inObjectScope(.transient) 53 | } 54 | } 55 | 56 | // MARK: - Stores Assembler 57 | 58 | public struct StoresAssembler { 59 | 60 | public static var shared: Resolver { 61 | assembler.resolver 62 | } 63 | 64 | public static let assembler: Assembler = { 65 | Assembler([ 66 | StoresAssembly() 67 | ], parent: ServicesAssembler.assembler) 68 | }() 69 | } 70 | -------------------------------------------------------------------------------- /AppScript/Sources/AppScript/Wrappers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Wrappers.swift 3 | // StackOv (AppScript module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import struct SwiftUI.ObservedObject 10 | import protocol SwiftUI.ObservableObject 11 | import protocol SwiftUI.DynamicProperty 12 | 13 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 14 | @propertyWrapper public struct Store: DynamicProperty { 15 | 16 | // MARK: - Initialization 17 | 18 | public init(name: String? = nil) { 19 | self.object = StoresAssembler.shared.resolve(ObjectType.self, name: name)! 20 | } 21 | 22 | // MARK: - Internal properties 23 | 24 | @ObservedObject var object: ObjectType = StoresAssembler.shared.resolve(ObjectType.self)! 25 | 26 | // MARK: - Public properties 27 | 28 | public var wrappedValue: ObjectType { object } 29 | public var projectedValue: ObservedObject.Wrapper { $object } 30 | } 31 | -------------------------------------------------------------------------------- /AppScript/Tests/AppScriptTests/AppScriptTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AppScript 3 | 4 | final class AppScriptTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Application/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 | -------------------------------------------------------------------------------- /Application/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Application/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 866124882645-3qjdsbj3o9rucsrd46d5m0tin027me88.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.866124882645-3qjdsbj3o9rucsrd46d5m0tin027me88 9 | API_KEY 10 | AIzaSyATAm4NyaeyQYndopZObZc9nbz-Fwqz0Kc 11 | GCM_SENDER_ID 12 | 866124882645 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.ephedra-software.StackOv 17 | PROJECT_ID 18 | stackov-42941 19 | STORAGE_BUCKET 20 | stackov-42941.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:866124882645:ios:0e4a8b7f6913e0dbf0385c 33 | 34 | -------------------------------------------------------------------------------- /Application/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.social-networking 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | NSExceptionDomains 30 | 31 | api.stackexchange.com 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | NSIncludesSubdomains 36 | 37 | 38 | 39 | 40 | NSHumanReadableCopyright 41 | Copyright © 2021 Erik Basargin. All rights reserved. 42 | UIApplicationSceneManifest 43 | 44 | UIApplicationSupportsMultipleScenes 45 | 46 | 47 | UIApplicationSupportsIndirectInputEvents 48 | 49 | UILaunchScreen 50 | 51 | UIRequiredDeviceCapabilities 52 | 53 | armv7 54 | 55 | UISupportedInterfaceOrientations 56 | 57 | UIInterfaceOrientationPortrait 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | UISupportedInterfaceOrientations~ipad 62 | 63 | UIInterfaceOrientationPortrait 64 | UIInterfaceOrientationPortraitUpsideDown 65 | UIInterfaceOrientationLandscapeLeft 66 | UIInterfaceOrientationLandscapeRight 67 | 68 | NSPhotoLibraryAddUsageDescription 69 | StackOv needs permission to access photos on your device 70 | 71 | 72 | -------------------------------------------------------------------------------- /Application/StackOv.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Application/StackOv.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Shared.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Application/StackOv.xcdatamodeld/Shared.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | --- 5 | 6 | ## 0.0.5 (1) 7 | 8 | - The last version of the demo app. -------------------------------------------------------------------------------- /Common/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 | import class Foundation.ProcessInfo 6 | 7 | let package = Package( 8 | name: "Common", 9 | platforms: [.iOS(.v14)], 10 | products: [ 11 | .library(name: "Common", targets: ["Common"]) 12 | ], 13 | dependencies: Package.dependencies, 14 | targets: [ 15 | .target(name: "Common", dependencies: Target.dependencies), 16 | .testTarget(name: "CommonTests", dependencies: ["Common"]) 17 | ] 18 | ) 19 | 20 | fileprivate extension Package { 21 | 22 | static var firebaseIsEnable: Bool { 23 | ProcessInfo.processInfo.environment["FIREBASE_DISABLED"] == nil 24 | } 25 | } 26 | 27 | fileprivate extension Target { 28 | 29 | static var dependencies: [Dependency] { 30 | var packages: [Dependency] = ["Introspect", "DataTransferObjects", "Errors", "Kingfisher"] 31 | if Package.firebaseIsEnable { 32 | packages.append(.product(name: "FirebaseCrashlytics", package: "Firebase")) 33 | } 34 | return packages 35 | } 36 | } 37 | 38 | fileprivate extension Package { 39 | 40 | static var dependencies: [Dependency] { 41 | internalDependencies + externalDependencies 42 | } 43 | 44 | static var internalDependencies: [Dependency] { 45 | return [ 46 | .package(name: "DataTransferObjects", path: "../DataTransferObjects"), 47 | .package(path: "../Errors") 48 | ] 49 | } 50 | 51 | static var externalDependencies: [Dependency] { 52 | var packages: [Dependency] = [ 53 | .package(name: "Introspect", url: "https://github.com/Puasonych/SwiftUI-Introspect.git", .branch("develop")), 54 | .package(name: "Kingfisher", url: "https://github.com/onevcat/Kingfisher.git", .upToNextMajor(from: "6.0.0")) 55 | ] 56 | if Package.firebaseIsEnable { 57 | packages.append( 58 | .package(name: "Firebase", url: "https://github.com/firebase/firebase-ios-sdk.git", .upToNextMajor(from: "7.3.0")) 59 | ) 60 | } 61 | return packages 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Common/README.md: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | This package contains general build blocks such as types, extensions, wrappers, or structures. 4 | -------------------------------------------------------------------------------- /Common/Sources/Common/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// https://www.swiftbysundell.com/articles/caching-in-swift/ 12 | public final class Cache { 13 | 14 | fileprivate let wrapped: NSCache 15 | 16 | public init() { 17 | self.wrapped = NSCache() 18 | self.wrapped.countLimit = 100 19 | } 20 | 21 | public subscript(key: Key) -> Value? { 22 | get { value(forKey: key) } 23 | set { 24 | guard let value = newValue else { 25 | removeValue(forKey: key) 26 | return 27 | } 28 | 29 | insert(value, forKey: key) 30 | } 31 | } 32 | 33 | public func insert(_ value: Value, forKey key: Key) { 34 | let entry = Entry(value: value) 35 | wrapped.setObject(entry, forKey: WrappedKey(key)) 36 | } 37 | 38 | public func value(forKey key: Key) -> Value? { 39 | let entry = wrapped.object(forKey: WrappedKey(key)) 40 | return entry?.value 41 | } 42 | 43 | public func removeValue(forKey key: Key) { 44 | wrapped.removeObject(forKey: WrappedKey(key)) 45 | } 46 | } 47 | 48 | fileprivate extension Cache { 49 | 50 | final class WrappedKey: NSObject { 51 | let key: Key 52 | 53 | init(_ key: Key) { 54 | self.key = key 55 | } 56 | 57 | override var hash: Int { key.hashValue } 58 | 59 | override func isEqual(_ object: Any?) -> Bool { 60 | if let value = object as? WrappedKey { 61 | return value.key == key 62 | } else { 63 | return false 64 | } 65 | } 66 | } 67 | 68 | final class Entry { 69 | let value: Value 70 | 71 | init(value: Value) { 72 | self.value = value 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /Common/Sources/Common/Constants/PageConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageConstrants.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit.UIDevice 11 | import struct UIKit.CGFloat 12 | 13 | public enum PageConstrants { 14 | 15 | public static let gridItemMinimumWidth: CGFloat = 267 16 | public static let gridItemMaximumHeight: CGFloat = 223 17 | public static var defaultSpacing: CGFloat { 18 | UIDevice.current.userInterfaceIdiom.isPad ? 18 : 12 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Common/Sources/Common/Constants/SidebarConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarConstants.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public enum SidebarConstants { 12 | static let regularWidth: CGFloat = 210 13 | static let compactNormalWidth: CGFloat = 50 14 | static let compactAccessibilityWidth: CGFloat = 70 15 | 16 | public static func sidebarWidth(style: SidebarStyle, isAccessibility: Bool) -> CGFloat { 17 | switch style { 18 | case .regular: 19 | return SidebarConstants.regularWidth 20 | case .compact: 21 | return isAccessibility ? SidebarConstants.compactAccessibilityWidth : SidebarConstants.compactNormalWidth 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Common/Sources/Common/Constants/ThreadItemConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadItemConstants.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import struct UIKit.CGFloat 10 | 11 | public enum ThreadItemConstants { 12 | 13 | public static let defaultPadding: CGFloat = 16 14 | } 15 | -------------------------------------------------------------------------------- /Common/Sources/Common/Environments/MainContent+Environments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainContent+Environments.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension EnvironmentValues { 12 | 13 | var mainContentSize: CGSize { 14 | get { self[MainContentSizeKey.self] } 15 | set { self[MainContentSizeKey.self] = newValue } 16 | } 17 | } 18 | 19 | // MARK: - Default values 20 | 21 | private struct MainContentSizeKey: EnvironmentKey { 22 | 23 | static var defaultValue: CGSize { 24 | UIApplication.shared.windows.filter { $0.isKeyWindow }.first!.frame.size 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Common/Sources/Common/Environments/Window+Environments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowSize.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension EnvironmentValues { 12 | 13 | var windowSize: CGSize { 14 | get { self[WindowSizeKey.self] } 15 | set { self[WindowSizeKey.self] = newValue } 16 | } 17 | } 18 | 19 | // MARK: - Default values 20 | 21 | private struct WindowSizeKey: EnvironmentKey { 22 | 23 | static var defaultValue: CGSize { 24 | UIApplication.shared.windows.filter { $0.isKeyWindow }.first!.frame.size 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/Data+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Extensions.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Errors 11 | 12 | public extension Data { 13 | 14 | func toJsonString(options: JSONSerialization.WritingOptions = []) throws -> String { 15 | let json = try JSONSerialization.jsonObject(with: self, options: .mutableContainers) 16 | let data = try JSONSerialization.data(withJSONObject: json, options: options) 17 | if let jsonString = String(data: data, encoding: .utf8) { 18 | return jsonString 19 | } else { 20 | throw DataError.wrongEncoding(data: data, encoding: .utf8) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/EdgeInsets+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeInsets+Extensions.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension EdgeInsets { 12 | 13 | static var zero: EdgeInsets { 14 | EdgeInsets(top: .zero, leading: .zero, bottom: .zero, trailing: .zero) 15 | } 16 | 17 | static func all(_ value: CGFloat) -> EdgeInsets { 18 | EdgeInsets(top: value, leading: value, bottom: value, trailing: value) 19 | } 20 | 21 | static func top(_ value: CGFloat) -> EdgeInsets { 22 | EdgeInsets(top: value, leading: .zero, bottom: .zero, trailing: .zero) 23 | } 24 | 25 | static func leading(_ value: CGFloat) -> EdgeInsets { 26 | EdgeInsets(top: .zero, leading: value, bottom: .zero, trailing: .zero) 27 | } 28 | 29 | static func bottom(_ value: CGFloat) -> EdgeInsets { 30 | EdgeInsets(top: .zero, leading: .zero, bottom: value, trailing: .zero) 31 | } 32 | 33 | static func trailing(_ value: CGFloat) -> EdgeInsets { 34 | EdgeInsets(top: .zero, leading: .zero, bottom: .zero, trailing: value) 35 | } 36 | 37 | static func horizontal(_ value: CGFloat) -> EdgeInsets { 38 | EdgeInsets(top: .zero, leading: value, bottom: .zero, trailing: value) 39 | } 40 | 41 | static func vertical(_ value: CGFloat) -> EdgeInsets { 42 | EdgeInsets(top: value, leading: .zero, bottom: value, trailing: .zero) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/HTTPURLResponse+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Errors 3 | 4 | public extension HTTPURLResponse { 5 | 6 | var status: HTTPStatusCode? { 7 | HTTPStatusCode(rawValue: statusCode) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension String { 12 | 13 | init?(htmlString: String) { 14 | guard let data = htmlString.data(using: .utf8) else { return nil } 15 | 16 | let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ 17 | .documentType: NSAttributedString.DocumentType.html, 18 | .characterEncoding: String.Encoding.utf8.rawValue 19 | ] 20 | guard let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else { 21 | return nil 22 | } 23 | 24 | self.init(attributedString.string) 25 | } 26 | 27 | static func roundNumberWithAbbreviations(number: Int?) -> String { 28 | guard let number = number else { 29 | return "" 30 | } 31 | 32 | let formatter = NumberFormatter() 33 | formatter.numberStyle = .decimal 34 | formatter.groupingSeparator = "," 35 | 36 | if number < 1000 { 37 | return formatter.string(from: NSNumber(value: number)) ?? "" 38 | } else { 39 | let numberWithAbbreviation = number / 1000 40 | return (formatter.string(from: NSNumber(value: numberWithAbbreviation)) ?? "") + "K" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/UIApplication+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+Extensions.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import class UIKit.UIApplication 11 | import struct CoreGraphics.CGFloat 12 | import struct CoreGraphics.CGSize 13 | 14 | public extension UIApplication { 15 | 16 | var keyWindowSize: CGSize { 17 | windows.filter { $0.isKeyWindow }.first?.frame.size ?? .zero 18 | } 19 | 20 | var statusBarHeight: CGFloat { 21 | windows.filter { $0.isKeyWindow }.first?.windowScene?.statusBarManager?.statusBarFrame.height ?? .zero 22 | } 23 | 24 | func tryOpen(url: URL?, options: [UIApplication.OpenExternalURLOptionsKey : Any] = [:], completionHandler completion: ((Bool) -> Void)? = nil) { 25 | guard let url = url else { 26 | completion?(false) 27 | return 28 | } 29 | if canOpenURL(url) { 30 | open(url, options: options, completionHandler: completion) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/UIImage+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extensions.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import class UIKit.UIImage 10 | 11 | extension UIImage: Identifiable { 12 | 13 | public var id: Int { hashValue } 14 | } 15 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/UIUserInterfaceIdiom+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIUserInterfaceIdiom.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import UIKit.UIDevice 10 | 11 | public extension UIUserInterfaceIdiom { 12 | 13 | var isPhone: Bool { 14 | self == .phone 15 | } 16 | 17 | var isPad: Bool { 18 | self == .pad 19 | } 20 | 21 | var isMacCatalyst: Bool { 22 | #if targetEnvironment(macCatalyst) 23 | return true 24 | #else 25 | return false 26 | #endif 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Common/Sources/Common/Extensions/URL+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Extensions.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL: Identifiable { 12 | 13 | public var id: Int { hashValue } 14 | } 15 | -------------------------------------------------------------------------------- /Common/Sources/Common/GlobalBanner/GlobalBanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationBannerData.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import Errors 12 | 13 | public enum GlobalBanner { 14 | 15 | // MARK: - Neasted types 16 | 17 | public typealias Style = NotificationBannerStyle 18 | public typealias Model = NotificationBannerModel 19 | 20 | // MARK: - Public properties 21 | 22 | public static var publisher: AnyPublisher { sender.eraseToAnyPublisher() } 23 | 24 | // MARK: - Interlnal properties 25 | 26 | static let sender = PassthroughSubject() 27 | 28 | // MARK: - Public methods 29 | 30 | public static func show(data: Model) { 31 | sender.send(data) 32 | } 33 | 34 | public static func show(error: Error) { 35 | guard let error = error as? DisplaybleError else { return } 36 | sender.send(error.toGlobalBannerModel) 37 | } 38 | } 39 | 40 | fileprivate extension DisplaybleError { 41 | 42 | var toGlobalBannerModel: GlobalBanner.Model { 43 | .init(title: title, description: description, style: .error) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Common/Sources/Common/Models/ShallowUserModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShallowUserModel.swift 3 | // StackOv (ShallowUserModel module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import struct DataTransferObjects.ShallowUserEntry 11 | 12 | public struct ShallowUserModel { 13 | 14 | // MARK: - Nested Types 15 | 16 | public enum UserType: String { 17 | case unregistered 18 | case registered 19 | case moderator 20 | case teamAdmin 21 | case doesNotExist 22 | } 23 | 24 | // MARK: - Properties 25 | 26 | public let reputation: Int? 27 | public let id: Int? 28 | public let type: UserType? 29 | public let acceptRate: Int? 30 | public let avatar: URL? 31 | public let name: String 32 | public let link: URL? 33 | } 34 | 35 | // MARK: - Entry converter 36 | 37 | public extension ShallowUserModel { 38 | 39 | static func from(entry: ShallowUserEntry) -> ShallowUserModel { 40 | ShallowUserModel(reputation: entry.reputation, 41 | id: entry.id, 42 | type: UserType(rawValue: entry.type.rawValue), 43 | acceptRate: entry.acceptRate, 44 | avatar: entry.avatar, 45 | name: entry.name, 46 | link: entry.link) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Common/Sources/Common/Models/UserModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersonInfoModel.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct UserModel { 12 | 13 | // MARK: - Nested Types 14 | 15 | public enum ActionType: String { 16 | case asked 17 | case edited 18 | case answered 19 | } 20 | 21 | // MARK: - Properties 22 | 23 | public let id: Int 24 | public let bronzeBadges: Int 25 | public let silverBadges: Int 26 | public let goldBadges: Int 27 | public let reputation: Int 28 | public let profileImage: URL 29 | public let displayName: String 30 | public let link: URL 31 | 32 | public let actionType: ActionType 33 | public let actionDate: Date 34 | 35 | public var formattedActionDate: String { 36 | let dateFormatter = DateFormatter() 37 | dateFormatter.dateFormat = "d MMM, yyyy 'at' HH:mm" 38 | return dateFormatter.string(from: actionDate) 39 | } 40 | 41 | public var formattedReputationDate: String { 42 | String.roundNumberWithAbbreviations(number: reputation) 43 | } 44 | 45 | // MARK: - Public Methods 46 | 47 | public func formattedBagesNumber(number: Int) -> String { 48 | String.roundNumberWithAbbreviations(number: number) 49 | } 50 | 51 | } 52 | 53 | public extension UserModel { 54 | 55 | static func mock() -> UserModel { 56 | UserModel(id: 0, 57 | bronzeBadges: 1001000, 58 | silverBadges: 10, 59 | goldBadges: 8, 60 | reputation: 1000, 61 | profileImage: URL(string: "https://www.gravatar.com/avatar/6d8ebb117e8d83d74ea95fbdd0f87e13?s=128&d=identicon&r=PG")!, 62 | displayName: "John Wick", 63 | link: URL(string: "http://example.stackexchange.com/users/1/example-user")!, 64 | actionType: .asked, 65 | actionDate: Date()) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Common/Sources/Common/Modifiers/ApplicationDidBecomeActiveViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationDidBecomeActiveViewModifier.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ApplicationDidBecomeActiveViewModifier: ViewModifier { 12 | 13 | // MARK: - Properties 14 | 15 | let action: () -> Void 16 | let didBecomeVisibleNotification = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) 17 | .makeConnectable() 18 | .autoconnect() 19 | 20 | // MARK: - Views 21 | 22 | func body(content: Content) -> some View { 23 | content 24 | .onAppear() 25 | .onReceive(didBecomeVisibleNotification) { _ in 26 | action() 27 | } 28 | } 29 | } 30 | 31 | // MARK: - View extension 32 | 33 | public extension View { 34 | 35 | func onApplicationDidBecomeActive(perform action: @escaping () -> Void) -> some View { 36 | modifier(ApplicationDidBecomeActiveViewModifier(action: action)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Common/Sources/Common/Modifiers/DeviceRotationViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceRotationViewModifier.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct DeviceRotationViewModifier: ViewModifier { 12 | 13 | // MARK: - Properties 14 | 15 | let action: (UIDeviceOrientation) -> Void 16 | let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) 17 | .makeConnectable() 18 | .autoconnect() 19 | 20 | // MARK: - View 21 | 22 | func body(content: Content) -> some View { 23 | content 24 | .onAppear() 25 | .onReceive(orientationChanged) { _ in 26 | action(UIDevice.current.orientation) 27 | } 28 | } 29 | } 30 | 31 | // MARK: - View extension 32 | 33 | public extension View { 34 | 35 | func onDeviceOrientationDidChange(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { 36 | modifier(DeviceRotationViewModifier(action: action)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Common/Sources/Common/NotificationBannerModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationBannerModel.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public enum NotificationBannerStyle { 12 | case info 13 | case success 14 | case error 15 | } 16 | 17 | public struct NotificationBannerModel { 18 | 19 | // MARK: - Properties 20 | 21 | public let title: String 22 | public let description: String? 23 | public let style: NotificationBannerStyle 24 | 25 | // MARK: - Initialization 26 | 27 | public init(title: String, description: String? = nil, style: NotificationBannerStyle) { 28 | self.title = title 29 | self.description = description 30 | self.style = style 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Common/Sources/Common/SidebarStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarStyle.swift 3 | // StackOv (SidebarStyle module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum SidebarStyle: Comparable { 12 | case regular 13 | case compact 14 | } 15 | -------------------------------------------------------------------------------- /Common/Sources/Common/Wrappers/Lazy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lazy.swift 3 | // StackOv (Common module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @propertyWrapper public enum Lazy { 12 | 13 | case uninitialized(() -> Value) 14 | case initialized(Value) 15 | 16 | public init(wrappedValue: @autoclosure @escaping () -> Value) { 17 | self = .uninitialized(wrappedValue) 18 | } 19 | 20 | public var wrappedValue: Value { 21 | mutating get { 22 | switch self { 23 | case .uninitialized(let initializer): 24 | let value = initializer() 25 | self = .initialized(value) 26 | return value 27 | case .initialized(let value): 28 | return value 29 | } 30 | } 31 | set { 32 | self = .initialized(newValue) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Common/Tests/CommonTests/CommonTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Common 3 | 4 | final class CommonTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Components/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: "Components", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "Components", targets: ["Components"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Palette"), 14 | .package(path: "../Icons"), 15 | .package(path: "../Common"), 16 | .package(path: "../HTMLMarkdown"), 17 | .package(path: "../AppScript") 18 | ], 19 | targets: [ 20 | .target(name: "Components", dependencies: ["Palette", "HTMLMarkdown", "AppScript", "Icons", "Common"]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Components/README.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | This package contains common ui components. 4 | -------------------------------------------------------------------------------- /Components/Sources/Components/ActivityView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | public struct ActivityView: UIViewControllerRepresentable { 13 | 14 | let activityItems: [Any] 15 | let applicationActivities: [UIActivity]? 16 | 17 | public init(activityItems: [Any], applicationActivities: [UIActivity]? = nil) { 18 | self.activityItems = activityItems 19 | self.applicationActivities = applicationActivities 20 | } 21 | 22 | public func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { 23 | let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) 24 | return controller 25 | } 26 | 27 | public func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {} 28 | } 29 | -------------------------------------------------------------------------------- /Components/Sources/Components/BadgeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BadgeView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct BadgeView: View { 13 | 14 | @Binding var value: Int 15 | 16 | var body: some View { 17 | GeometryReader { geometry in 18 | Text("\(value)") 19 | .font(.system(size: 11, weight: .medium)) 20 | .foregroundColor(Color.foreground) 21 | .padding(EdgeInsets(top: 0, leading: 3, bottom: 0, trailing: 3)) 22 | .background(Color.background) 23 | .cornerRadius(geometry.frame(in: .local).height / 2) 24 | } 25 | } 26 | } 27 | 28 | // MARK: - Previews 29 | 30 | struct BadgeView_Previews: PreviewProvider { 31 | 32 | static var previews: some View { 33 | Group { 34 | BadgeView(value: .constant(3)) 35 | .frame(width: 25, height: 25) 36 | .padding() 37 | .previewLayout(.sizeThatFits) 38 | 39 | BadgeView(value: .constant(999)) 40 | .frame(height: 25) 41 | .frame(maxWidth: 30) 42 | .padding() 43 | .previewLayout(.sizeThatFits) 44 | 45 | BadgeView(value: .constant(999)) 46 | .frame(width: 25, height: 25) 47 | .padding() 48 | .previewLayout(.sizeThatFits) 49 | } 50 | } 51 | } 52 | 53 | 54 | // MARK: - Colors 55 | 56 | fileprivate extension Color { 57 | 58 | static let foreground = Color.white 59 | static let background = Palette.main 60 | } 61 | -------------------------------------------------------------------------------- /Components/Sources/Components/EllipsisButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EllipsisButton.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | public struct EllipsisButton: View { 13 | 14 | let action: () -> Void 15 | 16 | public init(action: @escaping () -> Void) { 17 | self.action = action 18 | } 19 | 20 | public var body: some View { 21 | Button(action: action) { 22 | Image(systemName: "ellipsis") 23 | .resizable() 24 | .aspectRatio(contentMode: .fit) 25 | .frame(width: 18, height: 18) 26 | } 27 | .frame(width: 24, height: 24) 28 | .foregroundColor(Color.foreground) 29 | .clipShape(Circle()) 30 | .buttonStyle(EllipsisButtonStyle()) 31 | } 32 | } 33 | 34 | // MARK: - Previews 35 | 36 | struct EllipsisButton_Previews: PreviewProvider { 37 | 38 | static var previews: some View { 39 | EllipsisButton(action: {}) 40 | .padding() 41 | .previewLayout(.sizeThatFits) 42 | .background(Color(red: 0.276, green: 0.122, blue: 0.438)) 43 | } 44 | } 45 | 46 | // MARK: - Button styles 47 | 48 | struct EllipsisButtonStyle: ButtonStyle { 49 | 50 | func makeBody(configuration: Configuration) -> some View { 51 | configuration.label 52 | .frame(width: 24, height: 24) 53 | .background(Color.background(by: configuration.isPressed)) 54 | } 55 | } 56 | 57 | // MARK: - Colors 58 | 59 | fileprivate extension Color { 60 | 61 | static let foreground = Color.white 62 | static func background(by pressed: Bool) -> Color { 63 | Color.white.opacity(pressed ? 0.2 : 0.1) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Components/Sources/Components/LoaderView/LoaderCircle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoaderCircle.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Илья Князьков 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct LoaderCircle: View { 13 | 14 | // MARK: - Nested types 15 | 16 | private enum Constants { 17 | static let trimEnd: CGFloat = 0.8 18 | static let lineWidth: CGFloat = 3 19 | } 20 | 21 | // MARK: - Private properties 22 | 23 | private var gradient: AngularGradient { 24 | AngularGradient(gradient: Gradient(colors: [.clear, .loaderLineColor]), center: .center) 25 | } 26 | 27 | // MARK: - View 28 | 29 | var body: some View { 30 | Circle() 31 | .trim(from: .zero, to: Constants.trimEnd) 32 | .stroke(gradient, lineWidth: Constants.lineWidth) 33 | } 34 | } 35 | 36 | // MARK: - Previews 37 | 38 | struct LoaderCircle_Previews: PreviewProvider { 39 | 40 | static var previews: some View { 41 | LoaderCircle() 42 | } 43 | } 44 | 45 | // MARK: - Colors 46 | 47 | fileprivate extension Color { 48 | 49 | static let loaderLineColor = Palette.paleSky 50 | } 51 | -------------------------------------------------------------------------------- /Components/Sources/Components/LoaderView/LoaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoaderView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Илья Князьков 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct LoaderView: View, Animatable { 12 | 13 | // MARK: - Nested types 14 | 15 | private enum Constants { 16 | static let animationDuration: Double = 0.3 17 | } 18 | 19 | // MARK: - Public properties 20 | 21 | public var animatableData: Double { 22 | get { rotationAngle } 23 | set { rotationAngle = newValue } 24 | } 25 | 26 | // MARK: - Private properties 27 | 28 | private var animation: Animation { 29 | Animation.linear(duration: Constants.animationDuration) 30 | .repeatForever(autoreverses: false) 31 | } 32 | 33 | @State private var rotationAngle: Double = 0 34 | 35 | // MARK: - Initializers 36 | 37 | public init() { } 38 | 39 | 40 | // MARK: - View 41 | 42 | public var body: some View { 43 | LoaderCircle() 44 | .rotationEffect(Angle(degrees: rotationAngle)) 45 | .animation(animation) 46 | .onAppear { 47 | rotationAngle = 360 48 | } 49 | } 50 | } 51 | 52 | // MARK: - Previews 53 | 54 | struct LoaderView_Previews: PreviewProvider { 55 | 56 | static var previews: some View { 57 | LoaderView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Components/Sources/Components/MarkdownPostView/Blocks/BlockQuoteView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockQuoteView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import HTMLMarkdown 11 | import Palette 12 | 13 | extension MarkdownPostView { 14 | 15 | struct BlockQuoteView: StyleableUnitView { 16 | 17 | // MARK: - Properties 18 | 19 | let style: Style 20 | let unit: HTMLMarkdown.Unit 21 | 22 | // MARK: - View 23 | 24 | var body: some View { 25 | if unit.type == .blockQuote { 26 | HStack(alignment: .top, spacing: 6) { 27 | Color.background.frame(width: 4) 28 | RepetitiveView(style: style, unit: unit) 29 | } 30 | .fixedSize(horizontal: false, vertical: true) 31 | } else { 32 | fatalError("BlockQuoteView has got unsupported unit \(unit)") 33 | } 34 | } 35 | } 36 | } 37 | 38 | // MARK: - Colors 39 | 40 | fileprivate extension Color { 41 | 42 | static let background = Palette.dullGray 43 | } 44 | -------------------------------------------------------------------------------- /Components/Sources/Components/MarkdownPostView/Blocks/DocumentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import HTMLMarkdown 11 | 12 | extension MarkdownPostView { 13 | 14 | struct DocumentView: StyleableUnitView { 15 | 16 | // MARK: - Properties 17 | 18 | let style: Style 19 | let unit: HTMLMarkdown.Unit 20 | 21 | // MARK: - View 22 | 23 | var body: some View { 24 | if unit.type == .document { 25 | RepetitiveView(style: style, unit: unit) 26 | } else { 27 | fatalError("DocumentView has got unsupported unit \(unit)") 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Components/Sources/Components/PageButton/PageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageModel.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct PageModel: Identifiable { 12 | 13 | public let id: UUID 14 | public let title: String? 15 | 16 | public init(id: UUID = UUID(), title: String? = nil) { 17 | self.id = id 18 | self.title = title 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Components/Sources/Components/PageDevMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageDevMock.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct PageDevMock: View { 12 | 13 | public init() {} 14 | 15 | public var body: some View { 16 | #if DEBUG 17 | VStack { 18 | Text("🛠") 19 | .font(.system(size: 150)) 20 | Text("This page is not developed yet") 21 | .font(.largeTitle) 22 | } 23 | #else 24 | fatalError("PageDevMock must not be in a release version of the app") 25 | #endif 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Components/Sources/Components/SafariView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | import SafariServices 12 | 13 | struct SafariView: UIViewControllerRepresentable { 14 | 15 | let url: URL 16 | 17 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { 18 | let controller = SFSafariViewController(url: url) 19 | controller.preferredBarTintColor = UIColor.background 20 | controller.preferredControlTintColor = UIColor.title 21 | return controller 22 | } 23 | 24 | func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) {} 25 | } 26 | 27 | // MARK: - Colors 28 | 29 | fileprivate extension UIColor { 30 | 31 | static let title = PaletteCore.main 32 | static let background = PaletteCore.bluishblack 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Components/Sources/Components/ShimmerView/ShimmerConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShimmerConfig.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | public class ShimmerConfig: ObservableObject { 13 | 14 | // MARK: - States 15 | 16 | @Published var isActive: Bool = false 17 | 18 | // MARK: - Properties 19 | 20 | let bgColor: Color 21 | let fgColor: Color 22 | let shimmerColor: Color 23 | let shimmerAngle: Double 24 | let shimmerDuration: TimeInterval 25 | let shimmerDelay: TimeInterval 26 | 27 | // MARK: - Private Properties 28 | 29 | private var timer: AnyCancellable? 30 | 31 | // MARK: - Initialization and deinitialization 32 | 33 | public init(bgColor: Color = Color(white: 0.8), 34 | fgColor: Color = .white, 35 | shimmerColor: Color = Color(white: 1.0, opacity: 0.5), 36 | shimmerAngle: Double = 20, 37 | shimmerDuration: TimeInterval = 1, 38 | shimmerDelay: TimeInterval = 2) { 39 | self.bgColor = bgColor 40 | self.fgColor = fgColor 41 | self.shimmerColor = shimmerColor 42 | self.shimmerAngle = shimmerAngle 43 | self.shimmerDuration = shimmerDuration 44 | self.shimmerDelay = shimmerDelay 45 | } 46 | 47 | deinit { 48 | stopAnimation() 49 | } 50 | 51 | // MARK: - Public methods 52 | 53 | public func startAnimation() { 54 | stopAnimation() 55 | timer?.cancel() 56 | timer = Timer 57 | .publish(every: shimmerDelay, on: .main, in: .default) 58 | .autoconnect() 59 | .sink(receiveValue: { [weak self] _ in 60 | guard let self = self else { return } 61 | self.isActive = false 62 | withAnimation { self.isActive = true } 63 | }) 64 | } 65 | 66 | public func stopAnimation() { 67 | timer?.cancel() 68 | timer = nil 69 | isActive = false 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Components/Sources/Components/ShimmerView/ShimmerModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShimmerModifier.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ShimmerModifier: ViewModifier { 12 | 13 | // MARK: - Properties 14 | 15 | let isActive: Bool 16 | 17 | // MARK: - View Modifier 18 | 19 | @ViewBuilder func body(content: Content) -> some View { 20 | if isActive { 21 | content.overlay(ShimmerView().clipped()) 22 | } else { 23 | content 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Components/Sources/Components/ShimmerView/ShimmerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShimmerView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Common 11 | 12 | struct ShimmerView : View { 13 | 14 | // MARK: - States 15 | 16 | @EnvironmentObject var shimmerConfig: ShimmerConfig 17 | 18 | // MARK: - Properties 19 | 20 | var linearGradient: LinearGradient { 21 | let startGradient = Gradient.Stop(color: shimmerConfig.bgColor, location: 0.3) 22 | let endGradient = Gradient.Stop(color: shimmerConfig.bgColor, location: 0.7) 23 | let maskGradient = Gradient.Stop(color: shimmerConfig.shimmerColor, location: 0.5) 24 | let gradient = Gradient(stops: [startGradient, maskGradient, endGradient]) 25 | return LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing) 26 | } 27 | 28 | // MARK: - View 29 | 30 | var body: some View { 31 | GeometryReader { 32 | content(shimmerOffset: $0.size.width + CGFloat(2 * shimmerConfig.shimmerAngle)) 33 | } 34 | } 35 | 36 | // MARK: - View methods 37 | 38 | func content(shimmerOffset: CGFloat) -> some View { 39 | ZStack(alignment: .leading) { 40 | Rectangle() 41 | .background(shimmerConfig.bgColor) 42 | .foregroundColor(.clear) 43 | 44 | Rectangle() 45 | .foregroundColor(.clear) 46 | .background(linearGradient) 47 | .rotationEffect(Angle(degrees: shimmerConfig.shimmerAngle)) 48 | .offset(x: (shimmerConfig.isActive ? 1 : -1) * shimmerOffset, y: .zero) 49 | .transition(.move(edge: .leading)) 50 | .animation(.linear(duration: shimmerConfig.shimmerDuration)) 51 | } 52 | .padding(.vertical(-shimmerOffset)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Components/Sources/Components/ShimmerView/View+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Extension.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension View { 12 | 13 | public func shimmer(isActive: Bool) -> some View { 14 | modifier(ShimmerModifier(isActive: isActive)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Components/Sources/Components/SidebarLeftButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarLeftButton.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AppScript 11 | 12 | public struct SidebarLeftButton: View { 13 | 14 | // MARK: - States 15 | 16 | @Environment(\.horizontalSizeClass) var horizontalSizeClass 17 | @Store var store: SidebarStore 18 | 19 | // MARK: - Initialization 20 | 21 | public init() {} 22 | 23 | // MARK: - View 24 | 25 | public var body: some View { 26 | if horizontalSizeClass == .regular, store.sidebarStyle == .regular { 27 | Button(action: { withAnimation { store.toggle() } }) { 28 | Image(systemName: "sidebar.left") 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Components/Sources/Components/TagFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagFilter.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | public struct TagFilter: View { 13 | 14 | let tag: String 15 | @Binding var selected: String 16 | 17 | public init(tag: String, state: Binding) { 18 | self.tag = tag 19 | self._selected = state 20 | } 21 | 22 | public var body: some View { 23 | Button(action: { selected = tag }) { 24 | Text(tag) 25 | } 26 | .modifier(TagFilterStyle()) 27 | .background(Color.background(by: selected == tag)) 28 | .disabled(selected == tag) 29 | } 30 | } 31 | 32 | // MARK: - Previews 33 | 34 | struct TagFilter_Previews: PreviewProvider { 35 | 36 | static var previews: some View { 37 | Group { 38 | TagFilter(tag: "performance", state: .constant("123")) 39 | .padding() 40 | .previewLayout(.sizeThatFits) 41 | .environment(\.colorScheme, .light) 42 | 43 | TagFilter(tag: "performance", state: .constant("performance")) 44 | .padding() 45 | .previewLayout(.sizeThatFits) 46 | .environment(\.colorScheme, .dark) 47 | } 48 | } 49 | } 50 | 51 | // MARK: - View Modifiers 52 | 53 | fileprivate struct TagFilterStyle: ViewModifier { 54 | 55 | func body(content: Content) -> some View { 56 | content 57 | .buttonStyle(BorderlessButtonStyle()) 58 | .font(.system(size: 12, weight: .regular)) 59 | .lineLimit(1) 60 | .foregroundColor(.foreground) 61 | .padding([.top, .bottom], 4.5) 62 | .padding([.leading, .trailing], 10) 63 | .cornerRadius(6) 64 | } 65 | } 66 | 67 | // MARK: - Colors 68 | 69 | fileprivate extension Color { 70 | 71 | static let foreground = Color.white 72 | static let background = Color.white.opacity(0.1) 73 | static func background(by selected: Bool) -> Color { 74 | selected ? Palette.main : .background 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Components/Sources/Components/TagsCollection/AdaptiveTagsCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdaptiveTagsCollectionView.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct AdaptiveTagsCollectionView: View { 12 | 13 | public typealias CollectionSizeHandler = (_ mainContentWidth: CGFloat) -> CGFloat 14 | 15 | @Environment(\.mainContentSize) var mainContentSize 16 | @State private var preferredWidth: CGFloat = .zero 17 | 18 | let data: [String] 19 | let alignment: VerticalAlignment? 20 | let elementContent: (String) -> TagButton 21 | let prepareCollectionWidth: CollectionSizeHandler 22 | 23 | public init( 24 | _ data: [String], 25 | alignment: VerticalAlignment = .bottom, 26 | elementContent: @escaping (String) -> TagButton, 27 | prepareCollectionWidth: @escaping CollectionSizeHandler) { 28 | self.data = data 29 | self.alignment = alignment 30 | self.elementContent = elementContent 31 | self.prepareCollectionWidth = prepareCollectionWidth 32 | } 33 | 34 | public var body: some View { 35 | TagsCollectionView(data, alignment: alignment ?? .bottom, preferredWidth: $preferredWidth, elementContent: elementContent) 36 | .onAppear { 37 | preferredWidth = prepareCollectionWidth(mainContentSize.width) 38 | } 39 | .onChange(of: mainContentSize) { value in 40 | preferredWidth = prepareCollectionWidth(value.width) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Components/Sources/Components/ThreadItemView/ThreadItemNavigationLinkStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadItemNavigationLinkStyle.swift 3 | // StackOv (Components module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | public struct ThreadItemNavigationLinkStyle: ButtonStyle { 13 | 14 | // MARK: - Initialization 15 | 16 | public init() {} 17 | 18 | // MARK: - View methods 19 | 20 | public func makeBody(configuration: Configuration) -> some View { 21 | configuration.label 22 | .shadow(color: Color.shadow, radius: 16, x: 0, y: 14) 23 | .animation(.default) 24 | .opacity(configuration.isPressed ? 0.8 : 1) 25 | } 26 | } 27 | 28 | // MARK: - Colors 29 | 30 | fileprivate extension Color { 31 | 32 | static let shadow = Palette.darkShadow.opacity(0.18) | Color.clear 33 | } 34 | -------------------------------------------------------------------------------- /DataTransferObjects/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: "DataTransferObjects", 8 | products: [ 9 | .library(name: "DataTransferObjects", targets: ["DataTransferObjects"]) 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target(name: "DataTransferObjects", dependencies: []), 14 | .testTarget(name: "DataTransferObjectsTests", dependencies: ["DataTransferObjects"]) 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /DataTransferObjects/README.md: -------------------------------------------------------------------------------- 1 | # DataTransferObjects 2 | 3 | This package contains Entries and Entities modler. Additionally some functionality to convert an entry model to an entity. 4 | -------------------------------------------------------------------------------- /DataTransferObjects/Sources/DataTransferObjects/AnswerEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnswerEntry.swift 3 | // StackOv (DataTransferObjects module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias AnswersEntry = PostsEntry 12 | 13 | public struct AnswerEntry: Codable { 14 | 15 | // MARK: - Nested types 16 | 17 | public enum CodingKeys: String, CodingKey { 18 | case id = "answer_id" 19 | case questionId = "question_id" 20 | case isAccepted = "is_accepted" 21 | case score 22 | case link 23 | case body 24 | case owner 25 | case comments 26 | case lastEditDate = "last_edit_date" 27 | } 28 | 29 | // MARK: - Public properties 30 | 31 | public let id: Int 32 | public let questionId: Int 33 | public let isAccepted: Bool 34 | public let score: Int 35 | public let link: URL? 36 | public let body: String 37 | public let owner: ShallowUserEntry 38 | public let comments: [CommentEntry]? 39 | public let lastEditDate: Date? 40 | } 41 | -------------------------------------------------------------------------------- /DataTransferObjects/Sources/DataTransferObjects/CommentEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostsEntry.swift 3 | // StackOv (DataTransferObjects module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct CommentEntry: Codable { 12 | 13 | // MARK: - Nested Types 14 | 15 | public enum CodingKeys: String, CodingKey { 16 | case body 17 | case bodyMarkdown = "body_markdown" 18 | case commentId = "comment_id" 19 | case creationDate = "creation_date" 20 | case edited 21 | case link 22 | case owner 23 | case postId = "post_id" 24 | case postType = "post_type" 25 | case replyToUser = "reply_to_user" 26 | case score 27 | case upvoted 28 | } 29 | 30 | // MARK: - Public Properties 31 | 32 | public let body: String 33 | public let bodyMarkdown: String? 34 | public let commentId: Int 35 | public let creationDate: Date 36 | public let edited: Bool 37 | public let link: String? 38 | public let owner: ShallowUserEntry? 39 | public let postId: Int 40 | public let postType: String? 41 | public let replyToUser: ShallowUserEntry? 42 | public let score: Int? 43 | public let upvoted: Bool? 44 | 45 | } 46 | -------------------------------------------------------------------------------- /DataTransferObjects/Sources/DataTransferObjects/PostsEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostsEntry.swift 3 | // StackOv (DataTransferObjects module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | 12 | public struct PostsEntry: Codable { 13 | 14 | // MARK: - Nested types 15 | 16 | public enum CodingKeys: String, CodingKey { 17 | case items 18 | case hasMore = "has_more" 19 | case quotaMax = "quota_max" 20 | case quotaRemaining = "quota_remaining" 21 | } 22 | 23 | // MARK: - Public properties 24 | 25 | public let items: [Item] 26 | public let hasMore: Bool 27 | public let quotaMax: Int 28 | public let quotaRemaining: Int 29 | } 30 | -------------------------------------------------------------------------------- /DataTransferObjects/Sources/DataTransferObjects/QuestionEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestionEntry.swift 3 | // StackOv (DataTransferObjects module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias QuestionsEntry = PostsEntry 12 | 13 | public struct QuestionEntry: Codable { 14 | 15 | // MARK: - Nested types 16 | 17 | public enum CodingKeys: String, CodingKey { 18 | case id = "question_id" 19 | case title 20 | case isAnswered = "is_answered" 21 | case viewCount = "view_count" 22 | case answerCount = "answer_count" 23 | case score 24 | case tags 25 | case link 26 | case body 27 | case owner 28 | case acceptedAnswerId = "accepted_answer_id" 29 | case creationDate = "creation_date" 30 | case lastActivityDate = "last_activity_date" 31 | case comments 32 | } 33 | 34 | // MARK: - Public properties 35 | 36 | public let id: Int 37 | public let title: String 38 | public let isAnswered: Bool? 39 | public let viewCount: Int 40 | public let answerCount: Int 41 | public let score: Int 42 | public let tags: [String] 43 | public let link: URL? 44 | public let body: String? 45 | public let owner: ShallowUserEntry? 46 | public let acceptedAnswerId: Int? 47 | public let creationDate: Date? 48 | public let lastActivityDate: Date? 49 | public let comments: [CommentEntry]? 50 | } 51 | 52 | -------------------------------------------------------------------------------- /DataTransferObjects/Sources/DataTransferObjects/ShallowUserEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserEntry.swift 3 | // StackOv (DataTransferObjects module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ShallowUserEntry: Codable { 12 | 13 | // MARK: - Nested types 14 | 15 | public enum CodingKeys: String, CodingKey { 16 | case reputation 17 | case id = "user_id" 18 | case type = "user_type" 19 | case acceptRate = "accept_rate" 20 | case avatar = "profile_image" 21 | case name = "display_name" 22 | case link 23 | } 24 | 25 | public enum UserType: String, Codable { 26 | case unregistered 27 | case registered 28 | case moderator 29 | case teamAdmin = "team_admin" 30 | case doesNotExist = "does_not_exist" 31 | } 32 | 33 | // MARK: - Public properties 34 | 35 | public let reputation: Int? 36 | public let id: Int? 37 | public let type: UserType 38 | public let acceptRate: Int? 39 | public let avatar: URL? 40 | public let name: String 41 | public let link: URL? 42 | } 43 | -------------------------------------------------------------------------------- /DataTransferObjects/Tests/DataTransferObjectsTests/DataTransferObjectsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DataTransferObjects 3 | 4 | final class DataTransferObjectsTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Errors/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Errors/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: "Errors", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "Errors", targets: ["Errors"]) 11 | ], 12 | targets: [ 13 | .target(name: "Errors", dependencies: []) 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Errors/README.md: -------------------------------------------------------------------------------- 1 | # Errors 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Errors/Sources/Errors/DataError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataError.swift 3 | // StackOv (Errors module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum DataError: Error { 12 | case wrongEncoding(data: Data, encoding: String.Encoding) 13 | } 14 | -------------------------------------------------------------------------------- /Errors/Sources/Errors/DisplaybleError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplaybleError.swift 3 | // StackOv (Errors module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol DisplaybleError: LocalizedError { 12 | 13 | /// A short localized message describing what error occurred. 14 | var title: String { get } 15 | 16 | /// A localized message describing what error occurred. 17 | var description: String? { get } 18 | } 19 | 20 | public extension DisplaybleError { 21 | 22 | var description: String? { errorDescription } 23 | } 24 | -------------------------------------------------------------------------------- /Errors/Sources/Errors/HTTPError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPError.swift 3 | // StackOv (Errors module) 4 | // 5 | // Created by Влад Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct HTTPError: DisplaybleError { 12 | 13 | // MARK: - Properties 14 | 15 | public let statusCode: HTTPStatusCode 16 | 17 | // MARK: - Initialization 18 | 19 | public init?(_ error: Error) { 20 | guard let statusCode = error as? HTTPStatusCode else { 21 | return nil 22 | } 23 | 24 | switch statusCode.responseType { 25 | case .success, .informational, .redirection: 26 | return nil 27 | case .clientError, .serverError, .undefined: 28 | self.statusCode = statusCode 29 | } 30 | } 31 | } 32 | 33 | // MARK: - Extensions 34 | 35 | public extension HTTPError { 36 | 37 | var title: String { 38 | switch statusCode.responseType { 39 | case .clientError: 40 | return "Client error \(statusCode.rawValue)" 41 | case .serverError: 42 | return "Server error \(statusCode.rawValue)" 43 | case .success, .informational, .redirection, .undefined: 44 | return "Unknown error" 45 | } 46 | } 47 | 48 | var errorDescription: String? { 49 | #if DEBUG 50 | return statusCode.localizedDescription 51 | #else 52 | switch statusCode { 53 | case .badRequest, .unauthorized, .forbidden, 54 | .notFound, .teapot, .tooManyRequests, .noResponse: 55 | return statusCode.localizedDescription 56 | case .serviceUnavailable: 57 | return "The server is currently unavailable" 58 | default: 59 | return nil 60 | } 61 | #endif 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Errors/Sources/Errors/OpenURLError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenURLError.swift 3 | // StackOv (Errors module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum OpenURLError: DisplaybleError { 12 | case canNotBeOpened(URL) 13 | } 14 | 15 | // MARK: - Extensions 16 | 17 | public extension OpenURLError { 18 | 19 | var title: String { 20 | "🤷‍♂️" 21 | } 22 | 23 | var errorDescription: String? { 24 | switch self { 25 | case let .canNotBeOpened(url): 26 | return """ 27 | For some reason, the url \(url.absoluteString) cannot be oppened. 28 | This link is saved in the Clipboard. 29 | """ 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Errors/Sources/Errors/PasteboardError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasteboardError.swift 3 | // StackOv (Errors module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum PasteboardError: DisplaybleError { 12 | case unknown 13 | } 14 | 15 | // MARK: - Extensions 16 | 17 | public extension PasteboardError { 18 | 19 | var title: String { 20 | "🌚" 21 | } 22 | 23 | var errorDescription: String? { 24 | "Something went wrong..." 25 | } 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /Errors/Sources/Errors/ServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceError.swift 3 | // StackOv (Errors module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ServiceError: DisplaybleError { 12 | 13 | case url(code: URLError.Code, urlString: String) 14 | case urlComponents(URLComponents) 15 | } 16 | 17 | // MARK: - Extensions 18 | 19 | public extension ServiceError { 20 | 21 | var title: String { 22 | switch self { 23 | case let .url(code, urlString): 24 | #if DEBUG 25 | return "Client error \(code), url: \(urlString)" 26 | #else 27 | return "Client error \(code)" 28 | #endif 29 | case .urlComponents: 30 | return "Client error" 31 | } 32 | } 33 | 34 | var errorDescription: String? { 35 | switch self { 36 | case .url: 37 | return nil 38 | case let .urlComponents(components): 39 | #if DEBUG 40 | return "Url components: \(components)" 41 | #else 42 | return nil 43 | #endif 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Errors/Sources/Errors/URLSessionError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionError.swift 3 | // StackOv (Errors module) 4 | // 5 | // Created by  Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum URLSessionError: DisplaybleError { 12 | case invalidResponse(URLResponse) 13 | case unknownHTTPCode(response: HTTPURLResponse) 14 | } 15 | 16 | // MARK: - Extensions 17 | 18 | public extension URLSessionError { 19 | 20 | var title: String { 21 | #if DEBUG 22 | switch self { 23 | case .invalidResponse: 24 | return "Invalid response" 25 | case .unknownHTTPCode: 26 | return "Unknown HTTP code" 27 | } 28 | #else 29 | return "☕️" 30 | #endif 31 | } 32 | 33 | var errorDescription: String? { 34 | #if DEBUG 35 | switch self { 36 | case let .invalidResponse(response): 37 | return "\(response)" 38 | case let .unknownHTTPCode(response): 39 | return "\(response)" 40 | } 41 | #else 42 | return nil 43 | #endif 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Flows/FavoriteFlow/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: "FavoriteFlow", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "FavoriteFlow", targets: ["FavoriteFlow"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../Palette"), 15 | .package(path: "../Components"), 16 | .package(path: "../AppScript"), 17 | .package(path: "../Stores/FavoriteStore") 18 | ], 19 | targets: [ 20 | .target(name: "FavoriteFlow", dependencies: ["Common", "Palette", "Components", "AppScript", "FavoriteStore"]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Flows/FavoriteFlow/README.md: -------------------------------------------------------------------------------- 1 | # FavoriteFlow 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Flows/HomeFlow/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: "HomeFlow", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "HomeFlow", targets: ["HomeFlow"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../Palette"), 15 | .package(path: "../Icons"), 16 | .package(path: "../Components"), 17 | .package(path: "../AppScript"), 18 | .package(path: "ThreadFlow") 19 | ], 20 | targets: [ 21 | .target(name: "HomeFlow", dependencies: ["Common", "Palette", "Icons", "Components", "AppScript", "ThreadFlow"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Flows/HomeFlow/README.md: -------------------------------------------------------------------------------- 1 | # HomeFlow 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Flows/HomeFlow/Sources/HomeFlow/PagesView/PagesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PagesView.swift 3 | // StackOv (HomeFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | import Components 12 | 13 | /// This is View to make pages version of the app 14 | /// 15 | /// For example, the next code shows how we could use it: 16 | /// 17 | /// TabView(selection: $currentPage) { 18 | /// ForEach(pages) { page in 19 | /// PageView().tabItem { 20 | /// EmptyView() 21 | /// }.tag(page.id) 22 | /// } 23 | /// } 24 | /// 25 | struct PagesView: View { 26 | 27 | // MARK: - States 28 | 29 | @Binding var currentPage: UUID 30 | 31 | // MARK: - Properties 32 | 33 | let pages: [PageModel] 34 | 35 | // MARK: - View 36 | 37 | var body: some View { 38 | ScrollView(.horizontal, showsIndicators: false) { 39 | HStack(alignment: .center) { 40 | ForEach(pages) { model in 41 | PageButton(model, currentPage: $currentPage) 42 | } 43 | } 44 | .padding(.horizontal, 24) 45 | } 46 | .frame(height: 52) 47 | .background(Color.background) 48 | } 49 | } 50 | 51 | // MARK: - Previews 52 | 53 | struct PagesView_Previews: PreviewProvider { 54 | 55 | static var previews: some View { 56 | let id = UUID() 57 | PagesView(currentPage: .constant(id), pages: [PageModel(id: id, title: "Test Page")]) 58 | } 59 | } 60 | 61 | // MARK: - Colors 62 | 63 | fileprivate extension UIColor { 64 | 65 | static let background = PaletteCore.grayblue.withAlphaComponent(0.5).rgbaToRgb(by: PaletteCore.bluishblack) 66 | } 67 | 68 | fileprivate extension Color { 69 | 70 | static let background = Color(.background) 71 | } 72 | -------------------------------------------------------------------------------- /Flows/MainFlow/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: "MainFlow", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "MainFlow", targets: ["MainFlow"]) 11 | ], 12 | dependencies: [ 13 | .package(path: "../AppScript"), 14 | .package(path: "../Common"), 15 | .package(path: "../Components"), 16 | .package(path: "../Icons"), 17 | .package(path: "../Palette"), 18 | .package(path: "../SidebarStore"), 19 | .package(path: "HomeFlow"), 20 | .package(path: "FavoriteFlow"), 21 | .package(path: "MessagesFlow"), 22 | .package(path: "TagsFlow"), 23 | .package(path: "UsersFlow") 24 | ], 25 | targets: [ 26 | .target(name: "MainFlow", 27 | dependencies: [ 28 | "AppScript", 29 | "Common", 30 | "Components", 31 | "Icons", 32 | "Palette", 33 | "SidebarStore", 34 | "HomeFlow", 35 | "FavoriteFlow", 36 | "MessagesFlow", 37 | "TagsFlow", 38 | "UsersFlow" 39 | ]) 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /Flows/MainFlow/README.md: -------------------------------------------------------------------------------- 1 | # MainFlow 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/GlobalBannerModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalBannerModifier.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Common 11 | import Components 12 | import AppScript 13 | 14 | public struct GlobalBannerModifier: ViewModifier { 15 | 16 | // MARK: - States 17 | 18 | @Store var store: GlobalBannerStore 19 | 20 | // MARK: - Views 21 | 22 | public func body(content: Content) -> some View { 23 | ZStack { 24 | content 25 | 26 | if !store.notifications.isEmpty { 27 | notificationList 28 | .animation(.easeInOut) 29 | .transition(AnyTransition.move(edge: .top).combined(with: .opacity)) 30 | .onAppear { 31 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 32 | closeNotificationBanner() 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | // MARK: - View 40 | 41 | @ViewBuilder 42 | var notificationList: some View { 43 | if let model = store.notifications.last { 44 | if UIDevice.current.userInterfaceIdiom.isPhone { 45 | NotificationBannerView(model: .constant(model), action: closeNotificationBanner) 46 | } else { 47 | HStack { 48 | Spacer() 49 | NotificationBannerView(model: .constant(model), action: closeNotificationBanner) 50 | .frame(width: 393) 51 | } 52 | } 53 | } 54 | } 55 | 56 | // MARK: - Private Methods 57 | 58 | private func closeNotificationBanner() { 59 | withAnimation { 60 | store.hideAllBanners() 61 | } 62 | } 63 | } 64 | 65 | // MARK: - Extensions 66 | 67 | public extension View { 68 | 69 | func globalBanner() -> some View { 70 | modifier(GlobalBannerModifier()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/MainFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainFlow.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AppScript 11 | 12 | public struct MainFlow: View { 13 | 14 | // MARK: - Initialization 15 | 16 | public init() {} 17 | 18 | // MARK: - View 19 | 20 | public var body: some View { 21 | if UIDevice.current.userInterfaceIdiom.isPhone { 22 | PhoneContentView() 23 | .globalBanner() 24 | } else { 25 | PadContentView() 26 | .globalBanner() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/PhoneContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhoneContentView.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct PhoneContentView: View { 13 | 14 | // MARK: - View 15 | 16 | var body: some View { 17 | TabView { 18 | MainBar.tabs 19 | }.accentColor(Color.accentColor) 20 | } 21 | } 22 | 23 | // MARK: - Previews 24 | 25 | struct PhoneContentView_Previews: PreviewProvider { 26 | 27 | static var previews: some View { 28 | PhoneContentView() 29 | } 30 | } 31 | 32 | // MARK: - Colors 33 | 34 | fileprivate extension Color { 35 | 36 | static let accentColor = Palette.lightBlack | Color.white 37 | } 38 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/Sidebar/Compact/CompactSidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompactSidebarView.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct CompactSidebarView: View { 13 | 14 | // MARK: - States 15 | 16 | @Binding var state: MainBar.ItemType 17 | 18 | // MARK: - Initialization 19 | 20 | init(state: Binding) { 21 | self._state = state 22 | } 23 | 24 | // MARK: - View 25 | 26 | var body: some View { 27 | VStack(alignment: .center, spacing: .zero) { 28 | ForEach(MainBar.ItemType.allCases) { item in 29 | CompactSidebarButton(item, state: $state) 30 | } 31 | 32 | Spacer() 33 | 34 | CompactUserView() 35 | } 36 | .padding(.top, 11) 37 | .frame( 38 | minWidth: 0, 39 | maxWidth: .infinity, 40 | minHeight: 0, 41 | maxHeight: .infinity, 42 | alignment: .topLeading 43 | ) 44 | } 45 | } 46 | 47 | // MARK: - Previews 48 | 49 | struct CompactSidebarView_Previews: PreviewProvider { 50 | 51 | static var previews: some View { 52 | CompactSidebarView(state: .constant(.home)) 53 | .padding(EdgeInsets(top: 20, leading: 10, bottom: 20, trailing: 10)) 54 | .background(Palette.grayblue) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/Sidebar/Compact/CompactUserView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompactUserView.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct CompactUserView: View { 13 | 14 | var body: some View { 15 | Image(systemName: "person.crop.circle.fill") 16 | .resizable() 17 | .frame(width: 34, height: 34) 18 | } 19 | } 20 | 21 | // MARK: - Previews 22 | 23 | struct CompactUserView_Previews: PreviewProvider { 24 | 25 | static var previews: some View { 26 | CompactUserView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/Sidebar/Regular/RegularSidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegularSidebarView.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct RegularSidebarView: View { 13 | 14 | // MARK: - States 15 | 16 | @Binding var state: MainBar.ItemType 17 | 18 | // MARK: - Initialization 19 | 20 | init(state: Binding) { 21 | self._state = state 22 | } 23 | 24 | // MARK: - View 25 | 26 | var body: some View { 27 | VStack(alignment: .leading, spacing: .zero) { 28 | ForEach(MainBar.ItemType.allCases) { item in 29 | RegularSidebarButton(item, state: $state) 30 | } 31 | 32 | Spacer() 33 | 34 | RegularUserView() 35 | .padding(EdgeInsets.leading(10)) 36 | } 37 | .padding(.top, 11) 38 | .frame( 39 | minWidth: 0, 40 | maxWidth: .infinity, 41 | minHeight: 0, 42 | maxHeight: .infinity, 43 | alignment: .topLeading 44 | ) 45 | } 46 | } 47 | 48 | // MARK: - Previews 49 | 50 | struct RegularSidebarView_Previews: PreviewProvider { 51 | 52 | static var previews: some View { 53 | RegularSidebarView(state: .constant(.home)) 54 | .padding(EdgeInsets(top: 20, leading: 10, bottom: 20, trailing: 10)) 55 | .background(Palette.grayblue) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/Sidebar/Regular/RegularUserView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegularUserView.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct RegularUserView: View { 13 | 14 | var body: some View { 15 | HStack(alignment: .center, spacing: 8) { 16 | Image(systemName: "person.crop.circle.fill") 17 | .resizable() 18 | .frame(width: 34, height: 34) 19 | VStack(alignment: .leading) { 20 | Text("{Name}") 21 | .font(.system(size: 13, weight: .medium)) 22 | .foregroundColor(Color.nameForeground) 23 | Text("{email}") 24 | .font(.system(size: 10, weight: .regular)) 25 | .foregroundColor(Color.emailForeground) 26 | } 27 | }.frame(height: 34) 28 | } 29 | } 30 | 31 | // MARK: - Previews 32 | 33 | struct UserView_Previews: PreviewProvider { 34 | 35 | static var previews: some View { 36 | RegularUserView() 37 | .padding() 38 | .background(Palette.grayblue) 39 | .previewLayout(.sizeThatFits) 40 | } 41 | } 42 | 43 | // MARK: - Colors 44 | 45 | fileprivate extension Color { 46 | 47 | static let nameForeground = Color.white 48 | static let emailForeground = Palette.telegrey 49 | } 50 | -------------------------------------------------------------------------------- /Flows/MainFlow/Sources/MainFlow/Sidebar/SidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarView.swift 3 | // StackOv (MainFlow module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AppScript 11 | import SidebarStore 12 | import Palette 13 | import AppScript 14 | import Common 15 | 16 | struct SidebarView: View { 17 | 18 | // MARK: - States 19 | 20 | @Environment(\.sizeCategory) var sizeCategory: ContentSizeCategory 21 | @Binding var state: MainBar.ItemType 22 | @Store private var store: SidebarStore 23 | 24 | // MARK: - View 25 | 26 | var body: some View { 27 | switch store.sidebarStyle { 28 | case .regular: 29 | regularSidebar 30 | case .compact: 31 | compactSideBar 32 | } 33 | } 34 | 35 | var regularSidebar: some View { 36 | RegularSidebarView(state: $state) 37 | .padding(EdgeInsets(top: 20, leading: 10, bottom: 20, trailing: 10)) 38 | .background(Color.backgound) 39 | .frame(maxWidth: SidebarConstants.sidebarWidth(style: .regular, isAccessibility: sizeCategory.isAccessibilityCategory)) 40 | .ignoresSafeArea(.container, edges: .top) 41 | } 42 | 43 | var compactSideBar: some View { 44 | CompactSidebarView(state: $state) 45 | .padding(.vertical, 20) 46 | .background(Color.backgound) 47 | .frame(maxWidth: SidebarConstants.sidebarWidth(style: .compact, isAccessibility: sizeCategory.isAccessibilityCategory)) 48 | .ignoresSafeArea(.container, edges: .top) 49 | } 50 | 51 | } 52 | 53 | // MARK: - Previews 54 | 55 | struct SidebarView_Previews: PreviewProvider { 56 | 57 | static var previews: some View { 58 | SidebarView(state: .constant(.home)) 59 | } 60 | } 61 | 62 | // MARK: - Colors 63 | 64 | fileprivate extension Color { 65 | 66 | static let backgound = Palette.periwinkleCrayola | Palette.grayblue 67 | } 68 | -------------------------------------------------------------------------------- /Flows/MessagesFlow/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: "MessagesFlow", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "MessagesFlow", targets: ["MessagesFlow"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../Palette"), 15 | .package(path: "../Components") 16 | ], 17 | targets: [ 18 | .target(name: "MessagesFlow", dependencies: ["Common", "Palette", "Components"]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Flows/MessagesFlow/README.md: -------------------------------------------------------------------------------- 1 | # MessagesFlow 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Flows/MessagesFlow/Sources/MessagesFlow/MessagesFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessagesFlow.swift 3 | // StackOv (MessagesFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | import Components 12 | 13 | public struct MessagesFlow: View { 14 | 15 | public init() {} 16 | 17 | public var body: some View { 18 | PageDevMock() 19 | .foregroundColor(Color.foreground) 20 | } 21 | } 22 | 23 | // MARK: - Previews 24 | 25 | struct MessagesFlow_Previews: PreviewProvider { 26 | static var previews: some View { 27 | MessagesFlow() 28 | } 29 | } 30 | 31 | // MARK: - Colors 32 | 33 | fileprivate extension Color { 34 | static let foreground = Palette.dullGray 35 | static let background = Palette.bluishblack 36 | } 37 | -------------------------------------------------------------------------------- /Flows/TagsFlow/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: "TagsFlow", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "TagsFlow", targets: ["TagsFlow"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../Palette"), 15 | .package(path: "../Components") 16 | ], 17 | targets: [ 18 | .target(name: "TagsFlow", dependencies: ["Common", "Palette", "Components"]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Flows/TagsFlow/README.md: -------------------------------------------------------------------------------- 1 | # TagsFlow 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Flows/TagsFlow/Sources/TagsFlow/TagsFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagsFlow.swift 3 | // StackOv (TagsFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | import Components 12 | 13 | public struct TagsFlow: View { 14 | 15 | public init() {} 16 | 17 | public var body: some View { 18 | PageDevMock() 19 | .foregroundColor(Color.foreground) 20 | } 21 | } 22 | 23 | // MARK: - Previews 24 | 25 | struct TagsFlow_Previews: PreviewProvider { 26 | static var previews: some View { 27 | TagsFlow() 28 | } 29 | } 30 | 31 | // MARK: - Colors 32 | 33 | fileprivate extension Color { 34 | static let foreground = Palette.dullGray 35 | static let background = Palette.bluishblack 36 | } 37 | -------------------------------------------------------------------------------- /Flows/ThreadFlow/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: "ThreadFlow", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ThreadFlow", targets: ["ThreadFlow"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../Palette"), 15 | .package(path: "../Icons"), 16 | .package(path: "../Components"), 17 | .package(path: "../AppScript") 18 | ], 19 | targets: [ 20 | .target(name: "ThreadFlow", dependencies: ["Common", "Palette", "Icons", "Components", "AppScript"]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Flows/ThreadFlow/README.md: -------------------------------------------------------------------------------- 1 | # ThreadFlow 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Flows/ThreadFlow/Sources/ThreadFlow/AnswerView/AnswerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnswerView.swift 3 | // StackOv (ThreadFlow module) 4 | // 5 | // Created by Влад Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Common 11 | import AppScript 12 | 13 | struct AnswerView: View { 14 | 15 | // MARK: - States 16 | 17 | @Environment(\.horizontalSizeClass) var horizontalSizeClass 18 | @EnvironmentObject var store: ThreadStore 19 | 20 | // MARK: - Properties 21 | 22 | let model: AnswerModel 23 | let isLast: Bool 24 | 25 | // MARK: - Views 26 | 27 | var body: some View { 28 | VStack(spacing: .zero) { 29 | if UIDevice.current.userInterfaceIdiom.isPad && horizontalSizeClass == .regular { 30 | PadAnswerView(model: model) 31 | .environmentObject(store) 32 | } else { 33 | PhoneAnswerView(model: model) 34 | .environmentObject(store) 35 | } 36 | 37 | VStack(spacing: .zero) { 38 | Divider() 39 | .padding(.top, 24) 40 | .padding(.bottom, 8) 41 | 42 | if model.comments.count > 0 { 43 | CommentsView(isNeedShowButton: false) 44 | .environmentObject(StoresAssembler.shared.resolve(CommentsStore.self, argument: model.comments)!) 45 | } 46 | }.padding(.leading, 60) 47 | 48 | if !isLast { 49 | Divider() 50 | .padding(.vertical, 32) 51 | } 52 | } 53 | } 54 | 55 | } 56 | 57 | // MARK: - Previews 58 | 59 | struct AnswerView_Previews: PreviewProvider { 60 | static var previews: some View { 61 | AnswerView(model: AnswerModel.mock(), isLast: true) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Flows/ThreadFlow/Sources/ThreadFlow/AnswerView/PhoneAnswerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhoneAnswerView.swift 3 | // StackOv (ThreadFlow module) 4 | // 5 | // Created by Влад Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Common 11 | import Components 12 | import Palette 13 | import AppScript 14 | 15 | struct PhoneAnswerView: View { 16 | 17 | // MARK: - Properties 18 | 19 | let model: AnswerModel 20 | @EnvironmentObject var store: ThreadStore 21 | 22 | // MARK: - Views 23 | 24 | var body: some View { 25 | VStack(alignment: .leading, spacing: 24) { 26 | MarkdownPostView(store.unit(of: model), style: .post) 27 | .fixedSize(horizontal: false, vertical: true) 28 | buttons 29 | PersonInfoView(model: UserModel.mock(), indent: 12, isFullScreen: true) 30 | 31 | HStack { 32 | RetingView(viewed: model.formattedVotesNumber, isVertical: false) 33 | Spacer() 34 | Button("Add comment") { 35 | // TODO: Add functionality in the future 36 | } 37 | .font(.subheadline) 38 | .foregroundColor(Palette.main) 39 | } 40 | } 41 | } 42 | 43 | var buttons: some View { 44 | HStack(alignment: .center, spacing: 24) { 45 | Button("Share") { 46 | // TODO: Add functionality in the future 47 | } 48 | Button("Edit") { 49 | // TODO: Add functionality in the future 50 | } 51 | Button("Follow") { 52 | // TODO: Add functionality in the future 53 | } 54 | } 55 | .font(.subheadline) 56 | .foregroundColor(Palette.main) 57 | } 58 | 59 | } 60 | 61 | // MARK: - Previews 62 | 63 | struct PhoneAnswerView_Previews: PreviewProvider { 64 | 65 | static var previews: some View { 66 | PhoneAnswerView(model: AnswerModel.mock()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Flows/ThreadFlow/Sources/ThreadFlow/QuestionView/QuestionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestionView.swift 3 | // StackOv (ThreadFlow module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | import Common 12 | import Components 13 | import Icons 14 | import AppScript 15 | 16 | struct QuestionView: View { 17 | 18 | // MARK: - States 19 | 20 | @Environment(\.horizontalSizeClass) var horizontalSizeClass 21 | @EnvironmentObject var store: ThreadStore 22 | 23 | // MARK: - Properties 24 | 25 | let model: QuestionModel 26 | 27 | // MARK: - View 28 | 29 | var body: some View { 30 | VStack(spacing: .zero){ 31 | if UIDevice.current.userInterfaceIdiom.isPad && horizontalSizeClass == .regular { 32 | PadQuestionView(model: model) 33 | .environmentObject(store) 34 | } else { 35 | PhoneQuestionView(model: model) 36 | .environmentObject(store) 37 | } 38 | 39 | VStack(spacing: .zero) { 40 | Divider() 41 | .padding(.top, 24) 42 | .padding(.bottom, 8) 43 | 44 | if model.comments.count > 0 { 45 | CommentsView() 46 | .environmentObject(StoresAssembler.shared.resolve(CommentsStore.self, argument: model.comments)!) 47 | } 48 | }.padding(.leading, 60) 49 | } 50 | } 51 | } 52 | 53 | // MARK: - Previews 54 | 55 | struct QuestionView_Previews: PreviewProvider { 56 | 57 | static var previews: some View { 58 | QuestionView(model: QuestionModel.mock()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Flows/ThreadFlow/Sources/ThreadFlow/RetingView/RetingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetingView.swift 3 | // StackOv (ThreadFlow module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | 12 | struct RetingView: View { 13 | 14 | // MARK: - Properties 15 | 16 | var viewed: String 17 | var isVertical: Bool 18 | 19 | // MARK: - View 20 | 21 | var body: some View { 22 | Group { 23 | Button(action: {}, icon: .handThumbsupFill) 24 | .frame(width: 24, height: 24) 25 | Text(viewed) 26 | .font(.footnote) 27 | Button(action: {}, icon: .handThumbsdownFill) 28 | .frame(width: 24, height: 24) 29 | } 30 | .stack(axis: isVertical ? .vertical : .horizontal, spacing: isVertical ? 8 : 12) 31 | .foregroundColor(Palette.slateGray | Palette.dullGray) 32 | } 33 | } 34 | 35 | // MARK: - View Modifiers 36 | 37 | fileprivate struct StackModifier: ViewModifier { 38 | 39 | enum Axis { 40 | case vertical 41 | case horizontal 42 | } 43 | 44 | let axis: Axis 45 | let spacing: CGFloat? 46 | 47 | @ViewBuilder 48 | func body(content: Content) -> some View { 49 | switch axis { 50 | case .vertical: 51 | VStack(alignment: .center, spacing: spacing) { 52 | content 53 | } 54 | case .horizontal: 55 | HStack(alignment: .center, spacing: spacing) { 56 | content 57 | } 58 | } 59 | } 60 | } 61 | 62 | // MARK: - Extensions 63 | 64 | fileprivate extension View { 65 | 66 | func stack(axis: StackModifier.Axis, spacing: CGFloat? = nil) -> some View { 67 | modifier(StackModifier(axis: axis, spacing: spacing)) 68 | } 69 | } 70 | 71 | // MARK: - Previews 72 | 73 | struct RetingView_Previews: PreviewProvider { 74 | 75 | static var previews: some View { 76 | RetingView(viewed: "365", isVertical: false) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Flows/ThreadFlow/Sources/ThreadFlow/ThreadFlowScreenCofiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadFlowScreenConfiguration.swift 3 | // StackOv (ThreadFlow module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | enum ThreadFlowScreenConfiguration { 12 | 13 | static func horisontalInset(horizontalSizeClass: UserInterfaceSizeClass?) -> CGFloat { 14 | return UIDevice.current.userInterfaceIdiom.isPad && horizontalSizeClass == .regular ? 60 : 20 15 | } 16 | 17 | static func contentEdgeInsets(horizontalSizeClass: UserInterfaceSizeClass?) -> EdgeInsets { 18 | let vertical: CGFloat = UIDevice.current.userInterfaceIdiom.isPad ? 18 : 12 19 | let horisontal: CGFloat = horisontalInset(horizontalSizeClass: horizontalSizeClass) 20 | return EdgeInsets(top: vertical, leading: horisontal, bottom: vertical, trailing: horisontal) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Flows/UsersFlow/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: "UsersFlow", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "UsersFlow", targets: ["UsersFlow"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../Palette"), 15 | .package(path: "../Components") 16 | ], 17 | targets: [ 18 | .target(name: "UsersFlow", dependencies: ["Common", "Palette", "Components"]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Flows/UsersFlow/README.md: -------------------------------------------------------------------------------- 1 | # UsersFlow 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Flows/UsersFlow/Sources/UsersFlow/UsersFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersFlow.swift 3 | // StackOv (UsersFlow module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Palette 11 | import Components 12 | 13 | public struct UsersFlow: View { 14 | 15 | public init() {} 16 | 17 | public var body: some View { 18 | PageDevMock() 19 | .foregroundColor(Color.foreground) 20 | } 21 | } 22 | 23 | // MARK: - Previews 24 | 25 | struct UsersFlow_Previews: PreviewProvider { 26 | static var previews: some View { 27 | UsersFlow() 28 | } 29 | } 30 | 31 | // MARK: - Colors 32 | 33 | fileprivate extension Color { 34 | static let foreground = Palette.dullGray 35 | static let background = Palette.bluishblack 36 | } 37 | -------------------------------------------------------------------------------- /HTMLMarkdown/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: "HTMLMarkdown", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "HTMLMarkdown", targets: ["HTMLMarkdown"]) 11 | ], 12 | dependencies: [ 13 | .package(name: "SwiftSoup", url: "https://github.com/scinfu/SwiftSoup.git", .upToNextMajor(from: "2.3.2")) 14 | ], 15 | targets: [ 16 | .target(name: "HTMLMarkdown", dependencies: ["SwiftSoup"]), 17 | .testTarget(name: "HTMLMarkdownTests", dependencies: ["HTMLMarkdown"]) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /HTMLMarkdown/README.md: -------------------------------------------------------------------------------- 1 | # HTMLMarkdown 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /HTMLMarkdown/Sources/HTMLMarkdown/HTMLMarkdown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLMarkdown.swift 3 | // StackOv (HTMLMarkdown module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public enum HTMLMarkdown {} 12 | -------------------------------------------------------------------------------- /HTMLMarkdown/Sources/HTMLMarkdown/Unit/UnitView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnitView.swift 3 | // StackOv (HTMLMarkdown module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public protocol UnitView: View { 12 | var unit: HTMLMarkdown.Unit { get } 13 | } 14 | 15 | 16 | public protocol StyleableUnitView: UnitView { 17 | 18 | associatedtype Style 19 | 20 | var style: Style { get } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /HTMLMarkdown/Tests/HTMLMarkdownTests/HTMLMarkdownTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import HTMLMarkdown 3 | 4 | final class HTMLMarkdownTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Icons/.swiftpm/xcode/xcshareddata/xcschemes/Icons.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Icons/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: "Icons", 8 | platforms: [.iOS(.v14), .macOS(.v10_15)], 9 | products: [ 10 | .library(name: "Icons", targets: ["Icons"]) 11 | ], 12 | targets: [ 13 | .target(name: "Icons", dependencies: []) 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Icons/README.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | This package contains icons by design of StackOv. 4 | -------------------------------------------------------------------------------- /Icons/Scripts/swiftgen.yml: -------------------------------------------------------------------------------- 1 | xcassets: 2 | inputs: 3 | - ${SRCROOT}/Icons/Sources/Icons/Icons.xcassets 4 | outputs: 5 | - templatePath: ${SRCROOT}/Icons/Templates/icons.stencil 6 | params: 7 | enumName: Icons 8 | bundle: .module 9 | publicAccess: true 10 | output: ${SRCROOT}/Icons/Sources/Icons/Icons.swift 11 | 12 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/arrow.left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "arrow.left.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/arrow.left.imageset/arrow.left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bell.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bell.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bell.fill.imageset/bell.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bell.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bell.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bell.imageset/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bookmark.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bookmark.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bookmark.fill.imageset/bookmark.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bookmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bookmark.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/bookmark.imageset/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/checkmark.circle.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "checkmark.circle.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/checkmark.circle.fill.imageset/checkmark.circle.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/checkmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "checkmark.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/checkmark.imageset/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/crown.circle.bronze.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "crown.circle.bronze.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/crown.circle.bronze.fill.imageset/crown.circle.bronze.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/crown.circle.gold.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "crown.circle.gold.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/crown.circle.gold.fill.imageset/crown.circle.gold.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/crown.circle.silver.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "crown.circle.silver.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/crown.circle.silver.fill.imageset/crown.circle.silver.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/eye.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "eye.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/eye.fill.imageset/eye.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/hand.thumbsdown.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hand.thumbsdown.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/hand.thumbsdown.fill.imageset/hand.thumbsdown.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/hand.thumbsup.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hand.thumbsup.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/hand.thumbsup.fill.imageset/hand.thumbsup.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/magnifyingglass.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "magnifyingglass.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/magnifyingglass.imageset/magnifyingglass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/paperclip.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "paperclip.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/paperclip.imageset/paperclip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/pencil.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pencil.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/pencil.imageset/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/person.2.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "person.2.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/person.2.fill.imageset/person.2.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/planet.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "planet.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/planet.imageset/planet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/square.and.arrow.up.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "square.and.arrow.up.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/square.and.arrow.up.imageset/square.and.arrow.up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/star.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "star.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/star.fill.imageset/star.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/star.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "star.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/star.imageset/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/star.rounded.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "star.rounded.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/star.rounded.fill.imageset/star.rounded.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/tag.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tag.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/tag.fill.imageset/tag.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/tray.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tray.fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/tray.fill.imageset/tray.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/xmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "xmark.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/xmark.imageset/xmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/xmark.rounded.bold.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "xmark.rounded.bold.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Icons/Sources/Icons/Icons.xcassets/xmark.rounded.bold.imageset/xmark.rounded.bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Erik Basargin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Palette/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Palette/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: "Palette", 8 | platforms: [.iOS(.v14), .macOS(.v10_15)], 9 | products: [ 10 | .library(name: "Palette", targets: ["Palette"]) 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .target(name: "Palette", dependencies: []) 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /Palette/README.md: -------------------------------------------------------------------------------- 1 | # Palette 2 | 3 | This package contains a core palette by design of StackOv. 4 | 5 | |Color Name|Color Hax| 6 | |--------------|------------| 7 | |Main | #5856F2 | 8 | |White | #FFFFFF | 9 | |Gainsboro | #DFDEDF | 10 | |Telegrey | #929292 | 11 | |DullGray | #737373 | 12 | |Grayblue | #2C2D31 | 13 | |Bluishblack | #1F2022 | 14 | -------------------------------------------------------------------------------- /Palette/Scripts/swiftgen.yml: -------------------------------------------------------------------------------- 1 | xcassets: 2 | inputs: 3 | - ${SRCROOT}/Palette/Sources/Palette/Palette.xcassets 4 | outputs: 5 | - templatePath: ${SRCROOT}/Palette/Templates/palette.stencil 6 | params: 7 | bundle: .module 8 | publicAccess: true 9 | output: ${SRCROOT}/Palette/Sources/Palette/Palette.swift 10 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Black.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x3F", 9 | "green" : "0x39", 10 | "red" : "0x35" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Bluishblack.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x22", 9 | "green" : "0x20", 10 | "red" : "0x1F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Bluishwhite.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFC", 9 | "green" : "0xF9", 10 | "red" : "0xF9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/DarkDivider.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1F", 9 | "green" : "0x1D", 10 | "red" : "0x1C" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/DarkGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x29", 9 | "green" : "0x26", 10 | "red" : "0x26" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/DarkShadow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1E", 9 | "green" : "0x1E", 10 | "red" : "0x1E" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/DullGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x73", 9 | "green" : "0x73", 10 | "red" : "0x73" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Gainsboro.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xDF", 9 | "green" : "0xDE", 10 | "red" : "0xDF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF3", 9 | "green" : "0xEF", 10 | "red" : "0xEF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Grayblue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x31", 9 | "green" : "0x2D", 10 | "red" : "0x2C" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/LightBlack.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x29", 9 | "green" : "0x26", 10 | "red" : "0x26" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/LightDeepGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE7", 9 | "green" : "0xE3", 10 | "red" : "0xE1" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/LightDivider.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE9", 9 | "green" : "0xE1", 10 | "red" : "0xE1" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/LightGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF3", 9 | "green" : "0xEF", 10 | "red" : "0xEF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Main.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF2", 9 | "green" : "0x56", 10 | "red" : "0x58" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/PaleSky.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x85", 9 | "green" : "0x79", 10 | "red" : "0x6F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/PeriwinkleCrayola.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF7", 9 | "green" : "0xF2", 10 | "red" : "0xF2" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x6C", 9 | "green" : "0x1A", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/SlateGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x85", 9 | "green" : "0x79", 10 | "red" : "0x6F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/SlateGrayLight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x99", 9 | "green" : "0x8C", 10 | "red" : "0x81" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/SteelGray300.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xAD", 9 | "green" : "0xA2", 10 | "red" : "0x99" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/Palette.xcassets/Telegrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x92", 9 | "green" : "0x92", 10 | "red" : "0x92" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Palette/Sources/Palette/UIColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extensions.swift 3 | // StackOv (Palette module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 10 | import AppKit.NSColor 11 | #else 12 | import UIKit.UIColor 13 | #endif 14 | 15 | #if canImport(UIKit) 16 | 17 | public extension UIColor { 18 | 19 | typealias RGBAComponents = (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) 20 | 21 | var rgba: RGBAComponents { 22 | var red: CGFloat = 0 23 | var green: CGFloat = 0 24 | var blue: CGFloat = 0 25 | var alpha: CGFloat = 0 26 | 27 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) 28 | 29 | return (red, green, blue, alpha) 30 | } 31 | 32 | func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { 33 | UIGraphicsImageRenderer(size: size).image { rendererContext in 34 | setFill() 35 | rendererContext.fill(CGRect(origin: .zero, size: size)) 36 | } 37 | } 38 | 39 | /// http://marcodiiga.github.io/rgba-to-rgb-conversion 40 | func rgbaToRgb(by backgroundColor: UIColor) -> UIColor { 41 | let currentComponents = self.rgba 42 | let backgroundComponents = backgroundColor.rgba 43 | 44 | let alpha = currentComponents.alpha 45 | let delta = 1 - alpha 46 | return UIColor( 47 | red: delta * backgroundComponents.red + alpha * currentComponents.red, 48 | green: delta * backgroundComponents.green + alpha * currentComponents.green, 49 | blue: delta * backgroundComponents.blue + alpha * currentComponents.blue, 50 | alpha: 1 51 | ) 52 | } 53 | } 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | StackOv 2020 6 |
7 | StackOv 8 |
9 |

10 | 11 |

A SwiftUI Stackoverflow client

13 | 14 |

15 | 16 | AppStore 17 | 18 | platforms iOS | iPadOS | macOS 19 | 20 | License 21 | 22 | 23 | 24 | 25 |

26 | 27 | We are currently in the developing process of the next version of StackOv app. 28 | 29 | The demo version of StackOv is available in the [AppStore](https://apps.apple.com/gb/app/stackov/id1511838391) as just a small StackOverflow reader. 30 | 31 | ## **Building** 32 | 33 | - [SwiftGen](https://github.com/SwiftGen/SwiftGen) is the necessary dependency of this project. If you have `homebrew` installed then just call `brew install swiftgen`. 34 | - Open the project and build the `StackOv (iOS)` target. 🙃 35 | 36 | ## **Contributing** 37 | 38 | We have two types of issue templates. Use the `feature request`, if you are ready to clarify your idea or task; use the `bug report`, if you want to provide a bug. 39 | 40 | Don't hesitate to discuss with us your ideas and to ask questions, we have [the discussion room](https://github.com/surfstudio/StackOv/discussions/categories/ideas) for this. 😌 41 | If you are ready to start work on some issue then go ahead, please just find the [DEVPROCESS.md](https://github.com/surfstudio/StackOv/blob/develop/DEVPROCESS.md) for your consideration. 42 | -------------------------------------------------------------------------------- /Services/StackexchangeNetworkService/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: "StackexchangeNetworkService", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "StackexchangeNetworkService", targets: ["StackexchangeNetworkService"]) 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../DataTransferObjects") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "StackexchangeNetworkService", 19 | dependencies: [ 20 | "Common", 21 | "DataTransferObjects" 22 | ]), 23 | .testTarget(name: "StackexchangeNetworkServiceTests", dependencies: ["StackexchangeNetworkService"]) 24 | ] 25 | ) 26 | 27 | -------------------------------------------------------------------------------- /Services/StackexchangeNetworkService/README.md: -------------------------------------------------------------------------------- 1 | # StackexchangeNetworkService 2 | 3 | This package contains all stackexchage requests what we need. 4 | -------------------------------------------------------------------------------- /Services/StackexchangeNetworkService/Tests/StackexchangeNetworkServiceTests/StackexchangeNetworkServiceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import StackexchangeNetworkService 3 | 4 | final class StackexchangeNetworkServiceTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Shared/StackOvApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackOvApp.swift 3 | // StackOv 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import MainFlow 11 | #if canImport(Firebase) 12 | import Firebase 13 | #endif 14 | 15 | @main 16 | struct StackOvApp: App { 17 | 18 | // MARK: - View 19 | 20 | var body: some Scene { 21 | #if canImport(Firebase) 22 | FirebaseApp.configure() 23 | #endif 24 | 25 | return WindowGroup { 26 | GeometryReader { geometry in 27 | MainFlow() 28 | .environment(\.windowSize, geometry.size) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /StackOv.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /StackOv.xcodeproj/project.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // [NAME].swift 8 | // ___WORKSPACENAME___ ([NAME] module) 9 | // 10 | // Created by ___FULLUSERNAME___ 11 | // Copyright © ___YEAR___ Erik Basargin. All rights reserved. 12 | // 13 | 14 | 15 | -------------------------------------------------------------------------------- /StackOv.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StackOv.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // ___PROJECTNAME___ 9 | // 10 | // Created by ___FULLUSERNAME___ 11 | // Copyright © ___YEAR___ Erik Basargin. All rights reserved. 12 | // 13 | 14 | 15 | -------------------------------------------------------------------------------- /Stores/CommentsStore/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: "CommentsStore", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "CommentsStore", targets: ["CommentsStore"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../HTMLMarkdown") 15 | ], 16 | targets: [ 17 | .target(name: "CommentsStore", dependencies: ["Common", "HTMLMarkdown"]), 18 | .testTarget(name: "CommentsStoreTests", dependencies: ["CommentsStore"]), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Stores/CommentsStore/README.md: -------------------------------------------------------------------------------- 1 | # CommentsStore 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Stores/CommentsStore/Sources/CommentsStore/CommentsStore.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Combine 4 | import Common 5 | import HTMLMarkdown 6 | 7 | public final class CommentsStore: ObservableObject { 8 | 9 | // MARK: - Public properties 10 | 11 | @Published public private(set) var comments: [CommentModel] 12 | @Published public private(set) var numberOfFollowingItems: Int = 0 13 | 14 | // MARK: - Internal properties 15 | 16 | var models: [CommentModel] 17 | var activeModelsCount: Int = 0 18 | 19 | let step: Int = 5 20 | let unitsCash = Cache() 21 | 22 | // MARK: - Initialization 23 | 24 | public init(model: [CommentModel]) { 25 | self.models = model 26 | activeModelsCount = model.count > step ? step : model.count 27 | comments = Array(model[0.. Result { 34 | unit(forId: model.id, htmlText: model.body) 35 | } 36 | 37 | // MARK: - Internal methods 38 | 39 | func calculateNumberOfFollowingItems() { 40 | numberOfFollowingItems = models.count - activeModelsCount > step ? step : models.count - activeModelsCount 41 | } 42 | 43 | func unit(forId id: Int, htmlText: String) -> Result { 44 | if let unit = unitsCash[id] { return .success(unit) } 45 | do { 46 | let unit = try HTMLMarkdown.Unit.make(with: htmlText) 47 | unitsCash[id] = unit 48 | return .success(unit) 49 | } catch { 50 | return .failure(error) 51 | } 52 | } 53 | } 54 | 55 | // MARK: - Actions 56 | 57 | public extension CommentsStore { 58 | 59 | func showMore() { 60 | let newActiveModelsCount = activeModelsCount + numberOfFollowingItems 61 | comments += Array(models[activeModelsCount..) -> Void 16 | 17 | var data: CollectedData? { get } 18 | var isLoading: Bool { get } 19 | var hasMoreData: Bool { get } 20 | 21 | func fetch(receiveCompletion: @escaping ResultHandler) 22 | func reload(receiveCompletion: @escaping ResultHandler) 23 | func reset() 24 | } 25 | -------------------------------------------------------------------------------- /Stores/FavoriteStore/Sources/FavoriteStore/FavoriteStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteStore.swift 3 | // StackOv (FavoriteStore module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import StackexchangeNetworkService 12 | import Common 13 | 14 | public final class FavoriteStore: ObservableObject { 15 | 16 | // MARK: - Nested types 17 | 18 | public enum State { 19 | case unknown 20 | case emptyContent 21 | case content([QuestionModel]) 22 | case loading 23 | case error(Error) 24 | } 25 | 26 | // MARK: - Substores & Services 27 | 28 | let dataManager: FavoriteDataManagerProtocol 29 | 30 | // MARK: - Public properties 31 | 32 | @Published public private(set) var state: State = .unknown 33 | @Published public private(set) var loadMore: Bool = false 34 | 35 | // MARK: - Initialization and deinitialization 36 | 37 | public init(dataManager: FavoriteDataManagerProtocol) { 38 | self.dataManager = dataManager 39 | } 40 | } 41 | 42 | // MARK: - Actions 43 | 44 | public extension FavoriteStore { 45 | 46 | func loadNextQuestions() { 47 | if case .loading = state { return } 48 | loadMore = true 49 | dataManager.fetch { [unowned self] result in 50 | loadMore = false 51 | switch result { 52 | case let .success(models): 53 | if models.isEmpty { break } 54 | state = .content(models) 55 | case .failure: 56 | // need show error not by changing the state of screen 57 | break 58 | } 59 | } 60 | } 61 | 62 | func reloadQuestions() { 63 | loadMore = false 64 | state = .loading 65 | dataManager.reload { [unowned self] result in 66 | switch result { 67 | case let .success(models): 68 | state = models.isEmpty ? .emptyContent : .content(models) 69 | case .failure: 70 | switch state { 71 | case .emptyContent: 72 | // need show error state of screen 73 | break 74 | default: 75 | // need show error not by changing the state of screen 76 | break 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Stores/FavoriteStore/Tests/FavoriteStoreTests/FavoriteStoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FavoriteStore 3 | 4 | final class FavoriteStoreTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stores/FilterStore/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: "FilterStore", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "FilterStore", targets: ["FilterStore"]), 11 | ], 12 | targets: [ 13 | .target(name: "FilterStore", dependencies: []), 14 | .testTarget(name: "FilterStoreTests", dependencies: ["FilterStore"]) 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /Stores/FilterStore/README.md: -------------------------------------------------------------------------------- 1 | # FilterStore 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Stores/FilterStore/Sources/FilterStore/FilterStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterStore.swift 3 | // StackOv (FilterStore module) 4 | // 5 | // Created by Evgeny Novgorodov 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class FilterStore: ObservableObject { 12 | 13 | // MARK: - Nested types 14 | 15 | public typealias FilterState = Set 16 | 17 | public enum FilterOption: Int, CaseIterable, Identifiable { 18 | case noAnswers 19 | case noAcceptedAnswer 20 | case hasBounty 21 | } 22 | 23 | public enum SortOption: Int, CaseIterable, Identifiable { 24 | case newest 25 | case recentActivity 26 | case mostVotes 27 | case bountyEditingSoon 28 | } 29 | 30 | // MARK: - Public properties 31 | 32 | @Published public private(set) var filterState: FilterState = [] 33 | @Published public private(set) var sortState: SortOption = .newest 34 | 35 | // MARK: - Initialization and deinitialization 36 | 37 | public init() {} 38 | } 39 | 40 | // MARK: - Actions 41 | 42 | public extension FilterStore { 43 | 44 | func setFilterState(to filterState: FilterState) { 45 | self.filterState = filterState 46 | } 47 | 48 | func setSortState(to sortState: SortOption) { 49 | self.sortState = sortState 50 | } 51 | } 52 | 53 | // MARK: - Extensions 54 | 55 | public extension FilterStore.FilterOption { 56 | 57 | var id: Int { 58 | hashValue 59 | } 60 | } 61 | 62 | public extension FilterStore.SortOption { 63 | 64 | var id: Int { 65 | hashValue 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Stores/FilterStore/Tests/FilterStoreTests/FilterStoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FilterStore 3 | 4 | final class FilterStoreTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stores/GlobalBannerStore/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: "GlobalBannerStore", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "GlobalBannerStore", targets: ["GlobalBannerStore"]) 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "GlobalBannerStore", 18 | dependencies: ["Common"]), 19 | .testTarget( 20 | name: "GlobalBannerStoreTests", 21 | dependencies: ["GlobalBannerStore"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Stores/GlobalBannerStore/README.md: -------------------------------------------------------------------------------- 1 | # GlobalBannerStore 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Stores/GlobalBannerStore/Sources/GlobalBannerStore/GlobalBannerStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalBannerStore.swift 3 | // StackOv (GlobalBannerStore module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | import Combine 12 | 13 | public final class GlobalBannerStore: ObservableObject { 14 | 15 | // MARK: - Public properties 16 | 17 | @Published public private(set) var notifications: [GlobalBanner.Model] = [] 18 | 19 | // MARK: - Internal properties 20 | 21 | private(set) var globalBannerSubscription: AnyCancellable! 22 | 23 | // MARK: - Initialization and deinitialization 24 | 25 | public init() { 26 | self.globalBannerSubscription = GlobalBanner.publisher 27 | .receive(on: RunLoop.main) 28 | .sink { [weak self] model in 29 | self?.notifications.append(model) 30 | } 31 | } 32 | 33 | // MARK: - Actions 34 | 35 | public func hideAllBanners() { 36 | notifications = [] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Stores/GlobalBannerStore/Tests/GlobalBannerStoreTests/GlobalBannerStoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import GlobalBannerStore 3 | 4 | final class GlobalBannerStoreTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stores/PageStore/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Stores/PageStore/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: "PageStore", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "PageStore", targets: ["PageStore"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(path: "../Services/StackexchangeNetworkService"), 15 | .package(path: "../Palette"), 16 | .package(path: "../Stores/FilterStore") 17 | ], 18 | targets: [ 19 | .target(name: "PageStore", dependencies: ["Common", "StackexchangeNetworkService", "Palette", "FilterStore"]), 20 | .testTarget(name: "PageStoreTests", dependencies: ["PageStore"]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Stores/PageStore/README.md: -------------------------------------------------------------------------------- 1 | # PageStore 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Stores/PageStore/Sources/PageStore/PageDataManager/PageDataManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageDataManagerProtocol.swift 3 | // StackOv (PageStore module) 4 | // 5 | // Created by Erik Basargin 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | public protocol PageDataManagerProtocol: class { 13 | 14 | typealias CollectedData = [QuestionModel] 15 | typealias ResultHandler = (Result) -> Void 16 | 17 | var data: CollectedData? { get } 18 | var isLoading: Bool { get } 19 | var hasMoreData: Bool { get } 20 | 21 | func fetch(receiveCompletion: @escaping ResultHandler) 22 | func reload(receiveCompletion: @escaping ResultHandler) 23 | func reset() 24 | } 25 | -------------------------------------------------------------------------------- /Stores/PageStore/Tests/PageStoreTests/PageStoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PageStore 3 | 4 | final class PageStoreTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stores/SidebarStore/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: "SidebarStore", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "SidebarStore", targets: ["SidebarStore"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common") 14 | ], 15 | targets: [ 16 | .target(name: "SidebarStore", dependencies: ["Common"]), 17 | .testTarget(name: "SidebarStoreTests", dependencies: ["SidebarStore"]) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /Stores/SidebarStore/README.md: -------------------------------------------------------------------------------- 1 | # SidebarStore 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Stores/SidebarStore/Sources/SidebarStore/SidebarStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarStore.swift 3 | // StackOv (SidebarStore module) 4 | // 5 | // Created by Владислав Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | import enum SwiftUI.UserInterfaceSizeClass 13 | 14 | public final class SidebarStore: ObservableObject { 15 | 16 | // MARK: - Public properties 17 | 18 | @Published public private(set) var isShown: Bool = true 19 | @Published public private(set) var sidebarStyle: SidebarStyle = .regular 20 | 21 | // MARK: - Internal properties 22 | 23 | var canBeShown: Bool = true 24 | 25 | // MARK: - Initialization and deinitialization 26 | 27 | public init() {} 28 | } 29 | 30 | // MARK: - Actions 31 | 32 | public extension SidebarStore { 33 | 34 | func update(sidebarStyle: SidebarStyle) { 35 | self.sidebarStyle = sidebarStyle 36 | } 37 | 38 | func update(with sizeClass: UserInterfaceSizeClass?) { 39 | switch sizeClass { 40 | case .regular: 41 | isShown = canBeShown 42 | default: 43 | isShown = false 44 | } 45 | } 46 | 47 | func toggle() { 48 | canBeShown.toggle() 49 | isShown = canBeShown 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Stores/SidebarStore/Tests/SidebarStoreTests/SidebarStoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SidebarStore 3 | 4 | final class SidebarStoreTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stores/ThreadStore/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Stores/ThreadStore/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: "ThreadStore", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ThreadStore", targets: ["ThreadStore"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Services/StackexchangeNetworkService"), 14 | .package(path: "../Stores/PageStore"), 15 | .package(path: "../HTMLMarkdown") 16 | ], 17 | targets: [ 18 | .target(name: "ThreadStore", dependencies: ["StackexchangeNetworkService", "PageStore", "HTMLMarkdown"]), 19 | .testTarget(name: "ThreadStoreTests", dependencies: ["ThreadStore"]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Stores/ThreadStore/README.md: -------------------------------------------------------------------------------- 1 | # ThreadStore 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Stores/ThreadStore/Sources/ThreadStore/ThreadDataManager/ThreadDataManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadDataManagerProtocol.swift 3 | // StackOv (ThreadStore module) 4 | // 5 | // Created by Влад Климов 6 | // Copyright © 2021 Erik Basargin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | public protocol ThreadDataManagerProtocol { 13 | 14 | typealias CollectedData = [AnswerModel] 15 | typealias ResultHandler = (Result) -> Void 16 | 17 | var data: CollectedData? { get } 18 | var isLoading: Bool { get } 19 | var hasMoreData: Bool { get } 20 | 21 | func fetch(questionId: Int, receiveCompletion: @escaping ResultHandler) 22 | func reload(questionId: Int, receiveCompletion: @escaping ResultHandler) 23 | func reload(acceptedId: Int, receiveCompletion: @escaping ResultHandler) 24 | func reset() 25 | } 26 | -------------------------------------------------------------------------------- /Stores/ThreadStore/Tests/ThreadStoreTests/ThreadStoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ThreadStore 3 | 4 | final class ThreadStoreTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | throw XCTSkip(#function) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/StackOv/12af4af5a7fd968a2a9ef0bf5ed3ed1c4622295e/assets/logo.png --------------------------------------------------------------------------------