├── .sonarcloud.properties
├── codecov.yml
├── NovelReader
├── Resources
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── dot.imageset
│ │ │ ├── dot.png
│ │ │ └── Contents.json
│ │ ├── Pixel.imageset
│ │ │ ├── Pixel.png
│ │ │ └── Contents.json
│ │ ├── filter.imageset
│ │ │ ├── filter.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Icon-1024.png
│ │ │ ├── Icon-120.png
│ │ │ ├── Icon-121.png
│ │ │ ├── Icon-152.png
│ │ │ ├── Icon-167.png
│ │ │ ├── Icon-180.png
│ │ │ ├── Icon-20.png
│ │ │ ├── Icon-29.png
│ │ │ ├── Icon-40.png
│ │ │ ├── Icon-41.png
│ │ │ ├── Icon-42.png
│ │ │ ├── Icon-58.png
│ │ │ ├── Icon-59.png
│ │ │ ├── Icon-60.png
│ │ │ ├── Icon-76.png
│ │ │ ├── Icon-80.png
│ │ │ ├── Icon-81.png
│ │ │ ├── Icon-87.png
│ │ │ └── Contents.json
│ │ ├── fontSize.imageset
│ │ │ ├── fontSize.png
│ │ │ └── Contents.json
│ │ ├── navArrow.imageset
│ │ │ ├── leftArrow.png
│ │ │ ├── rightArrow.png
│ │ │ └── Contents.json
│ │ ├── close-circle.imageset
│ │ │ ├── close-circle.png
│ │ │ └── Contents.json
│ │ ├── TransparentPixel.imageset
│ │ │ ├── TransparentPixel.png
│ │ │ └── Contents.json
│ │ ├── NavigationBarDefault.imageset
│ │ │ ├── NavigationBarDefault@2x.png
│ │ │ ├── NavigationBarDefault@3x.png
│ │ │ └── Contents.json
│ │ └── LaunchImage.launchimage
│ │ │ └── Contents.json
│ ├── NovelServiceRules.plist
│ ├── Bindings
│ │ ├── ModelBindings
│ │ │ └── NRDataModel.mod.json
│ │ └── ServiceBindings
│ │ │ └── NRService.svc.json
│ ├── plist
│ │ └── Info.plist
│ └── Themes
│ │ └── Themes.json
├── NovelReader-Bridging-Header.h
├── Parser
│ ├── Rules
│ │ └── NRRules_defaults.swift
│ ├── ModelObjects
│ │ ├── SearchNovelModel.swift
│ │ ├── NovelListModel.swift
│ │ ├── NovelChapterModel.swift
│ │ ├── SearchFilterModel.swift
│ │ └── NovelModel.swift
│ ├── ServiceModel
│ │ ├── ServiceNovelChapters.swift
│ │ ├── ServiceNovelChapter.swift
│ │ ├── ServiceNovelList.swift
│ │ ├── ServiceSearchNovel.swift
│ │ ├── ServiceRecentUpdatesList.swift
│ │ └── ServiceSearchFilter.swift
│ └── NovelServiceProvider.swift
├── Utility+Components
│ ├── TabBarViewController.swift
│ ├── View+Utility.swift
│ ├── ViewController+Extension.swift
│ ├── CollectionViewController+Utility.swift
│ ├── Views
│ │ ├── SectionHeaderView.swift
│ │ └── SectionHeaderView.xib
│ ├── Constants.swift
│ ├── ConfigureNovelCellProtocol.swift
│ └── ScollableSegmentControl.swift
├── NovelReader.entitlements
├── AppDelegate.swift
├── Modules
│ ├── Reader
│ │ ├── ChapterList
│ │ │ ├── Views
│ │ │ │ ├── NovelChapterViewCell.swift
│ │ │ │ ├── NovelDetailsView.swift
│ │ │ │ ├── NovelDescriptionView.swift
│ │ │ │ ├── NovelChapterViewCell.xib
│ │ │ │ ├── NovelDescriptionView.xib
│ │ │ │ └── NovelDetailsView.xib
│ │ │ └── ChapterListViewController.swift
│ │ ├── Dashboard
│ │ │ ├── RecentUpdate
│ │ │ │ ├── RecentNovelCollectionViewCell.swift
│ │ │ │ └── RecentNovelCollectionViewCell.xib
│ │ │ ├── TopView
│ │ │ │ └── NovelCollectionViewCell.swift
│ │ │ ├── NovelCollectionViewModel.swift
│ │ │ └── NovelCollectionViewController.swift
│ │ └── ReaderView
│ │ │ └── ReaderViewController.swift
│ ├── Views
│ │ ├── SegmentCollectionHeaderView.swift
│ │ ├── SearchBarHeaderView.swift
│ │ ├── SelectionCollectionViewCell.swift
│ │ └── SelectionCollectionViewCell.xib
│ ├── Search
│ │ ├── SearchCollectionViewModel.swift
│ │ ├── SearchCollectionViewController.swift
│ │ ├── SearchFilterViewController.swift
│ │ └── SearchFilterViewModel.swift
│ └── FontPicker
│ │ ├── FontPickerViewController.swift
│ │ └── FontPickerView.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Manager
│ ├── AppManager.swift
│ ├── NRGoogleAuth.swift
│ └── DataSourceManager.swift
└── Main.storyboard
├── NovelReader.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── FTNovelReaderMockBundle.xcscheme
│ └── NovelReader.xcscheme
├── NovelReader.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── fastlane
└── Fastfile
├── NovelReaderTests
├── AppManagerTests.swift
└── Info.plist
├── .travis.yml
├── FTNovelReaderMockBundle
├── Info.plist
├── fetchChapter.json
├── searchFilter.json
└── fetchRecentUpdatesList.json
├── .gitignore
├── README.md
├── .swiftlint.yml
└── LICENSE
/.sonarcloud.properties:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | cat codecov.yml | curl --data-binary @- https://codecov.io/validate
2 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/dot.imageset/dot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/dot.imageset/dot.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/Pixel.imageset/Pixel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/Pixel.imageset/Pixel.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/filter.imageset/filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/filter.imageset/filter.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-121.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-121.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-152.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-180.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-41.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-42.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-58.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-59.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-59.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-80.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-81.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-81.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Icon-87.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/fontSize.imageset/fontSize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/fontSize.imageset/fontSize.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/navArrow.imageset/leftArrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/navArrow.imageset/leftArrow.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/navArrow.imageset/rightArrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/navArrow.imageset/rightArrow.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/close-circle.imageset/close-circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/close-circle.imageset/close-circle.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/TransparentPixel.imageset/TransparentPixel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/TransparentPixel.imageset/TransparentPixel.png
--------------------------------------------------------------------------------
/NovelReader.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/NovelReader.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/NovelReader/NovelReader-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #pragma mark - Mobile Core
6 |
7 | @import UIKit;
8 | @import Foundation;
9 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/NavigationBarDefault.imageset/NavigationBarDefault@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/NavigationBarDefault.imageset/NavigationBarDefault@2x.png
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/NavigationBarDefault.imageset/NavigationBarDefault@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ppraveentr/NovelReader/HEAD/NovelReader/Resources/Assets.xcassets/NavigationBarDefault.imageset/NavigationBarDefault@3x.png
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 |
2 | #update_fastlane
3 |
4 | default_platform(:ios)
5 |
6 | platform :ios do
7 | desc "Run test cases"
8 | lane :tests do
9 | run_tests(workspace: "NovelReader.xcworkspace",
10 | devices: ["iPhone X"],
11 | scheme: "NovelReader")
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/NovelReader/Parser/Rules/NRRules_defaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NRServiceProvider.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class NRRulesDefaults {
12 | }
13 |
--------------------------------------------------------------------------------
/NovelReader.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/NovelReader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/TabBarViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarViewController.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class TabBarViewController: UITabBarController {
12 | }
13 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ModelObjects/SearchNovelModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchNovelModel.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 13/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class SearchNovelModel: ServiceModel {
12 | var keyword: String?
13 | }
14 |
--------------------------------------------------------------------------------
/NovelReader/NovelReader.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/NovelReader/Resources/NovelServiceRules.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | default
6 |
7 | NRRulesDefaults
8 |
9 | fetchNovelList
10 |
11 | RulesNovelList
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/View+Utility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+Utility.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 23/12/20.
6 | // Copyright © 2020 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension IndexPath {
12 | public static func == (lhs: IndexPath, rhs: IndexPath) -> Bool {
13 | lhs.row == rhs.row && lhs.section == rhs.section
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/ViewController+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension UIViewController {
12 | func hideBottomBar(_ isHidden: Bool) {
13 | self.tabBarController?.tabBar.isHidden = isHidden
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/dot.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "dot.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/Pixel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Pixel.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/filter.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "filter.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/close-circle.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "close-circle.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/TransparentPixel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "TransparentPixel.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/NovelReaderTests/AppManagerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppManagerTests.swift
3 | // NovelReaderTests
4 | //
5 | // Created by Praveen Prabhakar on 25/12/20.
6 | // Copyright © 2020 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | @testable import NovelReader
10 | import XCTest
11 |
12 | class AppManagerTests: XCTestCase {
13 | func testManager() throws {
14 | // let
15 | let manager = AppManager.sharedInstance
16 | XCTAssertNotNil(manager, "Should not be nil")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/NavigationBarDefault.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "NavigationBarDefault@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "NavigationBarDefault@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/fontSize.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "fontSize.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | },
21 | "properties" : {
22 | "template-rendering-intent" : "template"
23 | }
24 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 | sudo: required
3 | language: swift
4 | xcode_workspace: NovelReader.xcworkspace
5 | osx_image: xcode10.2
6 | xcode_destination: platform=iOS Simulator,OS=12.0,name=iPhone X
7 | xcode_scheme: NovelReader
8 | before_install:
9 | - xcpretty --version
10 | - xcodebuild -version
11 | - xcodebuild -showsdks
12 | script:
13 | - xcodebuild clean build -workspace "NovelReader.xcworkspace" -scheme "NovelReader" -configuration Release -sdk iphonesimulator | xcpretty -c
14 | after_success:
15 | - bash <(curl -s https://codecov.io/bash)
16 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ServiceModel/ServiceNovelChapters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceNovelChapters.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class ServiceNovelChapters: ServiceClient {
12 | var serviceName = "fetchNovelChapters"
13 | var inputStack: ServiceModel?
14 | var responseStackType: Any? = NovelModel.self
15 |
16 | init(inputStack: ServiceModel?) {
17 | self.inputStack = inputStack
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ServiceModel/ServiceNovelChapter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceNovelChapter.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class ServiceNovelChapter: ServiceClient {
12 |
13 | var serviceName = "fetchChapter"
14 | var inputStack: ServiceModel?
15 | var responseStackType: Any? = NovelChapterModel.self
16 |
17 | init(inputStack: ServiceModel?) {
18 | self.inputStack = inputStack
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ServiceModel/ServiceNovelList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceNovelList.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class ServiceNovelList: ServiceClient {
12 |
13 | var serviceName = "fetchNovelList"
14 | var inputStack: NovelListModel?
15 | var responseStackType: Any? = NovelListModel.self
16 |
17 | init(inputStack: ServiceModel?) {
18 | self.inputStack = inputStack as? NovelListModel
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ServiceModel/ServiceSearchNovel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceSearchNovel.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 05/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class ServiceSearchNovel: ServiceClient {
12 | var inputStack: SearchNovelModel?
13 | var serviceName = "searchNovel"
14 | var responseStackType: Any? = NovelListModel.self
15 |
16 | init(inputStack: ServiceModel?) {
17 | self.inputStack = inputStack as? SearchNovelModel
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ModelObjects/NovelListModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelListModel.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class NovelListModel: ServiceModel {
12 | // TODO: Yet to setup model creator for Static data
13 | var state: String?
14 | var novelList: [NovelModel]?
15 | var page: String?
16 | var totalItems: String?
17 | var type: String?
18 | var category: String?
19 | var response: [NovelModel]?
20 | }
21 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ServiceModel/ServiceRecentUpdatesList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceRecentUpdatesList.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class ServiceRecentUpdatesList: ServiceClient {
12 | var inputStack: ServiceModel?
13 | var serviceName = "fetchRecentUpdatesList"
14 | var responseStackType: Any? = NovelListModel.self
15 |
16 | init(inputStack: ServiceModel?) {
17 | self.inputStack = inputStack
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ServiceModel/ServiceSearchFilter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceSearchFilter.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 13/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class ServiceSearchFilter: ServiceClient {
12 | var inputStack: SearchNovelModel?
13 | var serviceName = "searchFilter"
14 | var responseStackType: Any? = SearchFilterModel.self
15 |
16 | init(inputStack: ServiceModel?) {
17 | self.inputStack = inputStack as? SearchNovelModel
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/NovelReader/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | var window: UIWindow?
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions
16 | launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | AppManager.setupApplication()
18 | return true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ModelObjects/NovelChapterModel.swift:
--------------------------------------------------------------------------------
1 | import NetworkLayer
2 |
3 | final class NovelChapterModel: ServiceModel {
4 |
5 | // Request Model
6 | var identifier: String?
7 | var shortTitle: String?
8 | var title: String?
9 | var content: String?
10 | var releaseDate: String?
11 |
12 | // Response Model
13 | var response: NovelChapterModel?
14 |
15 | /* Coding Keys */
16 | enum CodingKeys: String, CodingKey {
17 | case identifier
18 | case shortTitle
19 | case title = "name"
20 | case content
21 | case releaseDate
22 | case response
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ModelObjects/SearchFilterModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchFilterModel.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 13/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import NetworkLayer
10 |
11 | final class FilterModel: ServiceModel {
12 | var data: String?
13 | var type: String?
14 | }
15 |
16 | final class SearchFilterModel: ServiceModel {
17 | var keyword: String?
18 |
19 | var novelType: [FilterModel]?
20 | var language: [FilterModel]?
21 | var genres: [FilterModel]?
22 | var completed: [FilterModel]?
23 |
24 | var response: SearchFilterModel?
25 | }
26 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ChapterList/Views/NovelChapterViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelChapterViewCell.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class NovelChapterViewCell: UITableViewCell {
12 | @IBOutlet
13 | private var chapterDate: UILabel?
14 | @IBOutlet
15 | private var titleLabel: UILabel?
16 | }
17 |
18 | extension NovelChapterViewCell: ConfigureNovelCellProtocol {
19 | func configureContent(content: AnyObject, indexPath: IndexPath?) {
20 | guard let novel = content as? NovelChapterModel else { return }
21 | self.titleLabel?.text = novel.shortTitle ?? novel.title
22 | self.chapterDate?.text = novel.releaseDate
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/NovelReaderTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/FTNovelReaderMockBundle/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 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | NSHumanReadableCopyright
22 | Copyright © 2018 Praveen Prabhakar. All rights reserved.
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/navArrow.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "language-direction" : "left-to-right"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "1x",
11 | "language-direction" : "right-to-left"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "rightArrow.png",
16 | "language-direction" : "left-to-right",
17 | "scale" : "2x"
18 | },
19 | {
20 | "idiom" : "universal",
21 | "filename" : "leftArrow.png",
22 | "language-direction" : "right-to-left",
23 | "scale" : "2x"
24 | },
25 | {
26 | "idiom" : "universal",
27 | "scale" : "3x",
28 | "language-direction" : "left-to-right"
29 | },
30 | {
31 | "idiom" : "universal",
32 | "scale" : "3x",
33 | "language-direction" : "right-to-left"
34 | }
35 | ],
36 | "info" : {
37 | "version" : 1,
38 | "author" : "xcode"
39 | },
40 | "properties" : {
41 | "template-rendering-intent" : "template"
42 | }
43 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Build generated
2 | build/
3 | DerivedData/
4 |
5 | ## Various settings
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 |
16 | ## Other
17 | *.moved-aside
18 | *.xccheckout
19 | *.xcscmblueprint
20 |
21 | ## Obj-C/Swift specific
22 | *.hmap
23 | *.ipa
24 | *.dSYM.zip
25 | *.dSYM
26 |
27 | ## Playgrounds
28 | timeline.xctimeline
29 | playground.xcworkspace
30 |
31 | # Swift Package Manager
32 | #
33 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
34 | Packages/
35 | Package.pins
36 | .build/
37 |
38 | # fastlane
39 | #
40 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
41 | # screenshots whenever they are needed.
42 | # For more information about the recommended setup visit:
43 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
44 |
45 | fastlane/report.xml
46 | fastlane/Preview.html
47 | fastlane/screenshots
48 | fastlane/test_output
49 |
50 | Frameworks/
51 |
52 | .DS_Store
53 |
54 | *.xcuserstate
55 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/Dashboard/RecentUpdate/RecentNovelCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecentNovelCollectionViewCell.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 28/04/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 |
11 | class RecentNovelCollectionViewCell: UICollectionViewCell {
12 | @IBOutlet
13 | private weak var novelTitle: UILabel?
14 | @IBOutlet
15 | private weak var lastUpdateTitleLabel: UILabel?
16 | @IBOutlet
17 | private weak var lastUpdateTimeLabel: UILabel?
18 |
19 | weak var novelItem: NovelModel?
20 | }
21 |
22 | extension RecentNovelCollectionViewCell: ConfigureNovelCellProtocol {
23 | func configureContent(content: AnyObject, indexPath: IndexPath?) {
24 | guard let novel = content as? NovelModel else { return }
25 | self.theme = ThemeStyle.defaultStyle.rawValue
26 | novelItem = novel
27 | novelTitle?.text = novel.title
28 | lastUpdateTitleLabel?.text = "Last Update:"
29 | lastUpdateTimeLabel?.text = novel.lastUpdated
30 | }
31 |
32 | override func layoutSubviews() {
33 | self.updateShadowPathIfNeeded()
34 | super.layoutSubviews()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/NovelReader/Parser/ModelObjects/NovelModel.swift:
--------------------------------------------------------------------------------
1 | import NetworkLayer
2 |
3 | final class NovelModel: ServiceModel {
4 | var identifier: String?
5 | var lastChapter: String?
6 | var chapterList: [NovelChapterModel]?
7 | var searchString: String?
8 | var rating: String?
9 | var author: String?
10 | var lastUpdated: String?
11 | var imageURL: String?
12 | var genres: [String]?
13 | var contentDescription: String?
14 | var artist: String?
15 | var status: String?
16 | var views: String?
17 | var title: String?
18 | var novelType: String?
19 |
20 | var response: NovelModel?
21 |
22 | /* Coding Keys */
23 | enum CodingKeys: String, CodingKey {
24 | case identifier
25 | case lastChapter = "lastchapter"
26 | case chapterList = "chapters"
27 | case searchString = "nameunsigned"
28 | case rating
29 | case author
30 | case lastUpdated = "lastUpdate"
31 | case imageURL = "image"
32 | case genres
33 | case contentDescription = "summary"
34 | case artist
35 | case status
36 | case views
37 | case title = "name"
38 | case novelType = "type"
39 | case response
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ChapterList/Views/NovelDetailsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelDetailsView.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 27/06/21.
6 | // Copyright © 2021 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class NovelDetailsView: UITableViewHeaderFooterView {
12 |
13 | @IBOutlet
14 | private var titleLabel: UILabel?
15 | @IBOutlet
16 | private var authorLabel: UILabel?
17 | @IBOutlet
18 | private var contentImageView: UIImageView?
19 | @IBOutlet
20 | private var statusLabel: UILabel?
21 | @IBOutlet
22 | private var viewsLabel: UILabel?
23 | @IBOutlet
24 | private var descriptionLabel: UILabel?
25 |
26 | func configureContent(content: AnyObject) {
27 | guard let novel = content as? NovelModel else { return }
28 | // if let url = novel.imageURL {
29 | // self.contentImageView?.downloadedFrom(url, defaultImage: nil)
30 | // }
31 | self.titleLabel?.text = novel.title
32 | self.authorLabel?.text = novel.lastUpdated
33 | self.statusLabel?.text = novel.status
34 | self.viewsLabel?.text = novel.views
35 | self.descriptionLabel?.text = novel.contentDescription
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Views/SegmentCollectionHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SegmentCollectionHeaderView.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 13/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 |
11 | final class SegmentCollectionHeaderView: UICollectionReusableView {
12 | lazy var segmentedControl: ScrollableSegmentControl = {
13 | let view = ScrollableSegmentControl()
14 | self.pin(view: view, edgeOffsets: UIEdgeInsets(20, 10, -20, 0))
15 | return view
16 | }()
17 |
18 | override init(frame: CGRect) {
19 | super.init(frame: frame)
20 | self.configureUI()
21 | }
22 |
23 | required init?(coder aDecoder: NSCoder) {
24 | super.init(coder: aDecoder)
25 | self.configureUI()
26 | }
27 |
28 | func configureUI() {
29 | self.theme = ThemeStyle.defaultStyle.rawValue
30 | setupSegmentView()
31 | }
32 |
33 | func setupSegmentView(_ items: [String] = [ Constants.recentUpdateString, Constants.topViews ]) {
34 | for (index, obj) in items.enumerated() {
35 | segmentedControl.insertSegment(withTitle: obj, image: nil, theme: "segment", at: index)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Bindings/ModelBindings/NRDataModel.mod.json:
--------------------------------------------------------------------------------
1 | {
2 | "NRNovels": {
3 | "type": "type",
4 | "category": "category",
5 | "state": "state",
6 | "page": "page",
7 | "totalItems": "totalItems",
8 | "novelList": {
9 | "bindKey": "novelList",
10 | "arrayOf": "NRNovel"
11 | }
12 | },
13 | "NRNovel": {
14 | "identifier": "identifier",
15 | "title": "name",
16 | "searchString": "nameunsigned",
17 | "author": "author",
18 | "artist": "artist",
19 | "status": "status",
20 | "novelType": "type",
21 | "imageURL": "image",
22 | "rating": "rating",
23 | "lastUpdated": "updatetime",
24 | "views": "views",
25 | "contentDescription": "summary",
26 | "lastChapter": "lastchapter",
27 | "genres": {
28 | "bindKey": "genres",
29 | "arrayOf": "String"
30 | },
31 | "chapterList": {
32 | "bindKey": "chapters",
33 | "arrayOf": "NRNovelChapter"
34 | }
35 | },
36 | "NRNovelChapter": {
37 | "content": "content",
38 | "title": "name",
39 | "identifier": "identifier",
40 | "shortTitle": "shortTitle",
41 | "releaseDate": "releaseDate"
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Search/SearchCollectionViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchCollectionViewModel.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 13/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 | import NetworkLayer
11 |
12 | typealias NRSearchCollectionLifeCycleDelegate = (ViewControllerProtocol & SearchCollectionViewModelProtocal)
13 |
14 | protocol SearchCollectionViewModelProtocal {
15 | func refreshCollectionView()
16 | }
17 |
18 | class SearchCollectionViewModel {
19 |
20 | weak var lifeDelegate: NRSearchCollectionLifeCycleDelegate?
21 | var modelStack: ServiceModel?
22 |
23 | init(delegate: NRSearchCollectionLifeCycleDelegate, modelStack: ServiceModel? = nil) {
24 | self.lifeDelegate = delegate
25 | self.modelStack = modelStack
26 | }
27 |
28 | // Update collectionView when contentList changes
29 | var currentNovelList: [NovelModel]? = [] {
30 | didSet {
31 | lifeDelegate?.refreshCollectionView()
32 | }
33 | }
34 |
35 | // get-Novels from backend
36 | func searchNovel(keywoard: String) {
37 | guard keywoard.isEmpty else { return }
38 |
39 | NovelServiceProvider.searchNovel(keyword: keywoard) { novelList in
40 | self.currentNovelList = novelList
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Views/SearchBarHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchBarHeaderView.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 13/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 | import CoreComponents
11 |
12 | class SearchBarHeaderView: FTView {
13 | var searchBar: UISearchBar?
14 | weak var searchBarDelegate: UISearchBarDelegate?
15 |
16 | override init(frame: CGRect) {
17 | super.init(frame: frame)
18 | self.setupSearchBar()
19 | }
20 |
21 | required init?(coder aDecoder: NSCoder) {
22 | super.init(coder: aDecoder)
23 | self.setupSearchBar()
24 | }
25 |
26 | convenience init(delegate: UISearchBarDelegate) {
27 | self.init()
28 | searchBarDelegate = delegate
29 | searchBar?.delegate = delegate
30 | }
31 |
32 | func setupSearchBar() {
33 | self.theme = ThemeStyle.defaultStyle.rawValue
34 | searchBar = UISearchBar(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 44)))
35 | searchBar?.theme = ThemeStyle.defaultStyle.rawValue
36 | searchBar?.delegate = searchBarDelegate
37 |
38 | if let searchBar = searchBar {
39 | searchBar.placeholder = "Search"
40 | searchBar.autoresizingMask = .flexibleHeight
41 | self.pin(view: searchBar, edgeOffsets: .zero)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ChapterList/Views/NovelDescriptionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelDescriptionView.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 25/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class NovelDescriptionView: UITableViewHeaderFooterView {
12 |
13 | @IBOutlet
14 | private var titleLabel: UILabel?
15 | @IBOutlet
16 | private var descriptionLabel: UILabel?
17 | @IBOutlet
18 | private var contentImageView: UIImageView?
19 |
20 | @IBOutlet
21 | private var chapterLabel: UILabel?
22 | @IBOutlet
23 | private var lastUpdateLabel: UILabel?
24 | @IBOutlet
25 | private var viewsButton: UIButton?
26 |
27 | func configureContent(content: AnyObject) {
28 | guard let novel = content as? NovelModel else { return }
29 |
30 | if let urlString = novel.imageURL, let url = URL(string: urlString) {
31 | self.contentImageView?.downloadedFrom(url, defaultImage: nil)
32 | }
33 | self.titleLabel?.text = novel.title
34 | self.descriptionLabel?.text = novel.contentDescription
35 |
36 | self.chapterLabel?.text = novel.lastChapter
37 | self.lastUpdateLabel?.text = novel.lastUpdated
38 | self.viewsButton?.setTitle(novel.views, for: .normal)
39 |
40 | setNeedsLayout()
41 | layoutIfNeeded()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Novel Reader
2 |
3 | Novel reader is iOS native Mobile application, based out Swift, is a working sample for [`MobileCore`](https://github.com/ppraveentr/MobileCore).
4 |
5 | [](https://developer.apple.com/iphone/index.action)
6 | [](https://developer.apple.com/swift)
7 | [](http://mit-license.org)
8 | [](https://travis-ci.org/ppraveentr/NovelReader)
9 |
10 | [](https://app.codacy.com/app/ppraveentr/NovelReader?utm_source=github.com&utm_medium=referral&utm_content=ppraveentr/NovelReader&utm_campaign=Badge_Grade_Dashboard)
11 |
12 | ## Screen Shots
13 |
16 |
17 | ## Disclaimer
18 | The novel and translations are not mine and are just crawled from other aggregators. I will get the data from actual translation sites after I'm done with some pending tasks in order to give them proper credits.
19 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Bindings/ServiceBindings/NRService.svc.json:
--------------------------------------------------------------------------------
1 | {
2 | "NovelBase": {
3 | "timeout": "60",
4 | "x-api-key": "bdb6d57f4df64420aa0b220cd924f85e"
5 | },
6 | "fetchRecentUpdatesList": {
7 | "super": "NovelBase",
8 | "type": "GET",
9 | "path": "novel/recentupdates",
10 | "responseType": {
11 | "bindAs": "NRNovels"
12 | }
13 | },
14 | "fetchNovelList": {
15 | "super": "NovelBase",
16 | "type": "GET",
17 | "path": "novel/top-list",
18 | "requestQuery": {
19 | "type": "type",
20 | "category": "category",
21 | "state": "state",
22 | "page": "page"
23 | },
24 | "responseType": {
25 | "bindAs": "NRNovels"
26 | }
27 | },
28 | "fetchNovelChapters": {
29 | "super": "NovelBase",
30 | "type": "GET",
31 | "path": "novel/chapters-list",
32 | "responseType": {
33 | "bindAs": "NRNovel"
34 | }
35 | },
36 | "fetchChapter": {
37 | "super": "NovelBase",
38 | "type": "GET",
39 | "path": "novel/chapter",
40 | "responseType": {
41 | "bindAs": "NRNovelChapter"
42 | }
43 | },
44 | "searchNovel": {
45 | "super": "NovelBase",
46 | "type": "POST",
47 | "path": "novel/search",
48 | "responseType": {
49 | "bindAs": "NRNovels"
50 | }
51 | },
52 | "searchFilter": {
53 | "super": "NovelBase",
54 | "type": "GET",
55 | "path": "novel/search-filter",
56 | "responseType": {
57 | "bindAs": "NRSearchFilterModel"
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/CollectionViewController+Utility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewController+Utility.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 30/09/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreUtility
10 |
11 | // default collectionView SectionInset
12 | private var defaultSectionInset: UIEdgeInsets {
13 | UIEdgeInsets(top: 25, left: 20, bottom: 10, right: 20)
14 | }
15 |
16 | extension UIViewController {
17 | // MARK: SetUp UICollectionView
18 | public func sectionInset() -> UIEdgeInsets {
19 | defaultSectionInset
20 | }
21 |
22 | @objc
23 | var flowLayout: UICollectionViewLayout {
24 | let layout = UICollectionViewFlowLayout()
25 | layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
26 | layout.sectionInset = sectionInset()
27 | layout.headerReferenceSize = CGSize(width: 0, height: 50)
28 | layout.sectionHeadersPinToVisibleBounds = true
29 | layout.minimumInteritemSpacing = 20
30 | layout.minimumLineSpacing = 20
31 | return layout
32 | }
33 | }
34 |
35 | extension UICollectionViewCell {
36 | override open
37 | func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
38 | var newFrame = layoutAttributes.frame
39 | let requiredWidth = ceil(screenWidth) - (defaultSectionInset.left + defaultSectionInset.right)
40 | newFrame.size = systemLayoutSizeFitting(CGSize(width: requiredWidth, height: 200),
41 | withHorizontalFittingPriority: .required,
42 | verticalFittingPriority: .fittingSizeLevel)
43 | layoutAttributes.frame = newFrame
44 | return layoutAttributes
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/Dashboard/TopView/NovelCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelCollectionViewCell.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 |
11 | final class NovelCollectionViewCell: UICollectionViewCell {
12 | @IBOutlet
13 | private var titleLabel: UILabel?
14 | @IBOutlet
15 | private var contentImageView: UIImageView?
16 |
17 | @IBOutlet
18 | private var chapterLabel: UILabel?
19 | @IBOutlet
20 | private var genresLabel: UILabel?
21 | @IBOutlet
22 | private var typeLabel: UILabel?
23 | @IBOutlet
24 | private var statusButton: UIButton?
25 | @IBInspectable
26 | private var defaultImage: UIImage?
27 |
28 | override func prepareForReuse() {
29 | super.prepareForReuse()
30 | // Reset image
31 | self.contentImageView?.image = nil
32 | }
33 |
34 | override func layoutSubviews() {
35 | self.updateShadowPathIfNeeded()
36 | super.layoutSubviews()
37 | }
38 | }
39 |
40 | extension NovelCollectionViewCell: ConfigureNovelCellProtocol {
41 | func configureContent(content: AnyObject, indexPath: IndexPath? = nil) {
42 | guard let novel = content as? NovelModel else { return }
43 | self.theme = ThemeStyle.defaultStyle.rawValue
44 | if let urlString = novel.imageURL, let url = URL(string: urlString) {
45 | self.contentImageView?.downloadedFrom(url, defaultImage: defaultImage)
46 | }
47 | self.titleLabel?.text = novel.title
48 | self.chapterLabel?.text = novel.author
49 | if var genres = novel.genres?.joined(separator: ", ") {
50 | genres = "Genres: " + genres
51 | self.genresLabel?.text = genres
52 | }
53 | self.typeLabel?.text = novel.novelType
54 | self.statusButton?.setTitle(novel.status ?? "", for: .normal)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/NovelReader/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/NovelReader/Modules/FontPicker/FontPickerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontPickerViewController.swift
3 | // MobileCoreUI
4 | //
5 | // Created by Praveen Prabhakar on 26/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class FontPickerViewController: UIViewController {
12 |
13 | lazy var pickerView: FontPickerView? = try? FontPickerView.loadNibFromBundle()
14 |
15 | open var fontPickerViewDelegate: FontPickerViewProtocol? {
16 | didSet {
17 | pickerView?.pickerDelegate = fontPickerViewDelegate
18 | }
19 | }
20 |
21 | open var fontPickerModel: FontPickerModel? {
22 | get { pickerView?.fontPickerModel }
23 | set {
24 | if let model = newValue {
25 | pickerView?.fontPickerModel = model
26 | }
27 | }
28 | }
29 |
30 | override open func loadView() {
31 | super.loadView()
32 | // Setup MobileCore
33 | setupCoreView()
34 | if let pickerView = pickerView {
35 | self.mainView?.pin(view: pickerView)
36 | }
37 | }
38 |
39 | @discardableResult
40 | open func setUpPopoverPresentation(from sender: UIView?, delegate: UIPopoverPresentationControllerDelegate? = nil, contentSize: CGSize = CGSize(width: 250, height: 300)) -> UIPopoverPresentationController? {
41 | self.modalPresentationStyle = .popover
42 | self.preferredContentSize = contentSize
43 |
44 | let ppc = self.popoverPresentationController
45 | ppc?.permittedArrowDirections = .any
46 | ppc?.delegate = delegate ?? self
47 |
48 | if let sender = sender {
49 | ppc?.sourceView = sender
50 | ppc?.sourceRect = sender.bounds
51 | }
52 |
53 | return ppc
54 | }
55 | }
56 |
57 | extension FontPickerViewController: UIPopoverPresentationControllerDelegate {
58 | public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
59 | .none
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Views/SelectionCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectionCollectionViewCell.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 23/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class SelectionCollectionViewCell: UICollectionViewCell {
12 |
13 | @IBOutlet
14 | private var titleLabel: UILabel?
15 | @IBOutlet
16 | private var checkMarkImage: UIImageView?
17 |
18 | override var isSelected: Bool {
19 | didSet {
20 | self.titleLabel?.isEnabled = isSelected
21 | checkMarkImage?.isHidden = !isSelected
22 |
23 | if isSelected {
24 | self.layer.borderColor = AppTheme.navigationBarColor.cgColor
25 | }
26 | else {
27 | self.layer.borderColor = UIColor.gray.cgColor
28 | }
29 | }
30 | }
31 |
32 | func addBorder() {
33 | self.layer.cornerRadius = 8
34 |
35 | // border
36 | self.layer.borderColor = AppTheme.navigationBarColor.cgColor
37 | self.layer.borderWidth = 0.5
38 |
39 | // drop shadow
40 | self.layer.shadowColor = UIColor.black.cgColor
41 | self.layer.shadowOpacity = 1.0
42 | self.layer.shadowRadius = 2.0
43 | self.layer.shadowOffset = CGSize(width: 3, height: 3)
44 | }
45 |
46 | override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes)
47 | -> UICollectionViewLayoutAttributes {
48 | setNeedsLayout()
49 | layoutIfNeeded()
50 |
51 | let size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
52 | var newFrame = layoutAttributes.frame
53 | newFrame.size.width = ceil(size.width)
54 | newFrame.size.height = ceil(size.height)
55 | layoutAttributes.frame = newFrame
56 | return layoutAttributes
57 | }
58 | }
59 |
60 | extension SelectionCollectionViewCell: ConfigureNovelCellProtocol {
61 | func configureContent(content: AnyObject, indexPath: IndexPath?) {
62 | if let searchFilter = content as? FilterModel {
63 | self.titleLabel?.text = searchFilter.type
64 | }
65 | setNeedsLayout()
66 | layoutIfNeeded()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/Views/SectionHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SectionHeaderView.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 24/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreUtility
10 |
11 | class SectionHeaderView: UICollectionReusableView {
12 |
13 | var titleString: String?
14 | var tapAction: ActionBlock?
15 |
16 | // Left Button
17 | @IBOutlet
18 | fileprivate var buttonLeft: UIButton?
19 | @IBOutlet
20 | fileprivate var titleLabel: UILabel?
21 | // Right Button
22 | @IBOutlet
23 | fileprivate var buttonRight: UIButton?
24 | fileprivate var contentXIBView: UIView?
25 |
26 | override init(frame: CGRect) {
27 | super.init(frame: frame)
28 | setupView()
29 | }
30 |
31 | required init?(coder aDecoder: NSCoder) {
32 | super.init(coder: aDecoder)
33 | }
34 |
35 | func setupView() {
36 | self.backgroundColor = .clear
37 | contentXIBView = self.xibSetup(className: SectionHeaderView.self)
38 | contentXIBView?.theme = self.theme
39 | }
40 |
41 | func setSectionHeader(title: String? = nil, image: UIImage? = nil) {
42 | self.titleString = title
43 |
44 | if let image = image {
45 | self.buttonLeft?.isHidden = false
46 | self.buttonLeft?.setImage(image, for: .normal)
47 | }
48 | else {
49 | self.buttonLeft?.removeSubviews()
50 | }
51 |
52 | // Left Button
53 | titleLabel?.text = title
54 | }
55 |
56 | func setRightButton(title: String? = nil, image: UIImage? = nil, tapAction: ActionBlock? = nil) {
57 | // Right Button
58 | self.buttonRight?.isHidden = false
59 | if !title.isNilOrEmpty {
60 | self.buttonRight?.setTitle(title, for: .normal)
61 | self.buttonRight?.setImage(image, for: .normal)
62 | }
63 | else {
64 | self.buttonRight?.setBackgroundImage(image, for: .normal)
65 | }
66 |
67 | // Button ACtion
68 | if tapAction != nil {
69 | self.tapAction = tapAction
70 | self.buttonRight?.addTapActionBlock {
71 | self.tapAction?()
72 | }
73 | }
74 | }
75 |
76 | // MARK: Customise View
77 | var rightButton: UIButton? {
78 | buttonRight
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 28/09/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 |
11 | // MARK: Application Theme
12 | enum AppTheme {
13 | static let fileName = "Themes.json"
14 | // Color base
15 | static let appBase = "appBase"
16 | static let navBar = "navBar"
17 |
18 | static var navigationBarColor: UIColor {
19 | ThemesManager.getColor(AppTheme.appBase) ?? UIColor.white
20 | }
21 |
22 | static var barColor: UIColor {
23 | ThemesManager.getColor(AppTheme.navBar) ?? UIColor.black
24 | }
25 | }
26 |
27 | enum EndPoint {
28 | // MARK: Service URL
29 | static let endpointURL = "EndpointURL"
30 | static let modelBindingsName = "Bindings/ModelBindings"
31 | static let serviceBindingsName = "Bindings/ServiceBindings"
32 | static let serviceBindingRulesName = "NovelServiceRules.plist"
33 |
34 | // MARK: Mock
35 | enum Mock {
36 | static let postmanURL = "https://9b5d34ef-b082-479b-87cb-a845d678b371.mock.pstmn.io"
37 | static let mockServerURL = "http://127.0.0.1:3000"
38 | static let mockBundleResource = "FTNovelReaderMockBundle.bundle".bundleURL()
39 | static let mockDataEnabled = true
40 | }
41 |
42 | // MARK: 3rd Party
43 | static let kMSAppCenter = "6778a47c-7742-4dea-ace3-63c28b424350"
44 | }
45 |
46 | enum Constants {
47 | // MARK: Constants
48 | static let serviceFailureAlertTitle = "Service Error!"
49 | static let serviceFailureAlertMessage = "Unable to reach service host, please try again."
50 |
51 | // MARK: App
52 | static let retryString = "Retry"
53 | static let recentUpdateString = "Recent Update"
54 | static let topViews = "Top Views"
55 |
56 | // MARK: Title
57 | static let novelReaderTitle = "Novel Reader"
58 | }
59 |
60 | enum Storyboard {
61 | // MARK: StoryboardID
62 | static let searchStoryboardID = "kSearchStoryboardID"
63 |
64 | enum Segue {
65 | // MARK: SegueID
66 | static let showNovelDetailsView = "kShowNovelDetailsView"
67 | static let showNovelChapterList = "kShowNovelChapterList"
68 | static let showFontPicker = "kShowFontPicker"
69 | static let showNovelReaderView = "kShowNovelReaderView"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ChapterList/ChapterListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChapterListViewController.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 | import CoreUtility
11 |
12 | final class ChapterListViewController: UIViewController, TableViewControllerProtocol {
13 |
14 | var novel: NovelModel?
15 | lazy var manager = DataSourceManager(delegate: self)
16 | // lazy var novelDescView: NovelDetailsView? = try? .loadNibFromBundle()
17 | lazy var novelDescView: NovelDescriptionView? = try? .loadNibFromBundle()
18 |
19 | func tableStyle() -> UITableView.Style { .grouped }
20 |
21 | override func topSafeAreaLayoutGuide() -> Bool { false }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 | configureTableView()
26 | if let novel = novel {
27 | fetchNovelDetails(novel: novel)
28 | }
29 | // setBarStatus(hidden: false)
30 | }
31 | }
32 |
33 | private extension ChapterListViewController {
34 | func fetchNovelDetails(novel: NovelModel) {
35 | NovelServiceProvider.getNovelChaptersList(novel) { [weak self] novelResponse in
36 | guard let self = self, let novelResponse = novelResponse else { return }
37 | self.novel?.merge(data: novelResponse)
38 | self.configureContent()
39 | }
40 | }
41 |
42 | func configureTableView() {
43 | setupNavigationBar()
44 | tableView.backgroundColor = .clear
45 | manager.register(NovelChapterViewCell.self)
46 | manager.dequeueView = { _ -> UITableViewCell.Type in
47 | NovelChapterViewCell.self
48 | }
49 | manager.configureDidSelect = { _, obj in
50 | self.performSegue(withIdentifier: Storyboard.Segue.showNovelReaderView, sender: obj)
51 | }
52 | }
53 |
54 | func configureContent() {
55 | guard let novel = novel else { return }
56 | novelDescView?.configureContent(content: novel)
57 | tableView.setTableHeaderView(view: self.novelDescView)
58 | if let list = novel.chapterList {
59 | manager.updateCellObjects([list])
60 | }
61 | }
62 |
63 | func setupNavigationBar() {
64 | self.setupNavigationbar(
65 | title: "",
66 | leftButton: UIBarButtonItem(itemType: .stop),
67 | rightButton: UIBarButtonItem(itemType: .bookmarks)
68 | )
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/Dashboard/NovelCollectionViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelCollectionViewModel.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 02/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 | import NetworkLayer
11 |
12 | typealias NovelCollectionLifeCycleDelegate = (ViewControllerProtocol & NovelCollectionViewModelProtocal)
13 |
14 | protocol NovelCollectionViewModelProtocal {
15 | func showRetryAlert()
16 | func reloadCellObjects()
17 | }
18 |
19 | final class NovelCollectionViewModel {
20 |
21 | weak var lifeDelegate: NovelCollectionLifeCycleDelegate?
22 | var modelStack: ServiceModel?
23 |
24 | init(delegate: NovelCollectionLifeCycleDelegate, modelStack: ServiceModel?) {
25 | self.lifeDelegate = delegate
26 | self.modelStack = modelStack
27 | }
28 |
29 | lazy var novel: NovelListModel? = NovelListModel()
30 |
31 | // Update collectionView when contentList changes
32 | var currentNovelList: [NovelModel]? = [] {
33 | didSet {
34 | lifeDelegate?.reloadCellObjects()
35 | }
36 | }
37 |
38 | // Collection Content Type
39 | var novelCollectionType: NovelCollectionType = .recentNovel {
40 | didSet {
41 | self.fetchNovelList()
42 | }
43 | }
44 |
45 | func fetchNovelList() {
46 | switch novelCollectionType {
47 | case .recentNovel:
48 | NovelServiceProvider.fetchRecentUpdateList { [weak self] novelList in
49 | if let novelList = novelList {
50 | self?.currentNovelList = novelList
51 | }
52 | else {
53 | self?.lifeDelegate?.showRetryAlert()
54 | }
55 | }
56 | case .topNovel:
57 | NovelServiceProvider.fetchNovelList(novel: self.novel) { [weak self] _ in
58 | self?.currentNovelList = self?.novel?.novelList
59 | }
60 | }
61 | }
62 | }
63 |
64 | // MARK: NovelCollectionType
65 | enum NovelCollectionType: Int {
66 | case recentNovel = 0
67 | case topNovel
68 |
69 | var cellType: UICollectionViewCell.Type {
70 | switch self {
71 | case .recentNovel:
72 | return RecentNovelCollectionViewCell.self
73 | case .topNovel:
74 | return NovelCollectionViewCell.self
75 | }
76 | }
77 |
78 | static func registerCell(_ manager: DataSourceManager) {
79 | manager.register(RecentNovelCollectionViewCell.self)
80 | manager.register(NovelCollectionViewCell.self)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-40.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-60.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-58.png",
19 | "scale" : "2x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-87.png",
25 | "scale" : "3x"
26 | },
27 | {
28 | "size" : "40x40",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-80.png",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-121.png",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-120.png",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-180.png",
49 | "scale" : "3x"
50 | },
51 | {
52 | "size" : "20x20",
53 | "idiom" : "ipad",
54 | "filename" : "Icon-20.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-42.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "29x29",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-29.png",
67 | "scale" : "1x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-59.png",
73 | "scale" : "2x"
74 | },
75 | {
76 | "size" : "40x40",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-41.png",
79 | "scale" : "1x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-81.png",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "76x76",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-76.png",
91 | "scale" : "1x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-152.png",
97 | "scale" : "2x"
98 | },
99 | {
100 | "size" : "83.5x83.5",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-167.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "1024x1024",
107 | "idiom" : "ios-marketing",
108 | "filename" : "Icon-1024.png",
109 | "scale" : "1x"
110 | }
111 | ],
112 | "info" : {
113 | "version" : 1,
114 | "author" : "xcode"
115 | }
116 | }
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/ConfigureNovelCellProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigureNovelCellProtocol.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 30/04/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: Novel Cell protocol
12 | typealias ConfigureNovelCellProtocol = DSConfigureContentProtocol
13 |
14 | // MARK: UIStoryboardSegue
15 | protocol StoryboardSegueProtocol {
16 | func configure(segue: UIStoryboardSegue, sender: Any?)
17 | }
18 |
19 | extension StoryboardSegueProtocol {
20 | func configure(segue: UIStoryboardSegue, sender: Any?) {
21 | if segue.identifier == Storyboard.Segue.showNovelDetailsView {
22 | // configureShowNovelDetailsView(segue, sender: sender)
23 | }
24 | else if segue.identifier == Storyboard.Segue.showNovelChapterList {
25 | configureShowNovelChapterList(segue, sender: sender)
26 | }
27 | else if segue.identifier == Storyboard.Segue.showNovelReaderView {
28 | configureShowNovelReaderView(segue, sender: sender)
29 | }
30 | // segue for the popover configuration window
31 | else if segue.identifier == Storyboard.Segue.showFontPicker {
32 | configureShowFontPicker(segue, sender: sender)
33 | }
34 | }
35 | }
36 |
37 | fileprivate extension StoryboardSegueProtocol {
38 | func configureShowNovelChapterList(_ segue: UIStoryboardSegue, sender: Any?) {
39 | if let nextViewController = segue.destination as? ChapterListViewController {
40 | nextViewController.novel = sender as? NovelModel
41 | }
42 | }
43 |
44 | func configureShowNovelReaderView(_ segue: UIStoryboardSegue, sender: Any?) {
45 | let readerController: ReaderViewController?
46 | if let nav = segue.destination as? UINavigationController {
47 | readerController = nav.viewControllers.first as? ReaderViewController
48 | }
49 | else {
50 | readerController = segue.destination as? ReaderViewController
51 | }
52 |
53 | // Available from recent-novel-page
54 | readerController?.novel = sender as? NovelModel
55 | // Available from chapter-list-page
56 | readerController?.novelChapter = sender as? NovelChapterModel
57 | }
58 |
59 | func configureShowFontPicker(_ segue: UIStoryboardSegue, sender: Any?) {
60 | if let controller = segue.destination as? FontPickerViewController {
61 | if let self = self as? FontPickerViewProtocol {
62 | controller.fontPickerViewDelegate = self
63 | }
64 | if let self = self as? UIPopoverPresentationControllerDelegate {
65 | controller.popoverPresentationController?.delegate = self
66 | }
67 | controller.preferredContentSize = CGSize(width: 250, height: 320)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Search/SearchCollectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchCollectionViewController.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 30/04/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 | import CoreUtility
11 | import NetworkLayer
12 |
13 | class SearchCollectionViewController: UIViewController, CollectionViewControllerProtocol,
14 | SearchCollectionViewModelProtocal {
15 |
16 | // View Model
17 | lazy var viewModel = {
18 | SearchCollectionViewModel(delegate: self, modelStack: self.modelStack as? ServiceModel)
19 | }()
20 |
21 | // MARK: Life Cycle
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | self.setupColletionView()
25 | }
26 |
27 | // MARK: setupColletionView
28 | func setupColletionView() {
29 | // Register Cell
30 | NovelCollectionViewCell.registerNib(for: collectionView)
31 | // Collection Header: Segment Control
32 | self.topPinnedView = SearchBarHeaderView(delegate: self)
33 | refreshCollectionView()
34 | }
35 |
36 | func refreshCollectionView() {
37 | }
38 | }
39 |
40 | extension SearchCollectionViewController: UISearchBarDelegate {
41 |
42 | // Dismiss keyboard on tap of search. Will invoke `searchBarTextDidEndEditing`
43 | public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
44 | endEditing()
45 | }
46 |
47 | public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
48 | if let searchText = searchBar.text {
49 | viewModel.searchNovel(keywoard: searchText)
50 | }
51 | }
52 | }
53 |
54 | extension SearchCollectionViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
55 |
56 | // numberOfItemsInSection
57 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
58 | viewModel.currentNovelList?.count ?? 0
59 | }
60 |
61 | // cellForItem
62 | func collectionView(_ collectionView: UICollectionView,
63 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
64 |
65 | guard let cell = try? NovelCollectionViewCell.dequeue(from: collectionView, for: indexPath),
66 | let novel = viewModel.currentNovelList?[indexPath.row]
67 | else {
68 | return UICollectionViewCell()
69 | }
70 |
71 | if let cell = cell as? ConfigureNovelCellProtocol {
72 | cell.configureContent(content: novel, indexPath: indexPath)
73 | }
74 |
75 | return cell
76 | }
77 |
78 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
79 | let cur = viewModel.currentNovelList?[indexPath.row]
80 | self.performSegue(withIdentifier: Storyboard.Segue.showNovelDetailsView, sender: cur)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - file_name
3 | - explicit_acl
4 | - explicit_top_level_acl
5 | - explicit_type_interface
6 | - explicit_enum_raw_value
7 | - no_extension_access_modifier
8 | - attributes # rule is too subjective
9 | - no_grouping_extension # grouping extensions are ok
10 | - vertical_whitespace_between_cases # increases code length without pros
11 | - todo # we love todos
12 | - trailing_whitespace # IDE will handle it for us
13 | - vertical_whitespace_opening_braces # readability improves
14 | - override_in_extension
15 | - private_outlet
16 | - private_action
17 |
18 | opt_in_rules:
19 | - array_init
20 | - anyobject_protocol
21 | - closure_body_length
22 | - closure_end_indentation
23 | - closure_spacing
24 | - collection_alignment
25 | - conditional_returns_on_newline
26 | - contains_over_first_not_nil
27 | - convenience_type
28 | - discarded_notification_center_observer
29 | - empty_count
30 | - empty_string
31 | - explicit_init
32 | - empty_xctest_method
33 | - fallthrough
34 | - fatal_error_message
35 | - file_name
36 | - file_length
37 | - first_where
38 | - force_unwrapping
39 | - function_default_parameter_at_end
40 | - implicit_return
41 | - joined_default_parameter
42 | - last_where
43 | - let_var_whitespace
44 | - legacy_random
45 | - literal_expression_end_indentation
46 | - lower_acl_than_parent
47 | - modifier_order
48 | - multiline_arguments
49 | - multiline_arguments_brackets
50 | - multiline_function_chains
51 | - multiline_parameters
52 | - multiline_literal_brackets
53 | - number_separator
54 | - operator_usage_whitespace
55 | - overridden_super_call
56 | - override_in_extension
57 | - pattern_matching_keywords
58 | - prohibited_super_call
59 | - redundant_nil_coalescing
60 | - redundant_type_annotation
61 | - single_test_class
62 | - sorted_first_last
63 | - sorted_imports
64 | - static_operator
65 | - toggle_bool
66 | - trailing_closure
67 | - unneeded_parentheses_in_closure_argument
68 | - unused_import
69 | - unused_private_declaration
70 | - vertical_parameter_alignment_on_call
71 | - vertical_whitespace_closing_braces
72 | - xct_specific_matcher
73 | - yoda_condition
74 |
75 | large_tuple: 4
76 | function_body_length : 50
77 | function_parameter_count: 5
78 | warning_threshold: 1
79 | closure_body_length: 16
80 | file_length: 750
81 |
82 | cyclomatic_complexity:
83 | warning: 5
84 | ignores_case_statements: true
85 |
86 | line_length:
87 | warning: 160
88 | ignores_function_declarations: true
89 | ignores_comments: true
90 |
91 | nesting:
92 | type_level: 3
93 |
94 | conditional_returns_on_newline:
95 | if_only: true
96 |
97 | identifier_name:
98 | max_length: 40
99 | min_length: 1
100 |
101 | statement_position:
102 | statement_mode: uncuddled_else
103 |
104 | file_name:
105 | prefix_pattern: ""
106 | suffix_pattern: "Extensions?|\\+.*"
107 |
108 | warning_threshold: 1
109 |
110 | custom_rules:
111 | no_break_with_label:
112 | regex: "[ ]+break [A-z0-9]+"
113 | message: "Please, don't use break with label"
114 |
115 | excluded: # paths to ignore during linting. Takes precedence over `included`.
116 | - Pods
117 |
118 | #included:
--------------------------------------------------------------------------------
/NovelReader.xcodeproj/xcshareddata/xcschemes/FTNovelReaderMockBundle.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/NovelReader/Manager/AppManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppManager.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 | import CoreComponents
11 | import CoreUtility
12 | import NetworkLayer
13 |
14 | // MSAppCenter
15 | #if canImport(AppCenter)
16 | import AppCenter
17 | import AppCenterAnalytics
18 | import AppCenterCrashes
19 | #endif
20 |
21 | final class AppManager {
22 | static var kBundle = Bundle(for: AppDelegate.self)
23 |
24 | // plist Endpoint
25 | static var endpointURL: String {
26 | Bundle.main.infoDictionary?[EndPoint.endpointURL] as? String ?? ""
27 | }
28 |
29 | // MARK: Setup
30 | static func setupApplication() {
31 | AppManager.configureAppBase()
32 | AppManager.configureAppTheme()
33 | // NRGoogleAuth.setupGoogleAuth()
34 | }
35 |
36 | // MARK: Model Binding
37 | static func configureAppBase() {
38 | // Service Binding
39 | NetworkMananger.serviceBindingPath = EndPoint.serviceBindingsName
40 | NetworkMananger.serviceBindingRulesName = EndPoint.serviceBindingRulesName
41 |
42 | // App Config
43 | NetworkMananger.appBaseURL = AppManager.endpointURL
44 |
45 | // Debug-only code
46 | self.configDebug()
47 | }
48 |
49 | // Config
50 | static func configDebug() {
51 | #if DEBUG
52 | // Console Loggin
53 | Logger.enableConsoleLogging = true
54 | // Debug-Postman
55 | // FTMobileConfig.appBaseURL = kPostmanURL
56 | // Debug-only code
57 | NetworkMananger.appBaseURL = EndPoint.Mock.mockServerURL
58 | NetworkMananger.mockBundleResource = EndPoint.Mock.mockBundleResource
59 | NetworkMananger.isMockData = EndPoint.Mock.mockDataEnabled
60 | #endif
61 | }
62 |
63 | func configureAppCenter() {
64 | #if canImport(AppCenter)
65 | // MSAppCenter.start(kMSAppCenter, withServices:[
66 | // MSAnalytics.self,
67 | // MSCrashes.self
68 | // ])
69 | #endif
70 | }
71 |
72 | func configureGoogleAuth() {
73 | #if canImport(GoogleSignIn)
74 | // NRGoogleAuth.setupGoogleAuth()
75 | #endif
76 | }
77 |
78 | // MARK: Theme
79 | static func configureAppTheme() {
80 | if let theme = Bundle.main.path(forResource: AppTheme.fileName, ofType: nil),
81 | let themeContent: ThemeModel = try? theme.jsonContentAtPath() {
82 | ThemesManager.setupThemes(themes: themeContent, imageSourceBundle: [kBundle])
83 | }
84 |
85 | // Loading Indicator
86 | setupLoadingIndicator()
87 | }
88 |
89 | // MARK: LoadingIndicator
90 | static func setupLoadingIndicator() {
91 | var config = LoaderConfig()
92 | config.backgroundColor = UIColor.clear
93 | config.spinnerColor = AppTheme.navigationBarColor
94 | config.titleTextColor = UIColor.white
95 | config.spinnerLineWidth = 8.0
96 | config.foregroundColor = AppTheme.barColor
97 | config.foregroundAlpha = 0.8
98 | config.title = ""
99 | LoadingIndicator.setConfig(config: config)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/NovelReader/Resources/plist/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleURLTypes
20 |
21 |
22 | CFBundleTypeRole
23 | Editor
24 | CFBundleURLSchemes
25 |
26 | com.googleusercontent.apps.769359482558-l9uifqdbpq49n1pjqk6vvb2qtekm0ktl
27 |
28 |
29 |
30 | CFBundleVersion
31 | 1
32 | EndpointURL
33 | https://novelreader-online.herokuapp.com/
34 | LSApplicationCategoryType
35 | public.app-category.entertainment
36 | LSRequiresIPhoneOS
37 |
38 | NSAppTransportSecurity
39 |
40 | NSAllowsArbitraryLoads
41 |
42 | NSExceptionDomains
43 |
44 | 127.0.0.1
45 |
46 | NSExceptionAllowsInsecureHTTPLoads
47 |
48 | NSExceptionRequiresForwardSecrecy
49 |
50 | NSIncludesSubdomains
51 |
52 | NSRequiresCertificateTransparency
53 |
54 | NSThirdPartyExceptionAllowsInsecureHTTPLoads
55 |
56 | NSThirdPartyExceptionRequiresForwardSecrecy
57 |
58 |
59 | onlinenovelreader.com/uploads/
60 |
61 | NSExceptionAllowsInsecureHTTPLoads
62 |
63 | NSIncludesSubdomains
64 |
65 |
66 |
67 |
68 | UIApplicationSceneManifest
69 |
70 | UIApplicationSupportsMultipleScenes
71 |
72 |
73 | UILaunchStoryboardName
74 | LaunchScreen
75 | UIMainStoryboardFile
76 | Main
77 | UIRequiredDeviceCapabilities
78 |
79 | armv7
80 |
81 | UIRequiresFullScreen
82 |
83 | UIStatusBarHidden
84 |
85 | UIStatusBarStyle
86 | UIStatusBarStyleLightContent
87 | UIStatusBarTintParameters
88 |
89 | UINavigationBar
90 |
91 | Style
92 | UIBarStyleDefault
93 | Translucent
94 |
95 |
96 |
97 | UISupportedInterfaceOrientations
98 |
99 | UIInterfaceOrientationLandscapeLeft
100 | UIInterfaceOrientationLandscapeRight
101 | UIInterfaceOrientationPortrait
102 |
103 | UISupportedInterfaceOrientations~ipad
104 |
105 | UIInterfaceOrientationPortrait
106 | UIInterfaceOrientationPortraitUpsideDown
107 | UIInterfaceOrientationLandscapeLeft
108 | UIInterfaceOrientationLandscapeRight
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ChapterList/Views/NovelChapterViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/NovelReader/Parser/NovelServiceProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelServiceProvider.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 20/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 | import NetworkLayer
11 |
12 | enum NovelServiceProvider {
13 | // Get list of all Novels
14 | static func fetchRecentUpdateList(completionHandler: @escaping (_ novelsList: [NovelModel]?) -> Void) {
15 | LoadingIndicator.show()
16 | ServiceRecentUpdatesList.make { response in
17 | LoadingIndicator.hide()
18 | let response = response.status.responseModel as? NovelListModel
19 | completionHandler(response?.response)
20 | }
21 | }
22 |
23 | // Get list of all Novels
24 | static func fetchNovelList(novel: NovelListModel?, completionHandler: @escaping (_ novelsList: NovelListModel?) -> Void) {
25 | // LoadingIndicator.show()
26 | // ServiceNovelList.make { response in
27 | // LoadingIndicator.hide()
28 | // let res = response.status.responseModel as? NovelListModel
29 | // if let novelList = res?.response {
30 | // let novel = novel ?? NovelListModel()
31 | //// if(novel.novelList == nil) {
32 | // novel.novelList = []
33 | //// }
34 | // novel.novelList?.append(contentsOf: novelList)
35 | // }
36 | // completionHandler(novel)
37 | //// //FIXIT: Has be done in ServiceClient
38 | //// if let novelResponse = res?.responseStack as? NRNovels {
39 | //// var novel = novel
40 | //// novel!.merge(data: novelResponse)
41 | //// completionHandler(novel)
42 | //// }
43 | // }
44 | }
45 |
46 | // Get list of all chapters from a single NRNovelObject
47 | static func getNovelChaptersList(_ novel: NovelModel,
48 | getChapters: Bool = true,
49 | completionHandler: @escaping (_ novel: NovelModel?) -> Void) {
50 | LoadingIndicator.show()
51 | let model: ServiceModel = ["id": novel.identifier]
52 | ServiceNovelChapters.make(modelStack: model) { response in
53 | LoadingIndicator.hide()
54 | let res = response.status.responseModel as? NovelModel
55 | completionHandler(res?.response)
56 | }
57 | }
58 |
59 | // Get chapter content
60 | static func getNovelChapter(_ identifier: String,
61 | completionHandler: @escaping (_ chapterContent: NovelChapterModel?) -> Void) {
62 | LoadingIndicator.show()
63 | let model: ServiceModel = ["id": identifier]
64 | ServiceNovelChapter.make(modelStack: model) { response in
65 | LoadingIndicator.hide()
66 | let res = response.status.responseModel as? NovelChapterModel
67 | completionHandler(res?.response)
68 | }
69 | }
70 |
71 | // Serch Novel
72 | static func searchNovel(keyword: String, completionHandler: @escaping (_ novels: [NovelModel]?) -> Void) {
73 | LoadingIndicator.show()
74 | let model = SearchNovelModel()
75 | model.keyword = keyword
76 | ServiceSearchNovel.make(modelStack: model) { response in
77 | LoadingIndicator.hide()
78 | let res = response.status.responseModel as? NovelListModel
79 | completionHandler(res?.response)
80 | }
81 | }
82 |
83 | // Serch Novel
84 | static func searchFilter(completionHandler: @escaping (_ novels: SearchFilterModel?) -> Void) {
85 | LoadingIndicator.show()
86 | ServiceSearchFilter.make(modelStack: nil) { response in
87 | LoadingIndicator.hide()
88 | let res = response.status.responseModel as? SearchFilterModel
89 | completionHandler(res?.response)
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/NovelReader/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Views/SelectionCollectionViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Search/SearchFilterViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchFilterViewController.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 13/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 |
11 | class SearchFilterViewController: UIViewController, CollectionViewControllerProtocol {
12 |
13 | // View Model
14 | lazy var viewModel = {
15 | SearchFilterViewModel(delegate: self, modelStack: self.modelStack as? SearchFilterModel)
16 | }()
17 |
18 | // MARK: Life Cycle
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 | self.setupColletionView()
22 |
23 | // update-filter-details
24 | viewModel.updateSearchFilter()
25 | }
26 |
27 | override var flowLayout: UICollectionViewLayout {
28 | let layout = super.flowLayout
29 | (layout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = false
30 | return layout
31 | }
32 |
33 | // MARK: setupColletionView
34 | func setupColletionView() {
35 |
36 | // Register Cell
37 | SelectionCollectionViewCell.registerNib(for: collectionView)
38 |
39 | // Collection Header: Segment Control
40 | collectionView.register(
41 | SectionHeaderView.self,
42 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
43 | withReuseIdentifier: "headerCell"
44 | )
45 | }
46 |
47 | @IBAction
48 | private func resetSelection(_ sender: Any) {
49 | viewModel.resetSelection()
50 | }
51 | }
52 |
53 | extension SearchFilterViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate {
54 |
55 | func numberOfSections(in collectionView: UICollectionView) -> Int {
56 | viewModel.numberOfSections
57 | }
58 |
59 | // numberOfItemsInSection
60 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
61 | viewModel.numberOfItemsInSection(section)
62 | }
63 |
64 | // viewForSupplementaryElement
65 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
66 |
67 | let headerView = collectionView.dequeueReusableSupplementaryView(
68 | ofKind: UICollectionView.elementKindSectionHeader,
69 | withReuseIdentifier: "headerCell",
70 | for: indexPath
71 | )
72 |
73 | if let headerView = headerView as? SectionHeaderView {
74 | headerView.setSectionHeader(title: viewModel.sectionTitleAt(indexPath), image: nil)
75 | headerView.setRightButton(title: nil, image: #imageLiteral(resourceName: "close-circle")) { [weak self] in
76 | self?.viewModel.resetSelection(indexPath)
77 | }
78 | }
79 |
80 | return headerView
81 | }
82 |
83 | // cellForItem
84 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
85 |
86 | guard let cell = try? SelectionCollectionViewCell.dequeue(from: collectionView, for: indexPath) else {
87 | return UICollectionViewCell()
88 | }
89 |
90 | // CheckMark
91 | cell.isSelected = viewModel.isSelected(indexPath).found
92 |
93 | // Config content
94 | if
95 | let cell = cell as? ConfigureNovelCellProtocol,
96 | let cur = viewModel.cellForItemAt(indexPath) {
97 | cell.configureContent(content: cur, indexPath: indexPath)
98 | }
99 |
100 | return cell
101 | }
102 |
103 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
104 |
105 | // Update User selection
106 | viewModel.updateSelection(indexPath)
107 | }
108 | }
109 |
110 | extension SearchFilterViewController: SearchFilterViewModelProtocal {
111 | func refreshCollectionView() {
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/Dashboard/NovelCollectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NovelCollectionViewController.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 21/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 | import CoreComponents
11 | import CoreUtility
12 | import NetworkLayer
13 |
14 | final class NovelCollectionViewController: UIViewController, CollectionViewControllerProtocol {
15 | // View Model
16 | lazy var viewModel = {
17 | NovelCollectionViewModel(delegate: self, modelStack: self.modelStack as? ServiceModel)
18 | }()
19 |
20 | var headerView: SegmentCollectionHeaderView?
21 | lazy var manager = DataSourceManager(collectionDelegate: self)
22 |
23 | // View lifecycle
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 | // Novel Segment type
27 | viewModel.novelCollectionType = .recentNovel
28 | // View Title
29 | setupNavigationBar()
30 | // Collection View
31 | setupColletionView()
32 | }
33 |
34 | // MARK: Setup CollectionView
35 | func setupColletionView() {
36 | // Register Cell
37 | NovelCollectionType.registerCell(manager)
38 | // Collection Header: Segment Control
39 | manager.register(SegmentCollectionHeaderView.self, kind: UICollectionView.elementKindSectionHeader)
40 | manager.layoutReferenceSize = { _ -> UICollectionReusableView? in
41 | self.headerView
42 | }
43 | manager.dequeueReusableView = { _, _ -> DataSourceManager.DequeueReusableViewReturn in
44 | let block: ActionWithObjectBlock = { obj in
45 | guard let headerView = obj as? SegmentCollectionHeaderView else { return }
46 | headerView.segmentedControl.handler = { [weak self] index in
47 | self?.updateNovelSegment(index)
48 | }
49 | self.headerView = headerView
50 | }
51 | return (SegmentCollectionHeaderView.self, block)
52 | }
53 | // Collection View Cell
54 | manager.dequeueView = { _ -> UICollectionViewCell.Type in
55 | self.viewModel.novelCollectionType.cellType
56 | }
57 | manager.configureDidSelect = { _, obj in
58 | let segue = self.viewModel.novelCollectionType == .recentNovel ?
59 | Storyboard.Segue.showNovelReaderView : Storyboard.Segue.showNovelChapterList
60 | self.performSegue(withIdentifier: segue, sender: obj)
61 | }
62 | }
63 |
64 | // Navigation bar Button action
65 | override func rightButtonAction() {
66 | performSegue(withIdentifier: Storyboard.searchStoryboardID, sender: nil)
67 | }
68 | }
69 |
70 | // MARK: Fetch - Novels from backend
71 | extension NovelCollectionViewController: NovelCollectionViewModelProtocal {
72 | func reloadCellObjects() {
73 | manager.removeAllObjects()
74 | guard let objects = viewModel.currentNovelList else { return }
75 | manager.updateCellObjects([objects])
76 | }
77 |
78 | func showRetryAlert() {
79 | let alert = UIAlertController(title: Constants.serviceFailureAlertTitle,
80 | message: Constants.serviceFailureAlertMessage,
81 | preferredStyle: UIAlertController.Style.alert)
82 | let action = UIAlertAction(title: Constants.retryString, style: .default) { [weak self] _ in
83 | self?.viewModel.fetchNovelList()
84 | }
85 | alert.addAction(action)
86 | self.present(alert, animated: true, completion: nil)
87 | }
88 |
89 | // Updates novelCollectionType, which interns fetchNovelList from backend
90 | func updateNovelSegment(_ index: Int) {
91 | viewModel.novelCollectionType = NovelCollectionType(rawValue: index) ?? .recentNovel
92 | }
93 | }
94 |
95 | // MARK: StoryboardSegueProtocol
96 | extension NovelCollectionViewController: StoryboardSegueProtocol {
97 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
98 | configure(segue: segue, sender: sender)
99 | }
100 |
101 | func setupNavigationBar() {
102 | // View Title
103 | let rightButtonItem = UIBarButtonItem(itemType: .search)
104 | self.setupNavigationbar(title: Constants.novelReaderTitle, rightButton: rightButtonItem)
105 | // Hide Navigation bar on Scroll
106 | // self.hideNavigationOnScroll(for: collectionView)
107 | self.view.theme = ThemeStyle.defaultStyle.rawValue
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/NovelReader.xcodeproj/xcshareddata/xcschemes/NovelReader.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
38 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
53 |
59 |
60 |
61 |
62 |
63 |
73 |
75 |
81 |
82 |
83 |
84 |
90 |
92 |
98 |
99 |
100 |
101 |
103 |
104 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/FTNovelReaderMockBundle/fetchChapter.json:
--------------------------------------------------------------------------------
1 | {
2 | "response": {
3 | "shortTitle": "Chapter 1: Prologue",
4 | "content": "Translator: Mango Cat Editor: DarkGem
“Young Miss, Young Miss.
Liu'er was standing in the room, her face paling drastically as she looked at her young miss.
Young Miss was a bit older than her at fourteen years of age. At the moment, she was standing on top of a stool with an embroidered cushion. She raised herself on her toes, hanging white silk from the beam of the rafters.
Liu'er raised her head. Young Miss was always slim and delicate, but now she looked thinner than ever.
Young Miss, stop with this farce, she stammered out, clutching at Young Miss' dress.
How could I do that!? she shouted.
She looked at the young servant girl with an angry gaze, her large eyes almost encompassing half of her face. She snapped her head back up to look at the white silk, the light bouncing off her chin like porcelain.
But she was not done talking.
...Grandmother refuses to get justice for me, so I can only do it myself...
She was obviously very angry, but her voice was so sweet and beautiful it still sounded gentle, but with a tapered point.
Liu'er walked around her, alarmed.
But, Young Miss, m-maybe it's just a rumor. Miss Lin's words aren't necessarily true, she said, her voice shaking.
At this, Young Miss lowered her head, furrowing her slender brows.
Rumors? If it were half a year ago, it could still be a rumor, but now it has gone on for this long. Aunt has already gone to the Ning Family many times. And what of it? The rumors haven't disappeared. Anyways, news of the engagement with the Yang Family's fifth young miss have spread. Jin'er and the Ning Family's seventeenth miss are on great terms, so Jin'er must be telling the truth.
When she said this, tears dripped from her eyes.
The Ning Family is betraying my engagement and giving Tenth Noble Son for some other marriage.
When she said this, she covered her face with her hands.
If Grandfather and Father were here, that Ning Family would not dare do this. But they can bully a motherless, fatherless child like me.
Her Grandfather and Father were her mountain. Now that they were not here, the mountain toppled. She also didn't have any brothers or sisters, leaving her an orphan. Anyone could bully her.
Liu'er thought of the days in the past and of their days now. Although she was just a servant, she was equally moved and also started to cry.
Grandmother is afraid of the Ning Family, but I’m not afraid. Young Miss suppressed her crying and flung her sleeves in anger. She grabbed the white silk, her beautiful face firm. Today I will hang myself, forced to die out of betrayal. See what face that Ning Family will have left. This world is not without justice!
Without hesitation, she poked her head through the silk loop.
Liu'er grabbed her legs in fright.
This caused both of them to sway. Young Miss' foot almost slipped on the stool, and she let out a half-shriek.
Stop grabbing me. Just let me do it, and then once there is a mark grab me again, scolded Young Miss angrily.
So she was never intending to truly die.
Liu'er let go of her somewhat shakily.
Stand back, Young Miss ordered.
Liu'er moved back several steps, looking at her with a white face.
Young Miss was satisfied, then after taking a deep breath, poked her head through the white silk again.
Remember to first go tell Grandmother. There's no point in talking to Aunt, she reminded.
Liu'er nodded vigorously.
Ning Family, let's see what you'll do now! Young Miss said forcefully. Then gritting her teeth, she grabbed the white silk and stepped off the stool.
I'm suffocating, suffocating. It hurts, hurts!
I can't take it!
She kept kicking in the air.
No more, no more.
Her gasps made Liu'er leap forward, but because she was too small, she couldn't grab onto Young Miss' flailing legs.
Young Miss, Young Miss, stop moving, she couldn't help but shout.
It was very difficult to grab on, and she found that she didn't have the strength to lift up Young Miss.
What to do? What to do?
Liu'er bit her teeth and desperately tried to push upwards.
But Young Miss started ceasing her struggles. She no longer made noises, and her body became limp.
The young servant girl couldn't help but look up and see her young miss' flower-like face so ashen. Her eyes were vacant, and her tongue was sticking out.
Someone… someone…”
Liu'er fell to the ground, muttering. Then she immediately scrambled to her feet.
Someone, oh someone, come here and save her, save her!
"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ReaderView/ReaderViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReaderViewController.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 26/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 | import CoreUtility
11 | import NetworkLayer
12 |
13 | class ReaderViewController: UIViewController, WebViewControllerProtocol {
14 |
15 | var novelChapter: NovelChapterModel?
16 | var novel: NovelModel?
17 |
18 | @IBOutlet
19 | private var fontPickerBarItem: UIBarButtonItem?
20 | // @IBOutlet var chapterToolBarItem: UIToolbar?
21 | // var sortedToolBarItems: [UIBarButtonItem]? {
22 | // get{
23 | // return self.chapterToolBarItem?.items?.sorted(by: { $0.tag > $1.tag })
24 | // }
25 | // }
26 |
27 | var fontPicker: FontPickerModel? {
28 | get {
29 | UserCacheManager.getCachedObject(forType: FontPickerModel.self) as? FontPickerModel
30 | }
31 | set {
32 | if let fontPicker = newValue {
33 | UserCacheManager.setCacheObject(fontPicker, forType: FontPickerModel.self)
34 | }
35 | }
36 | }
37 |
38 | override func topSafeAreaLayoutGuide() -> Bool { false }
39 |
40 | override func viewDidLoad() {
41 | super.viewDidLoad()
42 | setupCoreView()
43 | // setup view content
44 | setupViewContent()
45 | }
46 |
47 | override func viewWillAppear(_ animated: Bool) {
48 | super.viewWillAppear(animated)
49 | navigationController?.navigationBar.prefersLargeTitles = false
50 | }
51 |
52 | func setupViewContent() {
53 | // To hide navBar on scroll
54 | // self.hideNavigationOnScroll(for: contentView)
55 | // Fetch content
56 | let title = novelChapter?.shortTitle ?? novelChapter?.title ?? novel?.title ?? ""
57 | self.setupNavigationbar(
58 | title: title,
59 | leftButton: UIBarButtonItem(itemType: .stop),
60 | rightButton: fontPickerBarItem
61 | )
62 |
63 | self.mainView?.pin(view: contentView)
64 |
65 | if let url = novelChapter?.identifier ?? novel?.identifier {
66 | NovelServiceProvider.getNovelChapter(url) { [unowned self] chapter in
67 | if let content = chapter?.shortTitle {
68 | self.title = content
69 | }
70 | if let content = chapter?.content {
71 | self.loadWebContent(contnet: content)
72 | }
73 | self.configureWebview()
74 | }
75 | }
76 | }
77 |
78 | func loadWebContent(contnet: String) {
79 | contentView.loadHTMLBody(contnet)
80 | }
81 |
82 | func configureWebview() {
83 | // if let fontPicker = fontPicker {
84 | // fontSize(fontPicker.fontSize)
85 | // pickerColor(textColor: fontPicker.fontColor, backgroundColor: fontPicker.backgroundColor)
86 | // fontFamily(fontPicker.fontFamily)
87 | // }
88 | }
89 | }
90 |
91 | extension ReaderViewController {
92 | func fontSize(_ size: Float) {
93 | contentView.setContentFontSize(size)
94 | }
95 |
96 | func pickerColor(textColor: UIColor, backgroundColor: UIColor) {
97 | contentView.setContentColor(textColor: textColor, backgroundColor: backgroundColor)
98 | self.view.backgroundColor = backgroundColor
99 | self.mainView?.backgroundColor = backgroundColor
100 | // webview color
101 | contentView.backgroundColor = .clear
102 | contentView.isOpaque = false
103 | contentView.scrollView.backgroundColor = .clear
104 | }
105 |
106 | func fontFamily(_ fontName: String?) {
107 | contentView.setContentFontFamily(fontName)
108 | }
109 | }
110 |
111 | extension ReaderViewController: UIPopoverPresentationControllerDelegate, StoryboardSegueProtocol {
112 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
113 | configure(segue: segue, sender: sender)
114 | }
115 |
116 | func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
117 | if let fontPickerController = controller.presentedViewController as? FontPickerViewController {
118 | if self.fontPicker == nil {
119 | self.fontPicker = fontPickerController.fontPickerModel
120 | }
121 | else {
122 | fontPickerController.fontPickerModel = self.fontPicker
123 | }
124 | }
125 | return .none
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/ScollableSegmentControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScollableSegmentControl.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 26/06/21.
6 | // Copyright © 2021 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreUtility
10 |
11 | final class ScrollableSegmentControl: UIScrollView {
12 | typealias SegmentHandler = ( (_ index: Int) -> Void )
13 | var handler: SegmentHandler?
14 |
15 | private var segmentsList = [SegmentData]()
16 | private var selectedSegment: SegmentData? {
17 | willSet {
18 | selectedSegment?.isSelected = false
19 | }
20 | didSet {
21 | selectedSegment?.isSelected = true
22 | }
23 | }
24 |
25 | lazy var stackView: UIStackView = {
26 | let view = UIStackView()
27 | view.axis = .horizontal
28 | view.spacing = 30
29 | contentView.pin(view: view)
30 | setupScrollView(self)
31 | return view
32 | }()
33 | }
34 |
35 | extension ScrollableSegmentControl {
36 | public func insertSegment(withTitle title: String? = nil, image: UIImage? = nil,
37 | theme: String, at index: Int) {
38 | let segment = SegmentData(title: title, image: image, theme: theme)
39 | segment.actionBlock = { [weak self] in
40 | self?.didSelectSegement(data: segment)
41 | }
42 | segmentsList.insert(segment, at: index)
43 | // Update View
44 | updateList()
45 | }
46 |
47 | public func removeSegment(at index: Int) {
48 | guard segmentsList.count > index else { return }
49 | segmentsList.remove(at: index)
50 | // Update View
51 | updateList()
52 | }
53 | }
54 |
55 | private extension ScrollableSegmentControl {
56 | func didSelectSegement(data: SegmentData) {
57 | selectedSegment = data
58 | let index: Int = segmentsList.firstIndex(of: data) ?? 0
59 | // setContentOffset(CGPoint(x: data.view.frame.origin.x - 100, y: 0), animated: true)
60 | handler?(index)
61 | }
62 |
63 | func updateList() {
64 | stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
65 | segmentsList.forEach { stackView.addArrangedSubview($0.view) }
66 | if selectedSegment == nil {
67 | selectedSegment = segmentsList.first
68 | }
69 | }
70 |
71 | func setupScrollView(_ scrollView: UIScrollView) {
72 | scrollView.showsVerticalScrollIndicator = false
73 | scrollView.showsHorizontalScrollIndicator = false
74 | scrollView.alwaysBounceHorizontal = true
75 | }
76 | }
77 |
78 | // MARK: - SegmentData
79 | private extension ScrollableSegmentControl {
80 | final class SegmentData: Equatable {
81 | var title: String?
82 | var theme: String?
83 | var image: UIImage?
84 | var actionBlock: ActionBlock?
85 | var isSelected: Bool = false {
86 | didSet {
87 | buttonView.isSelected = isSelected
88 | underlineView.isHidden = !isSelected
89 | }
90 | }
91 |
92 | init(title: String? = nil, image: UIImage? = nil, theme: String) {
93 | self.title = title
94 | self.image = image?.withRenderingMode(.alwaysTemplate)
95 | self.theme = theme
96 | }
97 |
98 | lazy var view: UIStackView = {
99 | let view = UIStackView(arrangedSubviews: [buttonView, underlineView])
100 | view.axis = .vertical
101 | return view
102 | }()
103 |
104 | lazy var underlineView: UIView = {
105 | let view = UIView()
106 | view.theme = "underline"
107 | view.isHidden = !isSelected
108 | view.setViewHeight(6, createConstraint: true)
109 | return view
110 | }()
111 |
112 | lazy var buttonView: UIButton = {
113 | let button = UIButton()
114 | button.setTitle(self.title, for: .normal)
115 | button.setImage(self.image, for: .normal)
116 | button.contentVerticalAlignment = .center
117 | // button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: -5)
118 | button.theme = self.theme
119 | if let action = self.actionBlock {
120 | button.addTapActionBlock(action)
121 | }
122 | return button
123 | }()
124 |
125 | static func == (lhs: SegmentData, rhs: SegmentData) -> Bool {
126 | lhs.title == rhs.title && lhs.image == rhs.image && lhs.theme == rhs.theme
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Search/SearchFilterViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchFilterViewModel.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 22/10/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import CoreComponents
10 |
11 | typealias SearchFilterLifeCycleDelegate = (ViewControllerProtocol & SearchFilterViewModelProtocal)
12 |
13 | protocol SearchFilterViewModelProtocal {
14 | func refreshCollectionView()
15 | }
16 |
17 | class SearchFilterViewModel {
18 |
19 | weak var lifeDelegate: SearchFilterLifeCycleDelegate?
20 | var modelStack: SearchFilterModel? {
21 | didSet {
22 | lifeDelegate?.refreshCollectionView()
23 | }
24 | }
25 |
26 | init(delegate: SearchFilterLifeCycleDelegate, modelStack: SearchFilterModel? = nil) {
27 | self.lifeDelegate = delegate
28 | self.modelStack = modelStack
29 | }
30 |
31 | // MARK: Collection Content
32 | var selectedIndexPath = [IndexPath]()
33 |
34 | // get-filter from backend
35 | func updateSearchFilter() {
36 | NovelServiceProvider.searchFilter { [weak self] filter in
37 | self?.modelStack = filter
38 | }
39 | }
40 | }
41 |
42 | // MARK: Search Filter Cell Content
43 | enum NRSearchFilterType: String {
44 | case novelType, genres, language, completed
45 |
46 | var headerTitle: String {
47 | switch self {
48 | case .novelType:
49 | return "Novel Type:"
50 | case .genres:
51 | return "Genres:"
52 | case .language:
53 | return "Language:"
54 | case .completed:
55 | return "Completed:"
56 | }
57 | }
58 | }
59 |
60 | struct SearchFilterCellContent {
61 | var type: NRSearchFilterType
62 | var contentArray: [FilterModel]
63 | }
64 |
65 | extension SearchFilterViewModel {
66 |
67 | // Cell Items
68 | var sectionItems: [SearchFilterCellContent] {
69 | var items = [SearchFilterCellContent]()
70 |
71 | if let type = modelStack?.novelType, type.isEmpty {
72 | let item = SearchFilterCellContent(type: .novelType, contentArray: type)
73 | items.append(item)
74 | }
75 | if let type = modelStack?.genres, type.isEmpty {
76 | let item = SearchFilterCellContent(type: .genres, contentArray: type)
77 | items.append(item)
78 | }
79 | if let type = modelStack?.language, type.isEmpty {
80 | let item = SearchFilterCellContent(type: .language, contentArray: type)
81 | items.append(item)
82 | }
83 | if let type = modelStack?.completed, type.isEmpty {
84 | let item = SearchFilterCellContent(type: .completed, contentArray: type)
85 | items.append(item)
86 | }
87 |
88 | return items
89 | }
90 |
91 | // Collection cell
92 | var numberOfSections: Int {
93 | sectionItems.count
94 | }
95 |
96 | func numberOfItemsInSection(_ section: Int) -> Int {
97 | sectionItems[section].contentArray.count
98 | }
99 |
100 | func sectionTitleAt(_ indexPath: IndexPath) -> String {
101 | sectionItems[indexPath.section].type.headerTitle
102 | }
103 |
104 | func cellForItemAt(_ indexPath: IndexPath) -> FilterModel? {
105 |
106 | let filterItem = sectionItems[indexPath.section]
107 | let cur = filterItem.contentArray[indexPath.row]
108 | if !cur.data.isNilOrEmpty {
109 | return cur
110 | }
111 | return nil
112 | }
113 |
114 | // MARK: User Selection
115 | func isSelected(_ indexPath: IndexPath) -> (found: Bool, index: Int) {
116 |
117 | let indexArray = selectedIndexPath.filter { index -> Bool in
118 | index == indexPath
119 | }
120 |
121 | if let filterIndex = indexArray.first, let index = selectedIndexPath.firstIndex(of: filterIndex) {
122 | return (true, index)
123 | }
124 |
125 | return (false, -1)
126 | }
127 |
128 | func updateSelection(_ indexPath: IndexPath) {
129 | let index = isSelected(indexPath)
130 | if index.found {
131 | selectedIndexPath.remove(at: index.index)
132 | }
133 | else {
134 | selectedIndexPath.append(indexPath)
135 | }
136 |
137 | lifeDelegate?.refreshCollectionView()
138 | }
139 |
140 | func resetSelection(_ indexPath: IndexPath? = nil) {
141 | removeObjects(forSection: indexPath?.section)
142 | lifeDelegate?.refreshCollectionView()
143 | }
144 | }
145 |
146 | extension SearchFilterViewModel {
147 |
148 | func removeObjects(forSection section: Int?) {
149 | if let section = section {
150 | selectedIndexPath.removeAll { localIndex -> Bool in
151 | section == localIndex.section
152 | }
153 | }
154 | else {
155 | selectedIndexPath.removeAll()
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Assets.xcassets/LaunchImage.launchimage/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "orientation" : "portrait",
5 | "idiom" : "iphone",
6 | "extent" : "full-screen",
7 | "minimum-system-version" : "12.0",
8 | "subtype" : "2688h",
9 | "scale" : "3x"
10 | },
11 | {
12 | "orientation" : "landscape",
13 | "idiom" : "iphone",
14 | "extent" : "full-screen",
15 | "minimum-system-version" : "12.0",
16 | "subtype" : "2688h",
17 | "scale" : "3x"
18 | },
19 | {
20 | "orientation" : "portrait",
21 | "idiom" : "iphone",
22 | "extent" : "full-screen",
23 | "minimum-system-version" : "12.0",
24 | "subtype" : "1792h",
25 | "scale" : "2x"
26 | },
27 | {
28 | "orientation" : "landscape",
29 | "idiom" : "iphone",
30 | "extent" : "full-screen",
31 | "minimum-system-version" : "12.0",
32 | "subtype" : "1792h",
33 | "scale" : "2x"
34 | },
35 | {
36 | "orientation" : "portrait",
37 | "idiom" : "iphone",
38 | "extent" : "full-screen",
39 | "minimum-system-version" : "11.0",
40 | "subtype" : "2436h",
41 | "scale" : "3x"
42 | },
43 | {
44 | "orientation" : "landscape",
45 | "idiom" : "iphone",
46 | "extent" : "full-screen",
47 | "minimum-system-version" : "11.0",
48 | "subtype" : "2436h",
49 | "scale" : "3x"
50 | },
51 | {
52 | "orientation" : "portrait",
53 | "idiom" : "iphone",
54 | "extent" : "full-screen",
55 | "minimum-system-version" : "8.0",
56 | "subtype" : "736h",
57 | "scale" : "3x"
58 | },
59 | {
60 | "orientation" : "landscape",
61 | "idiom" : "iphone",
62 | "extent" : "full-screen",
63 | "minimum-system-version" : "8.0",
64 | "subtype" : "736h",
65 | "scale" : "3x"
66 | },
67 | {
68 | "orientation" : "portrait",
69 | "idiom" : "iphone",
70 | "extent" : "full-screen",
71 | "minimum-system-version" : "8.0",
72 | "subtype" : "667h",
73 | "scale" : "2x"
74 | },
75 | {
76 | "orientation" : "portrait",
77 | "idiom" : "iphone",
78 | "extent" : "full-screen",
79 | "minimum-system-version" : "7.0",
80 | "scale" : "2x"
81 | },
82 | {
83 | "orientation" : "portrait",
84 | "idiom" : "iphone",
85 | "extent" : "full-screen",
86 | "minimum-system-version" : "7.0",
87 | "subtype" : "retina4",
88 | "scale" : "2x"
89 | },
90 | {
91 | "orientation" : "portrait",
92 | "idiom" : "ipad",
93 | "extent" : "full-screen",
94 | "minimum-system-version" : "7.0",
95 | "scale" : "1x"
96 | },
97 | {
98 | "orientation" : "landscape",
99 | "idiom" : "ipad",
100 | "extent" : "full-screen",
101 | "minimum-system-version" : "7.0",
102 | "scale" : "1x"
103 | },
104 | {
105 | "orientation" : "portrait",
106 | "idiom" : "ipad",
107 | "extent" : "full-screen",
108 | "minimum-system-version" : "7.0",
109 | "scale" : "2x"
110 | },
111 | {
112 | "orientation" : "landscape",
113 | "idiom" : "ipad",
114 | "extent" : "full-screen",
115 | "minimum-system-version" : "7.0",
116 | "scale" : "2x"
117 | },
118 | {
119 | "orientation" : "portrait",
120 | "idiom" : "iphone",
121 | "extent" : "full-screen",
122 | "scale" : "1x"
123 | },
124 | {
125 | "orientation" : "portrait",
126 | "idiom" : "iphone",
127 | "extent" : "full-screen",
128 | "scale" : "2x"
129 | },
130 | {
131 | "orientation" : "portrait",
132 | "idiom" : "iphone",
133 | "extent" : "full-screen",
134 | "subtype" : "retina4",
135 | "scale" : "2x"
136 | },
137 | {
138 | "orientation" : "portrait",
139 | "idiom" : "ipad",
140 | "extent" : "to-status-bar",
141 | "scale" : "1x"
142 | },
143 | {
144 | "orientation" : "portrait",
145 | "idiom" : "ipad",
146 | "extent" : "full-screen",
147 | "scale" : "1x"
148 | },
149 | {
150 | "orientation" : "landscape",
151 | "idiom" : "ipad",
152 | "extent" : "to-status-bar",
153 | "scale" : "1x"
154 | },
155 | {
156 | "orientation" : "landscape",
157 | "idiom" : "ipad",
158 | "extent" : "full-screen",
159 | "scale" : "1x"
160 | },
161 | {
162 | "orientation" : "portrait",
163 | "idiom" : "ipad",
164 | "extent" : "to-status-bar",
165 | "scale" : "2x"
166 | },
167 | {
168 | "orientation" : "portrait",
169 | "idiom" : "ipad",
170 | "extent" : "full-screen",
171 | "scale" : "2x"
172 | },
173 | {
174 | "orientation" : "landscape",
175 | "idiom" : "ipad",
176 | "extent" : "to-status-bar",
177 | "scale" : "2x"
178 | },
179 | {
180 | "orientation" : "landscape",
181 | "idiom" : "ipad",
182 | "extent" : "full-screen",
183 | "scale" : "2x"
184 | }
185 | ],
186 | "info" : {
187 | "version" : 1,
188 | "author" : "xcode"
189 | }
190 | }
--------------------------------------------------------------------------------
/NovelReader/Manager/NRGoogleAuth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NRGoogleAuth.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 30/04/18.
6 | // Copyright © 2018 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | // import GoogleSignIn
11 |
12 | // Notification constans name
13 | public extension Notification.Name {
14 | // FTAuthentication - Google
15 | static let kFTAuthenticationGoogleSignInSignedIn = Notification.Name("kFTAuthentication.GoogleSignIn.SignedIn")
16 | static let kFTAuthenticationGoogleSignInSignedOut = Notification.Name("kFTAuthentication.GoogleSignIn.SignedOut")
17 | }
18 |
19 | class NRGoogleAuth: NSObject, GIDSignInDelegate {
20 |
21 | static let sharedInstance = NRGoogleAuth()
22 |
23 | static func setupGoogleAuth() {
24 |
25 | ThemesManager.addImageSourceBundle(imageSource: "GoogleSignIn".bundle())
26 |
27 | // Initialize sign-in
28 | // GIDSignIn.sharedInstance().clientID = "769359482558-l9uifqdbpq49n1pjqk6vvb2qtekm0ktl.apps.googleusercontent.com"
29 | // GIDSignIn.sharedInstance().delegate = NRGoogleAuth.sharedInstance
30 | // GIDSignIn.sharedInstance().uiDelegate = NRGoogleAuth.sharedInstance
31 | //
32 | // Uncomment to automatically sign in the user.
33 | // GIDSignIn.sharedInstance().signInSilently()
34 | }
35 |
36 | static func signInButton() -> UIButton {
37 |
38 | let signButtton = UIButton(type: .custom)
39 | signButtton.theme = "googleButton"
40 | signButtton.addSizeConstraint(44, 44)
41 |
42 | // Update with User Profile
43 | if let profile = GIDSignIn.sharedInstance().currentUser?.profile {
44 | profile.imageURL(withDimension: 44).downloadedImage {
45 | signButtton.setImage($0, for: .normal)
46 | }
47 | }
48 |
49 | // Update with User Profile after SignIn
50 | _ = NotificationCenter.default.addObserver(forName: .kFTAuthenticationGoogleSignInSignedIn, object: nil, queue: nil) { (notification: Notification) in
51 | if let userObject = notification.object as? GIDGoogleUser {
52 | userObject.profile.imageURL(withDimension: 44).downloadedImage { (image: UIImage?) in
53 | signButtton.setImage(image, for: .normal)
54 | }
55 | }
56 | }
57 |
58 | // Remove User Profile
59 | _ = NotificationCenter.default.addObserver(forName: .kFTAuthenticationGoogleSignInSignedOut, object: nil, queue: nil) { _ in
60 | signButtton.theme = "googleButton"
61 | }
62 |
63 | // SignIn Button Tap Action
64 | signButtton.addTapActionBlock {
65 | // if ((GIDSignIn.sharedInstance().uiDelegate != nil) || (GIDSignIn.sharedInstance().delegate != nil)) {
66 | // GIDSignIn.sharedInstance().signIn()
67 | // }
68 | }
69 |
70 | return signButtton
71 | }
72 |
73 | // GIDSignInDelegate
74 | func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
75 |
76 | if let error = error {
77 | print("\(error.localizedDescription)")
78 | }
79 | else {
80 | NotificationCenter.default.post(name: .kFTAuthenticationGoogleSignInSignedIn, object: user)
81 | }
82 | }
83 |
84 | func sign(_ signIn: GIDSignIn!, didDisconnectWith user: GIDGoogleUser!, withError error: Error!) {
85 | if let error = error {
86 | print("\(error.localizedDescription)")
87 | }
88 | else {
89 | NotificationCenter.default.post(name: .kFTAuthenticationGoogleSignInSignedOut, object: user)
90 | }
91 | }
92 |
93 | // GIDSignInUIDelegate
94 | func sign(inWillDispatch signIn: GIDSignIn!, error: Error!) {
95 | // Optional Protocal implementation: intentionally empty
96 | }
97 |
98 | // Present a view that prompts the user to sign in with Google
99 | func sign(_ signIn: GIDSignIn!,
100 | present viewController: UIViewController!) {
101 | NRAppDelegate.getRootController()?.present(viewController, animated: true, completion: nil)
102 | }
103 |
104 | // Dismiss the "Sign in with Google" view
105 | func sign(_ signIn: GIDSignIn!,
106 | dismiss viewController: UIViewController!) {
107 | NRAppDelegate.getRootController()?.dismiss(animated: true, completion: nil)
108 | }
109 | }
110 |
111 | extension NRAppDelegate {
112 |
113 | static func getRootController() -> UIViewController? {
114 | if let appDelegate = UIApplication.shared.delegate as? NRAppDelegate {
115 | return appDelegate.window?.rootViewController
116 | }
117 | return UIApplication.shared.delegate?.window??.rootViewController
118 | }
119 |
120 | // func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any])
121 | // -> Bool {
122 | // return GIDSignIn.sharedInstance().handle(url, sourceApplication:options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String,
123 | // annotation: options[UIApplication.OpenURLOptionsKey.annotation])
124 | // }
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/FTNovelReaderMockBundle/searchFilter.json:
--------------------------------------------------------------------------------
1 | {
2 | "response": {
3 | "name": "",
4 | "novelType": [
5 | {
6 | "data": "Web Novel",
7 | "type": "Web Novel"
8 | },
9 | {
10 | "data": "Light Novel",
11 | "type": "Light Novel"
12 | },
13 | {
14 | "data": "Chinese Novel",
15 | "type": "Chinese Novel"
16 | },
17 | {
18 | "data": "Korean Novel",
19 | "type": "Korean Novel"
20 | }
21 | ],
22 | "language": [
23 | {
24 | "data": "Chinese",
25 | "type": "Chinese"
26 | },
27 | {
28 | "data": "Japanese",
29 | "type": "Japanese"
30 | },
31 | {
32 | "data": "Korean",
33 | "type": "Korean"
34 | }
35 | ],
36 | "genres": [
37 | {
38 | "data": "4",
39 | "type": "Action"
40 | },
41 | {
42 | "data": "1",
43 | "type": "Adventure"
44 | },
45 | {
46 | "data": "39",
47 | "type": "Celebrity"
48 | },
49 | {
50 | "data": "12",
51 | "type": "Comedy"
52 | },
53 | {
54 | "data": "6",
55 | "type": "Drama"
56 | },
57 | {
58 | "data": "26",
59 | "type": "Ecchi"
60 | },
61 | {
62 | "data": "2",
63 | "type": "Fantasy"
64 | },
65 | {
66 | "data": "14",
67 | "type": "Gender Bender"
68 | },
69 | {
70 | "data": "15",
71 | "type": "Harem"
72 | },
73 | {
74 | "data": "22",
75 | "type": "Historical"
76 | },
77 | {
78 | "data": "31",
79 | "type": "Horror"
80 | },
81 | {
82 | "data": "21",
83 | "type": "Josei"
84 | },
85 | {
86 | "data": "18",
87 | "type": "Martial Arts"
88 | },
89 | {
90 | "data": "19",
91 | "type": "Mature"
92 | },
93 | {
94 | "data": "30",
95 | "type": "Mecha"
96 | },
97 | {
98 | "data": "7",
99 | "type": "Mystery"
100 | },
101 | {
102 | "data": "8",
103 | "type": "Psychological"
104 | },
105 | {
106 | "data": "9",
107 | "type": "Romance"
108 | },
109 | {
110 | "data": "10",
111 | "type": "School Life"
112 | },
113 | {
114 | "data": "3",
115 | "type": "Sci-fi"
116 | },
117 | {
118 | "data": "23",
119 | "type": "Seinen"
120 | },
121 | {
122 | "data": "35",
123 | "type": "Shotacon"
124 | },
125 | {
126 | "data": "11",
127 | "type": "Shoujo"
128 | },
129 | {
130 | "data": "34",
131 | "type": "Shoujo Ai"
132 | },
133 | {
134 | "data": "5",
135 | "type": "Shounen"
136 | },
137 | {
138 | "data": "32",
139 | "type": "Shounen Ai"
140 | },
141 | {
142 | "data": "13",
143 | "type": "Slice of Life"
144 | },
145 | {
146 | "data": "29",
147 | "type": "Smut"
148 | },
149 | {
150 | "data": "33",
151 | "type": "Sports"
152 | },
153 | {
154 | "data": "25",
155 | "type": "Supernatural"
156 | },
157 | {
158 | "data": "24",
159 | "type": "Tragedy"
160 | },
161 | {
162 | "data": "17",
163 | "type": "Wuxia"
164 | },
165 | {
166 | "data": "20",
167 | "type": "Xianxia"
168 | },
169 | {
170 | "data": "38",
171 | "type": "Xuanhuan"
172 | },
173 | {
174 | "data": "16",
175 | "type": "Yaoi"
176 | },
177 | {
178 | "data": "27",
179 | "type": "Yuri"
180 | }
181 | ],
182 | "completed": [
183 | {
184 | "data": "yes",
185 | "type": "Yes"
186 | },
187 | {
188 | "data": "no",
189 | "type": "No"
190 | }
191 | ]
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/NovelReader/Utility+Components/Views/SectionHeaderView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/NovelReader/Modules/FontPicker/FontPickerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontPickerView.swift
3 | // MobileCoreUI
4 | //
5 | // Created by Praveen Prabhakar on 26/08/17.
6 | // Copyright © 2017 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol FontPickerViewProtocol: AnyObject {
12 | func pickerColor(textColor: UIColor, backgroundColor: UIColor)
13 | func fontSize(_ size: Float)
14 | func fontFamily(_ fontName: String?)
15 | }
16 |
17 | open class FontPickerModel {
18 |
19 | var fontSizeStepValue: Float = 10.0
20 | public var fontSize: Float = 140.0
21 | public var fontColor: UIColor = .black
22 | public var backgroundColor: UIColor = .white
23 | public var fontFamily: String?
24 |
25 | // TODO: To support Custom Fonts
26 | let fontTypes = ["Arial", "Courier", "Georgia", "Helvetica", "Palatino", "Times", "Verdana"]
27 |
28 | // Avaialble Fonts
29 | var fontSizeString: String {
30 | String(fontSize)
31 | }
32 |
33 | func avalilableFonts() -> [String] {
34 | fontTypes
35 | }
36 |
37 | func increaseSize() {
38 | (fontSize += fontSizeStepValue)
39 | }
40 |
41 | func decreaseSize() {
42 | (fontSize -= fontSizeStepValue)
43 | }
44 | }
45 |
46 | open class FontPickerView: UIView {
47 |
48 | weak var pickerDelegate: FontPickerViewProtocol?
49 | public var fontPickerModel = FontPickerModel() {
50 | didSet {
51 | // Update View-source
52 | pickerDelegate?.fontSize(fontPickerModel.fontSize)
53 | pickerDelegate?.fontFamily(fontPickerModel.fontFamily)
54 | pickerDelegate?.pickerColor(
55 | textColor: fontPickerModel.fontColor,
56 | backgroundColor: fontPickerModel.backgroundColor
57 | )
58 |
59 | selectedColorButton?.addBorder()
60 | fontTableView.reloadData()
61 | }
62 | }
63 |
64 | @IBOutlet weak var decrementFontButton: UIButton!
65 | @IBOutlet weak var incrementFontButton: UIButton!
66 | @IBOutlet weak var fontSizeLabel: UILabel!
67 |
68 | @IBOutlet weak var whiteColorButton: UIButton!
69 | @IBOutlet weak var creameColorButton: UIButton!
70 | @IBOutlet weak var lightGrayColorButton: UIButton!
71 | @IBOutlet weak var blackColorButton: UIButton!
72 |
73 | weak var selectedColorButton: UIButton?
74 |
75 | @IBOutlet weak var fontTableView: UITableView!
76 |
77 | @IBAction func fontColorSelected(_ sender: UIButton) {
78 | // Clear Previous selected Button
79 | selectedColorButton?.addBorder(color: .clear)
80 | // Update latest button color
81 | selectedColorButton = sender
82 | selectedColorButton?.addBorder()
83 |
84 | // Update Font Color and BG color in ViewModel
85 | fontPickerModel.fontColor = sender.titleLabel?.textColor ?? .black
86 | fontPickerModel.backgroundColor = sender.backgroundColor ?? .white
87 |
88 | // Update soruce-View
89 | pickerDelegate?.pickerColor(textColor: fontPickerModel.fontColor, backgroundColor: fontPickerModel.backgroundColor)
90 | }
91 |
92 | @IBAction func fontSizeChanged(_ sender: UIButton?) {
93 | // Increase / Decrease fontSize
94 | if let button = sender {
95 | (button == decrementFontButton) ? fontPickerModel.decreaseSize() : fontPickerModel.increaseSize()
96 | }
97 |
98 | fontSizeLabel.text = fontPickerModel.fontSizeString
99 |
100 | pickerDelegate?.fontSize(fontPickerModel.fontSize)
101 | }
102 |
103 | var selectedFont: String? {
104 | get {
105 | fontPickerModel.fontFamily
106 | }
107 | set {
108 | fontPickerModel.fontFamily = newValue
109 | pickerDelegate?.fontFamily(newValue)
110 | }
111 | }
112 |
113 | // Avaialble Fonts
114 | var fontTypes: [String] {
115 | fontPickerModel.avalilableFonts()
116 | }
117 |
118 | override open func awakeFromNib() {
119 | super .awakeFromNib()
120 | // Color Button
121 | whiteColorButton.imageView?.image = nil
122 | creameColorButton.imageView?.image = nil
123 | lightGrayColorButton.imageView?.image = nil
124 | blackColorButton.imageView?.image = nil
125 | // TextSize Label
126 | // fontSizeLabel.text = fontPickerModel.fontSizeString
127 | // Font TableView
128 | fontTableView.backgroundView = nil
129 | fontTableView.register(UITableViewCell.self, forCellReuseIdentifier: "kFontType")
130 | fontTableView.estimatedRowHeight = 30
131 | }
132 | }
133 |
134 | extension FontPickerView: UITableViewDataSource, UITableViewDelegate {
135 |
136 | public func numberOfSections(in tableView: UITableView) -> Int {
137 | fontTypes.count
138 | }
139 |
140 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
141 | 1
142 | }
143 |
144 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
145 |
146 | let cell = tableView.dequeueReusableCell(withIdentifier: "kFontType", for: indexPath)
147 | cell.selectionStyle = .none
148 | cell.accessoryType = .none
149 | cell.backgroundColor = .clear
150 |
151 | let cur: String = fontTypes[indexPath.section]
152 | cell.textLabel?.text = cur
153 | cell.textLabel?.font = UIFont(name: cur, size: 14)
154 |
155 | if selectedFont == cur {
156 | cell.accessoryType = .checkmark
157 | }
158 |
159 | return cell
160 | }
161 |
162 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
163 | tableView.deselectRow(at: indexPath, animated: true)
164 |
165 | if fontTypes[indexPath.section] == selectedFont {
166 | selectedFont = nil
167 | }
168 | else {
169 | selectedFont = fontTypes[indexPath.section]
170 | }
171 |
172 | tableView.reloadData()
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/NovelReader/Resources/Themes/Themes.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Themes",
3 | "color": {
4 | "default": "#000000",
5 | "clear": "clear",
6 | "black": "#293551",
7 | "appBase": "#F8F8F8",
8 | "navBar": "#293551",
9 | "appWhite": "#F8F8F8",
10 | "halfBlack": "#9A9A9A99",
11 | "arrow": "#666666",
12 | "blue": "#1059ce",
13 | "green": "#2CA4BF",
14 | "yellow": "#cecb10",
15 | "white": "#E8E8E8",
16 | "gray": "#A2A2A2"
17 | },
18 | "layer": {
19 | "roundButton" : {
20 | "cornerRadius": 22
21 | },
22 | "elevatedGrey": {
23 | "cornerRadius": 10,
24 | "masksToBounds": false,
25 | "shadowPath": true,
26 | "shadowColor": "navBar",
27 | "shadowRadius": 5.0,
28 | "shadowOpacity": 0.15
29 | }
30 | },
31 | "font": {
32 | "default": {
33 | "name": "system",
34 | "size": "13.0"
35 | },
36 | "bold": {
37 | "name": "boldSystem",
38 | "size": "17.0"
39 | },
40 | "system14": {
41 | "_super": "default",
42 | "size": "14.0"
43 | },
44 | "system17": {
45 | "_super": "default",
46 | "size": "17.0"
47 | },
48 | "bold17": {
49 | "name": "boldSystem",
50 | "size": "17.0"
51 | }
52 | },
53 | "link": {
54 | "url": {
55 | "underlineColor": "blue",
56 | "foregroundColor": "blue"
57 | },
58 | "hash": {
59 | "underlineColor": "blue",
60 | "foregroundColor": "blue"
61 | }
62 | },
63 | "appearance": {
64 | "UIStatusBar": {
65 | "tintColor": "white"
66 | },
67 | "UINavigationBar:UINavigationController": {
68 | "barTintColor": "white",
69 | "tintColor": "white",
70 | "isTranslucent": false,
71 | "backgroundImage": {
72 | "default": "@Pixel",
73 | "landScape": "@Pixel"
74 | },
75 | "shadowImage": "@empty",
76 | "titleText": {
77 | "foregroundColor": "white"
78 | }
79 | },
80 | "UIToolbar:UINavigationController": {
81 | "barTintColor": "white",
82 | "isTranslucent": false
83 | }
84 | },
85 | "components": {
86 | "FTView": {
87 | "default": {
88 | "gradientLayer": {
89 | "colors": ["navBar", "navBar", "white"],
90 | "locations": [0.0, 0.5, 1.0]
91 | }
92 | },
93 | "clearView": {
94 | "backgroundColor": "clear"
95 | },
96 | "halfView": {
97 | "backgroundColor": "halfBlack"
98 | },
99 | "appWhite": {
100 | "backgroundColor": "appWhite"
101 | }
102 | },
103 | "UIView": {
104 | "default": {
105 | "backgroundColor": "appWhite"
106 | },
107 | "underline": {
108 | "backgroundColor": "green"
109 | }
110 | },
111 | "RecentNovelCollectionViewCell": {
112 | "default": {
113 | "layer": "elevatedGrey"
114 | }
115 | },
116 | "NovelCollectionViewCell": {
117 | "default": {
118 | "layer": "elevatedGrey"
119 | }
120 | },
121 | "SectionHeaderView": {
122 | "default": {
123 | "backgroundColor": "appWhite"
124 | }
125 | },
126 | "UICollectionView": {
127 | "default": {
128 | "backgroundColor": "clear"
129 | },
130 | "halfView": {
131 | "backgroundColor": "appWhite",
132 | "backgroundViewTheme": "default"
133 | }
134 | },
135 | "SegmentCollectionHeaderView": {
136 | "default": {
137 | "backgroundColor": "navBar"
138 | }
139 | },
140 | "SearchBarHeaderView": {
141 | "default": {
142 | "backgroundColor": "navBar"
143 | }
144 | },
145 | "UISearchBar": {
146 | "default": {
147 | "textcolor": "white",
148 | "tintColor": "white",
149 | "barTintColor": "navBar"
150 | }
151 | },
152 | "UISegmentedControl": {
153 | "default": {
154 | "tintColor": "white",
155 | "iOS12Style": true,
156 | "ios_13": {
157 | "tintColor": "navBar",
158 | "textcolor": "white"
159 | }
160 | }
161 | },
162 | "UIButton": {
163 | "default": {
164 | "textfont": "system14",
165 | "textcolor": "black",
166 | "backgroundColor": "clear"
167 | },
168 | "button14R": {
169 | "_super": "default",
170 | "textcolor": "appBase"
171 | },
172 | "button14R:highlighted": {
173 | "_super": "default",
174 | "textcolor": "green"
175 | },
176 | "button14R:disabled": {
177 | "_super": "default",
178 | "textcolor": "yellow"
179 | },
180 | "buttonR17bW": {
181 | "textfont": "bold17",
182 | "textcolor": "white",
183 | "backgroundColor": "appBase"
184 | },
185 | "segment": {
186 | "_super": "default",
187 | "textfont": "system17",
188 | "textcolor": "gray"
189 | },
190 | "segment:selected": {
191 | "textfont": "bold17",
192 | "textcolor": "white"
193 | },
194 | "googleButton": {
195 | "_super": "default",
196 | "image": "@google",
197 | "backgroundColor": "white",
198 | "layer": "roundButton"
199 | }
200 | },
201 | "UILabel": {
202 | "default": {
203 | "textfont": "system14",
204 | "textcolor": "black",
205 | "backgroundColor": "clear",
206 | "isLinkUnderlineEnabled": true,
207 | "isLinkDetectionEnabled": true
208 | },
209 | "system13Gray": {
210 | "_super": "default",
211 | "textcolor": "gray"
212 | },
213 | "system17R": {
214 | "_super": "default",
215 | "textfont": "system17",
216 | "textcolor": "black"
217 | },
218 | "selectionCell": {
219 | "_super": "default",
220 | "textfont": "system17",
221 | "textcolor": "black"
222 | },
223 | "selectionCell:disabled": {
224 | "_super": "default",
225 | "textcolor": "gray"
226 | }
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ChapterList/Views/NovelDescriptionView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/Dashboard/RecentUpdate/RecentNovelCollectionViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/NovelReader/Manager/DataSourceManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewDataManager.swift
3 | // NovelReader
4 | //
5 | // Created by Praveen Prabhakar on 30/06/21.
6 | // Copyright © 2021 Praveen Prabhakar. All rights reserved.
7 | //
8 |
9 | import AppTheming
10 | import CoreComponents
11 | import CoreUtility
12 | import Foundation
13 | import NetworkLayer
14 | import UIKit
15 |
16 | // MARK: Configure protocol
17 | protocol DSConfigureContentProtocol {
18 | func configureContent(content: AnyObject, indexPath: IndexPath?)
19 | }
20 |
21 | public extension DataSourceManager {
22 | typealias DataObjects = [[AnyObject]]
23 | // Collection View
24 | typealias DequeueReusableViewReturn = (type: UICollectionReusableView.Type, block: ActionWithObjectBlock?)
25 | typealias DequeueReusableViewBlock = (_ indexPath: IndexPath, _ kind: String) -> DequeueReusableViewReturn
26 | typealias ReferenceSizeBlock = (_ section: Int) -> UICollectionReusableView?
27 | // Common
28 | typealias DequeueViewBlock = (_ indexPath: IndexPath) -> UIView.Type
29 | typealias DidSelectCellBlock = (_ indexPath: IndexPath, _ object: AnyObject) -> Void
30 | }
31 |
32 | open class DataSourceManager: NSObject {
33 | var cellObjects = DataObjects()
34 | var dequeueView: DequeueViewBlock?
35 | var dequeueReusableView: DequeueReusableViewBlock?
36 | var configureDidSelect: DidSelectCellBlock?
37 | var layoutReferenceSize: ReferenceSizeBlock?
38 |
39 | var tableDelegate: TableViewControllerProtocol? {
40 | didSet {
41 | configureTable(delegate: self, source: self)
42 | }
43 | }
44 | var collectionDelegate: CollectionViewControllerProtocol? {
45 | didSet {
46 | configureCollection(delegate: self, source: self)
47 | }
48 | }
49 |
50 | init(delegate: TableViewControllerProtocol? = nil, collectionDelegate: CollectionViewControllerProtocol? = nil) {
51 | super.init()
52 | self.tableDelegate = delegate
53 | self.collectionDelegate = collectionDelegate
54 | configureDataSource()
55 | }
56 | }
57 |
58 | // MARK: Common
59 | extension DataSourceManager {
60 | func configureDataSource() {
61 | self.configureTable(delegate: self, source: self)
62 | self.configureCollection(delegate: self, source: self)
63 | }
64 |
65 | func reloadView() {
66 | DispatchQueue.main.async { [weak self] in
67 | self?.tableView?.reloadData()
68 | self?.collectionView?.reloadData()
69 | }
70 | }
71 | func removeAllObjects() {
72 | cellObjects.removeAll()
73 | reloadView()
74 | }
75 |
76 | func updateCellObjects(_ objects: DataObjects) {
77 | cellObjects.removeAll()
78 | cellObjects = objects
79 | reloadView()
80 | }
81 |
82 | func getObject(for indexPath: IndexPath) -> AnyObject? {
83 | let (section, row) = (indexPath.section, indexPath.row)
84 | guard section < cellObjects.count else { return nil }
85 | let sectionObj = cellObjects[section]
86 | guard row < sectionObj.count else { return nil }
87 | return sectionObj[row]
88 | }
89 | }
90 |
91 | // MARK: UITableViewDelegate & UITableViewDataSource
92 | extension DataSourceManager: UITableViewDelegate, UITableViewDataSource {
93 | var tableView: UITableView? {
94 | tableDelegate?.tableView
95 | }
96 |
97 | func register(_ cellType: UITableViewCell.Type) {
98 | guard let view = tableView else { return }
99 | if cellType.hasNib {
100 | cellType.registerNib(for: view)
101 | } else {
102 | cellType.registerClass(for: view)
103 | }
104 | }
105 |
106 | func configureTable(delegate: UITableViewDelegate? = nil, source: UITableViewDataSource? = nil) {
107 | guard let tableView = tableView else { return }
108 | // Setup TableView
109 | tableView.theme = ThemeStyle.defaultStyle.rawValue
110 | // TableView delegates
111 | tableView.dataSource = source
112 | tableView.delegate = delegate
113 | // Relaod TableView onces
114 | DispatchQueue.main.async { [weak tableView] in
115 | tableView?.reloadData()
116 | }
117 | }
118 |
119 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
120 | guard section < cellObjects.count else { return 0 }
121 | return cellObjects[section].count
122 | }
123 |
124 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
125 | guard let dequeueObj = dequeueView?(indexPath) as? UITableViewCell.Type,
126 | let cellType = try? dequeueObj.dequeue(from: tableView, for: indexPath) else {
127 | return UITableViewCell()
128 | }
129 | if let obj = getObject(for: indexPath), let cell = cellType as? DSConfigureContentProtocol {
130 | cell.configureContent(content: obj, indexPath: indexPath)
131 | }
132 | return cellType
133 | }
134 |
135 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
136 | if let obj = getObject(for: indexPath) {
137 | configureDidSelect?(indexPath, obj)
138 | }
139 | }
140 | }
141 |
142 | // MARK: UICollectionViewDelegate & UICollectionViewDataSource
143 | extension DataSourceManager {
144 | var collectionView: UICollectionView? {
145 | collectionDelegate?.collectionView
146 | }
147 |
148 | func register(_ viewType: UIView.Type, kind: String? = nil) {
149 | guard let view = collectionView else { return }
150 | if let cellType = viewType as? UICollectionReusableView.Type, let kind = kind {
151 | if cellType.hasNib {
152 | cellType.registerNib(for: view, forSupplementaryViewOfKind: kind)
153 | } else {
154 | cellType.registerClass(for: view, forSupplementaryViewOfKind: kind)
155 | }
156 | }
157 | if let cellType = viewType as? UICollectionViewCell.Type {
158 | if cellType.hasNib {
159 | cellType.registerNib(for: view)
160 | } else {
161 | cellType.registerClass(for: view)
162 | }
163 | }
164 | }
165 |
166 | func configureCollection(delegate: UICollectionViewDelegate? = nil, source: UICollectionViewDataSource? = nil) {
167 | guard let collectionView = collectionView else { return }
168 | // Setup TableView
169 | collectionView.theme = ThemeStyle.defaultStyle.rawValue
170 | // CollectionView delegates
171 | collectionView.dataSource = source
172 | collectionView.delegate = delegate
173 | // Relaod CollectionView onces
174 | DispatchQueue.main.async { [weak collectionView] in
175 | collectionView?.reloadData()
176 | }
177 | }
178 | }
179 |
180 | // MARK: UICollectionView delegates
181 | extension DataSourceManager: UICollectionViewDelegate, UICollectionViewDataSource {
182 | // viewForSupplementaryElement
183 | public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String,
184 | at indexPath: IndexPath) -> UICollectionReusableView {
185 | guard let dequeueObj = dequeueReusableView?(indexPath, kind),
186 | let cellType = try? dequeueObj.type.dequeue(from: collectionView, ofKind: kind, for: indexPath) else {
187 | return UICollectionReusableView()
188 | }
189 | dequeueObj.block?(cellType)
190 | return cellType
191 | }
192 |
193 | // numberOfItemsInSection
194 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
195 | guard section < cellObjects.count else { return 0 }
196 | return cellObjects[section].count
197 | }
198 |
199 | // cellForItem
200 | public func collectionView(_ collectionView: UICollectionView,
201 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
202 | guard let dequeueObj = dequeueView?(indexPath) as? UICollectionViewCell.Type,
203 | let cellType = try? dequeueObj.dequeue(from: collectionView, for: indexPath) else {
204 | return UICollectionViewCell()
205 | }
206 | if let obj = getObject(for: indexPath), let cell = cellType as? DSConfigureContentProtocol {
207 | cell.configureContent(content: obj, indexPath: indexPath)
208 | }
209 | return cellType
210 | }
211 |
212 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
213 | referenceSizeForHeaderInSection section: Int) -> CGSize {
214 | guard let view = layoutReferenceSize?(section) else { return .zero }
215 | let height = view.preferredLayoutAttributesFitting(.init()).frame.height
216 | return CGSize(width: view.frame.width, height: height)
217 | }
218 |
219 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
220 | guard let obj = getObject(for: indexPath) else { return }
221 | configureDidSelect?(indexPath, obj)
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/FTNovelReaderMockBundle/fetchRecentUpdatesList.json:
--------------------------------------------------------------------------------
1 | {
2 | "response": [
3 | {
4 | "identifier": "aXZlLWJlY2FtZS1hYmxlLXRvLWRvLWFueXRoaW5nLXdpdGgtbXktZ3Jvd3RoLWNoZWF0LWJ1dC1pLWNhbnQtc2VlbS10by1nZXQtb3V0LW9mLWJlaW5nLWpvYmxlc3Mvdm9sdW1lLTgvY2hhcHRlci0yMDc=",
5 | "name": "II’ve Became Able to Do Anything with My Growth Cheat, but I Can’t Seem to Get out of Being Jobless. Vol. 8 Ch. 207",
6 | "lastUpdate": " 15 Minutes ago"
7 | },
8 | {
9 | "identifier": "bXktZW50aXJlLWNsYXNzLWhhcy1iZWVuLXJlaW5jYXJuYXRlZC1pLWJlY2FtZS10aGUtd2Vha2VzdC1za2VsZXRvbi9jaGFwdGVyLTY0LTI=",
10 | "name": "My Entire Class Has Been Reincarnated – I Became the Weakest Skeleton! Ch. 64.2",
11 | "lastUpdate": " 15 Minutes ago"
12 | },
13 | {
14 | "identifier": "aS1nb3QtcmVpbmNhcm5hdGVkLWFuZC1taXN0YWtlbi1hcy1hLWdlbml1cy9jaGFwdGVyLTI5",
15 | "name": "I Got Reincarnated And Mistaken As A Genius? Ch. 29",
16 | "lastUpdate": " 16 Minutes ago"
17 | },
18 | {
19 | "identifier": "YS10aG91Z2h0LXRocm91Z2gtZXRlcm5pdHkvY2hhcHRlci01NzI=",
20 | "name": "A Thought Through Eternity Ch. 572",
21 | "lastUpdate": " 16 Minutes ago"
22 | },
23 | {
24 | "identifier": "YS10aG91Z2h0LXRocm91Z2gtZXRlcm5pdHkvY2hhcHRlci01NzM=",
25 | "name": "A Thought Through Eternity Ch. 573",
26 | "lastUpdate": " 16 Minutes ago"
27 | },
28 | {
29 | "identifier": "dGhlLWdhdGUtb2YtZ29vZC1mb3J0dW5lL2NoYXB0ZXItNDA2",
30 | "name": "The Gate Of Good Fortune Ch. 406",
31 | "lastUpdate": " 17 Minutes ago"
32 | },
33 | {
34 | "identifier": "bWFzdGVyLWRldmlsLWRvbnQta2lzcy1tZS9jaGFwdGVyLTIyOA==",
35 | "name": "Master Devil Don’t Kiss Me Ch. 228",
36 | "lastUpdate": " 17 Minutes ago"
37 | },
38 | {
39 | "identifier": "bWFzdGVyLWRldmlsLWRvbnQta2lzcy1tZS9jaGFwdGVyLTIyNw==",
40 | "name": "Master Devil Don’t Kiss Me Ch. 227",
41 | "lastUpdate": " 17 Minutes ago"
42 | },
43 | {
44 | "identifier": "dGFsaXNtYW4tZW1wZXJvci9jaGFwdGVyLTM1NA==",
45 | "name": "Talisman Emperor Ch. 354",
46 | "lastUpdate": " 18 Minutes ago"
47 | },
48 | {
49 | "identifier": "cGFpbnRpbmctb2YtdGhlLW5pbmUtaW1tb3J0YWxzL2NoYXB0ZXItMzU4",
50 | "name": "Painting of the Nine Immortals Ch. 358",
51 | "lastUpdate": " 40 Minutes ago"
52 | },
53 | {
54 | "identifier": "dGhlLWludmluY2libGUtZHJhZ29uLWVtcGVyb3IvY2hhcHRlci00MDQ=",
55 | "name": "The Invincible Dragon Emperor Ch. 404",
56 | "lastUpdate": " 50 Minutes ago"
57 | },
58 | {
59 | "identifier": "cmVhbG1zLWluLXRoZS1maXJtYW1lbnQvY2hhcHRlci04NDQ=",
60 | "name": "Realms In The Firmament Ch. 844",
61 | "lastUpdate": " 1 Hour, 5 Minutes ago"
62 | },
63 | {
64 | "identifier": "Z29kLWFuZC1kZXZpbC13b3JsZC9jaGFwdGVyLTkwOA==",
65 | "name": "God and Devil World Ch. 908",
66 | "lastUpdate": " 1 Hour, 20 Minutes ago"
67 | },
68 | {
69 | "identifier": "aS1hbS1zdXByZW1lL2NoYXB0ZXItNDY2",
70 | "name": "I Am Supreme Ch. 466",
71 | "lastUpdate": " 1 Hour, 40 Minutes ago"
72 | },
73 | {
74 | "identifier": "ZXZpbC1lbXBlcm9ycy13aWxkLWNvbnNvcnQvY2hhcHRlci03ODM=",
75 | "name": "Evil Emperor’s Wild Consort Ch. 783",
76 | "lastUpdate": " 2 Hours, 15 Minutes ago"
77 | },
78 | {
79 | "identifier": "dGhlLW1hZ3VzLWVyYS9jaGFwdGVyLTEyNjY=",
80 | "name": "The Magus Era Ch. 1266",
81 | "lastUpdate": " 2 Hours, 20 Minutes ago"
82 | },
83 | {
84 | "identifier": "bG9yZC14dWUteWluZy9jaGFwdGVyLTQ0Mw==",
85 | "name": "Lord Xue Ying Ch. 443",
86 | "lastUpdate": " 2 Hours, 25 Minutes ago"
87 | },
88 | {
89 | "identifier": "bWFydGlhbC13b3JsZC9jaGFwdGVyLTE1OTQ=",
90 | "name": "Martial World Ch. 1594",
91 | "lastUpdate": " 3 Hours, 39 Minutes ago"
92 | },
93 | {
94 | "identifier": "bWFydGlhbC13b3JsZC9jaGFwdGVyLTE1OTU=",
95 | "name": "Martial World Ch. 1595",
96 | "lastUpdate": " 3 Hours, 39 Minutes ago"
97 | },
98 | {
99 | "identifier": "aW0tYS1kdWxsYWhhbi1sb29raW5nLWZvci1teS1oZWFkL2NoYXB0ZXItMTA=",
100 | "name": "I’m a Dullahan, Looking for My Head Ch. 10",
101 | "lastUpdate": " 3 Hours, 40 Minutes ago"
102 | },
103 | {
104 | "identifier": "cmVpZ24tb2YtdGhlLWh1bnRlcnMvY2hhcHRlci0xOTctMQ==",
105 | "name": "Reign of the Hunters Ch. 197.1",
106 | "lastUpdate": " 3 Hours, 40 Minutes ago"
107 | },
108 | {
109 | "identifier": "c292ZXJlaWduLW9mLXRoZS10aHJlZS1yZWFsbXMvY2hhcHRlci05NTg=",
110 | "name": "Sovereign of the Three Realms Ch. 958",
111 | "lastUpdate": " 3 Hours, 41 Minutes ago"
112 | },
113 | {
114 | "identifier": "Z3UtZGFvaXN0LW1hc3Rlci9jaGFwdGVyLTIzOA==",
115 | "name": "Gu Daoist Master Ch. 238",
116 | "lastUpdate": " 4 Hours, 25 Minutes ago"
117 | },
118 | {
119 | "identifier": "a2luZy1vZi1teXJpYWQtZG9tYWluL2NoYXB0ZXItMzk4",
120 | "name": "King of Myriad Domain Ch. 398",
121 | "lastUpdate": " 4 Hours, 48 Minutes ago"
122 | },
123 | {
124 | "identifier": "aGlzdG9yeXMtc3Ryb25nZXN0LXNlbmlvci1icm90aGVyL2NoYXB0ZXItODQ5",
125 | "name": "History’s Strongest Senior Brother Ch. 849",
126 | "lastUpdate": " 4 Hours, 49 Minutes ago"
127 | },
128 | {
129 | "identifier": "Z3Jhc3BpbmctZXZpbC9jaGFwdGVyLTkxLTI=",
130 | "name": "Grasping Evil Ch. 91.2",
131 | "lastUpdate": " 4 Hours, 50 Minutes ago"
132 | },
133 | {
134 | "identifier": "aXNla2FpLXl1cnVyaS1raWtvdS1yYWlzaW5nLWNoaWxkcmVuLXdoaWxlLWJlaW5nLWFuLWFkdmVudHVyZXIvY2hhcHRlci0xMTg=",
135 | "name": "Isekai Yururi Kikou ~Raising Children While Being .. Ch. 118",
136 | "lastUpdate": " 4 Hours, 50 Minutes ago"
137 | },
138 | {
139 | "identifier": "c3Bpcml0LXJlYWxtL2NoYXB0ZXItODYz",
140 | "name": "Spirit Realm Ch. 863",
141 | "lastUpdate": " 4 Hours, 50 Minutes ago"
142 | },
143 | {
144 | "identifier": "c3Bpcml0LXJlYWxtL2NoYXB0ZXItODY0",
145 | "name": "Spirit Realm Ch. 864",
146 | "lastUpdate": " 4 Hours, 50 Minutes ago"
147 | },
148 | {
149 | "identifier": "YS1yZWNvcmQtb2YtYS1tb3J0YWxzLWpvdXJuZXktdG8taW1tb3J0YWxpdHkvY2hhcHRlci02MjQ=",
150 | "name": "A Record of a Mortal’s Journey to Immortality Ch. 624",
151 | "lastUpdate": " 4 Hours, 51 Minutes ago"
152 | },
153 | {
154 | "identifier": "c292ZXJlaWduLW9mLXRoZS10aHJlZS1yZWFsbXMvY2hhcHRlci05NTc=",
155 | "name": "Sovereign of the Three Realms Ch. 957",
156 | "lastUpdate": " 4 Hours, 52 Minutes ago"
157 | },
158 | {
159 | "identifier": "ZW1wZXJvcnMtZG9taW5hdGlvbi9jaGFwdGVyLTE0Mzc=",
160 | "name": "Emperor’s Domination Ch. 1437",
161 | "lastUpdate": " 4 Hours, 52 Minutes ago"
162 | },
163 | {
164 | "identifier": "ZW1wZXJvcnMtZG9taW5hdGlvbi9jaGFwdGVyLTE0Mzg=",
165 | "name": "Emperor’s Domination Ch. 1438",
166 | "lastUpdate": " 4 Hours, 52 Minutes ago"
167 | },
168 | {
169 | "identifier": "YWdhaW5zdC10aGUtZ29kcy9jaGFwdGVyLTEwODg=",
170 | "name": "Against the Gods Ch. 1088",
171 | "lastUpdate": " 4 Hours, 53 Minutes ago"
172 | },
173 | {
174 | "identifier": "YWxpY2UtdGFsZS1pbi1waGFudGFzbWFnb3JpYS92b2x1bWUtMi9jaGFwdGVyLTgw",
175 | "name": "Alice Tale in Phantasmagoria Vol. 2 Ch. 80",
176 | "lastUpdate": " 4 Hours, 53 Minutes ago"
177 | },
178 | {
179 | "identifier": "cGVyZmVjdC13b3JsZC9jaGFwdGVyLTUzOA==",
180 | "name": "Perfect World Ch. 538",
181 | "lastUpdate": " 4 Hours, 54 Minutes ago"
182 | },
183 | {
184 | "identifier": "cGVyZmVjdC13b3JsZC9jaGFwdGVyLTUzOQ==",
185 | "name": "Perfect World Ch. 539",
186 | "lastUpdate": " 4 Hours, 54 Minutes ago"
187 | },
188 | {
189 | "identifier": "cGVyZmVjdC13b3JsZC9jaGFwdGVyLTU0MA==",
190 | "name": "Perfect World Ch. 540",
191 | "lastUpdate": " 4 Hours, 54 Minutes ago"
192 | },
193 | {
194 | "identifier": "dGFsaXNtYW4tZW1wZXJvci9jaGFwdGVyLTM1Mw==",
195 | "name": "Talisman Emperor Ch. 353",
196 | "lastUpdate": " 4 Hours, 55 Minutes ago"
197 | },
198 | {
199 | "identifier": "cGFyYWRpc2Utb2YtZGVtb25zLWFuZC1nb2RzL2NoYXB0ZXItNjA4",
200 | "name": "Paradise of Demonic Gods Ch. 608",
201 | "lastUpdate": " 4 Hours, 55 Minutes ago"
202 | }
203 | ]
204 | }
205 |
--------------------------------------------------------------------------------
/NovelReader/Modules/Reader/ChapterList/Views/NovelDetailsView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------