├── .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 | [![Platform](http://img.shields.io/badge/platform-ios-blue.svg?style=flat)](https://developer.apple.com/iphone/index.action) 6 | [![Language](http://img.shields.io/badge/language-swift-brightgreen.svg?style=flat)](https://developer.apple.com/swift) 7 | [![License](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](http://mit-license.org) 8 | [![Build Status](https://travis-ci.org/ppraveentr/NovelReader.svg?branch=master)](https://travis-ci.org/ppraveentr/NovelReader) 9 | 10 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/d960d74eea7a4051890cc8a974af758d)](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 |
14 | 15 |
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 | 26 | 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 | 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 | 33 | 39 | 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 | 38 | 39 | 40 | 41 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 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 | 37 | 38 | 39 | 40 | 49 | 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 | 35 | 36 | 37 | 38 | 44 | 50 | 56 | 57 | 58 | 63 | 68 | 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 | --------------------------------------------------------------------------------